diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100755 index 000000000000..31872d63c7e5 --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,46 @@ +{{ range .Versions }} +# Welcome to the {{ .Tag.Name }} release of Sealos!🎉🎉! + + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) + +{{ range .CommitGroups -}} +### {{ .Title }} + +{{ range .Commits -}} +* {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ end }} +{{ end -}} + +{{- if .RevertCommits -}} +### Reverts + +{{ range .RevertCommits -}} +* {{ .Revert.Header }} +{{ end }} +{{ end -}} + +{{- if .MergeCommits -}} +### Pull Requests + +{{ range .MergeCommits -}} +* {{ .Header }} +{{ end }} +{{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} + +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100755 index 000000000000..e426bbe13bfe --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,33 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/labring/sealos +options: + tag_filter_pattern: '^v' + sort: "date" + commits: + sort_by: Type + filters: + Type: + - feat + - fix + - perf + - refactor + commit_groups: + group_by: Type + sort_by: Title + title_maps: + feat: New Features + fix: Bug Fixes + perf: Performance Improvements + refactor: Code Refactoring + header: + pattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$' + pattern_maps: + - Type + - Scope + - Subject + notes: + keywords: + - BREAKING CHANGE \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..d63af02a933c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/go/.devcontainer/base.Dockerfile + +# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster +ARG VARIANT="1-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends libgpgme-dev libbtrfs-dev libdevmapper-dev + +# [Optional] Uncomment the next lines to use go get to install anything else you need +USER vscode +RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest \ + && go install golang.org/x/tools/cmd/goimports@latest \ + && go install github.com/google/addlicense@latest \ + && go install k8s.io/code-generator/cmd/deepcopy-gen@latest \ + && go install k8s.io/code-generator/cmd/conversion-gen@latest + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..08be1dd980a8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,48 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/go +{ + "name": "Go", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a version of Go: 1, 1.18, 1.17 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "1-bullseye", + // Options + "NODE_VERSION": "lts/*" + } + }, + "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "golang.Go" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "docker-in-docker": "latest", + "kubectl-helm-minikube": "latest" + } +} diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 698eef3889dd..000000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Sealos Bug report -about: Problems and issues with code of Sealos -title: 'BUG: brief description of the bug' -assignees: '' - ---- - - -**Which command or component** - -eg. sealos init, sealos install ... - -**The Description of the bug** - -Describe the bug. - -**How to reproduce(pictures can be attached if necessary)** -1. -... - - -**Test Result** -1. -2. -3. - -**What you expected to happen** - -Expected to xxx - -**Operating environment** - -- Docker version: eg. 19.03 -- Kubernetes version: eg.kube1.16.tar.gz -- Sealos version: v3.1.0-alpha.2 -- Operating system: eg. centos7 -- Runtime environment: eg. physical machine (100G memory, 10 core cpu, 10T storage), virtual machine (virbox, 10G memory, 2 core cpu, 100G storage), cloud(aws, 10G memory, 2 core cpu, 100G storage) -- Cluster size: eg 3 master, 3 node -- Additional information: istio, dashboard ... - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..b7558325a172 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: 🐞 Sealos Bug report +description: Create a report to help us improve +title: 'BUG: brief description of the bug' +labels: kind/bug +body: + - type: markdown + attributes: + value: | + Thank you for reporting an issue. + + Please fill in as much of the following form as you're able. + - type: input + attributes: + label: Sealos Version + description: Sealos Version of your installtion + placeholder: eg. v4.1.3 + validations: + required: true + - type: textarea + attributes: + label: How to reproduce the bug? + description: Enter details about your bug. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + - type: textarea + attributes: + label: What is the expected behavior? + description: If possible please provide textual output instead of screenshots. + - type: textarea + attributes: + label: What do you see instead? + description: If possible please provide textual output instead of screenshots. + - type: textarea + attributes: + label: Operating environment + description: | + examples: + - **Sealos version**: eg. v4.1.3 + - **Docker version**: eg. v20.10.7 + - **Kubernetes version**: eg. v1.25.0 + - **Operating system**: eg. centos7 + - **Runtime environment**: eg. physical machine (100G memory, 10 core cpu, 10T storage), virtual machine (virbox, 10G memory, 2 core cpu, 100G storage), cloud(aws, 10G memory, 2 core cpu, 100G storage) + - **Cluster size**: eg 3 master, 3 node + - **Additional information**: istio, dashboard ... + value: | + - Sealos version: + - Docker version: + - Kubernetes version: + - Operating system: + - Runtime environment: + - Cluster size: + - Additional information: + render: markdown + - type: textarea + attributes: + label: Additional information + description: Tell us anything else you think we should know. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..0c647892018d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: true +contact_links: + - name: Ask a question (GitHub Discussions) + url: https://github.com/labring/sealos/discussions + about: | + Please do not submit "a bug report" for asking a question. + In most cases, GitHub Discussions is the best place to ask a question. + If you are not sure whether you are going to report a bug or ask a question, + please consider asking in GitHub Discussions first. + - name: Join Our Discord + url: https://discord.gg/eHyXHtSE + about: Join our community, we will keep you in the loop. diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml new file mode 100644 index 000000000000..2c746cd8d3b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -0,0 +1,34 @@ +name: 📝 Documentation Suggestion +description: Suggest an improvement or addition to the official documentation +title: 'Docs: brief description of your suggestion' +labels: kind/documentation +body: + - type: markdown + attributes: + value: | + Thank you for making a suggestion to improve our documentation. + + Please fill in as much of the following form as you're able. + - type: input + attributes: + label: Documentation Section + description: The section of the documentation where your suggestion applies. + placeholder: eg. Installation Guide + validations: + required: true + - type: textarea + attributes: + label: What is the current documentation state? + description: Briefly describe the current state of the section. + - type: textarea + attributes: + label: What is your suggestion? + description: Please describe your suggestion for improving or adding to the documentation. + - type: textarea + attributes: + label: Why is this change beneficial? + description: Explain why you believe your suggestion will improve the documentation. + - type: textarea + attributes: + label: Additional information + description: Provide any other details or context that might be helpful for us to understand your suggestion. diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md deleted file mode 100644 index 356cfc32f05a..000000000000 --- a/.github/ISSUE_TEMPLATE/feature.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Sealos feature request -about: Suggest an idea for this project -title: 'Feature: brief description of the feature' -assignees: '' - ---- - -**Which command or component** - -eg. sealos xxx, sealos init - -**Detailed description of the feature** -1. -2. -3. - -**If you have solution,please describe it** - -Complete this feature through XXX -1. -2. -3. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..5379a93bc9ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,20 @@ +name: "\U0001F680 Sealos feature request" +description: Suggest an idea for this project +labels: kind/feature +title: 'Feature: brief description of the feature' +body: + - type: markdown + attributes: + value: | + Thank you for suggesting an idea to improve Sealos. + Please fill in as much of the following form as you're able. + - type: textarea + attributes: + label: What is the problem this feature will solve? + - type: textarea + attributes: + label: If you have solution,please describe it + description: Describe the requests. If you already have something in mind... PRs are welcome! + - type: textarea + attributes: + label: What alternatives have you considered? diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 426a0925a56b..000000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Ask a question -about: Ask a question about using Sealos -title: 'Question: brief description of the question' -assignees: '' - ---- - -**Which command or component** -eg. sealos init, sealos install, kubernetes cni, docker and so on - -**The Description of the question** -Detailed description of the question. - -**Some reference materials you see** -eg. the reference of the documentation - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 96ddbb59b3a7..e68ddb380bb3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1 +1,6 @@ -[SKIP CI]sealos: 一句话简短描述该 PR 内容 + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..9709168c7333 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,214 @@ +# Sealos Cloud Operating System Development Instructions + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +> **Note**: These instructions are specifically designed for GitHub Copilot coding agents to work effectively with the sealos codebase. + +## Working Effectively + +### Prerequisites and Dependencies +- **Operating System**: Linux required (sealos binary requires CGO for overlay driver support) +- **Go**: Version 1.23+ (validated with go1.24.6) +- **Node.js**: Version 20.4.0+ for frontend (validated with v20.19.4) +- **Package Managers**: Make, pnpm (for frontend), npm +- **Container Tools**: Docker (optional for local development) + +Install required system dependencies: +```bash +sudo apt-get update && sudo apt-get install -y pkg-config libdevmapper-dev libbtrfs-dev libgpgme-dev build-essential +``` + +### Repository Structure +- **`/lifecycle`**: Main sealos CLI and core binaries (sealos, sealctl, lvscare, image-cri-shim) +- **`/controllers`**: Multiple Kubernetes controllers with individual Go modules and Makefiles +- **`/frontend`**: Next.js workspace with pnpm, 22 sub-projects (desktop, providers) +- **`/service`**: Backend Go services with individual modules +- **`/webhooks`**: Kubernetes admission webhooks +- **`/docs`**: Documentation and guides + +### Bootstrap and Build Process + +#### Core sealos CLI Build (lifecycle directory) +```bash +cd lifecycle +make build BINS=sealos # Build single binary - takes 4 minutes first time, NEVER CANCEL +make build # Build all binaries - takes 15 seconds after first build +``` + +**CRITICAL BUILD TIMING**: +- **First build**: 3-4 minutes - NEVER CANCEL. Set timeout to 300+ seconds +- **Subsequent builds**: 10-15 seconds - Set timeout to 60+ seconds +- **All core binaries**: sealos (101MB), sealctl (93MB), lvscare (36MB), image-cri-shim (20MB) + +#### Controller Build (any controller directory) +```bash +cd controllers/user # or any controller directory +make build # Takes 1-2 minutes first time - NEVER CANCEL +``` + +**Controller build times**: +- First build: 1-2 minutes per controller +- Subsequent builds: 15-30 seconds + +#### Frontend Build (requires network access) +```bash +cd frontend +npm install -g pnpm # Install pnpm if not available +pnpm install # May fail due to network restrictions - this is expected +``` + +**Frontend limitations**: The frontend requires external package downloads that may fail in restricted environments. Document as "network-dependent" if install fails. + +### Testing and Validation + +#### Unit Tests (limited due to environment requirements) +```bash +cd lifecycle +go test ./pkg/utils/... -v # Some tests may fail due to hardcoded paths - this is expected +``` + +#### Functional Validation (what works in sandboxed environment) +```bash +cd lifecycle +./bin/linux_amd64/sealos version # Always works +./bin/linux_amd64/sealos help # Always works +./bin/linux_amd64/sealos env # Always works +./bin/linux_amd64/sealos completion bash > /tmp/completion.bash # Always works +./bin/linux_amd64/sealctl hostname # Always works +``` + +#### Validation scenarios that require privileges (will fail in sandbox) +```bash +./bin/linux_amd64/sealos images # Fails: "Operation not permitted" +./bin/linux_amd64/sealos gen ... # Fails: needs unshare privileges +``` + +**Testing limitations**: Many sealos commands require Linux capabilities (unshare, overlay mounts) that are not available in sandboxed environments. Focus on build validation and basic command help/version testing. + +### Code Quality and Linting + +#### Format code +```bash +cd lifecycle +make format # Takes 3-5 seconds - NEVER CANCEL +``` + +#### Lint code (has known issues) +```bash +cd lifecycle +make lint # Takes 1 minute - NEVER CANCEL, may show type errors - this is expected +``` + +**Linting expectations**: The linter may show type checking errors in buildah integration code. These are known issues and should not block development. + +## Key Components and Binaries + +### Core Binaries +- **sealos**: Main CLI for Kubernetes cluster management and OCI image operations +- **sealctl**: System management utilities (network, IPVS, hostname management) +- **lvscare**: Load balancer and IPVS management for Kubernetes +- **image-cri-shim**: Container runtime interface shim + +### Main Commands Overview +- `sealos run/apply`: Deploy cloud native applications with or without existing cluster +- `sealos build`: Build OCI images using Containerfile/Kubefile +- `sealos gen`: Generate Clusterfile configurations +- `sealos add/delete`: Node management +- `sealos images/pull/push`: Image management operations + +## Development Workflows + +### Making Changes to Core CLI +1. Edit code in `/lifecycle` directory +2. Build: `cd lifecycle && make build` +3. Test basic functionality: `./bin/linux_amd64/sealos version` +4. Format: `make format` +5. Build again to ensure no errors + +### Making Changes to Controllers +1. Edit code in `/controllers/` directory +2. Build: `make build` +3. Test: Check that binary is created in `bin/manager` + +### Making Changes to Frontend +1. Edit code in `/frontend` directory +2. Install deps: `pnpm install` (may fail due to network) +3. Build individual packages: `pnpm -r --filter './packages/*' run build` + +## Critical Timing and Cancellation Warnings + +**NEVER CANCEL THESE OPERATIONS**: +- `make build` (first time): Wait 4+ minutes +- `make lint`: Wait 1+ minutes +- `make format`: Wait 5+ seconds +- Controller builds (first time): Wait 2+ minutes +- `pnpm install`: Wait 2+ minutes (may fail due to network) + +**Always set appropriate timeouts**: +- Build operations: 300+ seconds +- Lint operations: 120+ seconds +- Network operations: 180+ seconds + +## Cross-Platform Notes +- **sealos binary**: Must be built on Linux due to CGO dependencies for overlay driver +- **Other binaries**: Can be cross-compiled with `CGO_ENABLED=0` +- **Frontend**: Platform independent but requires Node.js 20.4.0+ + +## CI/CD Integration +- Main CI pipeline: `.github/workflows/ci.yml` +- Format checking: `.github/workflows/check-format-code.yml` +- License checking: `.github/workflows/check-license.yml` +- Frontend builds: `.github/workflows/frontend.yml` + +Always run `make format` before committing to pass CI format checks. + +## Pull Request Guidelines + +### PR Title Specification +All pull request titles **must** follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and include both **type** and **scope** fields. + +**Required format**: `(): ` + +**Available types**: +- `feat`: New features +- `fix`: Bug fixes +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code refactoring +- `perf`: Performance improvements +- `test`: Test additions or modifications +- `build`: Build system changes +- `ci`: CI/CD pipeline changes +- `chore`: Maintenance tasks +- `revert`: Revert previous changes + +**Example PR titles**: +- `feat(lifecycle): add new cluster management command` +- `fix(controllers): resolve user controller memory leak` +- `docs(frontend): update component usage examples` +- `ci(workflow): update build pipeline for Go 1.23` + +**CI validation**: PRs with non-compliant titles will fail automated checks and cannot be merged. + +## Troubleshooting Common Issues + +### Build Failures +- Missing dependencies: Install system packages listed in Prerequisites +- Go version: Ensure Go 1.23+ +- Network timeouts: Increase timeout settings, don't cancel + +### Test Failures +- Unit tests: May fail due to hardcoded paths - focus on build validation +- Privilege errors: Expected in sandboxed environments for sealos commands + +### Frontend Issues +- Network restrictions: pnpm install may fail - document this limitation +- Node version: Ensure Node.js 20.4.0+ + +## Working with Go Workspace +Sealos uses Go 1.18+ workspace feature. When adding new modules: +```bash +go work use -r . # Update workspace from root directory +``` + +This maintains the multi-module workspace structure across lifecycle, controllers, services, and webhooks. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..258e7caef717 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,87 @@ +#Usage in github: https://github.com/actions/labeler + +area/command: +- any: [ 'lifecycle/cmd/**/*' ] + +kind/documentation: +- any: [ 'docs/**/*' ] + +area/qa: +- 'QA.md' + +area/test: +- '**/*_test.go' +- 'lifecycle/test/**/*' + +area/buildah: +- any: [ 'lifecycle/pkg/buildah/**/*' ] + +area/imageCRIShim: +- any: [ 'lifecycle/staging/src/github.com/labring/image-cri-shim/**/*' ] + +area/lvscare: +- any: [ 'lifecycle/staging/src/github.com/labring/lvscare/**/*' ] + +area/bootstrap: +- any: [ 'lifecycle/pkg/bootstrap/**/*' ] + +area/clusterfile: +- any: [ 'lifecycle/pkg/clusterfile/**/*' ] + +area/config: +- any: [ 'lifecycle/pkg/config/**/*' ] +- any: [ 'lifecycle/pkg/env/**/*' ] +- any: [ 'lifecycle/pkg/template/**/*' ] + +area/guest: +- any: [ 'lifecycle/pkg/guest/**/*' ] + +area/env: +- any: [ 'lifecycle/pkg/system/**/*' ] + +area/filesystem: +- any: [ 'lifecycle/pkg/filesystem/**/*' ] + +area/apply: +- any: [ 'lifecycle/pkg/apply/**/*' ] + +area/check: +- any: [ 'lifecycle/pkg/checker/**/*' ] + +area/ipvs: +- any: [ 'lifecycle/pkg/ipvs/**/*' ] + +area/ssh: +- any: [ 'lifecycle/pkg/ssh/**/*' ] + +area/registry: +- any: [ 'lifecycle/pkg/registry/**/*' ] + +area/runtime: +- any: [ 'lifecycle/pkg/runtime/**/*' ] + +area/api: +- any: [ 'lifecycle/pkg/types/**/*' ] +- any: [ 'lifecycle/**/*_types.go' ] + +area/cloud: +- any: [ 'controllers/**/*' ] +- any: [ 'service/**/*' ] + +area/lifecycle-management: +- any: [ 'lifecycle/pkg/**/*' ] +- any: [ 'lifecycle/staging/**/*' ] + +area/frontend: +- any: [ 'frontend/**/*' ] + +area/ci: +- 'scripts/**/*' +- 'lifecycle/scripts/**/*' +- 'lifecycle/docker/**/*' +- '.golangci.yml' +- 'lifecycle/.goreleaser.yml' +- 'lifecycle/Makefile' + +area/workflow: +- any: [ '.github/**/*' ] diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000000..0f1ec5e7b78b --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,27 @@ +XS: + name: size/XS + lines: 0 + color: 3CBF00 +S: + name: size/S + lines: 10 + color: 5D9801 +M: + name: size/M + lines: 30 + color: 7F7203 +L: + name: size/L + lines: 100 + color: A14C05 +XL: + name: size/XL + lines: 500 + color: C32607 +XXL: + name: size/XXL + lines: 1000 + color: E50009 + comment: | + # Whoa! Easy there, Partner! + This PR is too big. Please break it up into smaller PRs. \ No newline at end of file diff --git a/.github/scripts/image_build_push.sh b/.github/scripts/image_build_push.sh new file mode 100644 index 000000000000..d1adc2f9c31b --- /dev/null +++ b/.github/scripts/image_build_push.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e +name=${1:-"kubernetes"} +version=${2:-"v1.28.15"} +TOKEN=${3:-"changeme"} +ARCH=${4:-"amd64"} +AUTHOR=${5:-"labring"} +COMMIT_ID=${6:-"latest"} +IMAGE_NAME="ghcr.io/${AUTHOR}/sealos/${name}:${version}" + +commitDATE=$(date +%Y%m%d%H%M%S) +sealos login -u "${AUTHOR}" -p "${TOKEN}" ghcr.io +sealos build --pull=always --platform=linux/"${ARCH}" -t "$IMAGE_NAME"-"${ARCH}" \ + --label org.opencontainers.image.description="kubernetes and app cluster image" \ + --label org.opencontainers.image.licenses="Sealos Sustainable Use License" \ + --label org.opencontainers.image.source="https://github.com/${AUTHOR}/sealos" \ + --label org.opencontainers.image.title="sealos-image" \ + --label org.opencontainers.image.time="${commitDATE}" \ + --label org.opencontainers.image.url="https://github.com/${AUTHOR}/sealos" \ + --label org.opencontainers.image.version="${version}" \ + --label org.opencontainers.image.revision="${COMMIT_ID}" . + +sealos push "$IMAGE_NAME"-"${ARCH}" +sealos rmi -p --force || true + diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh new file mode 100644 index 000000000000..ab80536673f6 --- /dev/null +++ b/.github/scripts/install.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e +timestamp() { + date +"%Y-%m-%d %T" +} + +error() { + flag=$(timestamp) + echo -e "\033[31m ERROR [$flag] >> $* \033[0m" + exit 1 +} + +logger() { + flag=$(timestamp) + echo -e "\033[36m INFO [$flag] >> $* \033[0m" +} + +warn() { + flag=$(timestamp) + echo -e "\033[33m WARN [$flag] >> $* \033[0m" +} + +debug() { + flag=$(timestamp) + echo -e "\033[32m DEBUG [$flag] >> $* \033[0m" +} + +check_file_exits() { + for f; do + if [[ -f $f ]]; then + logger "The machine $f is installed" + exit 0 + fi + done +} + +check_file_exits /usr/bin/sealos + +pushd "$(mktemp -d)" >/dev/null || exit + until curl -sLo "sealos.tar.gz" "https://github.com/labring/sealos/releases/download/v5.1.2-rc5/sealos_5.1.2-rc5_linux_amd64.tar.gz"; do sleep 3; done + tar -zxf sealos.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin + rm -rf sealos.tar.gz + sealos version +popd >/dev/null \ No newline at end of file diff --git a/.github/sources.list b/.github/sources.list deleted file mode 100644 index 79cf9d3a6c12..000000000000 --- a/.github/sources.list +++ /dev/null @@ -1,60 +0,0 @@ -# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to -# newer versions of the distribution. -deb http://archive.ubuntu.com/ubuntu/ focal main restricted -# deb-src http://archive.ubuntu.com/ubuntu/ focal main restricted - -## Major bug fix updates produced after the final release of the -## distribution. -deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted -# deb-src http://archive.ubuntu.com/ubuntu/ focal-updates main restricted - -## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu -## team. Also, please note that software in universe WILL NOT receive any -## review or updates from the Ubuntu security team. -deb http://archive.ubuntu.com/ubuntu/ focal universe -# deb-src http://archive.ubuntu.com/ubuntu/ focal universe -deb http://archive.ubuntu.com/ubuntu/ focal-updates universe -# deb-src http://archive.ubuntu.com/ubuntu/ focal-updates universe - -## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu -## team, and may not be under a free licence. Please satisfy yourself as to -## your rights to use the software. Also, please note that software in -## multiverse WILL NOT receive any review or updates from the Ubuntu -## security team. -deb http://archive.ubuntu.com/ubuntu/ focal multiverse -# deb-src http://archive.ubuntu.com/ubuntu/ focal multiverse -deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse -# deb-src http://archive.ubuntu.com/ubuntu/ focal-updates multiverse - -## N.B. software from this repository may not have been tested as -## extensively as that contained in the main release, although it includes -## newer versions of some applications which may provide useful features. -## Also, please note that software in backports WILL NOT receive any review -## or updates from the Ubuntu security team. -deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted universe multiverse -# deb-src http://archive.ubuntu.com/ubuntu/ focal-backports main restricted universe multiverse - -## Uncomment the following two lines to add software from Canonical's -## 'partner' repository. -## This software is not part of Ubuntu, but is offered by Canonical and the -## respective vendors as a service to Ubuntu users. -# deb http://archive.canonical.com/ubuntu focal partner -# deb-src http://archive.canonical.com/ubuntu focal partner - -deb http://security.ubuntu.com/ubuntu/ focal-security main restricted -# deb-src http://security.ubuntu.com/ubuntu/ focal-security main restricted -deb http://security.ubuntu.com/ubuntu/ focal-security universe -# deb-src http://security.ubuntu.com/ubuntu/ focal-security universe -deb http://security.ubuntu.com/ubuntu/ focal-security multiverse -# deb-src http://security.ubuntu.com/ubuntu/ focal-security multiverse - - -deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal main restricted universe multiverse - -deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal-updates main restricted universe multiverse - -deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal-backports main restricted universe multiverse - -deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal-security main restricted universe multiverse - -deb [arch=arm64] http://archive.canonical.com/ubuntu focal partner \ No newline at end of file diff --git a/.github/sync_code.yml b/.github/sync_code.yml new file mode 100644 index 000000000000..3bd531e8438b --- /dev/null +++ b/.github/sync_code.yml @@ -0,0 +1,14 @@ +labring/lvscare: + - source: lifecycle/staging/src/github.com/labring/lvscare/ + dest: . + deleteOrphaned: true + - source: LICENSE.md + dest: LICENSE.md + deleteOrphaned: true +labring/image-cri-shim: + - source: lifecycle/staging/src/github.com/labring/image-cri-shim/ + dest: . + deleteOrphaned: true + - source: LICENSE.md + dest: LICENSE.md + deleteOrphaned: true diff --git a/.github/workflows/bot-cherry-pick.yml b/.github/workflows/bot-cherry-pick.yml new file mode 100644 index 000000000000..6678c4ebf73a --- /dev/null +++ b/.github/workflows/bot-cherry-pick.yml @@ -0,0 +1,71 @@ +name: Github Rebot for Cherry Pick +on: + workflow_dispatch: + inputs: + prNumber: + description: 'PR Number to cherry pick' + required: true + default: '1234' + target: + description: 'Target branch to cherry pick to' + required: true + default: 'release-v5.1' +jobs: + cherry-pick: + permissions: + contents: write + pull-requests: write + issues: write + name: Cherry Pick + runs-on: ubuntu-24.04 + steps: + - name: Checkout the latest code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + - name: Automatic Cherry Pick + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ inputs.prNumber }} + TARGET_BRANCH: ${{ inputs.target }} + run: | + git config --global --add safe.directory /github/workspace + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + bash "./${PROJECT_PATH}/scripts/cherry-pick.sh" ${PR_NUMBER} ${TARGET_BRANCH} + - name: Get PR title + id: pr_title + env: + PR_NUMBER: ${{ inputs.prNumber }} + uses: actions/github-script@v6 + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: Number(process.env.PR_NUMBER) + }); + return pr.data.title; + - uses: peter-evans/create-pull-request@v5 + with: + title: ${{ fromJSON(steps.pr_title.outputs.result) }} + body: | + fix(cherry-pick): Automated CherryPick ${{ inputs.prNumber }} for ${{ inputs.target }} + + Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action + commit-message: | + 🤖 cherry-pick to ${{ inputs.target }} using robot. + branch: cherry-pick-${{ inputs.target }}-${{ inputs.prNumber }} + base: ${{ inputs.target }} + draft: true + signoff: true + labels: | + bot + delete-branch: true + token: ${{ secrets.GITHUB_TOKEN }} + reviewers: cuisongliu + milestone: cherry-pick + committer: github-actions[bot] + author: github-actions[bot] diff --git a/.github/workflows/bot-issues-translator.yml b/.github/workflows/bot-issues-translator.yml new file mode 100644 index 000000000000..b2bfdfd18a27 --- /dev/null +++ b/.github/workflows/bot-issues-translator.yml @@ -0,0 +1,25 @@ +name: 'Github Rebot for github-translate-action' +on: + issues: + types: [opened, edited] + issue_comment: + types: [created, edited] + discussion: + types: [created, edited] + discussion_comment: + types: [created, edited] + +jobs: + translate: + permissions: + issues: write + discussions: write + pull-requests: write + runs-on: ubuntu-24.04 + steps: + - uses: lizheming/github-translate-action@1.1.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + IS_MODIFY_TITLE: true + APPEND_TRANSLATION: true diff --git a/.github/workflows/bot-labels.yml b/.github/workflows/bot-labels.yml new file mode 100644 index 000000000000..1ffc4b6d2a13 --- /dev/null +++ b/.github/workflows/bot-labels.yml @@ -0,0 +1,13 @@ +name: "Github Rebot for Pull Request Labeler" +on: + - pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-24.04 + steps: + - uses: actions/labeler@v4 +# no more lines here. \ No newline at end of file diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml new file mode 100644 index 000000000000..41c4a60ed6bc --- /dev/null +++ b/.github/workflows/check-coverage.yml @@ -0,0 +1,51 @@ +name: Check-Coverage + +on: + workflow_dispatch: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + pull_request: + branches: ["*"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: check-coverage-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + # Common versions + GO_VERSION: "1.25" + PROJECT_PATH: "./lifecycle" + +jobs: + coverage: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Coverage + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + run: cd "$PROJECT_PATH" && make coverage + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v3 diff --git a/.github/workflows/check-license.yml b/.github/workflows/check-license.yml new file mode 100755 index 000000000000..498d5f5eef89 --- /dev/null +++ b/.github/workflows/check-license.yml @@ -0,0 +1,47 @@ +name: Check-License + +on: + workflow_dispatch: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + pull_request: + branches: ["*"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: check-license-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + # Common versions + GO_VERSION: "1.25" + PROJECT_PATH: "./lifecycle" + +jobs: + check-license: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Check License + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + run: cd "${PROJECT_PATH}" && make verify-license diff --git a/.github/workflows/check-pr-title-lint-semantic.yml b/.github/workflows/check-pr-title-lint-semantic.yml new file mode 100644 index 000000000000..fcbb24f21ee5 --- /dev/null +++ b/.github/workflows/check-pr-title-lint-semantic.yml @@ -0,0 +1,67 @@ +name: 'Check PR title semantic' +on: + pull_request_target: + types: + - opened + - edited + - synchronize + - labeled + - unlabeled +permissions: + pull-requests: write + statuses: write + +jobs: + validate-pr-title: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + requireScope: true + ignoreLabels: | + bot + subjectPattern: ^.+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + doesn't start with an uppercase character. + wip: true + headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$' + headerPatternCorrespondence: type, scope, subject + - uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. + + Details: + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + # Delete a previous comment when the issue has been resolved + - if: ${{ steps.lint_pr_title.outputs.error_message == null }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + delete: true \ No newline at end of file diff --git a/.github/workflows/check-semgrep.yml b/.github/workflows/check-semgrep.yml new file mode 100644 index 000000000000..520a44775ced --- /dev/null +++ b/.github/workflows/check-semgrep.yml @@ -0,0 +1,71 @@ +# Name of this GitHub Actions workflow. +name: Check-Semgrep + +on: + workflow_dispatch: + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + pull_request: + branches: ["*"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + +permissions: + contents: read + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: check-semgrep-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + semgrep: + # User-definable name of this GitHub Actions job: + name: Scan + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + container: + # A Docker image with Semgrep installed. Do not change this. + image: semgrep/semgrep:1.164.0 + + # allow fails due to too many risks + continue-on-error: true + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. + - uses: actions/checkout@v4 + # Run the "semgrep ci" command on the command line of the docker image. + - run: | + # On pushes, compare only against the previous commit so existing findings do not re-break main. + if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + export SEMGREP_BASELINE_REF="${{ github.event.before }}" + fi + + semgrep ci \ + --exclude-rule go.lang.security.audit.xss.import-text-template.import-text-template \ + --exclude-rule yaml.kubernetes.security.run-as-non-root.run-as-non-root \ + --exclude-rule yaml.github-actions.security.pull-request-target-code-checkout.pull-request-target-code-checkout \ + --exclude-rule yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha \ + --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag + env: + # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. + SEMGREP_RULES: p/default # more at semgrep.dev/explore + + + # Uncomment SEMGREP_TIMEOUT to set this job's timeout (in seconds): + # Default timeout is 1800 seconds (30 minutes). + # Set to 0 to disable the timeout. + # SEMGREP_TIMEOUT: 300 diff --git a/.github/workflows/ci-patch-image.yml b/.github/workflows/ci-patch-image.yml new file mode 100755 index 000000000000..958d1f3113b4 --- /dev/null +++ b/.github/workflows/ci-patch-image.yml @@ -0,0 +1,165 @@ +name: CI Patch Images Package + +env: + # Common versions + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + PROJECT_PATH: "./lifecycle" + +on: + workflow_dispatch: + inputs: + push_mage: + description: "Push images" + required: false + type: boolean + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + - "controllers/**" + - "service/**" + - "webhooks/**" + - "frontend/**" + pull_request: + branches: ["*"] + paths-ignore: + - "docs/**" + - "CHANGELOG/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + - "controllers/**" + - "service/**" + - "webhooks/**" + - "frontend/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: ci-patch-image-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + call_ci_workflow: + uses: ./.github/workflows/import-patch-image.yml + with: + arch: amd64,arm64 + + container-sealos: + needs: [call_ci_workflow] + runs-on: ubuntu-24.04 + permissions: + contents: write + packages: write + if: ${{ (github.event_name == 'push') || (inputs.push_mage == true) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Expose git commit data + uses: rlespinasse/git-commit-data-action@v1 + - # Add support for more platforms with QEMU (optional) + # https://github.com/docker/setup-qemu-action + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: ${{ env.PROJECT_PATH }}/docker/sealos/bin/sealos-amd64 + + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-arm64 + path: ${{ env.PROJECT_PATH }}/docker/sealos/bin/sealos-arm64 + + - name: build (and publish) main sealos image + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GIT_COMMIT_SHORT_SHA: ${{ env.GIT_COMMIT_SHORT_SHA }} + DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/sealos + working-directory: ${{ env.PROJECT_PATH }}/docker/sealos + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --label "org.opencontainers.image.source=https://github.com/${REPOSITORY_OWNER}/sealos" \ + --label "org.opencontainers.image.description=sealos container image" \ + --label "org.opencontainers.image.licenses=MIT" \ + --push \ + -t "${DOCKER_REPO}:${GIT_COMMIT_SHORT_SHA}" \ + -f Dockerfile.main \ + . + + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --label "org.opencontainers.image.source=https://github.com/${REPOSITORY_OWNER}/sealos" \ + --label "org.opencontainers.image.description=sealos container image" \ + --label "org.opencontainers.image.licenses=MIT" \ + --push \ + -t "${DOCKER_REPO}:latest" \ + -f Dockerfile.main \ + . + build-on-cluster-patch-image: + if: ${{ (github.event_name == 'push') || (inputs.push_mage == true) }} + needs: + - call_ci_workflow + runs-on: ubuntu-24.04 + permissions: + contents: write + packages: write + issues: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install sealos + run: | + sudo bash ./.github/scripts/install.sh + - name: Expose git commit data + uses: rlespinasse/git-commit-data-action@v1 + + - name: Download amd64 patch image tar + uses: actions/download-artifact@v4 + with: + name: patch-image-amd64.tar + path: /tmp/sealos/images/ + - name: Download arm64 patch image tar + uses: actions/download-artifact@v4 + with: + name: patch-image-arm64.tar + path: /tmp/sealos/images/ + + - name: Manifest Cluster Images + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + run: | + sudo sealos login -u "${REPOSITORY_OWNER}" -p "${GH_PAT}" --debug ghcr.io + sudo sealos load -i /tmp/sealos/images/patch-arm64.tar + sudo sealos load -i /tmp/sealos/images/patch-amd64.tar + sudo sealos images + bash ./scripts/manifest-cluster-images.sh + + - name: Manifest Cluster Images for latest + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GIT_COMMIT_SHORT_SHA: ${{ env.GIT_COMMIT_SHORT_SHA }} + run: | + sudo sealos tag "ghcr.io/${REPOSITORY_OWNER}/sealos-patch:${GIT_COMMIT_SHORT_SHA}-amd64" "ghcr.io/${REPOSITORY_OWNER}/sealos-patch:latest-amd64" + sudo sealos tag "ghcr.io/${REPOSITORY_OWNER}/sealos-patch:${GIT_COMMIT_SHORT_SHA}-arm64" "ghcr.io/${REPOSITORY_OWNER}/sealos-patch:latest-arm64" + sudo sealos images + bash ./scripts/manifest-cluster-images.sh "ghcr.io/${REPOSITORY_OWNER}/sealos-patch:latest" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 172c0dc2229c..e3607b588627 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,101 +1,119 @@ name: CI env: - # Common versions - GO_VERSION: "1.17" + GO_VERSION: "1.25" on: + workflow_call: + workflow_dispatch: + inputs: + push_mage: + description: "Push images" + required: false + type: boolean + push: + branches: ["main"] + paths-ignore: + - "docs/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + - "controllers/**" + - "service/**" + - "webhooks/**" + - "frontend/**" pull_request: branches: ["*"] paths-ignore: - "docs/**" - - "*.md" - - "*.yml" - push: - branches: ["main"] + - "CHANGELOG/**" + - "**/*.md" + - "**/*.yaml" + - "CHANGELOG/**" + - "controllers/**" + - "service/**" + - "webhooks/**" + - "frontend/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: ci-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: - check-license: - runs-on: ubuntu-latest + resolve-modules: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + has_modules: ${{ steps.set-matrix.outputs.has_modules }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/checkout@v4 with: - go-version: ${{ env.GO_VERSION }} - - name: Check License - run: make verify-license + fetch-depth: 0 - golangci-lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - name: Install Dependencies - run: sudo apt install -y libgpgme-dev libdevmapper-dev libbtrfs-dev - - name: Run Linter - run: make lint + - name: Resolve Modules + id: set-matrix + run: | + # Use a diff base when available so lifecycle pushes stay incremental. + BASE_REF="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_REF="${{ github.event.pull_request.base.sha }}" + HEAD_REF="${{ github.sha }}" + elif [[ "${{ github.event_name }}" == "push" && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then + BASE_REF="${{ github.event.before }}" + HEAD_REF="${{ github.sha }}" + else + HEAD_REF="${{ github.sha }}" + fi - coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GO_VERSION }} - - name: Install Dependencies - run: sudo apt install -y libgpgme-dev libdevmapper-dev libbtrfs-dev - - name: Run Coverage - run: make coverage - - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v2 + if [[ -n "${BASE_REF}" ]]; then + bash ./scripts/resolve-modules.sh ./lifecycle --changed "${BASE_REF}" "${HEAD_REF}" + else + bash ./scripts/resolve-modules.sh ./lifecycle + fi - build: - needs: [check-license, golangci-lint, coverage] - runs-on: ubuntu-latest + golangci-lint: + if: ${{ needs.resolve-modules.outputs.has_modules == 'true' }} + needs: [resolve-modules] + runs-on: ubuntu-24.04 strategy: - matrix: - binary: [sealos, sealctl] - arch: [arm64, amd64] + fail-fast: false + matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} steps: - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/checkout@v4 with: - go-version: ${{ env.GO_VERSION }} + fetch-depth: 0 - - name: Setup MultiArch Apt - if: ${{ matrix.arch == 'arm64' }} - run: | - sudo \cp .github/sources.list /etc/apt/sources.list - sudo apt update || true - sudo dpkg --add-architecture ${{ matrix.arch }} - - - name: Install Build Dependencies - run: | - sudo apt update || true - sudo apt install -y upx - sudo apt install -y libbtrfs-dev:${{ matrix.arch }} btrfs-progs:${{ matrix.arch }} - sudo apt install -y libgpgme-dev:${{ matrix.arch }} + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} - - name: Install gcc - if: ${{ matrix.arch == 'arm64' }} - run: sudo apt install -y gcc-aarch64-linux-gnu + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev - - name: Build Binaries + - name: Resolve Linter Args + id: lint-args run: | - make release-build BUILDSTEP=${{ matrix.binary }}-${{ matrix.arch }} + args="--color=always --config=${{ github.workspace }}/.golangci.yml" + # Match module resolution so historical lifecycle lint debt does not fail unrelated pushes. + new_from_rev="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + new_from_rev="${{ github.event.pull_request.base.sha }}" + elif [[ "${{ github.event_name }}" == "push" && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then + new_from_rev="${{ github.event.before }}" + fi + if [[ -n "${new_from_rev}" ]]; then + args="${args} --new-from-rev=${new_from_rev}" + fi + echo "args=${args}" >> "$GITHUB_OUTPUT" - - name: Save binaries - uses: actions/upload-artifact@v2 + - name: Run Linter + uses: golangci/golangci-lint-action@v8 with: - name: ${{ matrix.binary }}-${{ matrix.arch }} - path: dist/${{ matrix.binary }}-${{ matrix.arch }}_linux_${{ matrix.arch }}/${{ matrix.binary }} + version: v2.12.2 + working-directory: ${{ matrix.workdir }} + # args between =, not space + args: ${{ steps.lint-args.outputs.args }} diff --git a/.github/workflows/cloud-release.yml b/.github/workflows/cloud-release.yml new file mode 100644 index 000000000000..fcde71460b1d --- /dev/null +++ b/.github/workflows/cloud-release.yml @@ -0,0 +1,76 @@ +name: Release Cloud + +on: + workflow_call: + inputs: + tag: + description: "Tag for manual release" + required: false + default: "" + type: string + build_offline_tar_only: + description: "Build offline tar only" + required: false + default: false + type: boolean + workflow_dispatch: + inputs: + tag: + description: "Tag for manual release" + required: false + default: "" + type: string + build_offline_tar_only: + description: "Build offline tar only" + required: false + default: false + type: boolean + +env: + # Common versions + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + +permissions: + contents: write + actions: write + packages: write + +jobs: + release-controllers: + if: ${{ inputs.build_offline_tar_only == false }} + uses: ./.github/workflows/controllers.yml + with: + push_image: true + push_image_tag: ${{ inputs.tag }} + disable_cilint: true + force_all: true + secrets: inherit + + release-frontends: + if: ${{ inputs.build_offline_tar_only == false }} + uses: ./.github/workflows/frontends.yml + with: + push_image: true + push_image_tag: ${{ inputs.tag }} + force_all: true + secrets: inherit + + release-service: + if: ${{ inputs.build_offline_tar_only == false }} + uses: ./.github/workflows/services.yml + with: + push_image: true + push_image_tag: ${{ inputs.tag }} + disable_cilint: true + force_all: true + secrets: inherit + + release-webhook: + if: ${{ inputs.build_offline_tar_only == false }} + uses: ./.github/workflows/webhooks.yml + with: + push_image: true + push_image_tag: ${{ inputs.tag }} + disable_cilint: true + secrets: inherit diff --git a/.github/workflows/controller-build.yml b/.github/workflows/controller-build.yml new file mode 100644 index 000000000000..a1108fbe1904 --- /dev/null +++ b/.github/workflows/controller-build.yml @@ -0,0 +1,327 @@ +name: Build Controller + +on: + workflow_call: + inputs: + module_name: + description: "Module name (e.g., user, account)" + required: true + type: string + module_path: + description: "Module path relative to controllers/ (e.g., user, job/init)" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + module_name: + description: "Module name (e.g., user, account, job-init)" + required: true + type: string + module_path: + description: "Module path (e.g., user, job/init)" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + +env: + GO_VERSION: "1.24" + ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }} + ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }} + LICENSE_KEY: ${{ secrets.LICENSE_KEY }} + +jobs: + golangci-lint: + if: ${{ !inputs.disable_cilint && (github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Linter + uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + working-directory: controllers/${{ inputs.module_path }} + args: --color=always --config=${{ github.workspace }}/.golangci.yml + + unit-test: + name: Test ${{ inputs.module_name }} with race detector + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Tests with race detector + working-directory: controllers/${{ inputs.module_path }} + env: + USE_EXISTING_CLUSTER: "false" + run: make test + + image-build: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Build ${{ inputs.module_name }} amd64 + working-directory: controllers/${{ inputs.module_path }} + env: + MODULE_NAME: ${{ inputs.module_name }} + MODULE_PATH: ${{ inputs.module_path }} + run: | + GOARCH=amd64 TARGETARCH=amd64 make build + mv bin/manager "bin/controller-${MODULE_NAME}-amd64" + chmod +x "bin/controller-${MODULE_NAME}-amd64" + + - name: Build ${{ inputs.module_name }} arm64 + working-directory: controllers/${{ inputs.module_path }} + env: + MODULE_NAME: ${{ inputs.module_name }} + MODULE_PATH: ${{ inputs.module_path }} + run: | + GOARCH=arm64 TARGETARCH=arm64 make build + mv bin/manager "bin/controller-${MODULE_NAME}-arm64" + chmod +x "bin/controller-${MODULE_NAME}-arm64" + + - name: Set image repo + id: set-outputs + env: + REPO_OWNER: ${{ github.repository_owner }} + MODULE_NAME: ${{ inputs.module_name }} + run: | + echo "GHCR_REPO=ghcr.io/${REPO_OWNER}/sealos-${MODULE_NAME}-controller" >> $GITHUB_ENV + echo "image-name=ghcr.io/${REPO_OWNER}/sealos-${MODULE_NAME}-controller" >> $GITHUB_OUTPUT + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE_NAME}-controller" >> $GITHUB_ENV + fi + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: ${{ inputs.push_image }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + uses: docker/login-action@v3 + if: ${{ inputs.push_image && env.ALIYUN_REGISTRY }} + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: build (and publish) ${{ inputs.module_name }} main image + uses: docker/build-push-action@v6 + with: + context: ./controllers/${{ inputs.module_path }} + file: ./controllers/${{ inputs.module_path }}/Dockerfile + # Push if it's a push event or if push_image is true + push: ${{ inputs.push_image }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + cluster-image-build: + if: ${{ inputs.push_image }} + needs: [image-build] + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set image repo + env: + REPO_OWNER: ${{ github.repository_owner }} + MODULE_NAME: ${{ inputs.module_name }} + MODULE_PATH: ${{ inputs.module_path }} + run: | + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "MODULE_PATH=${MODULE_PATH}" >> $GITHUB_ENV + echo "OLD_DOCKER_REPO=ghcr.io/labring/sealos-${MODULE_NAME}-controller" >> $GITHUB_ENV + echo "GHCR_DOCKER_REPO=ghcr.io/${REPO_OWNER}/sealos-${MODULE_NAME}-controller" >> $GITHUB_ENV + echo "GHCR_CLUSTER_REPO=ghcr.io/${REPO_OWNER}/sealos-cloud-${MODULE_NAME}-controller" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_CLUSTER_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-cloud-${MODULE_NAME}-controller" >> $GITHUB_ENV + fi + + - name: Docker meta for cluster image + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_CLUSTER_REPO }} + ${{ env.ALIYUN_CLUSTER_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Install sealos + run: | + sudo bash ./.github/scripts/install.sh + + - name: Build ${{ inputs.module_name }}-controller cluster image + working-directory: controllers/${{ inputs.module_path }}/deploy + env: + MODULE_NAME: ${{ env.MODULE_NAME }} + MODULE_PATH: ${{ env.MODULE_PATH }} + OLD_DOCKER_REPO: ${{ env.OLD_DOCKER_REPO }} + GHCR_DOCKER_REPO: ${{ env.GHCR_DOCKER_REPO }} + run: | + # Build cluster images for each tag (amd64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + echo "Updating deploy files to: ${NEW_DOCKER_IMAGE}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_PATH}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_PATH}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-controller/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-controller/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-amd64" + sudo sealos build -t "${TAG}-amd64" --platform linux/amd64 -f Kubefile + done + + # Build cluster images for each tag (arm64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_PATH}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_PATH}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-controller/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-controller/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-arm64" + sudo sealos build -t "${TAG}-arm64" --platform linux/arm64 -f Kubefile + done + + - name: Sealos login to ghcr.io + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + run: | + sudo sealos login -u "${REPOSITORY_OWNER}" -p "${GH_PAT}" --debug ghcr.io + + - name: Sealos login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + env: + ALIYUN_USERNAME: ${{ secrets.ALIYUN_USERNAME }} + ALIYUN_PASSWORD: ${{ secrets.ALIYUN_PASSWORD }} + run: | + sudo sealos login -u "$ALIYUN_USERNAME" -p "$ALIYUN_PASSWORD" --debug ${{ env.ALIYUN_REGISTRY }} + + - name: Manifest Cluster Images + run: | + sudo sealos images + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + echo "Creating manifest for ${TAG}" + bash scripts/manifest-cluster-images.sh "$TAG" + done diff --git a/.github/workflows/controllers.yml b/.github/workflows/controllers.yml new file mode 100644 index 000000000000..5cce9cfa2c4f --- /dev/null +++ b/.github/workflows/controllers.yml @@ -0,0 +1,116 @@ +name: Build Controllers + +on: + workflow_call: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + force_all: + description: "Force build all modules (for release)" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + push: + branches: ["*"] + paths: + - "controllers/**" + - ".github/workflows/controllers.yml" + - ".github/workflows/controller-build.yml" + - "scripts/detect-build-modules.sh" + - "!**/*.md" + - "!**/*.yaml" + pull_request: + branches: ["*"] + paths: + - "controllers/**" + - ".github/workflows/controllers.yml" + - ".github/workflows/controller-build.yml" + - "scripts/detect-build-modules.sh" + - "!**/*.md" + - "!**/*.yaml" + +env: + GO_VERSION: "1.24" + DEFAULT_OWNER: "labring" + LICENSE_KEY: ${{ secrets.LICENSE_KEY }} + +permissions: + contents: read + packages: write + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: controllers-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + shared-unit-test: + name: Test shared controller packages with race detector + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version-file: controllers/pkg/go.mod + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Tests with race detector + working-directory: controllers/pkg + run: go test -race ./... -count=1 -v + + detect-changes: + uses: ./.github/workflows/detect-changes.yml + with: + type: controllers + force_all: ${{ inputs.force_all || false }} + + build-controllers: + if: ${{ needs.detect-changes.outputs.modules != '[]' }} + needs: [detect-changes] + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.detect-changes.outputs.modules) }} + uses: ./.github/workflows/controller-build.yml + with: + module_name: ${{ matrix.module.name }} + module_path: ${{ matrix.module.path }} + push_image: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }} + push_image_tag: ${{ inputs.push_image_tag || 'latest' }} + disable_cilint: ${{ inputs.disable_cilint || false }} + secrets: inherit diff --git a/.github/workflows/delete_workflow.yml b/.github/workflows/delete_workflow.yml index 263c2f41aa05..38cab465e511 100644 --- a/.github/workflows/delete_workflow.yml +++ b/.github/workflows/delete_workflow.yml @@ -30,7 +30,7 @@ on: jobs: del_runs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Delete workflow runs uses: Mattraks/delete-workflow-runs@v2 diff --git a/.github/workflows/detect-changes.yml b/.github/workflows/detect-changes.yml new file mode 100644 index 000000000000..3d8e00a81b77 --- /dev/null +++ b/.github/workflows/detect-changes.yml @@ -0,0 +1,61 @@ +name: Detect Changes + +on: + workflow_call: + inputs: + type: + description: "Module type to detect (controllers or service)" + required: true + type: string + force_all: + description: "Force build all modules" + required: false + type: boolean + default: false + outputs: + modules: + description: "JSON array of modules to build" + value: ${{ jobs.detect.outputs.modules }} + +jobs: + detect: + runs-on: ubuntu-24.04 + outputs: + modules: ${{ steps.detect.outputs.modules }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed modules + id: detect + run: | + set -x + + # Determine base ref and force_all flag + FORCE_ALL="false" + BASE_REF="" + + if [[ "${{ inputs.force_all }}" == "true" ]] || [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + # Force all for release builds or manual dispatch + FORCE_ALL="true" + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + # PR: compare with PR base commit + BASE_REF="${{ github.event.pull_request.base.sha }}" + elif git cat-file -e "${{ github.event.before }}^{commit}" 2>/dev/null; then + # Push: compare with previous commit if it exists + BASE_REF="${{ github.event.before }}" + else + # New branch or force push: build all + FORCE_ALL="true" + fi + + echo "Base ref: $BASE_REF" + echo "Force all: $FORCE_ALL" + + # Run detection script + MODULES=$(bash scripts/detect-build-modules.sh "${{ inputs.type }}" "$BASE_REF" "$FORCE_ALL") + + echo "Detected modules: $MODULES" + echo "modules=$MODULES" >> $GITHUB_OUTPUT diff --git a/.github/workflows/e2e_execution_timeout.yml b/.github/workflows/e2e_execution_timeout.yml new file mode 100644 index 000000000000..eb11ef239af0 --- /dev/null +++ b/.github/workflows/e2e_execution_timeout.yml @@ -0,0 +1,115 @@ +name: E2E Execution Timeout Test + +on: + workflow_dispatch: + push: + branches: ["**"] + paths: + - ".github/workflows/e2e_execution_timeout.yml" + - "lifecycle/cmd/sealos/cmd/**" + - "lifecycle/pkg/ssh/**" + - "lifecycle/pkg/exec/**" + - "lifecycle/pkg/guest/**" + - "lifecycle/test/e2e/**" + pull_request: + branches: ["*"] + paths: + - ".github/workflows/e2e_execution_timeout.yml" + - "lifecycle/cmd/sealos/cmd/**" + - "lifecycle/pkg/ssh/**" + - "lifecycle/pkg/exec/**" + - "lifecycle/pkg/guest/**" + - "lifecycle/test/e2e/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: e2e-execution-timeout-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + call_ci_workflow: + uses: ./.github/workflows/import-patch-image.yml + with: + arch: amd64 + e2e: true + image: false + + e2e-execution-timeout-test: + needs: [call_ci_workflow] + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + unit: [ + E2E_sealos_execution_timeout_test, + ] + steps: + - name: Download image-cri-shim + uses: actions/download-artifact@v4 + with: + name: image-cri-shim-amd64 + path: /tmp/ + + - name: Download sealctl + uses: actions/download-artifact@v4 + with: + name: sealctl-amd64 + path: /tmp/ + + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/ + + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + + - name: Verify sealos + run: | + sudo chmod a+x /tmp/{sealos,image-cri-shim,sealctl} + sudo mv /tmp/sealos /usr/bin/ + sudo sealos version + + - name: Remove containerd & docker + uses: labring/sealos-action@v0.0.7 + with: + type: prune + + - name: Run execution timeout E2E tests with environment variables + env: + UNIT: ${{ matrix.unit }} + SEALOS_EXECUTION_TIMEOUT: 600s + SEALOS_MAX_RETRY: 10 + run: | + sudo apt-get remove docker docker-engine docker.io containerd runc + sudo apt-get purge docker-ce docker-ce-cli containerd.io + sudo apt-get remove -y moby-engine moby-cli moby-buildx moby-compose + sudo rm -rf /var/run/docker.sock + sudo rm -rf /run/containerd/containerd.sock + sudo chmod a+x /tmp/e2e.test + sudo /tmp/e2e.test --ginkgo.v --ginkgo.focus="$UNIT" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.unit }} + path: | + /tmp/*.log + /tmp/test-results/ + retention-days: 7 + + - name: Verify environment variable configuration + if: always() + run: | + echo "Verifying timeout configuration via environment variables..." + echo "Testing SEALOS_EXECUTION_TIMEOUT environment variable..." + SEALOS_EXECUTION_TIMEOUT=600s SEALOS_MAX_RETRY=10 sudo sealos env | grep -i "EXECUTION_TIMEOUT" && echo "✓ EXECUTION_TIMEOUT env var is configured" || echo "✗ EXECUTION_TIMEOUT env var not found" + SEALOS_EXECUTION_TIMEOUT=600s SEALOS_MAX_RETRY=10 sudo sealos env | grep -i "MAX_RETRY" && echo "✓ MAX_RETRY env var is configured" || echo "✗ MAX_RETRY env var not found" + echo "" + echo "Environment variables are now used for timeout configuration instead of command-line flags." + echo "Set SEALOS_EXECUTION_TIMEOUT and SEALOS_MAX_RETRY to control execution behavior." diff --git a/.github/workflows/e2e_k3s_multi_node.yml b/.github/workflows/e2e_k3s_multi_node.yml new file mode 100644 index 000000000000..57b58c042a9d --- /dev/null +++ b/.github/workflows/e2e_k3s_multi_node.yml @@ -0,0 +1,100 @@ +name: E2E k3s Multi Node Test + +on: + workflow_dispatch: + pull_request: + types: [labeled] + +env: + TERRAFORM_VERSION: "1.5.7" + +jobs: + call_ci_workflow: + uses: ./.github/workflows/import-patch-image.yml + if: ${{ github.repository_owner == 'labring' && github.event.action == 'labeled' && github.event.label.name == 'need-e2e-apply-test' }} + with: + arch: amd64,arm64 + e2e: true + image: true + target_pull: true + e2e_apply_test: + if: github.repository_owner == 'labring' + needs: [call_ci_workflow] + runs-on: ubuntu-24.04 + permissions: + issues: write + strategy: + fail-fast: false + matrix: + arch: [arm64, amd64] + outputs: + test_arm64_result: ${{ steps.apply_test.outputs.test_arm64_result }} + test_amd64_result: ${{ steps.apply_test.outputs.test_amd64_result }} + steps: + - name: Download terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + - name: Download sealos binary + uses: actions/download-artifact@v4 + with: + name: sealos-${{ matrix.arch }} + path: /tmp/sealos/bin/ + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/verify/ + - name: Verify sealos + run: | + sudo chmod a+x /tmp/verify/sealos + sudo /tmp/verify/sealos version + - name: Download patch image tar + uses: actions/download-artifact@v4 + with: + name: patch-image-${{ matrix.arch }}.tar + path: /tmp/sealos/images/ + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + - name: Install ginkgo && Run e2e apply test + id: apply_test + shell: bash + env: + SEALOS_TEST_ARCH: ${{ matrix.arch }} + SEALOS_E2E_TEST_IMAGE_NAME: registry.cn-shanghai.aliyuncs.com/labring/k3s:v1.28.4 + SEALOS_E2E_TEST_PATCH_IMAGE_TAR: /tmp/sealos/images/patch-${{ matrix.arch }}.tar.gz + SEALOS_E2E_TEST_PATCH_IMAGE_NAME: ghcr.io/labring/sealos-patch:${{ env.GIT_COMMIT_SHORT_SHA }}-${{ matrix.arch }} + SEALOS_E2E_TEST_SEALOS_BIN_PATH: /tmp/sealos/bin/sealos + ALIYUN_ACCESS_KEY_ID: ${{ secrets.E2E_ALIYUN_ACCESS_KEY_ID }} + ALIYUN_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIYUN_ACCESS_KEY_SECRET }} + ALIYUN_RESOURCE_GROUP_ID: ${{ secrets.E2E_ALIYUN_RESOURCE_GROUP_ID }} + ALIYUN_REGION_ID: ${{ secrets.E2E_ALIYUN_REGION_ID }} + run: | + set -ex + sudo su + chmod a+x /tmp/e2e.test + gzip /tmp/sealos/images/patch-"${SEALOS_TEST_ARCH}".tar + /tmp/e2e.test --ginkgo.v --ginkgo.focus="E2E_sealos_multi_node_test" + echo "test_${SEALOS_TEST_ARCH}_result=success" >> "$GITHUB_OUTPUT" + issue_commit: + needs: [e2e_apply_test] + runs-on: ubuntu-24.04 + permissions: + issues: write + if: ${{ always() && github.repository_owner == 'labring' && github.event.label.name == 'need-e2e-apply-test' }} #success() || failure() + steps: + - name: Add comment to PR with test status + uses: peter-evans/create-or-update-comment@v1 + env: + ARM_TEST_RESULT: ${{ needs.e2e_apply_test.outputs.test_arm64_result == 'success' && 'Success 🎉🎉!' || '‼️ Failure' }} + AMD_TEST_RESULT: ${{ needs.e2e_apply_test.outputs.test_amd64_result == 'success' && 'Success 🎉🎉!' || '‼️ Failure' }} + with: + issue-number: ${{ github.event.number }} + comment-author: "github-actions[bot]" + repository: ${{ github.repository }} + body: | + 🤖 says: E2E k3s multi node `ARM64` Test Result: ${{ env.ARM_TEST_RESULT }} , E2E k3s multi node `AMD64` Apply Test Result: ${{ env.AMD_TEST_RESULT }}
See: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/e2e_k8s_multi_node.yml b/.github/workflows/e2e_k8s_multi_node.yml new file mode 100644 index 000000000000..84d70832e557 --- /dev/null +++ b/.github/workflows/e2e_k8s_multi_node.yml @@ -0,0 +1,100 @@ +name: E2E k8s Multi Node Test + +on: + workflow_dispatch: + pull_request: + types: [labeled] + +env: + TERRAFORM_VERSION: "1.5.7" + +jobs: + call_ci_workflow: + uses: ./.github/workflows/import-patch-image.yml + if: ${{ github.repository_owner == 'labring' && github.event.action == 'labeled' && github.event.label.name == 'need-e2e-apply-test' }} + with: + arch: amd64,arm64 + e2e: true + image: true + target_pull: true + e2e_apply_test: + if: github.repository_owner == 'labring' + needs: [call_ci_workflow] + runs-on: ubuntu-24.04 + permissions: + issues: write + strategy: + fail-fast: false + matrix: + arch: [arm64, amd64] + outputs: + test_arm64_result: ${{ steps.apply_test.outputs.test_arm64_result }} + test_amd64_result: ${{ steps.apply_test.outputs.test_amd64_result }} + steps: + - name: Download terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + - name: Download sealos binary + uses: actions/download-artifact@v4 + with: + name: sealos-${{ matrix.arch }} + path: /tmp/sealos/bin/ + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/verify/ + - name: Verify sealos + run: | + sudo chmod a+x /tmp/verify/sealos + sudo /tmp/verify/sealos version + - name: Download patch image tar + uses: actions/download-artifact@v4 + with: + name: patch-image-${{ matrix.arch }}.tar + path: /tmp/sealos/images/ + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + - name: Install ginkgo && Run e2e apply test + id: apply_test + shell: bash + env: + SEALOS_TEST_ARCH: ${{ matrix.arch }} + SEALOS_E2E_TEST_IMAGE_NAME: registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.28.4 + SEALOS_E2E_TEST_PATCH_IMAGE_TAR: /tmp/sealos/images/patch-${{ matrix.arch }}.tar.gz + SEALOS_E2E_TEST_PATCH_IMAGE_NAME: ghcr.io/labring/sealos-patch:${{ env.GIT_COMMIT_SHORT_SHA }}-${{ matrix.arch }} + SEALOS_E2E_TEST_SEALOS_BIN_PATH: /tmp/sealos/bin/sealos + ALIYUN_ACCESS_KEY_ID: ${{ secrets.E2E_ALIYUN_ACCESS_KEY_ID }} + ALIYUN_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIYUN_ACCESS_KEY_SECRET }} + ALIYUN_RESOURCE_GROUP_ID: ${{ secrets.E2E_ALIYUN_RESOURCE_GROUP_ID }} + ALIYUN_REGION_ID: ${{ secrets.E2E_ALIYUN_REGION_ID }} + run: | + set -ex + sudo su + chmod a+x /tmp/e2e.test + gzip /tmp/sealos/images/patch-"${SEALOS_TEST_ARCH}".tar + /tmp/e2e.test --ginkgo.v --ginkgo.focus="E2E_sealos_multi_node_test" + echo "test_${SEALOS_TEST_ARCH}_result=success" >> "$GITHUB_OUTPUT" + issue_commit: + needs: [e2e_apply_test] + runs-on: ubuntu-24.04 + permissions: + issues: write + if: ${{ always() && github.repository_owner == 'labring' && github.event.label.name == 'need-e2e-apply-test' }} #success() || failure() + steps: + - name: Add comment to PR with test status + uses: peter-evans/create-or-update-comment@v1 + env: + ARM_TEST_RESULT: ${{ needs.e2e_apply_test.outputs.test_arm64_result == 'success' && 'Success 🎉🎉!' || '‼️ Failure' }} + AMD_TEST_RESULT: ${{ needs.e2e_apply_test.outputs.test_amd64_result == 'success' && 'Success 🎉🎉!' || '‼️ Failure' }} + with: + issue-number: ${{ github.event.number }} + comment-author: "github-actions[bot]" + repository: ${{ github.repository }} + body: | + 🤖 says: E2E k8s multi node `ARM64` Test Result: ${{ env.ARM_TEST_RESULT }} , E2E k8s multi node `AMD64` Test Result: ${{ env.AMD_TEST_RESULT }}
See: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/e2e_test_core.yml b/.github/workflows/e2e_test_core.yml new file mode 100644 index 000000000000..32bf922fdc60 --- /dev/null +++ b/.github/workflows/e2e_test_core.yml @@ -0,0 +1,144 @@ +name: E2E Sealos Core Test + +on: + workflow_dispatch: + push: + branches: ["**"] + paths: + - ".github/workflows/e2e_test_core.yml" + - "lifecycle/cmd/**" + - "lifecycle/pkg/**" + - "lifecycle/test/**" + pull_request: + branches: ["*"] + paths: + - ".github/workflows/e2e_test_core.yml" + - "lifecycle/cmd/**" + - "lifecycle/pkg/**" + - "lifecycle/test/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: e2e-test-core-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + resolve-changes: + runs-on: ubuntu-24.04 + outputs: + run_e2e: ${{ steps.set.outputs.run_e2e }} + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + - name: Detect non-test lifecycle changes + id: set + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_REF="${{ github.event.pull_request.base.sha }}" + else + BASE_REF="${{ github.event.before }}" + fi + + if bash ./scripts/has-non-test-changes.sh ./lifecycle "${BASE_REF}" "${{ github.sha }}"; then + echo "run_e2e=true" >> "$GITHUB_OUTPUT" + else + echo "run_e2e=false" >> "$GITHUB_OUTPUT" + fi + + call_ci_workflow: + needs: [resolve-changes] + if: ${{ needs.resolve-changes.outputs.run_e2e == 'true' }} + uses: ./.github/workflows/import-patch-image.yml + with: + arch: amd64 + e2e: true + image: false + e2e-core-test: + needs: [resolve-changes, call_ci_workflow] + if: ${{ needs.resolve-changes.outputs.run_e2e == 'true' }} + strategy: + fail-fast: false + matrix: + unit: [ + E2E_sealos_images_test, + E2E_sealos_images_buildrun_test, + E2E_sealos_images_buildrun_feature_test, + E2E_sealos_cert_test, + E2E_sealos_run_test, + E2E_sealos_run_docker_test, + E2E_sealos_run_other_test, + E2E_sealos_inspect_test, + E2E_sealos_apply_test, + E2E_sealos_apply_docker_test, + E2E_sealos_apply_other_test, + E2E_sealos_filesystem_test, + E2E_sealos_run_patchimage_test, + E2E_sealos_execution_timeout_test, + E2E_sealos_runtime_version_122_test, + E2E_sealos_runtime_version_123_test, + E2E_sealos_runtime_version_124_test, + E2E_sealos_runtime_version_125_test, + E2E_sealos_runtime_version_126_test, + E2E_sealos_runtime_version_127_test, + E2E_sealos_runtime_version_128_test, + E2E_sealos_runtime_version_129_test, + E2E_sealos_runtime_version_130_test, + E2E_sealos_runtime_version_131_test, + E2E_sealos_runtime_version_132_test, + E2E_sealos_runtime_version_docker_122_test, + E2E_sealos_runtime_version_docker_123_test, + E2E_sealos_runtime_version_docker_124_test, + E2E_sealos_runtime_version_docker_125_test, + E2E_sealos_runtime_version_docker_126_test, + E2E_sealos_runtime_version_docker_127_test, + E2E_sealos_runtime_version_docker_128_test + E2E_sealos_runtime_version_docker_129_test, + E2E_sealos_runtime_version_docker_130_test, + E2E_sealos_runtime_version_docker_131_test, + E2E_sealos_runtime_version_docker_132_test, + ] + runs-on: ubuntu-24.04 + steps: + - name: Download image-cri-shim + uses: actions/download-artifact@v4 + with: + name: image-cri-shim-amd64 + path: /tmp/ + - name: Download sealctl + uses: actions/download-artifact@v4 + with: + name: sealctl-amd64 + path: /tmp/ + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/ + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + - name: Verify sealos + run: | + sudo chmod a+x /tmp/{sealos,image-cri-shim,sealctl} + sudo mv /tmp/sealos /usr/bin/ + sudo sealos version + - name: Remove containerd && docker + uses: labring/sealos-action@v0.0.7 + with: + type: prune + + - name: Verify E2e test + env: + UNIT: ${{ matrix.unit }} + run: | + sudo apt-get remove docker docker-engine docker.io containerd runc + sudo apt-get purge docker-ce docker-ce-cli containerd.io # docker-compose-plugin + sudo apt-get remove -y moby-engine moby-cli moby-buildx moby-compose + sudo rm -rf /var/run/docker.sock + sudo rm -rf /run/containerd/containerd.sock + sudo chmod a+x /tmp/e2e.test + sudo /tmp/e2e.test --ginkgo.v --ginkgo.focus="$UNIT" diff --git a/.github/workflows/e2e_test_core_k3s.yml b/.github/workflows/e2e_test_core_k3s.yml new file mode 100644 index 000000000000..aa6fc44abf40 --- /dev/null +++ b/.github/workflows/e2e_test_core_k3s.yml @@ -0,0 +1,106 @@ +name: E2E Sealos Core Test for K3s + +on: + workflow_dispatch: + push: + branches: ["**"] + paths: + - ".github/workflows/e2e_test_core_k3s.yml" + - "lifecycle/cmd/**" + - "lifecycle/pkg/**" + - "lifecycle/test/**" + pull_request: + branches: ["*"] + paths: + - ".github/workflows/e2e_test_core_k3s.yml" + - "lifecycle/cmd/**" + - "lifecycle/pkg/**" + - "lifecycle/test/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: e2e-test-core-k3s-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + resolve-changes: + runs-on: ubuntu-24.04 + outputs: + run_e2e: ${{ steps.set.outputs.run_e2e }} + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + - name: Detect non-test lifecycle changes + id: set + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_REF="${{ github.event.pull_request.base.sha }}" + else + BASE_REF="${{ github.event.before }}" + fi + + if bash ./scripts/has-non-test-changes.sh ./lifecycle "${BASE_REF}" "${{ github.sha }}"; then + echo "run_e2e=true" >> "$GITHUB_OUTPUT" + else + echo "run_e2e=false" >> "$GITHUB_OUTPUT" + fi + + call_ci_workflow: + needs: [resolve-changes] + if: ${{ needs.resolve-changes.outputs.run_e2e == 'true' }} + uses: ./.github/workflows/import-patch-image.yml + with: + arch: amd64 + e2e: true + image: false + e2e-core-test: + needs: [resolve-changes, call_ci_workflow] + if: ${{ needs.resolve-changes.outputs.run_e2e == 'true' }} + strategy: + fail-fast: false + matrix: + unit: [E2E_sealos_k3s_basic_test] + runs-on: ubuntu-24.04 + steps: + - name: Download image-cri-shim + uses: actions/download-artifact@v4 + with: + name: image-cri-shim-amd64 + path: /tmp/ + - name: Download sealctl + uses: actions/download-artifact@v4 + with: + name: sealctl-amd64 + path: /tmp/ + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/ + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + - name: Verify sealos + run: | + sudo chmod a+x /tmp/{sealos,image-cri-shim,sealctl} + sudo mv /tmp/sealos /usr/bin/ + sudo sealos version + - name: Remove containerd && docker + uses: labring/sealos-action@v0.0.7 + with: + type: prune + - name: Verify E2e test + env: + UNIT: ${{ matrix.unit }} + run: | + sudo apt-get remove docker docker-engine docker.io containerd runc + sudo apt-get purge docker-ce docker-ce-cli containerd.io # docker-compose-plugin + sudo apt-get remove -y moby-engine moby-cli moby-buildx moby-compose + sudo rm -rf /var/run/docker.sock + sudo rm -rf /run/containerd/containerd.sock + sudo chmod a+x /tmp/e2e.test + sudo /tmp/e2e.test --ginkgo.v --ginkgo.focus="$UNIT" diff --git a/.github/workflows/e2e_test_image_cri_shim.yml b/.github/workflows/e2e_test_image_cri_shim.yml new file mode 100644 index 000000000000..f549d5f972e6 --- /dev/null +++ b/.github/workflows/e2e_test_image_cri_shim.yml @@ -0,0 +1,73 @@ +name: E2E Test sealos image-cri-shim service + +on: + workflow_dispatch: + push: + branches: ["**"] + paths: + - ".github/workflows/e2e_test_image_cri_shim.yml" + - "lifecycle/cmd/**" + - "lifecycle/staging/src/github.com/image-cri-shim/**" + - "lifecycle/test/**" + pull_request: + branches: ["*"] + paths: + - ".github/workflows/e2e_test_image_cri_shim.yml" + - "lifecycle/cmd/**" + - "lifecycle/staging/src/github.com/image-cri-shim/**" + - "lifecycle/test/**" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: e2e-test-image-cri-shim-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + call_ci_workflow: + uses: ./.github/workflows/import-patch-image.yml + with: + arch: amd64 + e2e: true + image: false + verify-image_cri_shim: + needs: [call_ci_workflow] + runs-on: ubuntu-24.04 + steps: + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + - name: Download image-cri-shim + uses: actions/download-artifact@v4 + with: + name: image-cri-shim-amd64 + path: /tmp/ + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-amd64 + path: /tmp/ + - name: Verify sealos + run: | + sudo chmod a+x /tmp/{sealos,image-cri-shim} + sudo mv /tmp/sealos /usr/bin/ + sudo sealos version + - name: Download e2e test + uses: actions/download-artifact@v4 + with: + name: e2e.test + path: /tmp/ + - name: Remove containerd && docker + uses: labring/sealos-action@v0.0.7 + with: + type: prune + + - name: Run image-cri-shim test + shell: bash + working-directory: /tmp/ + run: | + sudo apt-get remove docker docker-engine docker.io containerd runc + sudo apt-get purge docker-ce docker-ce-cli containerd.io # docker-compose-plugin + sudo apt-get remove -y moby-engine moby-cli moby-buildx moby-compose + sudo rm -rf /var/run/docker.sock + sudo rm -rf /run/containerd/containerd.sock + sudo chmod a+x /tmp/e2e.test + sudo /tmp/e2e.test --ginkgo.v --ginkgo.focus="E2E_image-cri-shim_run_test" diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 000000000000..55a922906379 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,334 @@ +name: Build Frontend Image + +on: + workflow_call: + inputs: + module: + description: "Module" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + workflow_dispatch: + inputs: + module: + description: "Module (e.g., desktop, providers/dbprovider)" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + +env: + # Common versions + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }} + ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }} + +jobs: + image-build: + strategy: + matrix: + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract module name + id: module_name + env: + MODULE_INPUT: ${{ inputs.module }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + MODULE_NAME=$(basename "$MODULE_INPUT") + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + fi + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Hub + if: ${{ inputs.push_image }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + if: ${{ inputs.push_image && env.ALIYUN_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Build + id: build + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile + platforms: linux/${{ matrix.arch }} + build-args: | + name=${{ env.MODULE_NAME }} + path=${{ inputs.module }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,"name=${{ env.GHCR_REPO }}${{ env.ALIYUN_REPO && format(',{0}', env.ALIYUN_REPO) || '' }}",name-canonical=true,push-by-digest=${{ inputs.push_image }},push=${{ inputs.push_image }} + + - name: Export digest + env: + TEMP_DIR: ${{ runner.temp }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p "${TEMP_DIR}/digests" + touch "${TEMP_DIR}/digests/${DIGEST#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.MODULE_NAME }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + image-release: + name: Push Docker Images + needs: image-build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + if: ${{ inputs.push_image }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Login to Github Container Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Extract module name + id: module_name + env: + MODULE_INPUT: ${{ inputs.module }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + MODULE_NAME=$(basename "$MODULE_INPUT") + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + fi + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-${{ env.MODULE_NAME }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + env: + TEMP_DIR: ${{ runner.temp }} + GHCR_REPO: ${{ env.GHCR_REPO }} + run: | + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + docker buildx imagetools create -t $TAG \ + $(printf "${GHCR_REPO}@sha256:%s " *) + sleep 5 + done + + - name: Inspect image + env: + GHCR_REPO: ${{ env.GHCR_REPO }} + IMAGE_VERSION: ${{ steps.meta.outputs.version }} + run: | + docker buildx imagetools inspect "${GHCR_REPO}:${IMAGE_VERSION}" + + cluster-image-build: + needs: + - image-release + if: ${{ inputs.push_image }} + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set image repo + env: + MODULE_INPUT: ${{ inputs.module }} + REPO_OWNER: ${{ github.repository_owner }} + run: | + MODULE_NAME=$(basename "$MODULE_INPUT") + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "OLD_DOCKER_REPO=ghcr.io/labring/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + # Docker image repo (always use GHCR for values.yaml to avoid Aliyun bandwidth costs) + echo "GHCR_DOCKER_REPO=ghcr.io/${REPO_OWNER}/sealos-${MODULE_NAME}-frontend" >> $GITHUB_ENV + # Cluster image repos + echo "GHCR_CLUSTER_REPO=ghcr.io/${REPO_OWNER}/sealos-cloud-${MODULE_NAME}-frontend" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_CLUSTER_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-cloud-${MODULE_NAME}-frontend" >> $GITHUB_ENV + fi + + - name: Docker meta for cluster image + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_CLUSTER_REPO }} + ${{ env.ALIYUN_CLUSTER_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Install sealos + run: | + sudo bash ./.github/scripts/install.sh + + - name: Build ${{ env.MODULE_NAME }}-frontend cluster image + working-directory: frontend/${{ inputs.module }}/deploy + env: + MODULE_NAME: ${{ env.MODULE_NAME }} + OLD_DOCKER_REPO: ${{ env.OLD_DOCKER_REPO }} + GHCR_DOCKER_REPO: ${{ env.GHCR_DOCKER_REPO }} + run: | + # Build cluster images for each tag (amd64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + # Always use GHCR docker image to avoid Aliyun bandwidth costs + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + echo "Updating deploy files to: ${NEW_DOCKER_IMAGE}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-frontend/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-frontend/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-amd64" + sudo sealos build -t "${TAG}-amd64" --platform linux/amd64 -f Kubefile + done + + # Build cluster images for each tag (arm64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-frontend/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-frontend/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-arm64" + sudo sealos build -t "${TAG}-arm64" --platform linux/arm64 -f Kubefile + done + + - name: Sealos login to ghcr.io + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + run: | + sudo sealos login -u "$REPOSITORY_OWNER" -p "$GH_PAT" --debug ghcr.io + + - name: Sealos login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + env: + ALIYUN_USERNAME: ${{ secrets.ALIYUN_USERNAME }} + ALIYUN_PASSWORD: ${{ secrets.ALIYUN_PASSWORD }} + run: | + sudo sealos login -u "$ALIYUN_USERNAME" -p "$ALIYUN_PASSWORD" --debug ${{ env.ALIYUN_REGISTRY }} + + - name: Manifest Cluster Images + run: | + sudo sealos images + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + echo "Creating manifest for ${TAG}" + bash scripts/manifest-cluster-images.sh "$TAG" + done diff --git a/.github/workflows/frontends.yml b/.github/workflows/frontends.yml new file mode 100644 index 000000000000..e578cb0336ca --- /dev/null +++ b/.github/workflows/frontends.yml @@ -0,0 +1,160 @@ +name: Build Frontends Image + +on: + workflow_call: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + force_all: + description: "Force build all modules (for release)" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + push: + branches: ["*"] + paths: + - "frontend/desktop/**" + - "frontend/providers/**" + - "frontend/packages/**" + - "frontend/package.json" + - "frontend/pnpm-lock.yaml" + - "frontend/pnpm-workspace.yaml" + - "frontend/tsconfig*.json" + - ".github/workflows/frontend.yml" + - ".github/workflows/frontends.yml" + - "!**/*.md" + pull_request: + branches: ["*"] + paths: + - "frontend/desktop/**" + - "frontend/providers/**" + - "frontend/packages/**" + - "frontend/package.json" + - "frontend/pnpm-lock.yaml" + - "frontend/pnpm-workspace.yaml" + - "frontend/tsconfig*.json" + - ".github/workflows/frontend.yml" + - ".github/workflows/frontends.yml" + - "!**/*.md" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: frontends-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + modules: ${{ steps.set-matrix.outputs.modules }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + packages: + - 'frontend/packages/**' + root-config: + - 'frontend/package.json' + - 'frontend/pnpm-lock.yaml' + - 'frontend/pnpm-workspace.yaml' + - 'frontend/tsconfig*.json' + - '.github/workflows/frontend.yml' + - '.github/workflows/frontends.yml' + license: + - 'frontend/providers/license/**' + template: + - 'frontend/providers/template/**' + applaunchpad: + - 'frontend/providers/applaunchpad/**' + dbprovider: + - 'frontend/providers/dbprovider/**' + costcenter: + - 'frontend/providers/costcenter/**' + desktop: + - 'frontend/desktop/**' + - name: Generate build matrix + id: set-matrix + env: + PACKAGES_CHANGED: ${{ steps.filter.outputs.packages }} + ROOT_CONFIG_CHANGED: ${{ steps.filter.outputs.root-config }} + FORCE_ALL: ${{ inputs.force_all }} + EVENT_NAME: ${{ github.event_name }} + LICENSE_CHANGED: ${{ steps.filter.outputs.license }} + TEMPLATE_CHANGED: ${{ steps.filter.outputs.template }} + APPLAUNCHPAD_CHANGED: ${{ steps.filter.outputs.applaunchpad }} + DBPROVIDER_CHANGED: ${{ steps.filter.outputs.dbprovider }} + COSTCENTER_CHANGED: ${{ steps.filter.outputs.costcenter }} + DESKTOP_CHANGED: ${{ steps.filter.outputs.desktop }} + run: | + modules=() + + # Check if we need to build all modules + if [[ "$PACKAGES_CHANGED" == "true" || \ + "$ROOT_CONFIG_CHANGED" == "true" || \ + "$FORCE_ALL" == "true" || \ + "$EVENT_NAME" == "workflow_dispatch" ]]; then + modules=( + "providers/license" + "providers/template" + "providers/applaunchpad" + "providers/dbprovider" + "providers/costcenter" + "desktop" + ) + else + # Only build changed modules + [[ "$LICENSE_CHANGED" == "true" ]] && modules+=("providers/license") + [[ "$TEMPLATE_CHANGED" == "true" ]] && modules+=("providers/template") + [[ "$APPLAUNCHPAD_CHANGED" == "true" ]] && modules+=("providers/applaunchpad") + [[ "$DBPROVIDER_CHANGED" == "true" ]] && modules+=("providers/dbprovider") + [[ "$COSTCENTER_CHANGED" == "true" ]] && modules+=("providers/costcenter") + [[ "$DESKTOP_CHANGED" == "true" ]] && modules+=("desktop") + fi + + # Convert to JSON array (compact format for GitHub Actions) + if [ ${#modules[@]} -eq 0 ]; then + json_modules="[]" + else + json_modules=$(printf '%s\n' "${modules[@]}" | jq -R . | jq -s -c .) + fi + echo "modules=$json_modules" >> "$GITHUB_OUTPUT" + echo "Building modules: $json_modules" + + image-build: + needs: detect-changes + if: needs.detect-changes.outputs.modules != '[]' + uses: ./.github/workflows/frontend.yml + permissions: + contents: read + packages: write + secrets: inherit + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.detect-changes.outputs.modules) }} + with: + module: ${{ matrix.module }} + push_image: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }} + push_image_tag: ${{ inputs.push_image_tag }} diff --git a/.github/workflows/import-patch-image.yml b/.github/workflows/import-patch-image.yml new file mode 100755 index 000000000000..65b1ca6afa65 --- /dev/null +++ b/.github/workflows/import-patch-image.yml @@ -0,0 +1,236 @@ +name: Import Patch Images Package + +env: + # Common versions + GO_VERSION: "1.25" + PROJECT_PATH: "./lifecycle" + +on: + workflow_call: + inputs: + e2e: + required: false + type: boolean + default: false + arch: + required: true + type: string + default: amd64 + image: + required: false + type: boolean + default: true + target_pull: + required: false + type: boolean + default: false + +jobs: + resolve-modules-arch: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout + if: ${{ (inputs.target_pull == false) }} + uses: actions/checkout@v4 + - name: Checkout by Pull Request Target + uses: actions/checkout@v4 + if: ${{ (inputs.target_pull == true) }} + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Resolve Modules + id: set-matrix + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + INPUT_ARCH: ${{ inputs.arch }} + run: bash "./${PROJECT_PATH}/scripts/resolve-patch-modules.sh" "${INPUT_ARCH}" "true" + resolve-modules: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout + if: ${{ (inputs.target_pull == false) }} + uses: actions/checkout@v4 + - name: Checkout by Pull Request Target + uses: actions/checkout@v4 + if: ${{ (inputs.target_pull == true) }} + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Resolve Modules + id: set-matrix + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + INPUT_ARCH: ${{ inputs.arch }} + run: bash "./${PROJECT_PATH}/scripts/resolve-patch-modules.sh" "${INPUT_ARCH}" + build: + needs: [ resolve-modules ] + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + strategy: + matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} + steps: + - name: Checkout + if: ${{ (inputs.target_pull == false) }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Checkout by Pull Request Target + uses: actions/checkout@v4 + if: ${{ (inputs.target_pull == true) }} + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Install Dependencies + if: ${{ (matrix.binary == 'sealos' || matrix.binary == 'sealctl') && matrix.arch == 'arm64' }} + run: | + sudo apt update && sudo apt install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu + + - name: Build Binaries + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + MATRIX_BINARY: ${{ matrix.binary }} + MATRIX_ARCH: ${{ matrix.arch }} + run: | + cd "${PROJECT_PATH}" && make build BINS="${MATRIX_BINARY}" PLATFORM="linux_${MATRIX_ARCH}" + + - name: Save Binaries + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary }}-${{ matrix.arch }} + path: ${{ env.PROJECT_PATH }}/bin/linux_${{ matrix.arch}}/${{ matrix.binary }} + test: + runs-on: ubuntu-24.04 + if: ${{ (inputs.e2e == true) }} + steps: + - name: Checkout + if: ${{ (inputs.target_pull == false) }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Checkout by Pull Request Target + uses: actions/checkout@v4 + if: ${{ (inputs.target_pull == true) }} + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + - name: Build E2e Test + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + run: | + cd "${PROJECT_PATH}" && go install github.com/onsi/ginkgo/v2/ginkgo + cd test/e2e && ginkgo build . + - name: Save E2e Test + uses: actions/upload-artifact@v4 + with: + name: e2e.test + path: ${{ env.PROJECT_PATH }}/test/e2e/e2e.test + docker: + needs: [ resolve-modules-arch,build ] + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + if: ${{ (inputs.image == true) }} + services: + registry: + image: registry:2 + ports: + - 5000:5000 + strategy: + matrix: ${{ fromJson(needs.resolve-modules-arch.outputs.matrix) }} + steps: + - name: Checkout + if: ${{ (inputs.target_pull == false) }} + uses: actions/checkout@v4 + - name: Checkout by Pull Request Target + uses: actions/checkout@v4 + if: ${{ (inputs.target_pull == true) }} + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Expose git commit data + uses: rlespinasse/git-commit-data-action@v1 + + - name: Download lvscare + uses: actions/download-artifact@v4 + with: + name: lvscare-${{ matrix.arch }} + path: ${{ env.PROJECT_PATH }}/docker/lvscare + + - name: Download sealctl + uses: actions/download-artifact@v4 + with: + name: sealctl-${{ matrix.arch }} + path: ${{ env.PROJECT_PATH }}/docker/patch + + - name: Download image-cri-shim + uses: actions/download-artifact@v4 + with: + name: image-cri-shim-${{ matrix.arch }} + path: ${{ env.PROJECT_PATH }}/docker/patch + + - name: Download sealos + uses: actions/download-artifact@v4 + with: + name: sealos-${{ matrix.arch }} + path: ${{ env.PROJECT_PATH }}/docker/sealos + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Chmod a+x for docker + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + run: | + chmod a+x "${PROJECT_PATH}/docker/sealos"/* + chmod a+x "${PROJECT_PATH}/docker/patch"/* + chmod a+x "${PROJECT_PATH}/docker/lvscare"/* + + - name: Build and Push lvscare Image + uses: docker/build-push-action@v6 + with: + context: ${{ env.PROJECT_PATH }}/docker/lvscare + file: ${{ env.PROJECT_PATH }}/docker/lvscare/Dockerfile.main + push: true + build-args: Bin=lvscare + platforms: linux/${{ matrix.arch }} + tags: localhost:5000/${{ github.repository_owner }}/lvscare:${{ env.GIT_COMMIT_SHORT_SHA }}-${{ matrix.arch }} + + - name: Build and Save Cluster Images + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + ARCH: ${{ matrix.arch }} + OWNER: ${{ github.repository_owner }} + run: | + "${PROJECT_PATH}/docker/patch/save-cluster-images.sh" + + - name: Upload Cluster Images + uses: actions/upload-artifact@v4 + with: + name: patch-image-${{ matrix.arch }}.tar + path: patch-${{ matrix.arch }}.tar + + - name: Delete Artifacts + uses: geekyeggo/delete-artifact@v1 + with: + name: | + lvscare-${{ matrix.arch }} + sealctl-${{ matrix.arch }} + image-cri-shim-${{ matrix.arch }} + failOnError: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml old mode 100755 new mode 100644 index df17bb31639e..047ca9c3326c --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,74 +1,65 @@ name: Release +env: + # Common versions + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + on: + workflow_dispatch: push: - branches-ignore: - - "**" tags: - "*" jobs: - goreleaser: - runs-on: ubuntu-latest + releaser: + runs-on: ubuntu-22.04 + permissions: + contents: write + packages: write steps: - name: Checkout - uses: actions/checkout@master - - - name: Set up Go - uses: actions/setup-go@master + uses: actions/checkout@v4 with: - go-version: 1.16.x + fetch-depth: 0 - - name: Prepare - id: prepare - run: | - TAG=${GITHUB_REF#refs/tags/} - echo ::set-output name=tag_name::${TAG} + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} - - name: Note - id: Note - env: - VERSION: ${{ steps.prepare.outputs.tag_name }} - BUCKETNAME: ${{ secrets.BUCKETNAME }} - OSSENDPOINT: ${{ secrets.OSSENDPOINT }} - DOCKER_REPO: ${{ secrets.DOCKER_REPO }} + - name: Install Dependencies run: | - cd scripts/release && sh note.sh - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + sudo apt update && sudo apt install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libbtrfs-dev libgpgme-dev libdevmapper-dev \ + qemu-user-static binfmt-support - - name: Build in Docker - uses: docker/build-push-action@v2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 with: - context: ./ - file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} - push: false - load: true - tags: sealos:latest - build-args: | - ACTION=release - GITHUB_TOKEN=${{ secrets.GH_TOKEN }} - secrets: GITHUB_TOKEN=${{ secrets.GH_TOKEN }} - # https://github.com/docker/build-push-action + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} - - name: Copy Out Binaries - run: | - docker run -itd --name sealos --rm sealos:latest bash - docker cp sealos:/work/dist . - - - name: Build binaries to oss + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + args: release --clean --release-notes=../CHANGELOG/CHANGELOG-latest.md + workdir: ./lifecycle env: - BUCKETNAME: ${{ secrets.BUCKETNAME }} - run: | - echo ${{ secrets.OSS_CONFIG }} |base64 --decode >> .ossutilconfig - wget http://gosspublic.alicdn.com/ossutil/1.6.19/ossutil64 && chmod 755 ossutil64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealos-amd64_linux_amd64/sealos oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/${{ steps.prepare.outputs.tag_name }}/sealos-amd64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealos-arm64_linux_arm64/sealos oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/${{ steps.prepare.outputs.tag_name }}/sealos-arm64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealos-amd64_linux_amd64/sealos oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/latest/sealos-amd64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealos-arm64_linux_arm64/sealos oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/latest/sealos-arm64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_amd64/sealctl oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/${{ steps.prepare.outputs.tag_name }}/sealctl-amd64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_arm64/sealctl oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/${{ steps.prepare.outputs.tag_name }}/sealctl-arm64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_amd64/sealctl oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/latest/sealctl-amd64 - ./ossutil64 -c .ossutilconfig cp -f dist/sealctl_linux_arm64/sealctl oss://${BUCKETNAME:-sealyun-home}/sealos-4.0/latest/sealctl-arm64 + USERNAME: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FURY_TOKEN: ${{ secrets.FURY_TOKEN }} + + cloud-release: + uses: ./.github/workflows/cloud-release.yml + needs: + - releaser + permissions: + contents: write + packages: write + actions: write + secrets: inherit + with: + tag: ${{ github.ref_name }} + build_offline_tar_only: false \ No newline at end of file diff --git a/.github/workflows/service-build.yml b/.github/workflows/service-build.yml new file mode 100644 index 000000000000..60894027feea --- /dev/null +++ b/.github/workflows/service-build.yml @@ -0,0 +1,365 @@ +name: Build Service + +on: + workflow_call: + inputs: + module: + description: "Service module name (e.g., database, pay)" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + module: + description: "Service module name (e.g., database, pay, account)" + required: true + type: string + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + +env: + GO_VERSION: "1.25" + ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }} + ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }} + +jobs: + golangci-lint: + if: ${{ !inputs.disable_cilint && (github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run Linter + uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + working-directory: service/${{ inputs.module }} + args: --color=always --config=${{ github.workspace }}/.golangci.yml + + unit-test: + name: Test ${{ inputs.module }} with race detector + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run Tests with race detector + working-directory: service/${{ inputs.module }} + run: go test -race ./... -count=1 -v + + image-build: + strategy: + matrix: + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set image repo + id: set_repo + env: + MODULE: ${{ inputs.module }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE}-service" >> $GITHUB_ENV + fi + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Hub + if: ${{ inputs.push_image }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + if: ${{ inputs.push_image && env.ALIYUN_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Build + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./service/${{ inputs.module }}/Dockerfile + platforms: linux/${{ matrix.arch }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,"name=${{ env.GHCR_REPO }}${{ env.ALIYUN_REPO && format(',{0}', env.ALIYUN_REPO) || '' }}",name-canonical=true,push-by-digest=${{ inputs.push_image }},push=${{ inputs.push_image }} + + - name: Export digest + env: + TEMP_DIR: ${{ runner.temp }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p "${TEMP_DIR}/digests" + touch "${TEMP_DIR}/digests/${DIGEST#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ inputs.module }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + image-release: + name: Push Docker Images + needs: image-build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + if: ${{ inputs.push_image }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Login to Github Container Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Set image repo + id: set_repo + env: + MODULE: ${{ inputs.module }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + echo "GHCR_REPO=ghcr.io/${REPOSITORY_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE}-service" >> $GITHUB_ENV + fi + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-${{ inputs.module }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + env: + GHCR_REPO: ${{ env.GHCR_REPO }} + IMAGE_SOURCE: https://github.com/${{ github.repository }} + run: | + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + docker buildx imagetools create \ + --annotation "index:org.opencontainers.image.source=${IMAGE_SOURCE}" \ + -t $TAG \ + $(printf "${GHCR_REPO}@sha256:%s " *) + sleep 5 + done + + cluster-image-build: + needs: + - image-release + if: ${{ inputs.push_image }} + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set image repo + env: + REPO_OWNER: ${{ github.repository_owner }} + MODULE: ${{ inputs.module }} + run: | + echo "MODULE_NAME=${MODULE}" >> $GITHUB_ENV + echo "OLD_DOCKER_REPO=ghcr.io/labring/sealos-${MODULE}-service" >> $GITHUB_ENV + echo "GHCR_DOCKER_REPO=ghcr.io/${REPO_OWNER}/sealos-${MODULE}-service" >> $GITHUB_ENV + echo "GHCR_CLUSTER_REPO=ghcr.io/${REPO_OWNER}/sealos-cloud-${MODULE}-service" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_CLUSTER_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-cloud-${MODULE}-service" >> $GITHUB_ENV + fi + + - name: Docker meta for cluster image + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_CLUSTER_REPO }} + ${{ env.ALIYUN_CLUSTER_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Install sealos + run: | + sudo bash ./.github/scripts/install.sh + + - name: Build ${{ inputs.module }}-service cluster image + working-directory: service/${{ inputs.module }}/deploy + env: + MODULE_NAME: ${{ env.MODULE_NAME }} + OLD_DOCKER_REPO: ${{ env.OLD_DOCKER_REPO }} + GHCR_DOCKER_REPO: ${{ env.GHCR_DOCKER_REPO }} + run: | + # Build cluster images for each tag (amd64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + echo "Updating deploy files to: ${NEW_DOCKER_IMAGE}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-service/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-service/values.yaml + fi + if [[ -f charts/${MODULE_NAME}-monitor/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}-monitor/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-amd64" + sudo sealos build -t "${TAG}-amd64" --platform linux/amd64 -f Kubefile + done + + # Build cluster images for each tag (arm64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + if [[ -d manifests ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + fi + if [[ -f charts/${MODULE_NAME}/values.yaml ]]; then + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" charts/${MODULE_NAME}/values.yaml + fi + sudo rm -rf registry + echo "Building ${TAG}-arm64" + sudo sealos build -t "${TAG}-arm64" --platform linux/arm64 -f Kubefile + done + + - name: Sealos login to ghcr.io + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + run: | + sudo sealos login -u "$REPOSITORY_OWNER" -p "$GH_PAT" --debug ghcr.io + + - name: Sealos login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + env: + ALIYUN_USERNAME: ${{ secrets.ALIYUN_USERNAME }} + ALIYUN_PASSWORD: ${{ secrets.ALIYUN_PASSWORD }} + run: | + sudo sealos login -u "$ALIYUN_USERNAME" -p "$ALIYUN_PASSWORD" --debug ${{ env.ALIYUN_REGISTRY }} + + - name: Manifest Cluster Images + run: | + sudo sealos images + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + echo "Creating manifest for ${TAG}" + bash scripts/manifest-cluster-images.sh "$TAG" + done diff --git a/.github/workflows/services.yml b/.github/workflows/services.yml new file mode 100644 index 000000000000..9f60ff07779b --- /dev/null +++ b/.github/workflows/services.yml @@ -0,0 +1,113 @@ +name: Build Services + +on: + workflow_call: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + force_all: + description: "Force build all modules (for release)" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + push: + branches: ["*"] + paths: + - "service/**" + - "controllers/**" + - ".github/workflows/services.yml" + - ".github/workflows/service-build.yml" + - "scripts/detect-build-modules.sh" + - "!**/*.md" + - "!**/*.yaml" + pull_request: + branches: ["*"] + paths: + - "service/**" + - "controllers/**" + - ".github/workflows/services.yml" + - ".github/workflows/service-build.yml" + - "scripts/detect-build-modules.sh" + - "!**/*.md" + - "!**/*.yaml" + +env: + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + +permissions: + contents: read + packages: write + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: services-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + shared-unit-test: + name: Test shared service packages with race detector + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run Tests with race detector + working-directory: service + run: go test -race ./... -count=1 -v + + detect-changes: + uses: ./.github/workflows/detect-changes.yml + with: + type: service + force_all: ${{ inputs.force_all || false }} + + build-services: + if: ${{ needs.detect-changes.outputs.modules != '[]' }} + needs: [detect-changes] + strategy: + fail-fast: false + matrix: + module: ${{ fromJSON(needs.detect-changes.outputs.modules) }} + uses: ./.github/workflows/service-build.yml + with: + module: ${{ matrix.module.name }} + push_image: ${{ (github.event_name == 'push') || (github.event_name == 'create') || (inputs.push_image == true) }} + push_image_tag: ${{ inputs.push_image_tag || 'latest' }} + disable_cilint: ${{ inputs.disable_cilint || false }} + secrets: inherit diff --git a/.github/workflows/sync_code.yml b/.github/workflows/sync_code.yml new file mode 100644 index 000000000000..dabeb31b6c37 --- /dev/null +++ b/.github/workflows/sync_code.yml @@ -0,0 +1,33 @@ +name: Sync Code +on: + push: + branches: + - main + paths: + - lifecycle/staging/src/github.com/labring/** + - LICENSE.md + - .github/workflows/sync_code.yml + - .github/sync_code.yml + workflow_dispatch: +jobs: + sync: + # Only run this workflow when the repository owner is `labring`. + # This prevents the sync action from running in forks or mirrors owned by others. + if: github.repository_owner == 'labring' + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run GitHub File Sync + # Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved + uses: BetaHuhn/repo-file-sync-action@v1 + with: + GH_PAT: ${{ secrets.SYNC_CODE_PAT }} + CONFIG_PATH: .github/sync_code.yml + ORIGINAL_MESSAGE: true + SKIP_PR: true + COMMIT_EACH_FILE: false + COMMIT_BODY: "Change-type: patch" + GIT_EMAIL: "sealos-ci-robot@sealos.io" + GIT_USERNAME: "sealos-ci-robot" diff --git a/.github/workflows/tagpr.yml b/.github/workflows/tagpr.yml new file mode 100644 index 000000000000..421d2b8ae7f4 --- /dev/null +++ b/.github/workflows/tagpr.yml @@ -0,0 +1,111 @@ +name: 🚀 Tagpr for GitHub Actions +on: + workflow_dispatch: + inputs: + pre-version: + description: 'Pre Version to release. Has v prefix' + required: true + default: 'v5.0.1' + version: + description: 'Version to release. Has v prefix' + required: true + default: 'v5.1.0' + push: + branches: [ "**" ] + paths: + - "CHANGELOG/CHANGELOG.md" +jobs: + tagpr: + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + actions: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: | + if [[ -z "$VERSION" || -z "$PRE_VERSION" ]]; then + echo "Error: VERSION and PRE_VERSION inputs are required." + exit 1 + fi + cat << EOF > .tagpr.json + { + "name": "sealos", + "version": "${VERSION}", + "command": "bash scripts/changelog.sh ${VERSION} ${PRE_VERSION}", + "pre-version": "${PRE_VERSION}" + } + EOF + env: + VERSION: "${{ inputs.version }}" + PRE_VERSION: "${{ inputs.pre-version }}" + if: ${{ github.event_name == 'workflow_dispatch' }} + name: Add and parse .tagpr.json + - run: | + # check if .tagpr.json exists + if [ ! -f .tagpr.json ]; then + echo "No .tagpr.json file found." + exit 1 + else + echo ".tagpr.json file already exists." + fi + version=$(cat .tagpr.json | jq -r '.version') + command=$(cat .tagpr.json | jq -r '.command') + name=$(cat .tagpr.json | jq -r '.name') + echo name=${name} >> $GITHUB_OUTPUT + echo version=${version} >> $GITHUB_OUTPUT + echo command=${command} >> $GITHUB_OUTPUT + id: tagpr + env: + VERSION: "${{ inputs.version }}" + PRE_VERSION: "${{ inputs.pre-version }}" + name: Print git-chglog tag + - name: Use command to generate version + if: ${{ (github.event_name == 'workflow_dispatch') && (steps.tagpr.outputs.version != '') }} + run: | + echo "Running command: ${{ steps.tagpr.outputs.command }}" + eval "${{ steps.tagpr.outputs.command }}" + - uses: peter-evans/create-pull-request@v5 + if: ${{ (github.event_name == 'workflow_dispatch') && (steps.tagpr.outputs.version != '') }} + with: + title: 'Release ${{ steps.tagpr.outputs.name }} note and fix version for ${{ steps.tagpr.outputs.version }}' + body: | + + Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action + commit-message: | + 🤖 Release ${{ steps.tagpr.outputs.name }} for ${{ steps.tagpr.outputs.name }} using github-actions. + branch: tagpr-${{ steps.tagpr.outputs.version }} + draft: true + signoff: true + labels: | + bot + delete-branch: true + token: ${{ secrets.GITHUB_TOKEN }} + committer: github-actions[bot] + author: github-actions[bot] + - name: Auto tag and push changes + if: ${{ (github.event_name == 'push') && (steps.tagpr.outputs.version != '') }} + run: | + echo "Running new version: ${{ steps.tagpr.outputs.version }}" + branch=$(git rev-parse --abbrev-ref HEAD) + echo "Current branch: $branch" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git pull + git fetch --tags + git tag ${{ steps.tagpr.outputs.version }} + git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + git push -f origin ${{ steps.tagpr.outputs.version }} + - name: Trigger Release Workflow(only when tagged) + uses: actions/github-script@v6 + if: ${{ (github.event_name == 'push') && (steps.tagpr.outputs.version != '') }} + with: + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'release.yml', + ref: "refs/tags/${{ steps.tagpr.outputs.version }}", + }) \ No newline at end of file diff --git a/.github/workflows/webhooks.yml b/.github/workflows/webhooks.yml new file mode 100644 index 000000000000..80081c9b4774 --- /dev/null +++ b/.github/workflows/webhooks.yml @@ -0,0 +1,340 @@ +name: Build Webhooks image + +on: + workflow_call: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + workflow_dispatch: + inputs: + push_image: + description: "Push image" + required: false + type: boolean + default: false + push_image_tag: + description: "Push image tag" + default: "latest" + required: false + type: string + disable_cilint: + description: "Disable golangci-lint" + default: false + required: false + type: boolean + push: + branches: ["*"] + paths: + - "webhooks/**" + - ".github/workflows/webhooks.yml" + - "!**/*.md" + - "!**/*.yaml" + + pull_request: + branches: ["*"] + paths: + - "webhooks/**" + - ".github/workflows/webhooks.yml" + - "!**/*.md" + - "!**/*.yaml" + +# Avoid using ${{ github.workflow }} - when called via workflow_call, it inherits the caller's name causing conflicts +concurrency: + group: webhooks-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + # Common versions + GO_VERSION: "1.25" + DEFAULT_OWNER: "labring" + ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }} + ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }} + +jobs: + resolve-modules: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve Modules + id: set-matrix + run: bash ./scripts/resolve-modules.sh ./webhooks + + golangci-lint: + if: ${{ !inputs.disable_cilint && (github.event_name == 'push' || github.event_name == 'pull_request') }} + needs: [resolve-modules] + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: | + sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Linter + uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + working-directory: ${{ matrix.workdir }} + # args between =, not space + args: --color=always --config=${{ github.workspace }}/.golangci.yml + + unit-test: + name: Test ${{ matrix.workdir }} with race detector + needs: [resolve-modules] + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Dependencies + run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + + - name: Run Tests with race detector + working-directory: ${{ matrix.workdir }} + run: make test + + image-build: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + strategy: + matrix: + include: + - module: admission + binary: manager + ghcr_repository: sealos-admission-webhook + - module: stargz + binary: stargz-webhook + ghcr_repository: sealos/stargz-webhook + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Golang with cache + uses: magnetikonline/action-golang-cache@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build ${{ matrix.module }} amd64 + working-directory: webhooks/${{ matrix.module }} + env: + MODULE: ${{ matrix.module }} + BINARY: ${{ matrix.binary }} + run: | + GOARCH=amd64 make build + mv "bin/${BINARY}" "bin/webhook-${MODULE}-amd64" + chmod +x "bin/webhook-${MODULE}-amd64" + + - name: Build ${{ matrix.module }} arm64 + working-directory: webhooks/${{ matrix.module }} + env: + MODULE: ${{ matrix.module }} + BINARY: ${{ matrix.binary }} + run: | + GOARCH=arm64 make build + mv "bin/${BINARY}" "bin/webhook-${MODULE}-arm64" + chmod +x "bin/webhook-${MODULE}-arm64" + + - name: Set image repo + env: + REPO_OWNER: ${{ github.repository_owner }} + MODULE_NAME: ${{ matrix.module }} + GHCR_REPOSITORY: ${{ matrix.ghcr_repository }} + run: | + echo "GHCR_REPO=ghcr.io/${REPO_OWNER}/${GHCR_REPOSITORY}" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-${MODULE_NAME}-webhook" >> $GITHUB_ENV + fi + + - # Add support for more platforms with QEMU (optional) + # https://github.com/docker/setup-qemu-action + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Login to Aliyun Registry + uses: docker/login-action@v3 + if: ${{ ((github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true)) && env.ALIYUN_REGISTRY }} + with: + registry: ${{ env.ALIYUN_REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_REPO }} + ${{ env.ALIYUN_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: build (and publish) ${{ matrix.module }} main image + uses: docker/build-push-action@v6 + with: + context: ./webhooks/${{ matrix.module }} + file: ./webhooks/${{ matrix.module }}/Dockerfile + # Push if it's a push event or if push_image is true + push: ${{ (github.event_name == 'push')||(github.event_name == 'create') || (inputs.push_image == true) }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + build-cluster-image: + if: ${{ (github.event_name == 'push') ||(github.event_name == 'create') || (inputs.push_image == true) }} + needs: + - image-build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + strategy: + matrix: + module: [admission] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set image repo + env: + REPO_OWNER: ${{ github.repository_owner }} + MODULE_NAME: ${{ matrix.module }} + run: | + echo "MODULE_NAME=${MODULE_NAME}" >> $GITHUB_ENV + echo "OLD_DOCKER_REPO=ghcr.io/labring/sealos-${MODULE_NAME}-webhook" >> $GITHUB_ENV + # Docker image repo (always use GHCR for manifests to avoid Aliyun bandwidth costs) + echo "GHCR_DOCKER_REPO=ghcr.io/${REPO_OWNER}/sealos-${MODULE_NAME}-webhook" >> $GITHUB_ENV + # Cluster image repos + echo "GHCR_CLUSTER_REPO=ghcr.io/${REPO_OWNER}/sealos-cloud-${MODULE_NAME}-webhook" >> $GITHUB_ENV + if [[ -n "${{ env.ALIYUN_REPO_PREFIX }}" ]]; then + echo "ALIYUN_CLUSTER_REPO=${{ env.ALIYUN_REPO_PREFIX }}/sealos-cloud-${MODULE_NAME}-webhook" >> $GITHUB_ENV + fi + + - name: Docker meta for cluster image + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_CLUSTER_REPO }} + ${{ env.ALIYUN_CLUSTER_REPO }} + tags: | + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=${{ inputs.push_image_tag }},enable=${{ inputs.push_image_tag != '' && inputs.push_image_tag != 'latest' }} + type=ref,event=branch + type=ref,event=tag + type=sha + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 9 + + - name: Install sealos + run: | + sudo bash ./.github/scripts/install.sh + + - name: Build ${{ matrix.module }}-webhook cluster image + working-directory: webhooks/${{ matrix.module }}/deploy + env: + MODULE_NAME: ${{ env.MODULE_NAME }} + OLD_DOCKER_REPO: ${{ env.OLD_DOCKER_REPO }} + GHCR_DOCKER_REPO: ${{ env.GHCR_DOCKER_REPO }} + run: | + # Build cluster images for each tag (amd64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + # Always use GHCR docker image to avoid Aliyun bandwidth costs + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + echo "Updating manifests to: ${NEW_DOCKER_IMAGE}" + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + sudo rm -rf registry + echo "Building ${TAG}-amd64" + sudo sealos build -t "${TAG}-amd64" --platform linux/amd64 -f Kubefile + done + + # Build cluster images for each tag (arm64) + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + IMAGE_TAG="${TAG##*:}" + NEW_DOCKER_IMAGE="${GHCR_DOCKER_REPO}:${IMAGE_TAG}" + sed -i -E "s;(${OLD_DOCKER_REPO}|${GHCR_DOCKER_REPO}):[^[:space:]\"']*;${NEW_DOCKER_IMAGE};" manifests/* + sudo rm -rf registry + echo "Building ${TAG}-arm64" + sudo sealos build -t "${TAG}-arm64" --platform linux/arm64 -f Kubefile + done + + - name: Sealos login to ghcr.io + env: + REPOSITORY_OWNER: ${{ github.repository_owner }} + GH_PAT: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }} + run: | + sudo sealos login -u "${REPOSITORY_OWNER}" -p "${GH_PAT}" --debug ghcr.io + + - name: Sealos login to Aliyun Registry + if: ${{ env.ALIYUN_REGISTRY }} + env: + ALIYUN_USERNAME: ${{ secrets.ALIYUN_USERNAME }} + ALIYUN_PASSWORD: ${{ secrets.ALIYUN_PASSWORD }} + run: | + sudo sealos login -u "$ALIYUN_USERNAME" -p "$ALIYUN_PASSWORD" --debug ${{ env.ALIYUN_REGISTRY }} + + - name: Manifest Cluster Images + run: | + sudo sealos images + for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do + echo "Creating manifest for ${TAG}" + bash scripts/manifest-cluster-images.sh "$TAG" + done diff --git a/.gitignore b/.gitignore index abc40fd2e808..4a32515bbf97 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,22 @@ ossutil64 main.exe main dist -.vscode scripts/release/Note.md .ossutil_checkpoint bin .run -tools coverage.out +tools/* +tools/!*.go +tmp +**/.DS_Store +node_modules +vendor +pkg/registry/save/testdata/registry +.dummy.report.md +.vscode/ +/lifecycle/tools/ + +**.env* +!scripts/cloud/sealos.env +**/helloagents/** diff --git a/.golangci.yml b/.golangci.yml index 47c936c15038..418bb51b2e2b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,53 +1,155 @@ -run: - # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m +version: "2" - # include test files or not, default is true - tests: true +run: + go: "1.24" + relative-path-mode: gomod + modules-download-mode: readonly - # default is true. Enables skipping of directories: - # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - skip-dirs-use-default: true - modules-download-mode: mod +issues: + max-issues-per-linter: 0 + max-same-issues: 0 linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true + default: none enable: - - gofmt - - goimports - - revive - - stylecheck - - goconst - - gosimple - - misspell - - whitespace - - ineffassign - - vet - - typecheck - - deadcode + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck - errcheck + - errchkjson + - errname + - errorlint + - exptostd + - fatcontext + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives + - gocritic + - gocyclo + - goprintffuncname + - gosec - govet + - iface + - importas + - iotamixing + - inamedparam + - ineffassign + - intrange + - loggercheck + - mirror + - misspell + - modernize + - musttag + - nakedret + - noctx + - nolintlint + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - revive + - rowserrcheck + - sloglint + - spancheck + - sqlclosecheck - staticcheck - - structcheck - - unused - - varcheck - - nilerr - - ifshort + - thelper + - tparallel - unconvert + - unparam + - unused + - usestdlibvars + - usetesting + - wastedassign + - whitespace + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + settings: + revive: + rules: + - name: var-naming + arguments: + - [] + - [] + - [{ skipPackageNameChecks: true }] + copyloopvar: + check-alias: true + cyclop: + max-complexity: 15 + errcheck: + check-type-assertions: true + forbidigo: + forbid: + - pattern: ^print(ln)?$ + analyze-types: true + prealloc: + for-loops: true + staticcheck: + dot-import-whitelist: [] + http-status-code-whitelist: [] + modernize: + disable: + - forvar + - omitzero + perfsprint: + concat-loop: false + usestdlibvars: + time-date-month: true + time-month: true + time-layout: true + crypto-hash: true + default-rpc-path: true + sql-isolation-level: true + tls-signature-scheme: true + constant-kind: true + usetesting: + os-temp-dir: true + gocritic: + disabled-checks: + - exitAfterDefer + gocyclo: + min-complexity: 50 + gosec: + excludes: + - G404 + - G306 + - G101 + - G402 -linters-settings: - errcheck: - check-type-assertions: false - ignore: fmt:.*,io/ioutil:^Read.* - exclude-functions: - - io/ioutil.ReadFile - - io.Copy(*bytes.Buffer) - - io.Copy(os.Stdout) - -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - # use the fixed version to not introduce new linters unexpectedly - golangci-lint-version: 1.39.0 +formatters: + enable: + - gci + - gofmt + - gofumpt + - golines + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ + settings: + gofmt: + rewrite-rules: + - pattern: "interface{}" + replacement: "any" + - pattern: "a[b:len(a)]" + replacement: "a[b:]" + gofumpt: + extra-rules: true diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 4cf66903eb1b..000000000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,166 +0,0 @@ -project_name: sealos -#gomod: -# # Proxy a module from proxy.golang.org, making the builds verifiable. -# # This will only be effective if running against a tag. Snapshots will ignore this setting. -# # Notice: for this to work your `build.main` must be a package, not a `.go` file. -# # -# # Default is false. -# proxy: true -# -# # If proxy is true, use these environment variables when running `go mod` commands (namely, `go mod tidy`). -# # Defaults to `os.Environ()`. -# env: -# - GOPROXY=https://goproxy.cn - -before: - hooks: - # You may remove this if you don't use go modules. - #- go mod download - - go generate ./... -# - apt-get install -y upx -builds: - - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-gnu-gcc - - CC_FOR_TARGET=gcc-aarch64-linux-gnu - - PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ - main: ./cmd/sealos - id: sealos-arm64 - binary: sealos - hooks: - # pre: apt-get install -y libgpgme-dev:arm64 libdevmapper-dev:arm64 - post: upx "{{ .Path }}" - goos: - - linux - goarch: - - arm64 - tags: - - netgo - - exclude_graphdriver_devicemapper - - static - - osusergo - - exclude_graphdriver_btrfs - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -extldflags "-static -fpic" - - -s -w - - -linkmode external - - - env: - - CGO_ENABLED=1 - - CC=x86_64-linux-gnu-gcc - - CC_FOR_TARGET=gcc-x86_64-linux-gnu - - PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig/ - main: ./cmd/sealos - id: sealos-amd64 - hooks: - # pre: apt-get install -y libgpgme-dev libdevmapper-dev - post: upx "{{ .Path }}" - binary: sealos - goos: - - linux - goarch: - - amd64 - tags: - - netgo - - exclude_graphdriver_devicemapper - - static - - osusergo - - exclude_graphdriver_btrfs - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -extldflags "-static -fpic" - - -s -w - - -linkmode external - - - env: - - CGO_ENABLED=0 - main: ./cmd/sealctl - id: sealctl-amd64 - hooks: - post: upx "{{ .Path }}" - binary: sealctl - goos: - - linux - goarch: - - amd64 - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -s -w - - - env: - - CGO_ENABLED=0 - main: ./cmd/sealctl - id: sealctl-arm64 - hooks: - post: upx "{{ .Path }}" - binary: sealctl - goos: - - linux - goarch: - - arm64 - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -s -w - - - env: - - CGO_ENABLED=0 - main: ./cmd/lvscare - id: lvscare - hooks: - post: upx "{{ .Path }}" - binary: lvscare - goos: - - linux - goarch: - - arm64 - - amd64 - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -s -w - - - env: - - CGO_ENABLED=0 - main: ./cmd/image-cri-shim - id: image-cri-shim - hooks: - post: upx "{{ .Path }}" - binary: image-cri-shim - goos: - - linux - goarch: - - arm64 - - amd64 - ldflags: - - -X github.com/labring/sealos/pkg/version.gitVersion={{.Version}} - - -X github.com/labring/sealos/pkg/version.gitCommit={{.ShortCommit}} - - -X github.com/labring/sealos/pkg/version.buildDate={{.Date}} - - -s -w - -#archives: -# - replacements: -# darwin: Darwin -# linux: Linux -# amd64: x86_64 -# arm64: aarch64 -checksum: - disable: true - name_template: "{{ .ProjectName }}_checksums.txt" -snapshot: - name_template: "{{ .Tag }}-next" -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" diff --git a/.tagpr.json b/.tagpr.json new file mode 100644 index 000000000000..89f2b69871e7 --- /dev/null +++ b/.tagpr.json @@ -0,0 +1,6 @@ +{ + "name": "sealos", + "version": "v5.1.0", + "command": "bash scripts/changelog.sh v5.1.0 v5.0.1", + "pre-version": "v5.0.1" +} diff --git a/CHANGELOG/CHANGELOG-4.0.0.md b/CHANGELOG/CHANGELOG-4.0.0.md new file mode 100644 index 000000000000..91de5397ff01 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.0.0.md @@ -0,0 +1,187 @@ +- [v4.0.0](#v400httpsgithubcomlabringsealosreleasestagv400) + - [Downloads for v4.0.0](#downloads-for-v400) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.0.0-alpha.1](#changelog-since-v400-alpha1) + - [NewContributors](#new-contributors) + + +# [v4.0.0](https://github.com/labring/sealos/releases/tag/v4.0.0) + +## Downloads for v4.0.0 + + +### Source Code + +filename | +-------- | +[v4.0.0.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.0.0.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.0.0_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.0.0/sealos_4.0.0_linux_amd64.tar.gz) | +[sealos_4.0.0_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.0.0/sealos_4.0.0_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.0.0/sealos_4.0.0_linux_amd64.tar.gz && \ + tar -zxvf sealos_4.0.0_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.0.0/sealos_4.0.0_linux_arm64.tar.gz && \ + tar -zxvf sealos_4.0.0_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.0.0-alpha.1 + +* refactor(master): support docs for sealos 4.0 by @cuisongliu in https://github.com/labring/sealos/pull/921 +* refactor(master): support docs for sealos 4.0 by @cuisongliu in https://github.com/labring/sealos/pull/922 +* refactor(master): add multi image module by @cuisongliu in https://github.com/labring/sealos/pull/923 +* refactor(master): add multi image and go func by @cuisongliu in https://github.com/labring/sealos/pull/924 +* refactor(master): add note by @cuisongliu in https://github.com/labring/sealos/pull/925 +* feature(main): add cmd feature for main branch by @cuisongliu in https://github.com/labring/sealos/pull/934 +* feature(main): fix release config by @cuisongliu in https://github.com/labring/sealos/pull/935 +* feature(main): fix release config by @cuisongliu in https://github.com/labring/sealos/pull/936 +* feature(main): add build cmd by @cuisongliu in https://github.com/labring/sealos/pull/937 +* feature(main): support skip error process by @cuisongliu in https://github.com/labring/sealos/pull/940 +* feature(main): cmd \n and process in pull images by @cuisongliu in https://github.com/labring/sealos/pull/944 +* feature(main): fix images build by @cuisongliu in https://github.com/labring/sealos/pull/945 +* feature(main): add patch package in run func by @cuisongliu in https://github.com/labring/sealos/pull/949 +* feature(main): add auths data by @cuisongliu in https://github.com/labring/sealos/pull/950 +* feature(main): sealos run repeat exec by @cuisongliu in https://github.com/labring/sealos/pull/952 +* feature(main): support auth build by @cuisongliu in https://github.com/labring/sealos/pull/954 +* feature(main): guest run in host cmd for bash by @cuisongliu in https://github.com/labring/sealos/pull/957 +* feature(main): no change images by @cuisongliu in https://github.com/labring/sealos/pull/956 +* bugfix by @cuisongliu in https://github.com/labring/sealos/pull/959 +* feature(main): fix #960 for controller and schedule bind 0.0.0.0 by @cuisongliu in https://github.com/labring/sealos/pull/961 +* feature(main): fix add feature and delete node feature by @cuisongliu in https://github.com/labring/sealos/pull/962 +* feature(main): kubeproxy bind by @cuisongliu in https://github.com/labring/sealos/pull/964 +* feature(main): 4.0 docs fix by @cuisongliu in https://github.com/labring/sealos/pull/965 +* feature(main): fix tag images by @cuisongliu in https://github.com/labring/sealos/pull/967 +* update readme by @fanux in https://github.com/labring/sealos/pull/968 +* add roadmap by @fanux in https://github.com/labring/sealos/pull/969 +* feature(main): fix vendor for sealos by @cuisongliu in https://github.com/labring/sealos/pull/970 +* feature(main): fix goinstall by @cuisongliu in https://github.com/labring/sealos/pull/971 +* feature(main): fix go release by @cuisongliu in https://github.com/labring/sealos/pull/972 +* feature(main): fix rename go mod by @cuisongliu in https://github.com/labring/sealos/pull/973 +* feature(main): fix images mount to status by @cuisongliu in https://github.com/labring/sealos/pull/974 +* add example for building cloudimage from helm by @fanux in https://github.com/labring/sealos/pull/978 +* feature(main): add ssh exec/copy feature by @cuisongliu in https://github.com/labring/sealos/pull/980 +* feature(main): rename sealyun to sealos.io by @cuisongliu in https://github.com/labring/sealos/pull/979 +* feature(main): fix install config by @cuisongliu in https://github.com/labring/sealos/pull/977 +* feature(main): fix scale check logic by @cuisongliu in https://github.com/labring/sealos/pull/981 +* feature: add list images using buildah by @whybeyoung in https://github.com/labring/sealos/pull/988 +* Fix #997 support arm64 using buildx by @whybeyoung in https://github.com/labring/sealos/pull/1003 +* feature: add pull commands using buildah #986 by @yyf1986 in https://github.com/labring/sealos/pull/1002 +* feature(main): fix buildah config sync problem by @cuisongliu in https://github.com/labring/sealos/pull/1006 +* feature: add pull commands using buildah #986 by @yyf1986 in https://github.com/labring/sealos/pull/1011 +* feature(main): http push registry by @cuisongliu in https://github.com/labring/sealos/pull/1009 +* feature(main): fix confirm bug by @cuisongliu in https://github.com/labring/sealos/pull/1008 +* feature(main): fix create check bug by @cuisongliu in https://github.com/labring/sealos/pull/1010 +* fix release workflows for arm64 by @whybeyoung in https://github.com/labring/sealos/pull/1014 +* feature: add push commands using buildah #986 by @yyf1986 in https://github.com/labring/sealos/pull/1015 +* modify pkg/guest/guest.go by @gebilxs in https://github.com/labring/sealos/pull/1020 +* feature: add login/logout commands using buildah #986 by @yyf1986 in https://github.com/labring/sealos/pull/1021 +* hotfix: port not default 22 by @cuisongliu in https://github.com/labring/sealos/pull/1026 +* fix: when operator the registry,defautl is tls_verify=false #986 by @yyf1986 in https://github.com/labring/sealos/pull/1029 +* feature(main): fix delete add node and skip error by @cuisongliu in https://github.com/labring/sealos/pull/1027 +* Fix: Release GIT AUTH TOKEN missing error fixed by @whybeyoung in https://github.com/labring/sealos/pull/1030 +* add create/delete/list/inspect commands using buildah #986 by @yyf1986 in https://github.com/labring/sealos/pull/1041 +* feature(main): fix add error send app images by @cuisongliu in https://github.com/labring/sealos/pull/1042 +* fix cross build arm ,using statically build by @whybeyoung in https://github.com/labring/sealos/pull/1045 +* add 4.0 readme by @fanux in https://github.com/labring/sealos/pull/1049 +* fix: arm64 cross netgo panic by @whybeyoung in https://github.com/labring/sealos/pull/1051 +* Fix: set default logurs log-level to warn by @whybeyoung in https://github.com/labring/sealos/pull/1046 +* fix build images tag not work by @whybeyoung in https://github.com/labring/sealos/pull/1054 +* fix release using wrong token by @whybeyoung in https://github.com/labring/sealos/pull/1057 +* feature(main): fix actions token by @cuisongliu in https://github.com/labring/sealos/pull/1058 +* update v4.0 README, add quickstart by @fanux in https://github.com/labring/sealos/pull/1053 +* Add v4.0 README in English by @SignorMercurio in https://github.com/labring/sealos/pull/1059 +* Using English readme instread Chinese by @fanux in https://github.com/labring/sealos/pull/1060 +* Update README.md by @yangchuansheng in https://github.com/labring/sealos/pull/1061 +* feature: add users and MAINTAINERS.md by @whybeyoung in https://github.com/labring/sealos/pull/1062 +* Update README.md by @yangchuansheng in https://github.com/labring/sealos/pull/1063 +* feature(main): fix check image type after image pull by @cuisongliu in https://github.com/labring/sealos/pull/1066 +* Add default registry mirror by @SignorMercurio in https://github.com/labring/sealos/pull/1069 +* add rust registry by @fanux in https://github.com/labring/sealos/pull/1072 +* feat: make cluster root dir customizable with root cmd flags (#995) by @SignorMercurio in https://github.com/labring/sealos/pull/1073 +* refactor: split ssh cmd into exec and scp (#996) by @SignorMercurio in https://github.com/labring/sealos/pull/1075 +* feature(main): rename image using default domain and namespace by @cuisongliu in https://github.com/labring/sealos/pull/1076 +* feat: support config override by @SignorMercurio in https://github.com/labring/sealos/pull/1080 +* feat: customize maximum goroutines for pulling by @SignorMercurio in https://github.com/labring/sealos/pull/1081 +* fix: remove max-pull-procs flag in BuildOptions.String() by @SignorMercurio in https://github.com/labring/sealos/pull/1082 +* fix: panic when building offline and syntax error in registry config by @SignorMercurio in https://github.com/labring/sealos/pull/1086 +* feat: CRI defaults to containerd by @SignorMercurio in https://github.com/labring/sealos/pull/1090 +* feature(main): refactor: split ssh cmd into exec and scp(#996) by @cuisongliu in https://github.com/labring/sealos/pull/1079 +* Modify characters in README.md by @Ficus-f in https://github.com/labring/sealos/pull/1093 +* feat: include .tmpl files when searching for images by @SignorMercurio in https://github.com/labring/sealos/pull/1096 +* fix sealos pull images default domain localhost to docker.io #1097 by @yyf1986 in https://github.com/labring/sealos/pull/1102 +* modify help by @gebilxs in https://github.com/labring/sealos/pull/1105 +* fix doc,add prefix labring by @yyf1986 in https://github.com/labring/sealos/pull/1103 +* Indicate fork the source code of sealer by @fanux in https://github.com/labring/sealos/pull/1112 +* add en-help by @gebilxs in https://github.com/labring/sealos/pull/1111 +* feature(main): fix build not running .yaml by @cuisongliu in https://github.com/labring/sealos/pull/1114 +* feature(main): support cache http registry #1022 by @cuisongliu in https://github.com/labring/sealos/pull/1116 +* update release note by @fanux in https://github.com/labring/sealos/pull/1118 +* add contributors by @gitccl in https://github.com/labring/sealos/pull/1120 +* add develop and contribute guide by @fanux in https://github.com/labring/sealos/pull/1124 +* feature(main): reset cluster fixd masters and nodes by @cuisongliu in https://github.com/labring/sealos/pull/1125 +* update readme arch link by @fanux in https://github.com/labring/sealos/pull/1127 +* fix: validate vip before initialization by @fengxsong in https://github.com/labring/sealos/pull/1130 +* docs(main): add oscs by @cuisongliu in https://github.com/labring/sealos/pull/1131 +* remove unuseful code by @gitccl in https://github.com/labring/sealos/pull/1134 +* docs: add more description for delete command by @gitccl in https://github.com/labring/sealos/pull/1135 +* feat: add create feature by @SignorMercurio in https://github.com/labring/sealos/pull/1138 +* docs: update issue templates to English by @gitccl in https://github.com/labring/sealos/pull/1139 +* Add rpm & deb build script with nfpm by @Vonng in https://github.com/labring/sealos/pull/1136 +* refactor: change "contants" to "constants" by @SignorMercurio in https://github.com/labring/sealos/pull/1142 +* ci: improve Makefile design by @SignorMercurio in https://github.com/labring/sealos/pull/1147 +* parse images in helm charts #1004 by @yyf1986 in https://github.com/labring/sealos/pull/1146 +* fix: build error in makefile by @SignorMercurio in https://github.com/labring/sealos/pull/1152 +* feature(main): sealos run not save clusterfile when init failed by @cuisongliu in https://github.com/labring/sealos/pull/1148 +* feat: support entrypoint in Dockerfile by @gitccl in https://github.com/labring/sealos/pull/1151 +* Update cmd typos to match unified code format. by @zzjin in https://github.com/labring/sealos/pull/1149 +* typo: fix typo clonne in develop guide by @th2zz in https://github.com/labring/sealos/pull/1164 +* docs: update go version by @Abingcbc in https://github.com/labring/sealos/pull/1174 +* Feat typo by @zzjin in https://github.com/labring/sealos/pull/1170 +* copy linux_amd64.yml&&linux_arm64.yml to scripts by @gebilxs in https://github.com/labring/sealos/pull/1167 +* update user design by @fanux in https://github.com/labring/sealos/pull/1117 +* Using module to simpler vsc source tree. Closes #1771 by @zzjin in https://github.com/labring/sealos/pull/1172 +* fix: fix bug in gen.clean and use local binaries for tools by @SignorMercurio in https://github.com/labring/sealos/pull/1181 +* feature(main): seactl - sealctl by @cuisongliu in https://github.com/labring/sealos/pull/1183 +* feature: sealos gen by @gitccl in https://github.com/labring/sealos/pull/1201 +* feature(main): generator code deepcopy by @cuisongliu in https://github.com/labring/sealos/pull/1200 +* fix: can't specify dockerfile by @whybeyoung in https://github.com/labring/sealos/pull/1213 +* ci: adjust makefile release targets by @SignorMercurio in https://github.com/labring/sealos/pull/1216 +* modify run cmd tips by @runzhliu in https://github.com/labring/sealos/pull/1217 +* Add support for command override by @SignorMercurio in https://github.com/labring/sealos/pull/1218 +* feature(main): update lvscare sdk multiple network by @cuisongliu in https://github.com/labring/sealos/pull/1212 + +## New Contributors +* @yangchuansheng made their first contribution in https://github.com/labring/sealos/pull/1061 +* @Vonng made their first contribution in https://github.com/labring/sealos/pull/1136 +* @th2zz made their first contribution in https://github.com/labring/sealos/pull/1164 +* @runzhliu made their first contribution in https://github.com/labring/sealos/pull/1217 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.0.0-alpha.1...v4.0.0 + + + + + + + diff --git a/CHANGELOG/CHANGELOG-4.1.0-rc1.md b/CHANGELOG/CHANGELOG-4.1.0-rc1.md new file mode 100644 index 000000000000..b51777b98902 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.0-rc1.md @@ -0,0 +1,163 @@ +- [v4.1.0-rc1](#v410-rc1httpsgithubcomlabringsealosreleasestagv410-rc1) + - [Downloads for v4.1.0-rc1](#downloads-for-v410-rc1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.0.0](#changelog-since-v400) + - [NewContributors](#new-contributors) + + +# [v4.1.0-rc1](https://github.com/labring/sealos/releases/tag/v4.1.0-rc1) + +## Downloads for v4.1.0-rc1 + + +### Source Code + +filename | +-------- | +[v4.1.0-rc1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.0-rc1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.0-rc1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc1/sealos_4.1.0-rc1_linux_amd64.tar.gz) | +[sealos_4.1.0-rc1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc1/sealos_4.1.0-rc1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc1/sealos_4.1.0-rc1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.0-rc1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc1/sealos_4.1.0-rc1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.0-rc1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.0.0 + +### What's Changed + +* fix: merge go.build.%.sealctl/sealos by @SignorMercurio in https://github.com/labring/sealos/pull/1226 +* update readme by @fanux in https://github.com/labring/sealos/pull/1228 +* Update DEVELOPGUIDE.md by @Ficus-f in https://github.com/labring/sealos/pull/1229 +* feat: support docs generation by @SignorMercurio in https://github.com/labring/sealos/pull/1231 +* feature(main): delete lvscare ipvs and route by @cuisongliu in https://github.com/labring/sealos/pull/1230 +* refactor: improve scale and reset params by @SignorMercurio in https://github.com/labring/sealos/pull/1238 +* fix a word spell problem by @shy-Xu in https://github.com/labring/sealos/pull/1239 +* feat: sealos inspect by @gitccl in https://github.com/labring/sealos/pull/1225 +* Update install-sealos.md by @Ficus-f in https://github.com/labring/sealos/pull/1247 +* refactor(main): pkg/hosts->utils, pkg/token->pkg/runtime/token by @cuisongliu in https://github.com/labring/sealos/pull/1246 +* ci: enable CGO for building sealos by @SignorMercurio in https://github.com/labring/sealos/pull/1244 +* feat: refactor infra yaml struct by @taorzhang in https://github.com/labring/sealos/pull/1234 +* fix: no longer explicitly specify cgo_enabled by @berlinsaint in https://github.com/labring/sealos/pull/1253 +* refactor(main): delete node using hostname by @cuisongliu in https://github.com/labring/sealos/pull/1254 +* refactor(main): merge code from lvscare by @cuisongliu in https://github.com/labring/sealos/pull/1249 +* refactor(main): add test workflows by @cuisongliu in https://github.com/labring/sealos/pull/1256 +* docs: improve DEVELOPGUIDE.md by @SignorMercurio in https://github.com/labring/sealos/pull/1258 +* fix: reduce ci time by @berlinsaint in https://github.com/labring/sealos/pull/1260 +* ci: improve workflow and fix ut error by @SignorMercurio in https://github.com/labring/sealos/pull/1261 +* Add license scan report and status by @fossabot in https://github.com/labring/sealos/pull/1264 +* refactor(main): add codecov img by @cuisongliu in https://github.com/labring/sealos/pull/1263 +* Fix ci requirement by @zzjin in https://github.com/labring/sealos/pull/1267 +* refactor(main): add test delete node workflows by @cuisongliu in https://github.com/labring/sealos/pull/1262 +* refactor(main): add fossa workflows by @cuisongliu in https://github.com/labring/sealos/pull/1268 +* refactor(main): add fossa workflows by @cuisongliu in https://github.com/labring/sealos/pull/1269 +* refactor(main): delete fossa workflows by @cuisongliu in https://github.com/labring/sealos/pull/1273 +* refactor(main): add registry logger by @cuisongliu in https://github.com/labring/sealos/pull/1274 +* ci: multiple improvements on CI workflow by @SignorMercurio in https://github.com/labring/sealos/pull/1270 +* ci: Update ci.yml to use simpler make build to build testing binaries by @SignorMercurio in https://github.com/labring/sealos/pull/1278 +* refactor(main): run app image fix port by @cuisongliu in https://github.com/labring/sealos/pull/1281 +* ci: build binaries for lvscare and image-cri-shim by @SignorMercurio in https://github.com/labring/sealos/pull/1283 +* refactor(main): Execute the command If it is a local IP, use the os package. by @cuisongliu in https://github.com/labring/sealos/pull/1282 +* fix disposition by @gebilxs in https://github.com/labring/sealos/pull/1286 +* ci: add workflows to build and push images for lvscare by @SignorMercurio in https://github.com/labring/sealos/pull/1288 +* feat: oauth2 to generate kubeconfig by @Abingcbc in https://github.com/labring/sealos/pull/1219 +* refactor(main): set default Transport for buildah by @cuisongliu in https://github.com/labring/sealos/pull/1289 +* refactor(main): add transport param for save and load by @cuisongliu in https://github.com/labring/sealos/pull/1297 +* fix system user design doc typo by @xiaospider in https://github.com/labring/sealos/pull/1291 +* ci: improve go.build in Makefile, setup multi-arch docker image build… by @SignorMercurio in https://github.com/labring/sealos/pull/1296 +* refactor(main): sealos run other server by @cuisongliu in https://github.com/labring/sealos/pull/1292 +* ci: use goreleaser to release docker images & deb and rpm packages, r… by @SignorMercurio in https://github.com/labring/sealos/pull/1299 +* feat(desktop): casdoor k8s yaml by @Abingcbc in https://github.com/labring/sealos/pull/1302 +* refactor(main): fix release to any one by @cuisongliu in https://github.com/labring/sealos/pull/1304 +* refactor(main): reset add getSSHInterface for shim by @cuisongliu in https://github.com/labring/sealos/pull/1308 +* refactor(main): add note for image and apt deb by @cuisongliu in https://github.com/labring/sealos/pull/1307 +* add infra controller scratch by @fanux in https://github.com/labring/sealos/pull/1294 +* refactor(main): add note for image and apt deb (#1307) by @cuisongliu in https://github.com/labring/sealos/pull/1310 +* bugfix: fix dashboard css typo by @xiaospider in https://github.com/labring/sealos/pull/1313 +* init support of .devcontainer, Impl #1241, Closes #1241. by @zzjin in https://github.com/labring/sealos/pull/1298 +* feat: support custom apiserver port by @gitccl in https://github.com/labring/sealos/pull/1316 +* doc: add dev container badge and adjust badges order by @SignorMercurio in https://github.com/labring/sealos/pull/1318 +* fix(main): port set error by @cuisongliu in https://github.com/labring/sealos/pull/1323 +* refactor(desktop): replace panic with errors by @Abingcbc in https://github.com/labring/sealos/pull/1328 +* fix: generate .kube dir in home dir by @gitccl in https://github.com/labring/sealos/pull/1324 +* ci: enable CGO by @SignorMercurio in https://github.com/labring/sealos/pull/1319 +* Unite log system with zap. by @zzjin in https://github.com/labring/sealos/pull/1300 +* add infra CRD spec by @fanux in https://github.com/labring/sealos/pull/1331 +* update contribute guide by @fanux in https://github.com/labring/sealos/pull/1333 +* add aws reconcile instances by @fanux in https://github.com/labring/sealos/pull/1334 +* fix add node failed, apiserver port is 0 by @fanux in https://github.com/labring/sealos/pull/1339 +* upgrade IsIpv4 function use to system api by @zsyaoo in https://github.com/labring/sealos/pull/1326 +* Del applications dir by @zzjin in https://github.com/labring/sealos/pull/1340 +* ci: support code and docs syncing by @SignorMercurio in https://github.com/labring/sealos/pull/1332 +* ci: fix docs sync error by @SignorMercurio in https://github.com/labring/sealos/pull/1341 +* docs: update notes about cross-platform building by @SignorMercurio in https://github.com/labring/sealos/pull/1344 +* fix(main): add local ip logger by @cuisongliu in https://github.com/labring/sealos/pull/1346 +* Add infra driver interface by @fanux in https://github.com/labring/sealos/pull/1343 +* fix(main): rename AddonsImage to PatchImage by @cuisongliu in https://github.com/labring/sealos/pull/1348 +* Update go1.18 to support workspace by @zzjin in https://github.com/labring/sealos/pull/1350 +* update develop guide about workspace. by @zzjin in https://github.com/labring/sealos/pull/1352 +* refactor: clean flag of lvscare by @fengxsong in https://github.com/labring/sealos/pull/1354 +* docs: remove badges for lvscare by @SignorMercurio in https://github.com/labring/sealos/pull/1358 +* ci: fix sync_code trigger paths by @SignorMercurio in https://github.com/labring/sealos/pull/1360 +* fix(main): delete logger for isLocal by @cuisongliu in https://github.com/labring/sealos/pull/1359 +* fix(main): sync is exists by @cuisongliu in https://github.com/labring/sealos/pull/1361 +* add & init sealos desktop frontend project by @maslow in https://github.com/labring/sealos/pull/1357 +* logger support fulltext color by @zzjin in https://github.com/labring/sealos/pull/1362 +* Feature: Services-Auth by @zzjin in https://github.com/labring/sealos/pull/1347 +* feature(main): local ip not ping by @cuisongliu in https://github.com/labring/sealos/pull/1363 +* feat: auto create dummy link if needed by @fengxsong in https://github.com/labring/sealos/pull/1367 +* fix(main): unexpected end of file by @xuehaipeng in https://github.com/labring/sealos/pull/1372 +* update pull request template to supress showing by @zzjin in https://github.com/labring/sealos/pull/1371 +* perf: singleton just fine by @fengxsong in https://github.com/labring/sealos/pull/1374 +* Revert "feature(main): add lvscare docker build" by @cuisongliu in https://github.com/labring/sealos/pull/1375 +* ci: skip pr when syncing by @SignorMercurio in https://github.com/labring/sealos/pull/1377 +* remove duplicate codes by @fengxsong in https://github.com/labring/sealos/pull/1376 +* fix: go.build.verify by @xuehaipeng in https://github.com/labring/sealos/pull/1379 +* fix: deprecated grpc dial option by @xuehaipeng in https://github.com/labring/sealos/pull/1380 +* refactor ssh client by @fengxsong in https://github.com/labring/sealos/pull/1381 +* add driver framework, define driver interface and reconcile controller by @fanux in https://github.com/labring/sealos/pull/1387 +* feature(main): add lvscare docker build by @cuisongliu in https://github.com/labring/sealos/pull/1382 +* ci: improve ci workflow by @SignorMercurio in https://github.com/labring/sealos/pull/1390 +* update readme deadlink by @fanux in https://github.com/labring/sealos/pull/1392 +* add controller reconcile, add events recorder. by @fanux in https://github.com/labring/sealos/pull/1394 +* feature(main): single module kubernetes cluster by @cuisongliu in https://github.com/labring/sealos/pull/1396 +* update auth service by @zzjin in https://github.com/labring/sealos/pull/1370 +* add terminal CRD spec by @gitccl in https://github.com/labring/sealos/pull/1398 +* remove unused mod use with `go mod tidy` by @zzjin in https://github.com/labring/sealos/pull/1400 +* feature(main): add lvscare docker build by @cuisongliu in https://github.com/labring/sealos/pull/1399 + +## New Contributors + +* @shy-Xu made their first contribution in https://github.com/labring/sealos/pull/1239 +* @taorzhang made their first contribution in https://github.com/labring/sealos/pull/1234 +* @fossabot made their first contribution in https://github.com/labring/sealos/pull/1264 +* @xiaospider made their first contribution in https://github.com/labring/sealos/pull/1291 +* @zsyaoo made their first contribution in https://github.com/labring/sealos/pull/1326 +* @maslow made their first contribution in https://github.com/labring/sealos/pull/1357 +* @xuehaipeng made their first contribution in https://github.com/labring/sealos/pull/1372 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.0.0...v4.1.0-rc1 + + diff --git a/CHANGELOG/CHANGELOG-4.1.0-rc2.md b/CHANGELOG/CHANGELOG-4.1.0-rc2.md new file mode 100644 index 000000000000..06c1702bdb95 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.0-rc2.md @@ -0,0 +1,157 @@ +- [v4.1.0-rc2](#v410-rc2httpsgithubcomlabringsealosreleasestagv410-rc2) + - [Downloads for v4.1.0-rc2](#downloads-for-v410-rc2) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.0-rc2](#changelog-since-v410-rc1) + - [NewContributors](#new-contributors) + + +# [v4.1.0-rc2](https://github.com/labring/sealos/releases/tag/v4.1.0-rc2) + +## Downloads for v4.1.0-rc2 + + +### Source Code + +filename | +-------- | +[v4.1.0-rc2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.0-rc2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.0-rc2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc2/sealos_4.1.0-rc2_linux_amd64.tar.gz) | +[sealos_4.1.0-rc2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc2/sealos_4.1.0-rc2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc2/sealos_4.1.0-rc2_linux_amd64.tar.gz && tar -zxvf sealos_4.1.0-rc2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc2/sealos_4.1.0-rc2_linux_arm64.tar.gz && tar -zxvf sealos_4.1.0-rc2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.0-rc1 + +### What's Changed + +* fixed readme run command by @fanux in https://github.com/labring/sealos/pull/1409 +* fix(auth): retain mysql pv when delete pvc by @Abingcbc in https://github.com/labring/sealos/pull/1410 +* add sleaos cloud overview by @fanux in https://github.com/labring/sealos/pull/1414 +* docs(main): fix targz by @cuisongliu in https://github.com/labring/sealos/pull/1411 +* add trackgit by @fanux in https://github.com/labring/sealos/pull/1415 +* add docker and readme by @zzjin in https://github.com/labring/sealos/pull/1407 +* fix docs that have english by @willzhang in https://github.com/labring/sealos/pull/1416 +* ci: fix path error in sync_docs.yml by @SignorMercurio in https://github.com/labring/sealos/pull/1418 +* ci: set COMMIT_EACH_FILE to false in sync by @SignorMercurio in https://github.com/labring/sealos/pull/1419 +* fix(auth): connect to casdoor by @Abingcbc in https://github.com/labring/sealos/pull/1428 +* docs: add more instructions on docs contributing by @SignorMercurio in https://github.com/labring/sealos/pull/1430 +* docs(main): fix miss mount in add nodes by @cuisongliu in https://github.com/labring/sealos/pull/1429 +* fixed go file name clinet.go change to client.go by @zsyaoo in https://github.com/labring/sealos/pull/1435 +* add desktop Dockerfile and makefile by @zzjin in https://github.com/labring/sealos/pull/1417 +* feat: support netlink mode by @fengxsong in https://github.com/labring/sealos/pull/1424 +* fix(auth): get ca cert from kubeconfig by @Abingcbc in https://github.com/labring/sealos/pull/1439 +* fix chart parse image and add unit test by @yyf1986 in https://github.com/labring/sealos/pull/1438 +* docs:Add registry docs by @willzhang in https://github.com/labring/sealos/pull/1443 +* add blazeface docs by @luanshaotong in https://github.com/labring/sealos/pull/1442 +* add create/get aws instance success by @fanux in https://github.com/labring/sealos/pull/1444 +* Add Customize Docs by @willzhang in https://github.com/labring/sealos/pull/1448 +* Add Prerequisites Docs by @willzhang in https://github.com/labring/sealos/pull/1447 +* add delete aws instance by @fanux in https://github.com/labring/sealos/pull/1446 +* feat(auth): deploy for service auth by @Abingcbc in https://github.com/labring/sealos/pull/1450 +* add reconcile instance, test aws reconcile instance success by @fanux in https://github.com/labring/sealos/pull/1456 +* test(main): add chart image unit test by @cuisongliu in https://github.com/labring/sealos/pull/1458 +* fixed docs/4.0/i18n/zh-Hans/design/user.md by @1oda in https://github.com/labring/sealos/pull/1461 +* update login redirect by @zzjin in https://github.com/labring/sealos/pull/1455 +* feat: support removing digest from image tags by @SignorMercurio in https://github.com/labring/sealos/pull/1466 +* Fix sealos build by @fengxsong in https://github.com/labring/sealos/pull/1471 +* fix private registry docs by @willzhang in https://github.com/labring/sealos/pull/1464 +* init applications controller by @fanux in https://github.com/labring/sealos/pull/1477 +* Improve contributing docs by @willzhang in https://github.com/labring/sealos/pull/1445 +* feature(main): guest env render by @cuisongliu in https://github.com/labring/sealos/pull/1486 +* Fix ingress-nginx route rewrite. by @zzjin in https://github.com/labring/sealos/pull/1474 +* Set aws infra name & index tag. (#1462) by @Ficus-f in https://github.com/labring/sealos/pull/1489 +* fix(cmd): unifiy the format of print columns by @runkecheng in https://github.com/labring/sealos/pull/1492 +* fix: iptables tool missing in lvscare image by @fengxsong in https://github.com/labring/sealos/pull/1494 +* feat: add terminal controller by @gitccl in https://github.com/labring/sealos/pull/1437 +* refactor: unify the use of utils by @SignorMercurio in https://github.com/labring/sealos/pull/1498 +* Update frontend/dashboard typo by @zzjin in https://github.com/labring/sealos/pull/1497 +* fix: only snat packets marked by @fengxsong in https://github.com/labring/sealos/pull/1496 +* Fix lint error by replace `os`&`io` by @zzjin in https://github.com/labring/sealos/pull/1504 +* Fix fmt and goimports by @zzjin in https://github.com/labring/sealos/pull/1505 +* fix workspace usage by @zzjin in https://github.com/labring/sealos/pull/1506 +* feat: add terminal keepalived by @gitccl in https://github.com/labring/sealos/pull/1502 +* fix: incompatible with cilium by @fengxsong in https://github.com/labring/sealos/pull/1501 +* fix typo by `go fmt` by @zzjin in https://github.com/labring/sealos/pull/1507 +* add issue no response auto close it by @fanux in https://github.com/labring/sealos/pull/1509 +* no reply issue auto closer by @fanux in https://github.com/labring/sealos/pull/1510 +* refactor: upgrade to go 1.19 by @zzjin in https://github.com/labring/sealos/pull/1500 +* Reconcile aws infra count. (#1490) by @Ficus-f in https://github.com/labring/sealos/pull/1491 +* Reconcile aws infra count. (#1490) by @Ficus-f in https://github.com/labring/sealos/pull/1517 +* Update desktop/frontend add user login token by @zzjin in https://github.com/labring/sealos/pull/1520 +* fix: revert version of github.com/containers/storage by @fengxsong in https://github.com/labring/sealos/pull/1522 +* feat(main): generate user controller by @cuisongliu in https://github.com/labring/sealos/pull/1529 +* fix create and delete instance bug by @HURUIZHE in https://github.com/labring/sealos/pull/1533 +* Feature/infra: add docs and aws yaml files by @fanux in https://github.com/labring/sealos/pull/1534 +* fix: chart deps rendering by @fengxsong in https://github.com/labring/sealos/pull/1536 +* fixed build out of memory by @fanux in https://github.com/labring/sealos/pull/1542 +* update the docs sidebar by @fanux in https://github.com/labring/sealos/pull/1540 +* docs: fix docs typo by @x893675 in https://github.com/labring/sealos/pull/1547 +* fix typo by @zzjin in https://github.com/labring/sealos/pull/1548 +* fix(main): add max goroutine num by @cuisongliu in https://github.com/labring/sealos/pull/1543 +* init applications controller by @fanux in https://github.com/labring/sealos/pull/1538 +* docs: update add athenaserving from iflytek by @berlinsaint in https://github.com/labring/sealos/pull/1551 +* ci: add image syncing in sync_docs workflow by @SignorMercurio in https://github.com/labring/sealos/pull/1555 +* fix(main): add docker-shim support by @cuisongliu in https://github.com/labring/sealos/pull/1553 +* docs: fix some refs by @berlinsaint in https://github.com/labring/sealos/pull/1557 +* Some doc optimizations. by @Shigure-kai-2 in https://github.com/labring/sealos/pull/1558 +* docs: remove some extra pics ref by @berlinsaint in https://github.com/labring/sealos/pull/1560 +* fix: sealos build(revert saveBlobs function) by @fengxsong in https://github.com/labring/sealos/pull/1556 +* init kubernetes sdk by @zzjin in https://github.com/labring/sealos/pull/1525 +* add wechat payment handle by @fanux in https://github.com/labring/sealos/pull/1559 +* update gitignore by @zzjin in https://github.com/labring/sealos/pull/1562 +* add recharge cli by @fanux in https://github.com/labring/sealos/pull/1563 +* fix: oom occured when sealos build by @fengxsong in https://github.com/labring/sealos/pull/1568 +* feature(main): add client for k8s by @cuisongliu in https://github.com/labring/sealos/pull/1569 +* Fix ca data not returned under incluster mode by @zzjin in https://github.com/labring/sealos/pull/1570 +* feature(main): add user generator kubeconfig by @cuisongliu in https://github.com/labring/sealos/pull/1561 +* feature(main): add user load ca for kubeconfig by @cuisongliu in https://github.com/labring/sealos/pull/1572 +* feat: load clusterfile from template by @fengxsong in https://github.com/labring/sealos/pull/1523 +* update zh docs by @fanux in https://github.com/labring/sealos/pull/1573 +* feat: deploy ingress for terminal by @gitccl in https://github.com/labring/sealos/pull/1544 +* fix(main): fix terminal code,using config host by @cuisongliu in https://github.com/labring/sealos/pull/1576 +* docs: experimental usage for sealos apply by @fengxsong in https://github.com/labring/sealos/pull/1578 +* ci(main): fix lvscare arm docker image by @cuisongliu in https://github.com/labring/sealos/pull/1579 +* feature(main): add user controller logic by @cuisongliu in https://github.com/labring/sealos/pull/1575 +* add account and charge CRD by @fanux in https://github.com/labring/sealos/pull/1580 +* fix: unexpected config merge by @fengxsong in https://github.com/labring/sealos/pull/1574 +* refactor: optimize the creation of terminal ingress by @gitccl in https://github.com/labring/sealos/pull/1581 +* add payment controller, user can apply a crd to recharge his account by @fanux in https://github.com/labring/sealos/pull/1583 +* update fix go module by @zzjin in https://github.com/labring/sealos/pull/1584 +* feature(main): add csr to kubeconfig by @cuisongliu in https://github.com/labring/sealos/pull/1585 +* feature(main): delete go generate for gorelease by @cuisongliu in https://github.com/labring/sealos/pull/1586 + +## New Contributors + +* @willzhang made their first contribution in https://github.com/labring/sealos/pull/1416 +* @luanshaotong made their first contribution in https://github.com/labring/sealos/pull/1442 +* @1oda made their first contribution in https://github.com/labring/sealos/pull/1461 +* @runkecheng made their first contribution in https://github.com/labring/sealos/pull/1492 +* @HURUIZHE made their first contribution in https://github.com/labring/sealos/pull/1533 +* @x893675 made their first contribution in https://github.com/labring/sealos/pull/1547 +* @Shigure-kai-2 made their first contribution in https://github.com/labring/sealos/pull/1558 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.0-rc1...v4.1.0-rc2 diff --git a/CHANGELOG/CHANGELOG-4.1.0-rc3.md b/CHANGELOG/CHANGELOG-4.1.0-rc3.md new file mode 100644 index 000000000000..2dc5da67f54f --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.0-rc3.md @@ -0,0 +1,75 @@ +- [v4.1.0-rc3](#v410-rc3httpsgithubcomlabringsealosreleasestagv410-rc3) + - [Downloads for v4.1.0-rc3](#downloads-for-v410-rc3) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.0-rc3](#changelog-since-v410-rc2) + - [NewContributors](#new-contributors) + + +# [v4.1.0-rc3](https://github.com/labring/sealos/releases/tag/v4.1.0-rc3) + +## Downloads for v4.1.0-rc3 + + +### Source Code + +filename | +-------- | +[v4.1.0-rc3.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.0-rc3.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.0-rc3_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc3/sealos_4.1.0-rc3_linux_amd64.tar.gz) | +[sealos_4.1.0-rc3_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0-rc3/sealos_4.1.0-rc3_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc3/sealos_4.1.0-rc3_linux_amd64.tar.gz && tar -zxvf sealos_4.1.0-rc3_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0-rc3/sealos_4.1.0-rc3_linux_arm64.tar.gz && tar -zxvf sealos_4.1.0-rc3_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.0-rc2 + +### What's Changed + +* feat: add terminal status by @gitccl in https://github.com/labring/sealos/pull/1587 +* docs: add example for Config by @fengxsong in https://github.com/labring/sealos/pull/1591 +* update terminal frontend svc get by @zzjin in https://github.com/labring/sealos/pull/1582 +* style: correct flag usage by @fengxsong in https://github.com/labring/sealos/pull/1593 +* feature(main): buildx user-controller by @cuisongliu in https://github.com/labring/sealos/pull/1588 +* Update client-js to support crd get status return by @zzjin in https://github.com/labring/sealos/pull/1595 +* Fix work replace by @zzjin in https://github.com/labring/sealos/pull/1597 +* feat: Attach aws Volume (#1512) by @HURUIZHE in https://github.com/labring/sealos/pull/1589 +* feature(main): add usergroups controller for user by @cuisongliu in https://github.com/labring/sealos/pull/1600 +* fixed typo by @fanux in https://github.com/labring/sealos/pull/1607 +* style: file rename by @LeezQ in https://github.com/labring/sealos/pull/1602 +* Fix #1605 by @muicoder in https://github.com/labring/sealos/pull/1606 +* update account controller to listen the payment crd by @fanux in https://github.com/labring/sealos/pull/1609 +* add semgrep sast scan by @zzjin in https://github.com/labring/sealos/pull/1610 +* feature(main): add tips in running routeMode by @cuisongliu in https://github.com/labring/sealos/pull/1611 +* feature(main): add image for sealos by @cuisongliu in https://github.com/labring/sealos/pull/1613 +* feature(main): add v1.25 k8s support by @cuisongliu in https://github.com/labring/sealos/pull/1616 +* fix payment and account controller recharge failed by @fanux in https://github.com/labring/sealos/pull/1617 + +## New Contributors + +* @LeezQ made their first contribution in https://github.com/labring/sealos/pull/1602 +* @muicoder made their first contribution in https://github.com/labring/sealos/pull/1606 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.0-rc2...v4.1.0-rc3 diff --git a/CHANGELOG/CHANGELOG-4.1.0.md b/CHANGELOG/CHANGELOG-4.1.0.md new file mode 100644 index 000000000000..66fcdeea6071 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.0.md @@ -0,0 +1,74 @@ +- [v4.1.0](#v410httpsgithubcomlabringsealosreleasestagv410) + - [Downloads for v4.1.0](#downloads-for-v410) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.0](#changelog-since-v410-rc3) + + +# [v4.1.0](https://github.com/labring/sealos/releases/tag/v4.1.0) + +## Downloads for v4.1.0 + + +### Source Code + +filename | +-------- | +[v4.1.0.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.0.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.0_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0/sealos_4.1.0_linux_amd64.tar.gz) | +[sealos_4.1.0_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.0/sealos_4.1.0_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0/sealos_4.1.0_linux_amd64.tar.gz && tar -zxvf sealos_4.1.0_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.0/sealos_4.1.0_linux_arm64.tar.gz && tar -zxvf sealos_4.1.0_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.0-rc3 + +### What's Changed + +* feature(main): add usergroupbindings controller for user by @cuisongliu in https://github.com/labring/sealos/pull/1618 +* fix: use bufio.reader to avoid token too long error by @fengxsong in https://github.com/labring/sealos/pull/1624 +* add payment spec verify by @fanux in https://github.com/labring/sealos/pull/1622 +* add terminal record to show how to use sealos install kubernetes cluster by @fanux in https://github.com/labring/sealos/pull/1626 +* update install doc by @zzjin in https://github.com/labring/sealos/pull/1627 +* add first contribution about github workflow by @fanux in https://github.com/labring/sealos/pull/1628 +* fix vercel render failed by @fanux in https://github.com/labring/sealos/pull/1630 +* fix vercel render failed by @fanux in https://github.com/labring/sealos/pull/1631 +* fixed vercel build failed by @fanux in https://github.com/labring/sealos/pull/1634 +* Update repo-file-sync-action version to v1 by @SignorMercurio in https://github.com/labring/sealos/pull/1635 +* add english docs for cluster lifecycle by @fanux in https://github.com/labring/sealos/pull/1637 +* style: unifying definitions of command line flags by @fengxsong in https://github.com/labring/sealos/pull/1638 +* add uuid address for terminal by @gitccl in https://github.com/labring/sealos/pull/1639 +* add some notes by @HURUIZHE in https://github.com/labring/sealos/pull/1644 +* change 'auth/main.go log' by @Rushmmmc in https://github.com/labring/sealos/pull/1642 +* ci: auto format code with `make format` by @SignorMercurio in https://github.com/labring/sealos/pull/1645 +* feature(main): add docker support by @cuisongliu in https://github.com/labring/sealos/pull/1604 +* feat:init metering by @xiao-jay in https://github.com/labring/sealos/pull/1647 +* translate en docs by @fanux in https://github.com/labring/sealos/pull/1646 +* fix auto-format by pull from right repo url by @zzjin in https://github.com/labring/sealos/pull/1653 +* feature(main): add convert code from kubernetes by @cuisongliu in https://github.com/labring/sealos/pull/1652 +* feature(main): fix shim for docker by @cuisongliu in https://github.com/labring/sealos/pull/1657 + + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.0-rc3...v4.1.0 diff --git a/CHANGELOG/CHANGELOG-4.1.1.md b/CHANGELOG/CHANGELOG-4.1.1.md new file mode 100644 index 000000000000..c673cb10b312 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.1.md @@ -0,0 +1,60 @@ +- [v4.1.1](#v411httpsgithubcomlabringsealosreleasestagv411) + - [Downloads for v4.1.1](#downloads-for-v411) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.1](#changelog-since-v410) + + +# [v4.1.1](https://github.com/labring/sealos/releases/tag/v4.1.1) + +## Downloads for v4.1.1 + + +### Source Code + +filename | +-------- | +[v4.1.1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.1/sealos_4.1.1_linux_amd64.tar.gz) | +[sealos_4.1.1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.1/sealos_4.1.1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.1/sealos_4.1.1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.1/sealos_4.1.1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.0 + +### What's Changed + +* feat(auth): local cdn by @Abingcbc in https://github.com/labring/sealos/pull/1664 +* feature(main): override registry config for buildah by @cuisongliu in https://github.com/labring/sealos/pull/1665 +* feature(main): doc: add docker images for sealos by @cuisongliu in https://github.com/labring/sealos/pull/1667 +* feature(main): docs fix title by @cuisongliu in https://github.com/labring/sealos/pull/1669 +* update install on single node, add calico and version requirement by @fanux in https://github.com/labring/sealos/pull/1671 +* update docs by @zzjin in https://github.com/labring/sealos/pull/1673 +* Update developguide by @Rushmmmc in https://github.com/labring/sealos/pull/1678 +* feat: support merging configs on run command by @fengxsong in https://github.com/labring/sealos/pull/1658 +* feature(main): hotfix kubelet 10250 dead by @cuisongliu in https://github.com/labring/sealos/pull/1681 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.0...v4.1.1 diff --git a/CHANGELOG/CHANGELOG-4.1.2-rc1.md b/CHANGELOG/CHANGELOG-4.1.2-rc1.md new file mode 100644 index 000000000000..e0289a758099 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.2-rc1.md @@ -0,0 +1,52 @@ +- [v4.1.2-rc1](#v412-rc1httpsgithubcomlabringsealosreleasestagv412-rc1) + - [Downloads for v4.1.2-rc1](#downloads-for-v412-rc1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.2-rc1](#changelog-since-v411) + + +# [v4.1.2-rc1](https://github.com/labring/sealos/releases/tag/v4.1.2-rc1) + +## Downloads for v4.1.2-rc1 + + +### Source Code + +filename | +-------- | +[v4.1.2-rc1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.2-rc1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.2-rc1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.2-rc1/sealos_4.1.2-rc1_linux_amd64.tar.gz) | +[sealos_4.1.2-rc1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.2-rc1/sealos_4.1.2-rc1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.2-rc1/sealos_4.1.2-rc1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.2-rc1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.2-rc1/sealos_4.1.2-rc1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.2-rc1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.1 + +### What's Changed + +* fix: override spec value from flags by @fengxsong in https://github.com/labring/sealos/pull/1685 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.1...v4.1.2-rc1 diff --git a/CHANGELOG/CHANGELOG-4.1.2.md b/CHANGELOG/CHANGELOG-4.1.2.md new file mode 100644 index 000000000000..805e7e36c665 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.2.md @@ -0,0 +1,54 @@ +- [v4.1.2](#v412httpsgithubcomlabringsealosreleasestagv412) + - [Downloads for v4.1.2](#downloads-for-v412) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.2](#changelog-since-v411) + + +# [v4.1.2](https://github.com/labring/sealos/releases/tag/v4.1.2) + +## Downloads for v4.1.2 + + +### Source Code + +filename | +-------- | +[v4.1.2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.2/sealos_4.1.2_linux_amd64.tar.gz) | +[sealos_4.1.2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.2/sealos_4.1.2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.2/sealos_4.1.2_linux_amd64.tar.gz && tar -zxvf sealos_4.1.2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.2/sealos_4.1.2_linux_arm64.tar.gz && tar -zxvf sealos_4.1.2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.1 + +### What's Changed + +* fix: override spec value from flags by @fengxsong in https://github.com/labring/sealos/pull/1685 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.1...v4.1.2 + + diff --git a/CHANGELOG/CHANGELOG-4.1.3-rc1.md b/CHANGELOG/CHANGELOG-4.1.3-rc1.md new file mode 100644 index 000000000000..a0b66645851f --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.3-rc1.md @@ -0,0 +1,62 @@ +- [v4.1.3-rc1](#v413-rc1httpsgithubcomlabringsealosreleasestagv413-rc1) + - [Downloads for v4.1.3-rc1](#downloads-for-v413-rc1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.3-rc1](#changelog-since-v412) + + +# [v4.1.3-rc1](https://github.com/labring/sealos/releases/tag/v4.1.3-rc1) + +## Downloads for v4.1.3-rc1 + + +### Source Code + +filename | +-------- | +[v4.1.3-rc1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.3-rc1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.3-rc1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.3-rc1/sealos_4.1.3-rc1_linux_amd64.tar.gz) | +[sealos_4.1.3-rc1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.3-rc1/sealos_4.1.3-rc1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.3-rc1/sealos_4.1.3-rc1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.3-rc1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.3-rc1/sealos_4.1.3-rc1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.3-rc1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.2 + +### What's Changed + +* desktop ui by @LeezQ in https://github.com/labring/sealos/pull/1675 +* feature(main): add docker image for sealos by @cuisongliu in https://github.com/labring/sealos/pull/1636 +* feature(main): add usergroupbindings controller for namespace by @cuisongliu in https://github.com/labring/sealos/pull/1632 +* fix: release binary segfault on centos by @SignorMercurio in https://github.com/labring/sealos/pull/1694 +* feature(main): fix shim image for online by @cuisongliu in https://github.com/labring/sealos/pull/1693 +* update gorelease typo locaion by @zzjin in https://github.com/labring/sealos/pull/1695 +* feat(auth): casdoor static file server by @Abingcbc in https://github.com/labring/sealos/pull/1698 +* feature(main): fix upx for release by @cuisongliu in https://github.com/labring/sealos/pull/1702 +* optimize go step action by @zzjin in https://github.com/labring/sealos/pull/1699 +* fix sealos will not use local images if there is a newer one remote by @yyf1986 in https://github.com/labring/sealos/pull/1689 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.2...v4.1.3-rc1 + diff --git a/CHANGELOG/CHANGELOG-4.1.3.md b/CHANGELOG/CHANGELOG-4.1.3.md new file mode 100644 index 000000000000..0ea56b48af66 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.3.md @@ -0,0 +1,57 @@ +- [v4.1.3](#v413httpsgithubcomlabringsealosreleasestagv413) + - [Downloads for v4.1.3](#downloads-for-v413) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.3](#changelog-since-v413-rc1) + + +# [v4.1.3](https://github.com/labring/sealos/releases/tag/v4.1.3) + +## Downloads for v4.1.3 + + +### Source Code + +filename | +-------- | +[v4.1.3.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.3.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.3_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.3/sealos_4.1.3_linux_amd64.tar.gz) | +[sealos_4.1.3_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.3/sealos_4.1.3_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.3/sealos_4.1.3_linux_amd64.tar.gz && tar -zxvf sealos_4.1.3_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.3/sealos_4.1.3_linux_arm64.tar.gz && tar -zxvf sealos_4.1.3_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.3-rc1 + +### What's Changed +* remove upx compress on sealos binary by @zzjin in https://github.com/labring/sealos/pull/1705 +* Frontend updates by @zzjin in https://github.com/labring/sealos/pull/1707 +* feature(main): add binary for buildah by @cuisongliu in https://github.com/labring/sealos/pull/1709 +* feature(main): add upx for sealos by @cuisongliu in https://github.com/labring/sealos/pull/1710 + + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.3-rc1...v4.1.3 + + diff --git a/CHANGELOG/CHANGELOG-4.1.4-rc1.md b/CHANGELOG/CHANGELOG-4.1.4-rc1.md new file mode 100644 index 000000000000..f1a90510c76e --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.4-rc1.md @@ -0,0 +1,197 @@ +- [v4.1.4-rc1](#v414-rc1httpsgithubcomlabringsealosreleasestagv414-rc1) + - [Downloads for v4.1.4-rc1](#downloads-for-v414-rc1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.4-rc1](#changelog-since-v413) + - [NewContributors](#new-contributors) + + +# [v4.1.4-rc1](https://github.com/labring/sealos/releases/tag/v4.1.4-rc1) + +## Downloads for v4.1.4-rc1 + + +### Source Code + +filename | +-------- | +[v4.1.4-rc1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.4-rc1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.4-rc1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc1/sealos_4.1.4-rc1_linux_amd64.tar.gz) | +[sealos_4.1.4-rc1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc1/sealos_4.1.4-rc1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc1/sealos_4.1.4-rc1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.4-rc1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc1/sealos_4.1.4-rc1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.4-rc1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.3 + +### What's Changed + +* feature(main): optimize Dockerfile(#1711) by @muicoder in https://github.com/labring/sealos/pull/1713 +* fix: add index page by @LeezQ in https://github.com/labring/sealos/pull/1715 +* Update go module to match workspace by @zzjin in https://github.com/labring/sealos/pull/1716 +* Fix Optimized to use buildah sdk with push and pull by @yyf1986 in https://github.com/labring/sealos/pull/1714 +* update readme by @pangqyy in https://github.com/labring/sealos/pull/1721 +* update readme by @pangqyy in https://github.com/labring/sealos/pull/1722 +* feature(main): sealos merge fearure by @cuisongliu in https://github.com/labring/sealos/pull/1706 +* update readme by @pangqyy in https://github.com/labring/sealos/pull/1725 +* Update and fix login method. by @zzjin in https://github.com/labring/sealos/pull/1719 +* feature(main): add sync user role by @cuisongliu in https://github.com/labring/sealos/pull/1718 +* Update Cloud Frontend. by @zzjin in https://github.com/labring/sealos/pull/1726 +* fix: issue 1724 by @fengxsong in https://github.com/labring/sealos/pull/1733 +* refactor: applier by @fengxsong in https://github.com/labring/sealos/pull/1736 +* Fix user controller module by @zzjin in https://github.com/labring/sealos/pull/1739 +* update go work modules by @zzjin in https://github.com/labring/sealos/pull/1742 +* feat: generate config of components by @fengxsong in https://github.com/labring/sealos/pull/1743 +* update terminal controller image, disable CGO by @gitccl in https://github.com/labring/sealos/pull/1745 +* fix ci terminal controller build error by @gitccl in https://github.com/labring/sealos/pull/1748 +* feature(main): add sync user role by @cuisongliu in https://github.com/labring/sealos/pull/1749 +* feature(main): add sync user role by @cuisongliu in https://github.com/labring/sealos/pull/1750 +* feature(main): add registry sdk by @cuisongliu in https://github.com/labring/sealos/pull/1746 +* Init support of terminal and kubernetes-dashboard. by @zzjin in https://github.com/labring/sealos/pull/1738 +* feature(main): fix user group role by @cuisongliu in https://github.com/labring/sealos/pull/1754 +* tmp fix terminal waiting by @zzjin in https://github.com/labring/sealos/pull/1757 +* Update cert manager to use dns01 for wildcard. by @zzjin in https://github.com/labring/sealos/pull/1761 +* Add Controller Cluster by @HURUIZHE in https://github.com/labring/sealos/pull/1697 +* Update favicon& typo by @zzjin in https://github.com/labring/sealos/pull/1763 +* Temp Add `terminal`'s namespace by @zzjin in https://github.com/labring/sealos/pull/1762 +* remove certificate from terminal-controller by @gitccl in https://github.com/labring/sealos/pull/1765 +* update: window manage by @LeezQ in https://github.com/labring/sealos/pull/1767 +* add user-controller webhook by @cuisongliu in https://github.com/labring/sealos/pull/1766 +* Dev front by @zzjin in https://github.com/labring/sealos/pull/1774 +* add start menu & fix iframe url bug by @LeezQ in https://github.com/labring/sealos/pull/1780 +* Dev front by @zzjin in https://github.com/labring/sealos/pull/1778 +* update docs: update calico version by @fanux in https://github.com/labring/sealos/pull/1781 +* update readme: add cri-docker and new desktop image by @fanux in https://github.com/labring/sealos/pull/1782 +* feature(main): update base image is ubuntu by @cuisongliu in https://github.com/labring/sealos/pull/1784 +* Update DEVELOPGUIDE.md by @Dan81067 in https://github.com/labring/sealos/pull/1786 +* bugfix: display the correct icon in app store by @BambooSword in https://github.com/labring/sealos/pull/1789 +* Update ci dep. by @zzjin in https://github.com/labring/sealos/pull/1794 +* feature(main): fix user-controller-rbac for csr by @cuisongliu in https://github.com/labring/sealos/pull/1796 +* feature(main): add Platform for sealos pull/create by @cuisongliu in https://github.com/labring/sealos/pull/1797 +* feat: add pwa support by @LeezQ in https://github.com/labring/sealos/pull/1785 +* Cluster Debug by @HURUIZHE in https://github.com/labring/sealos/pull/1792 +* add helm when install calico v3.24.1 by @fanux in https://github.com/labring/sealos/pull/1800 +* fixed docs, calico new version needs helm by @fanux in https://github.com/labring/sealos/pull/1802 +* feat: generate kubeconfig by user controller by @Abingcbc in https://github.com/labring/sealos/pull/1799 +* Update seg to standalone version. by @zzjin in https://github.com/labring/sealos/pull/1803 +* Allow semgpeg fails. by @zzjin in https://github.com/labring/sealos/pull/1806 +* Generate certificate suitable for use with any Kubernetes Webhook. by @muicoder in https://github.com/labring/sealos/pull/1805 +* fix: empty status error of quick query by @Abingcbc in https://github.com/labring/sealos/pull/1810 +* feature(main): add user namespace Pod Security Admission by @cuisongliu in https://github.com/labring/sealos/pull/1811 +* feature(main): add disable_webhook_env by @cuisongliu in https://github.com/labring/sealos/pull/1809 +* Update user's kubeconfig using new user-controller's resp. by @zzjin in https://github.com/labring/sealos/pull/1814 +* CREATE SSH pair key when creating instance by @HURUIZHE in https://github.com/labring/sealos/pull/1808 +* feat: add user access account permissions by @xiao-jay in https://github.com/labring/sealos/pull/1815 +* Dev front by @zzjin in https://github.com/labring/sealos/pull/1818 +* fix:add create status when payment create by @xiao-jay in https://github.com/labring/sealos/pull/1821 +* fix: ineffective env and cmd override by @SignorMercurio in https://github.com/labring/sealos/pull/1825 +* fix #1775: update doc calico version to v3.24.1 by @a1576471428 in https://github.com/labring/sealos/pull/1826 +* Feat client sdk and wechat pay by @LeezQ in https://github.com/labring/sealos/pull/1827 +* feat: wechat pay by @LeezQ in https://github.com/labring/sealos/pull/1828 +* feat:update role of user access ownerreference account by @xiao-jay in https://github.com/labring/sealos/pull/1833 +* Update accounts.user.sealos.io sdk usage. by @zzjin in https://github.com/labring/sealos/pull/1832 +* Fix terminal rbac permission. by @zzjin in https://github.com/labring/sealos/pull/1834 +* fix:change log level by @xiao-jay in https://github.com/labring/sealos/pull/1838 +* fix: copy binary with execute permission by @fengxsong in https://github.com/labring/sealos/pull/1842 +* fix: return error if any error occured by @fengxsong in https://github.com/labring/sealos/pull/1844 +* fix: Hydration failed by @LeezQ in https://github.com/labring/sealos/pull/1840 +* feat:add payment secvet set env by @xiao-jay in https://github.com/labring/sealos/pull/1848 +* docs: fix some typo in CONTRIBUTING.md. by @Raving-hash in https://github.com/labring/sealos/pull/1852 +* Update Frontend apps by @zzjin in https://github.com/labring/sealos/pull/1847 +* feat: appstore by @BambooSword in https://github.com/labring/sealos/pull/1837 +* feature(main): add sa kubeconfig by @cuisongliu in https://github.com/labring/sealos/pull/1836 +* feature(main): fix image pull for user-controller by @cuisongliu in https://github.com/labring/sealos/pull/1857 +* feature(main): add auto build cluster-image by @cuisongliu in https://github.com/labring/sealos/pull/1859 +* Fix appstore typo by @zzjin in https://github.com/labring/sealos/pull/1856 +* fix:sealos creat xxxx image not known by @xiao-jay in https://github.com/labring/sealos/pull/1860 +* feat: support pulling in sealos save and loading in sealos run by @SignorMercurio in https://github.com/labring/sealos/pull/1861 +* optimize the scale process by @fengxsong in https://github.com/labring/sealos/pull/1855 +* fix:sealos creat xxxx image not known by @xiao-jay in https://github.com/labring/sealos/pull/1864 +* feat:account delete payment delay 5 minutes by @xiao-jay in https://github.com/labring/sealos/pull/1863 +* feat:add metering module by @xiao-jay in https://github.com/labring/sealos/pull/1824 +* add support kubernetes versions in readme by @fanux in https://github.com/labring/sealos/pull/1868 +* feature(main): fix miss rbac by @cuisongliu in https://github.com/labring/sealos/pull/1874 +* Frontend and ssh add pkdata by @HURUIZHE in https://github.com/labring/sealos/pull/1831 +* docs: update sealos API docs by @SignorMercurio in https://github.com/labring/sealos/pull/1879 +* Fix terminal rbac usage by @zzjin in https://github.com/labring/sealos/pull/1885 +* fix: semgrep scan issues by @SignorMercurio in https://github.com/labring/sealos/pull/1881 +* fix: the rest of semgrep scan issues by @SignorMercurio in https://github.com/labring/sealos/pull/1886 +* feat: deploy terminal in terminal-app ns by @gitccl in https://github.com/labring/sealos/pull/1835 +* feature(main): add log for user controller by @cuisongliu in https://github.com/labring/sealos/pull/1888 +* fix: watch+select to get kubeconfig by @Abingcbc in https://github.com/labring/sealos/pull/1892 +* Add apps by @zzjin in https://github.com/labring/sealos/pull/1893 +* improve:make SetClusterRunArgs conform to 'return fast' by @NTH19 in https://github.com/labring/sealos/pull/1889 +* add mock dapps by @fanux in https://github.com/labring/sealos/pull/1898 +* docs: improve existing docs by @SignorMercurio in https://github.com/labring/sealos/pull/1896 +* Dev front by @zzjin in https://github.com/labring/sealos/pull/1901 +* feature(main): add registry sdk by @cuisongliu in https://github.com/labring/sealos/pull/1880 +* Fix frontend by @LeezQ in https://github.com/labring/sealos/pull/1895 +* Fix app import typo by @zzjin in https://github.com/labring/sealos/pull/1904 +* doc: update k8s image name by @xiao-jay in https://github.com/labring/sealos/pull/1907 +* add copy token by @LeezQ in https://github.com/labring/sealos/pull/1906 +* fix: alert cause copy fail by @LeezQ in https://github.com/labring/sealos/pull/1908 +* ci: fix docs site build error by @SignorMercurio in https://github.com/labring/sealos/pull/1910 +* docs: A few typos by @TomatoQt in https://github.com/labring/sealos/pull/1911 +* desktop/frontend/README.md by @cubxxw in https://github.com/labring/sealos/pull/1912 +* fix: use unknown user instead of running useradd by @SignorMercurio in https://github.com/labring/sealos/pull/1913 +* add contribute guide, add golang install and FAQ by @fanux in https://github.com/labring/sealos/pull/1919 +* fix readme and intro by @zyj-111 in https://github.com/labring/sealos/pull/1918 +* temp comments out L by @zzjin in https://github.com/labring/sealos/pull/1925 +* feature(main): add user-controller rbac for sa by @cuisongliu in https://github.com/labring/sealos/pull/1926 +* [fix]add pull policy by @yyf1986 in https://github.com/labring/sealos/pull/1921 +* feature(main): add user-controller rbac for sa (#1926) by @cuisongliu in https://github.com/labring/sealos/pull/1928 +* Update doc typo. by @zzjin in https://github.com/labring/sealos/pull/1933 +* fix: get host arch using SSH for apply/run, apply/reset and apply/scale by @SignorMercurio in https://github.com/labring/sealos/pull/1927 +* shell:creat a sealos cluster based on multipass by @xiao-jay in https://github.com/labring/sealos/pull/1934 +* feature(main): sync ttl for token and kubeadm-certs by @cuisongliu in https://github.com/labring/sealos/pull/1936 +* fix: casdoor non root security by @Abingcbc in https://github.com/labring/sealos/pull/1940 +* feature(main): fix nodes for single by @cuisongliu in https://github.com/labring/sealos/pull/1938 +* feature(main): add GetContextDir for build by @cuisongliu in https://github.com/labring/sealos/pull/1935 +* fix: error link by @ining7 in https://github.com/labring/sealos/pull/1947 +* del: update readme by @ining7 in https://github.com/labring/sealos/pull/1948 +* feature(main): add sealos action for docs by @cuisongliu in https://github.com/labring/sealos/pull/1949 +* Try fix goreleaser for arm package. by @zzjin in https://github.com/labring/sealos/pull/1952 +* feature(main): fix cri socket to multiple CRI by @cuisongliu in https://github.com/labring/sealos/pull/1945 +* docs: update workspace preparation steps by @wxharry in https://github.com/labring/sealos/pull/1955 +* add sealos cloud example: hello world demo by @fanux in https://github.com/labring/sealos/pull/1960 +* fix: goreleaser actions failure by @SignorMercurio in https://github.com/labring/sealos/pull/1962 +* ci: publish docker image for the release workflow with goreleaser by @SignorMercurio in https://github.com/labring/sealos/pull/1964 +* feature(main): fix docker build image by @cuisongliu in https://github.com/labring/sealos/pull/1966 +* feature(main): fix default registry ip by @cuisongliu in https://github.com/labring/sealos/pull/1965 + +## New Contributors + +* @pangqyy made their first contribution in https://github.com/labring/sealos/pull/1721 +* @Dan81067 made their first contribution in https://github.com/labring/sealos/pull/1786 +* @BambooSword made their first contribution in https://github.com/labring/sealos/pull/1789 +* @a1576471428 made their first contribution in https://github.com/labring/sealos/pull/1826 +* @Raving-hash made their first contribution in https://github.com/labring/sealos/pull/1852 +* @NTH19 made their first contribution in https://github.com/labring/sealos/pull/1889 +* @TomatoQt made their first contribution in https://github.com/labring/sealos/pull/1911 +* @zyj-111 made their first contribution in https://github.com/labring/sealos/pull/1918 +* @ining7 made their first contribution in https://github.com/labring/sealos/pull/1947 +* @wxharry made their first contribution in https://github.com/labring/sealos/pull/1955 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.3...v4.1.4-rc1 diff --git a/CHANGELOG/CHANGELOG-4.1.4-rc2.md b/CHANGELOG/CHANGELOG-4.1.4-rc2.md new file mode 100644 index 000000000000..15e35f38507e --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.4-rc2.md @@ -0,0 +1,193 @@ +- [v4.1.4-rc2](#v414-rc2httpsgithubcomlabringsealosreleasestagv414-rc2) + - [Downloads for v4.1.4-rc2](#downloads-for-v414-rc2) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.4-rc2](#changelog-since-v414-rc1) + - [NewContributors](#new-contributors) + + +# [v4.1.4-rc2](https://github.com/labring/sealos/releases/tag/v4.1.4-rc2) + +## Downloads for v4.1.4-rc2 + + +### Source Code + +filename | +-------- | +[v4.1.4-rc2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.4-rc2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.4-rc2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc2/sealos_4.1.4-rc2_linux_amd64.tar.gz) | +[sealos_4.1.4-rc2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc2/sealos_4.1.4-rc2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc2/sealos_4.1.4-rc2_linux_amd64.tar.gz && tar -zxvf sealos_4.1.4-rc2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc2/sealos_4.1.4-rc2_linux_arm64.tar.gz && tar -zxvf sealos_4.1.4-rc2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.4-rc1 + +### What's Changed +* feature(main): separate registry from master0 by @xuehaipeng in https://github.com/labring/sealos/pull/1963 +* feature(main): separate registry from master0 by @cuisongliu in https://github.com/labring/sealos/pull/1970 +* feature(main): fix already installed buildah by @cuisongliu in https://github.com/labring/sealos/pull/1968 +* Add docker image builder workflow for service auth by @maslow in https://github.com/labring/sealos/pull/1976 +* feature(main): fix buildah download and sealos build by @cuisongliu in https://github.com/labring/sealos/pull/1971 +* docs: a small number of errors by @cubxxw in https://github.com/labring/sealos/pull/1978 +* feature(main): add detach for sealos run, never prompt by @cuisongliu in https://github.com/labring/sealos/pull/1979 +* init whitelist webhook by @fanux in https://github.com/labring/sealos/pull/1972 +* feature(main): add controllers,webhooks,services ci by @cuisongliu in https://github.com/labring/sealos/pull/1984 +* feat: use env instead of hard code by @xiao-jay in https://github.com/labring/sealos/pull/1985 +* Update listCRD func by @zzjin in https://github.com/labring/sealos/pull/1992 +* feature(main): add auth service deploy ci by @cuisongliu in https://github.com/labring/sealos/pull/1989 +* feature(main): replace render charts dir to scripts dir by @cuisongliu in https://github.com/labring/sealos/pull/1990 +* fix: remove prompt arg by @fengxsong in https://github.com/labring/sealos/pull/1999 +* feature(main): payment controller disable from env by @cuisongliu in https://github.com/labring/sealos/pull/1995 +* feature(main): add container label for repo by @cuisongliu in https://github.com/labring/sealos/pull/2002 +* update issue-template by @willzhang in https://github.com/labring/sealos/pull/2005 +* feature(main): add docs for DEVELOPGUIDE.md by @cuisongliu in https://github.com/labring/sealos/pull/2006 +* Remove mandatory requirements by @willzhang in https://github.com/labring/sealos/pull/2007 +* feature(main): add save image to build cmd by @cuisongliu in https://github.com/labring/sealos/pull/1997 +* GitHub Actions: Deprecating save-state and set-output commands by @muicoder in https://github.com/labring/sealos/pull/2010 +* feat. image hub by @lingdie in https://github.com/labring/sealos/pull/1988 +* add imagehub controller deply yaml by @lingdie in https://github.com/labring/sealos/pull/2016 +* feature(main): add TryParse for loadClusterfile by @cuisongliu in https://github.com/labring/sealos/pull/2017 +* feature(main): add webhook config to sdk by @cuisongliu in https://github.com/labring/sealos/pull/2013 +* feature(main): delete pyament env from deploy by @cuisongliu in https://github.com/labring/sealos/pull/2032 +* fix terminal hardcoded problem by @gitccl in https://github.com/labring/sealos/pull/2028 +* add infra other field by @wuxming in https://github.com/labring/sealos/pull/1994 +* add sealos cloud provider fontend by @zjy365 in https://github.com/labring/sealos/pull/2004 +* Fix yum package manager command. by @zzjin in https://github.com/labring/sealos/pull/2038 +* Add affine app demo. by @zzjin in https://github.com/labring/sealos/pull/2039 +* add build sealos on macos ARM using multipass by @fanux in https://github.com/labring/sealos/pull/2043 +* feature(main): add BINS=sealos for docs by @cuisongliu in https://github.com/labring/sealos/pull/2052 +* feature(main): add pyament deploy by @cuisongliu in https://github.com/labring/sealos/pull/2035 +* fixed sealos run manifests conflict by @fanux in https://github.com/labring/sealos/pull/2048 +* feature(main): fix guest func for exec shell by @cuisongliu in https://github.com/labring/sealos/pull/2054 +* add build cloud image docs by @willzhang in https://github.com/labring/sealos/pull/2055 +* fix: remove dependency to casdoor by @Abingcbc in https://github.com/labring/sealos/pull/2033 +* fix: misuse of *cluster pointer by @fengxsong in https://github.com/labring/sealos/pull/2057 +* add development guide - what is sealos by @fanux in https://github.com/labring/sealos/pull/2053 +* feature(main): fix registry bugs by @cuisongliu in https://github.com/labring/sealos/pull/2060 +* fix: replace text/template from html/template by @fengxsong in https://github.com/labring/sealos/pull/2067 +* feature(main): fix gen doc by @cuisongliu in https://github.com/labring/sealos/pull/2070 +* feature(main): fix user controller deploy by @cuisongliu in https://github.com/labring/sealos/pull/2065 +* Begin proposal process by @zzjin in https://github.com/labring/sealos/pull/1916 +* Support ingress static cache by @zzjin in https://github.com/labring/sealos/pull/2072 +* Start Infra and add status by @wuxming in https://github.com/labring/sealos/pull/2078 +* feature(main): fix auth deploy for cluster-image by @cuisongliu in https://github.com/labring/sealos/pull/2083 +* Update frontend nonRoot user. by @zzjin in https://github.com/labring/sealos/pull/2085 +* fix: copy registry to the correct path by @fengxsong in https://github.com/labring/sealos/pull/2087 +* reduction Makefile and add build-multi-arch by @wuxming in https://github.com/labring/sealos/pull/2093 +* example: how to run Redis and Pgsql instance on sealos cloud by @cdjianghan in https://github.com/labring/sealos/pull/2104 +* Update frontend Readme. by @zzjin in https://github.com/labring/sealos/pull/2102 +* fix typo by @fengxsong in https://github.com/labring/sealos/pull/2113 +* del:useless go mod by @xiao-jay in https://github.com/labring/sealos/pull/2118 +* cloud-provider-fontend page by @zjy365 in https://github.com/labring/sealos/pull/2094 +* docs example : how to use env by @fanux in https://github.com/labring/sealos/pull/2109 +* fix scp password and support delete scp by @zjy365 in https://github.com/labring/sealos/pull/2119 +* fixed sealos\docs\4.0\i18n\zh-Hans\getting-started\build-example-cloudimage.md by @loda13 in https://github.com/labring/sealos/pull/2124 +* fix infra controller status by @fanux in https://github.com/labring/sealos/pull/2128 +* feat. image hub: datapack rebuild, add finalizer by @lingdie in https://github.com/labring/sealos/pull/2050 +* bug: fixed using ssh private key to login instance by @fanux in https://github.com/labring/sealos/pull/2133 +* bug: fixed parse image name and tag by @muicoder in https://github.com/labring/sealos/pull/2135 +* use user data to permit root login using ssh private key by @fanux in https://github.com/labring/sealos/pull/2136 +* modify spec and add default value by @zjy365 in https://github.com/labring/sealos/pull/2130 +* feat: support registries HA by @fengxsong in https://github.com/labring/sealos/pull/2096 +* fixed cluster operator auto to create kuberentes by @fanux in https://github.com/labring/sealos/pull/2139 +* fixed infra recocile vms by @fanux in https://github.com/labring/sealos/pull/2142 +* fix: delete sealer by @xiaohan1202 in https://github.com/labring/sealos/pull/2150 +* Update README.md, add command for untaint tag when low version by @tanshilingithub in https://github.com/labring/sealos/pull/2149 +* Init support image-hub get&list. by @zzjin in https://github.com/labring/sealos/pull/2143 +* scp modify the style of the details page by @zjy365 in https://github.com/labring/sealos/pull/2141 +* fixed apply cluster on infra by @fanux in https://github.com/labring/sealos/pull/2158 +* wait for infra create when get cluster by @fanux in https://github.com/labring/sealos/pull/2160 +* instead echo to tee by @fanux in https://github.com/labring/sealos/pull/2161 +* Update auto build paths config. by @zzjin in https://github.com/labring/sealos/pull/2162 +* fixed set system disk to 40G by default, the device name must be /dev… by @fanux in https://github.com/labring/sealos/pull/2164 +* Update Controller Readme. by @zzjin in https://github.com/labring/sealos/pull/2168 +* fixed some problem about scp frontend by @zjy365 in https://github.com/labring/sealos/pull/2170 +* encapsulate the infra api by @zjy365 in https://github.com/labring/sealos/pull/2169 +* Dev apisix by @zzjin in https://github.com/labring/sealos/pull/2144 +* Fix the scp timestamp by @zjy365 in https://github.com/labring/sealos/pull/2173 +* refactor(user): using finalizer utils replace it by @cuisongliu in https://github.com/labring/sealos/pull/2172 +* feat: run sealos without root privileges by @fengxsong in https://github.com/labring/sealos/pull/2163 +* Fix: fix aws ec2 price by @xiao-jay in https://github.com/labring/sealos/pull/2174 +* fix: copy empty merged dir anyway by @fengxsong in https://github.com/labring/sealos/pull/2182 +* feat: add describe image api and get root device name from ami. by @whybeyoung in https://github.com/labring/sealos/pull/2178 +* fix: create instance disk size error and adjust user define disk device name by @whybeyoung in https://github.com/labring/sealos/pull/2179 +* fix: kustomize makefile with a little enhance by @whybeyoung in https://github.com/labring/sealos/pull/2185 +* fix: coredns crash issue. by @fengxsong in https://github.com/labring/sealos/pull/2180 +* Refactor ssh copy by @fengxsong in https://github.com/labring/sealos/pull/2186 +* feat: add back create subcommand and support manually unshare by @fengxsong in https://github.com/labring/sealos/pull/2187 +* fix: adjust sealos reset process order by @fengxsong in https://github.com/labring/sealos/pull/2190 +* update readme, add new product show and some descriptions by @fanux in https://github.com/labring/sealos/pull/2188 +* fix: Clusterfile must has right sort of hosts, host[0] must be master… by @gopherWxf in https://github.com/labring/sealos/pull/2191 +* Imagehub refactor: delete controller update delete interface. by @lingdie in https://github.com/labring/sealos/pull/2184 +* fix: remove detach by @xiaohan1202 in https://github.com/labring/sealos/pull/2193 +* enchance: optimizing ssh copy by @fengxsong in https://github.com/labring/sealos/pull/2195 +* go get k8s.io/api@v0.24.3 to fixed build failed by @fanux in https://github.com/labring/sealos/pull/2197 +* Fix infra&cluster operator event pkg. by @zzjin in https://github.com/labring/sealos/pull/2198 +* refactor(user): rename doReconcile to reconcile by @cuisongliu in https://github.com/labring/sealos/pull/2199 +* Dev ci to fix controllers dep. by @zzjin in https://github.com/labring/sealos/pull/2201 +* feat: add more print column like status by @whybeyoung in https://github.com/labring/sealos/pull/2204 +* feat: add finalizer to stop/terminate aws instance. by @whybeyoung in https://github.com/labring/sealos/pull/2196 +* fix: reconcile instance count by @xiaohan1202 in https://github.com/labring/sealos/pull/2203 +* feat: add get volumes api when using get instnaces by @whybeyoung in https://github.com/labring/sealos/pull/2200 +* feat. sealos login hub.sealos.io by kubeconfig. by @lingdie in https://github.com/labring/sealos/pull/2176 +* fix: move IsRootless out of pkg/buildah by @fengxsong in https://github.com/labring/sealos/pull/2207 +* feat. sealos registry hub.sealos.io auth server. by @lingdie in https://github.com/labring/sealos/pull/2177 +* add appstore product show by @fanux in https://github.com/labring/sealos/pull/2208 +* Imagehub.deploy by @lingdie in https://github.com/labring/sealos/pull/2214 +* fix deploy.yaml: add service/hub to workflow. by @lingdie in https://github.com/labring/sealos/pull/2215 +* feature(main): Support configuration file custom vip by @cuisongliu in https://github.com/labring/sealos/pull/2115 +* Hub.deploy by @lingdie in https://github.com/labring/sealos/pull/2216 +* feat: delete key pair by @xiaohan1202 in https://github.com/labring/sealos/pull/2217 +* fix: only run guest command for app type cloudimage by @fengxsong in https://github.com/labring/sealos/pull/2218 +* Remove uuid package to build-in crypto. by @zzjin in https://github.com/labring/sealos/pull/2219 +* Fix go mod caused by #2217 by @zzjin in https://github.com/labring/sealos/pull/2220 +* hub.deploy by @lingdie in https://github.com/labring/sealos/pull/2221 +* fix: reconnect when specified error occur with sftp connection by @fengxsong in https://github.com/labring/sealos/pull/2225 +* fix: replace new line sep by @fengxsong in https://github.com/labring/sealos/pull/2228 +* feature(main): add delete build file scan by @cuisongliu in https://github.com/labring/sealos/pull/2229 +* ci: pin ubuntu version in CI to 20.04 by @SignorMercurio in https://github.com/labring/sealos/pull/2235 +* fix: #2232 force override when the prompted answer is yes by @fengxsong in https://github.com/labring/sealos/pull/2237 +* fix: get the full root command name by @fengxsong in https://github.com/labring/sealos/pull/2236 +* feature(main): support v1 and v1alpha2 version cri by @cuisongliu in https://github.com/labring/sealos/pull/2238 +* feature(main): support sys env by @cuisongliu in https://github.com/labring/sealos/pull/2240 +* feature(main): add build-in env docs by @cuisongliu in https://github.com/labring/sealos/pull/2241 +* fix: set tls-verify option default to false by @fengxsong in https://github.com/labring/sealos/pull/2239 +* Fix: image hub delete useless image detail info. by @lingdie in https://github.com/labring/sealos/pull/2243 +* feature(main): add kubernetes support and changelog by @cuisongliu in https://github.com/labring/sealos/pull/2242 +* Pin ubuntu version in Release workflow, use correct goreleaser args for sealctl and add release checksum by @SignorMercurio in https://github.com/labring/sealos/pull/2249 +* ci: adjust files to be checksumed by @SignorMercurio in https://github.com/labring/sealos/pull/2250 + +## New Contributors +* @lingdie made their first contribution in https://github.com/labring/sealos/pull/1988 +* @wuxming made their first contribution in https://github.com/labring/sealos/pull/1994 +* @zjy365 made their first contribution in https://github.com/labring/sealos/pull/2004 +* @cdjianghan made their first contribution in https://github.com/labring/sealos/pull/2104 +* @xiaohan1202 made their first contribution in https://github.com/labring/sealos/pull/2150 +* @tanshilingithub made their first contribution in https://github.com/labring/sealos/pull/2149 +* @gopherWxf made their first contribution in https://github.com/labring/sealos/pull/2191 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.4-rc1...v4.1.4-rc2 diff --git a/CHANGELOG/CHANGELOG-4.1.4-rc3.md b/CHANGELOG/CHANGELOG-4.1.4-rc3.md new file mode 100644 index 000000000000..687dd363a789 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.4-rc3.md @@ -0,0 +1,64 @@ +- [v4.1.4-rc3](#v414-rc3httpsgithubcomlabringsealosreleasestagv414-rc3) + - [Downloads for v4.1.4-rc3](#downloads-for-v414-rc3) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.4-rc3](#changelog-since-v414-rc2) + + +# [v4.1.4-rc3](https://github.com/labring/sealos/releases/tag/v4.1.4-rc3) + +## Downloads for v4.1.4-rc3 + + +### Source Code + +filename | +-------- | +[v4.1.4-rc3.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.4-rc3.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.4-rc3_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc3/sealos_4.1.4-rc3_linux_amd64.tar.gz) | +[sealos_4.1.4-rc3_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc3/sealos_4.1.4-rc3_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc3/sealos_4.1.4-rc3_linux_amd64.tar.gz && tar -zxvf sealos_4.1.4-rc3_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc3/sealos_4.1.4-rc3_linux_arm64.tar.gz && tar -zxvf sealos_4.1.4-rc3_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.4-rc2 + +### What's Changed + +* use admisswebhook sdk to mutate and validate image create, update, delete. by @lingdie in https://github.com/labring/sealos/pull/2244 +* Enhancement: imagehub webhook get usernamespace by env. by @lingdie in https://github.com/labring/sealos/pull/2253 +* debug: more verbose logging for scp by @fengxsong in https://github.com/labring/sealos/pull/2254 +* feat: add reconcile disks and modify infra_types by @xiaohan1202 in https://github.com/labring/sealos/pull/2248 +* feat. add repo webhook to set default labels and validate it. by @lingdie in https://github.com/labring/sealos/pull/2257 +* feature(main): fix changelogs by @cuisongliu in https://github.com/labring/sealos/pull/2251 +* Update SEALOS_SYS_KUBE_VERSION usage. by @zzjin in https://github.com/labring/sealos/pull/2264 +* feature(main): env compatible by @cuisongliu in https://github.com/labring/sealos/pull/2265 +* feature(main): delete warn by @cuisongliu in https://github.com/labring/sealos/pull/2269 + + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.4-rc2...v4.1.4-rc3 + + + diff --git a/CHANGELOG/CHANGELOG-4.1.4-rc4.md b/CHANGELOG/CHANGELOG-4.1.4-rc4.md new file mode 100644 index 000000000000..069d1311ad88 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.4-rc4.md @@ -0,0 +1,89 @@ +- [v4.1.4-rc4](#v414-rc4httpsgithubcomlabringsealosreleasestagv414-rc4) + - [Downloads for v4.1.4-rc4](#downloads-for-v414-rc4) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.4-rc3](#changelog-since-v414-rc3) + - [NewContributors](#new-contributors) + + +# [v4.1.4-rc4](https://github.com/labring/sealos/releases/tag/v4.1.4-rc4) + +## Downloads for v4.1.4-rc4 + + +### Source Code + +filename | +-------- | +[v4.1.4-rc4.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.4-rc4.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.4-rc4_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc4/sealos_4.1.4-rc4_linux_amd64.tar.gz) | +[sealos_4.1.4-rc4_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4-rc4/sealos_4.1.4-rc4_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc4/sealos_4.1.4-rc4_linux_amd64.tar.gz && tar -zxvf sealos_4.1.4-rc4_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4-rc4/sealos_4.1.4-rc4_linux_arm64.tar.gz && tar -zxvf sealos_4.1.4-rc4_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.4-rc3 + +### What's Changed +* Move applications manifests dir by @zzjin in https://github.com/labring/sealos/pull/2274 +* feat. imagehub webhook fix. by @lingdie in https://github.com/labring/sealos/pull/2260 +* fix: skip handling app type image while scaling cluster by @fengxsong in https://github.com/labring/sealos/pull/2273 +* Update terminal frontend, use user‘s namespace. by @zzjin in https://github.com/labring/sealos/pull/2270 +* modify the scp yaml field by @zjy365 in https://github.com/labring/sealos/pull/2261 +* fix: only set default --platform flag when it's not changed by @fengxsong in https://github.com/labring/sealos/pull/2276 +* mark tls-verify flag hidden by @fengxsong in https://github.com/labring/sealos/pull/2279 +* feature(main): change apply logic by @cuisongliu in https://github.com/labring/sealos/pull/2278 +* feature(main): add rc3 change_log by @cuisongliu in https://github.com/labring/sealos/pull/2277 +* add action operator implement and crd implement by @cdjianghan in https://github.com/labring/sealos/pull/2247 +* feat: deploy terminal in user's namespace and support apisix ingress by @gitccl in https://github.com/labring/sealos/pull/2280 +* refactor buildah interface, arguments as flagsetters by @fengxsong in https://github.com/labring/sealos/pull/2281 +* add registry deply config. by @lingdie in https://github.com/labring/sealos/pull/2275 +* feature(main): add cert cmd for append certs by @cuisongliu in https://github.com/labring/sealos/pull/2116 +* bugfix: Apply twice will call initCluster twice that causes kubelet port occupied. by @mond77 in https://github.com/labring/sealos/pull/2292 +* build pgsql & add pgsql service api by @zjy365 in https://github.com/labring/sealos/pull/2256 +* fix:fix queryPrice error by @xiao-jay in https://github.com/labring/sealos/pull/2293 +* fix: ensure consistency when crash & fix some bugs by @xiaohan1202 in https://github.com/labring/sealos/pull/2259 +* fix: more friendly error output when running rootless mode by @fengxsong in https://github.com/labring/sealos/pull/2287 +* Fix crypto usage. by @zzjin in https://github.com/labring/sealos/pull/2296 +* add pgsql users & delete iops througput by @zjy365 in https://github.com/labring/sealos/pull/2297 +* fix:payment add defaultcallbackURL by @xiao-jay in https://github.com/labring/sealos/pull/2300 +* feat.add image keywords as repo label. by @lingdie in https://github.com/labring/sealos/pull/2291 +* feature(main): change default archive is oci by @cuisongliu in https://github.com/labring/sealos/pull/2304 +* update datapacks rbac. by @lingdie in https://github.com/labring/sealos/pull/2303 +* Update registry doc by @zzjin in https://github.com/labring/sealos/pull/2298 +* feature(main): add status cmd for post check by @cuisongliu in https://github.com/labring/sealos/pull/2299 +* feat. add org muator, add admit for kube-system group by @lingdie in https://github.com/labring/sealos/pull/2309 +* fix: set SkipTLSVerify to true for SystemConext by @fengxsong in https://github.com/labring/sealos/pull/2316 +* fix infra status & add loading created by @zjy365 in https://github.com/labring/sealos/pull/2307 +* feature: add merge feature by @cuisongliu in https://github.com/labring/sealos/pull/2283 +* feat: refactor metering ,support Third-party resource access and idempotent by @xiao-jay in https://github.com/labring/sealos/pull/2258 +* feature(main): fix etcd config by @cuisongliu in https://github.com/labring/sealos/pull/2323 + +## New Contributors +* @mond77 made their first contribution in https://github.com/labring/sealos/pull/2292 + + + + diff --git a/CHANGELOG/CHANGELOG-4.1.4.md b/CHANGELOG/CHANGELOG-4.1.4.md new file mode 100644 index 000000000000..a8dbb2c76550 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.4.md @@ -0,0 +1,67 @@ +- [v4.1.4](#v414httpsgithubcomlabringsealosreleasestagv414) + - [Downloads for v4.1.4](#downloads-for-v414) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.4](#changelog-since-v414-rc4) + + +# [v4.1.4](https://github.com/labring/sealos/releases/tag/v4.1.4) + +## Downloads for v4.1.4 + + +### Source Code + +filename | +-------- | +[v4.1.4.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.4.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.4_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_amd64.tar.gz) | +[sealos_4.1.4_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_amd64.tar.gz && tar -zxvf sealos_4.1.4_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.4/sealos_4.1.4_linux_arm64.tar.gz && tar -zxvf sealos_4.1.4_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.4-rc4 + +### What's Changed + +* feature(main): add changelog by @cuisongliu in https://github.com/labring/sealos/pull/2325 +* feature(main): fix ci error by @cuisongliu in https://github.com/labring/sealos/pull/2326 +* feature(main): change calico docs by @cuisongliu in https://github.com/labring/sealos/pull/2327 +* add ImageCRDBuilder implement by @cdjianghan in https://github.com/labring/sealos/pull/2311 +* better code : use config.GetAllCredentials func to place CheckLoginStatus func in imagecrdbuilder by @cdjianghan in https://github.com/labring/sealos/pull/2328 +* update download sealos command by @fanux in https://github.com/labring/sealos/pull/2324 +* change some func name to better understand add Success and Faild output by @cdjianghan in https://github.com/labring/sealos/pull/2332 +* feature(main): fix registry find manifest logic by @cuisongliu in https://github.com/labring/sealos/pull/2314 +* add infra e2e test by @xiaohan1202 in https://github.com/labring/sealos/pull/2331 +* imagecrbuilder better code by @cdjianghan in https://github.com/labring/sealos/pull/2334 +* feature(main): add cert for old version by @cuisongliu in https://github.com/labring/sealos/pull/2333 +* add size to imagegrid. fix repository reconcile. by @lingdie in https://github.com/labring/sealos/pull/2337 +* fix image cr doc by @cdjianghan in https://github.com/labring/sealos/pull/2338 +* docs: add hub.sealos.cn and imagehub CRD desigin docs. by @lingdie in https://github.com/labring/sealos/pull/2131 +* Docs: add build template docs by @zzjin in https://github.com/labring/sealos/pull/2335 +* Fixed the application window switch problem by @zjy365 in https://github.com/labring/sealos/pull/2342 + + diff --git a/CHANGELOG/CHANGELOG-4.1.5-alpha1.md b/CHANGELOG/CHANGELOG-4.1.5-alpha1.md new file mode 100644 index 000000000000..f5503c16f29f --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5-alpha1.md @@ -0,0 +1,118 @@ +- [v4.1.5-alpha1](#v415-alpha1httpsgithubcomlabringsealosreleasestagv415-alpha1) + - [Downloads for v4.1.5-alpha1](#downloads-for-v415-alpha1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5-alpha1](#changelog-since-v414) + - [NewContributors](#new-contributors) + + +# [v4.1.5-alpha1](https://github.com/labring/sealos/releases/tag/v4.1.5-alpha1) + +## Downloads for v4.1.5-alpha1 + + +### Source Code + +filename | +-------- | +[v4.1.5-alpha1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5-alpha1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5-alpha1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-alpha1/sealos_4.1.5-alpha1_linux_amd64.tar.gz) | +[sealos_4.1.5-alpha1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-alpha1/sealos_4.1.5-alpha1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-alpha1/sealos_4.1.5-alpha1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5-alpha1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-alpha1/sealos_4.1.5-alpha1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5-alpha1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.4 + +### What's Changed +* Update terminal image tag by @zzjin in https://github.com/labring/sealos/pull/2345 +* init image hub page & image hub api by @zjy365 in https://github.com/labring/sealos/pull/2321 +* add discord link by @fanux in https://github.com/labring/sealos/pull/2350 +* Fix metering go module by @zzjin in https://github.com/labring/sealos/pull/2346 +* docs(main): add changelog for sealos by @cuisongliu in https://github.com/labring/sealos/pull/2351 +* feat. imagehub export func and add URL to image_types. by @lingdie in https://github.com/labring/sealos/pull/2353 +* Add static file cdn by @zzjin in https://github.com/labring/sealos/pull/2355 +* Feature: upgrade the cluster by @mond77 in https://github.com/labring/sealos/pull/2340 +* update build cloud image docs by @willzhang in https://github.com/labring/sealos/pull/2320 +* refactor: implement exponential backoff retry for ssh connect by @fengxsong in https://github.com/labring/sealos/pull/2358 +* Fixed Firefox scrollbar & switch app problem by @zjy365 in https://github.com/labring/sealos/pull/2362 +* add ssh-privatekey secret to prevent ssh problems by @xiaohan1202 in https://github.com/labring/sealos/pull/2371 +* add infra user guide by @fanux in https://github.com/labring/sealos/pull/2373 +* Fix ci semgrep by @zzjin in https://github.com/labring/sealos/pull/2374 +* feat. imagehub: add size to tagdata. by @lingdie in https://github.com/labring/sealos/pull/2376 +* feat.imagehub mutate image cr. by @lingdie in https://github.com/labring/sealos/pull/2377 +* bugfix: 'sealos run' cannot scale cluster by @mond77 in https://github.com/labring/sealos/pull/2366 +* bugfix: sealos run error by @mond77 in https://github.com/labring/sealos/pull/2382 +* New Cloud doc location. by @zzjin in https://github.com/labring/sealos/pull/2391 +* Fix doc typo. by @zzjin in https://github.com/labring/sealos/pull/2392 +* bugfix: set kubeconfig server by @mond77 in https://github.com/labring/sealos/pull/2393 +* feature(main): fix cert for init masters by @cuisongliu in https://github.com/labring/sealos/pull/2383 +* add clean all aws key pairs by @fanux in https://github.com/labring/sealos/pull/2381 +* refactor: rename apply pkg by @cuisongliu in https://github.com/labring/sealos/pull/2397 +* fix desktop switching app & add error page and skeleton screen by @zjy365 in https://github.com/labring/sealos/pull/2390 +* bugfix: add node failure by @mond77 in https://github.com/labring/sealos/pull/2395 +* Fix typos by @loda13 in https://github.com/labring/sealos/pull/2403 +* refactor: autoLoad apiServer ip and port by @cuisongliu in https://github.com/labring/sealos/pull/2401 +* fix: modify delete logic to support cloud provider by @xiaohan1202 in https://github.com/labring/sealos/pull/2402 +* docs:pull private image tutorial in cloud by @xiao-jay in https://github.com/labring/sealos/pull/2404 +* feat: refactor buildah's inspect method so we can inspect remote image by @fengxsong in https://github.com/labring/sealos/pull/2405 +* fix: export function FormatReferenceWithTransportName by @fengxsong in https://github.com/labring/sealos/pull/2408 +* docs(main): fix gen before pull image and inspect it by @cuisongliu in https://github.com/labring/sealos/pull/2367 +* fix: inspect image in gen command by @fengxsong in https://github.com/labring/sealos/pull/2411 +* add image detail skeleton by @zjy365 in https://github.com/labring/sealos/pull/2400 +* add scp document by @xiaohan1202 in https://github.com/labring/sealos/pull/2417 +* Pgsql Product Document by @cdjianghan in https://github.com/labring/sealos/pull/2415 +* enable cloud provider to scale cluster by @xiaohan1202 in https://github.com/labring/sealos/pull/2387 +* fix pg doc img bug by @cdjianghan in https://github.com/labring/sealos/pull/2419 +* feat: apply feature #2405 on sealos inspect command by @fengxsong in https://github.com/labring/sealos/pull/2416 +* Pgsql readme by @cdjianghan in https://github.com/labring/sealos/pull/2420 +* add scp english docs by @xiaohan1202 in https://github.com/labring/sealos/pull/2424 +* add terminal user guide by @fanux in https://github.com/labring/sealos/pull/2421 +* write a pgsql Stress Test in /test by @cdjianghan in https://github.com/labring/sealos/pull/2423 +* sealos cloud provider version upgraded by @zjy365 in https://github.com/labring/sealos/pull/2426 +* Update README.md by @sxxpqp in https://github.com/labring/sealos/pull/2414 +* update sealos version to 4.1.4 by @fanux in https://github.com/labring/sealos/pull/2429 +* docs. imagehub docs by @lingdie in https://github.com/labring/sealos/pull/2425 +* add terminal english docs by @fanux in https://github.com/labring/sealos/pull/2427 +* docs:add metering design docs by @xiao-jay in https://github.com/labring/sealos/pull/2121 +* pgsql form validation by @zjy365 in https://github.com/labring/sealos/pull/2428 +* app header add document link by @zjy365 in https://github.com/labring/sealos/pull/2431 +* Bug. fix latest tag. by @lingdie in https://github.com/labring/sealos/pull/2435 +* Add cname doc by @zzjin in https://github.com/labring/sealos/pull/2441 +* update userData to support multiple AMIs by @xiaohan1202 in https://github.com/labring/sealos/pull/2440 +* add sealos intro and quick start by @fanux in https://github.com/labring/sealos/pull/2436 +* Need Discussion: Fix sealos push require imagehub module. by @zzjin in https://github.com/labring/sealos/pull/2442 +* image hub list paging & pagination component by @zjy365 in https://github.com/labring/sealos/pull/2437 +* feat: add account recharge way by @xiao-jay in https://github.com/labring/sealos/pull/2432 +* Add sealos cloud docs link in readme by @fanux in https://github.com/labring/sealos/pull/2445 + +## New Contributors +* @sxxpqp made their first contribution in https://github.com/labring/sealos/pull/2414 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.4...v4.1.5-alpha1 + + + + diff --git a/CHANGELOG/CHANGELOG-4.1.5-alpha2.md b/CHANGELOG/CHANGELOG-4.1.5-alpha2.md new file mode 100644 index 000000000000..675c950e13fe --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5-alpha2.md @@ -0,0 +1,82 @@ +- [v4.1.5-alpha2](#v415-alpha2httpsgithubcomlabringsealosreleasestagv415-alpha2) + - [Downloads for v4.1.5-alpha2](#downloads-for-v415-alpha2) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5-alpha2](#changelog-since-) + - [NewContributors](#new-contributors) + + +# [v4.1.5-alpha2](https://github.com/labring/sealos/releases/tag/v4.1.5-alpha2) + +## Downloads for v4.1.5-alpha2 + + +### Source Code + +filename | +-------- | +[v4.1.5-alpha2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5-alpha2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5-alpha2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-alpha2/sealos_4.1.5-alpha2_linux_amd64.tar.gz) | +[sealos_4.1.5-alpha2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-alpha2/sealos_4.1.5-alpha2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-alpha2/sealos_4.1.5-alpha2_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5-alpha2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-alpha2/sealos_4.1.5-alpha2_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5-alpha2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.5-alpha1 + +### What's Changed +* fix(main): fix inspect local image by @cuisongliu in https://github.com/labring/sealos/pull/2410 +* fix(main): fix ci for buildx by @cuisongliu in https://github.com/labring/sealos/pull/2446 +* docs(main): fix 404 for changelog addr by @cuisongliu in https://github.com/labring/sealos/pull/2448 +* docs(main): fix sealos design by @cuisongliu in https://github.com/labring/sealos/pull/2449 +* change icon color & delete fingerpoint by @zjy365 in https://github.com/labring/sealos/pull/2457 +* Fixed application drag boundaries by @zjy365 in https://github.com/labring/sealos/pull/2458 +* Update cloud docs. by @zzjin in https://github.com/labring/sealos/pull/2462 +* update docs to v4.1.4 by @fanux in https://github.com/labring/sealos/pull/2461 +* fixed issues with creating infra by @zjy365 in https://github.com/labring/sealos/pull/2463 +* fix: Metering fix bug by @xiao-jay in https://github.com/labring/sealos/pull/2460 +* bugfix: kubelet cgroup driver not match containerd by @xiao-jay in https://github.com/labring/sealos/pull/2466 +* feat: keypair garbage collection by @xiaohan1202 in https://github.com/labring/sealos/pull/2467 +* bugfix: nodename should be lower case by @mond77 in https://github.com/labring/sealos/pull/2474 +* bugfix by @mond77 in https://github.com/labring/sealos/pull/2478 +* Add desktio UX by @c121914yu in https://github.com/labring/sealos/pull/2477 +* fix: configure default clustername by @xiaohan1202 in https://github.com/labring/sealos/pull/2476 +* feat:add deduction details in accountbalance by @xiao-jay in https://github.com/labring/sealos/pull/2480 +* add how to run bytebase on sealos cloud by @fanux in https://github.com/labring/sealos/pull/2472 +* feat: add aws instance gc by @xiaohan1202 in https://github.com/labring/sealos/pull/2481 +* update sealos cloud intro by @fanux in https://github.com/labring/sealos/pull/2484 +* update run bytebase on sealos cloud by @fanux in https://github.com/labring/sealos/pull/2491 +* fix:go mod tidy error by @xiao-jay in https://github.com/labring/sealos/pull/2490 +* Update to go version 1.20 by @zzjin in https://github.com/labring/sealos/pull/2488 +* fix a blinking delete dialog by @zjy365 in https://github.com/labring/sealos/pull/2486 + +## New Contributors +* @c121914yu made their first contribution in https://github.com/labring/sealos/pull/2477 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5-alpha1...v4.1.5-alpha2 + + + + diff --git a/CHANGELOG/CHANGELOG-4.1.5-rc1.md b/CHANGELOG/CHANGELOG-4.1.5-rc1.md new file mode 100644 index 000000000000..2249e9ee9780 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5-rc1.md @@ -0,0 +1,88 @@ +- [v4.1.5-rc1](#v415-rc1httpsgithubcomlabringsealosreleasestagv415-rc1) + - [Downloads for v4.1.5-rc1](#downloads-for-v415-rc1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5-rc1](#changelog-since-v415-alpha2) + - [NewContributors](#new-contributors) + + +# [v4.1.5-rc1](https://github.com/labring/sealos/releases/tag/v4.1.5-rc1) + +## Downloads for v4.1.5-rc1 + + +### Source Code + +filename | +-------- | +[v4.1.5-rc1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5-rc1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5-rc1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc1/sealos_4.1.5-rc1_linux_amd64.tar.gz) | +[sealos_4.1.5-rc1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc1/sealos_4.1.5-rc1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc1/sealos_4.1.5-rc1_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5-rc1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc1/sealos_4.1.5-rc1_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5-rc1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.5-alpha2 + +### What's Changed +* feature(main): add changelog for sealos by @cuisongliu in https://github.com/labring/sealos/pull/2495 +* docs: fix mistakes in the English version of cloud docs by @ShuaoZhang in https://github.com/labring/sealos/pull/2493 +* add a period at the end of each line by @fanux in https://github.com/labring/sealos/pull/2499 +* feature(main): add changelog for sealos by @cuisongliu in https://github.com/labring/sealos/pull/2498 +* add a period at the end of each line by @fanux in https://github.com/labring/sealos/pull/2500 +* fix(desktop): drag icon, first login fail by @c121914yu in https://github.com/labring/sealos/pull/2483 +* feat. add repo.spec.IsPrivate by @lingdie in https://github.com/labring/sealos/pull/2504 +* bug. fix cluster status. by @lingdie in https://github.com/labring/sealos/pull/2506 +* feat(desktop): double click, open app by @c121914yu in https://github.com/labring/sealos/pull/2512 +* feat:split user and account by @xiao-jay in https://github.com/labring/sealos/pull/2470 +* feat: add ami ConfigMap && allow changable sealos version by @xiaohan1202 in https://github.com/labring/sealos/pull/2502 +* add kubernetes version cri version output in sealos verison cmd by @cdjianghan in https://github.com/labring/sealos/pull/2511 +* apply process optimization by @fengxsong in https://github.com/labring/sealos/pull/2520 +* fix: account image build ci by @xiao-jay in https://github.com/labring/sealos/pull/2522 +* docs:add english pull private image in sealos cloud docs by @xiao-jay in https://github.com/labring/sealos/pull/2521 +* fix: reduce error output by @fengxsong in https://github.com/labring/sealos/pull/2525 +* feat scp add mirror selection and validation by @zjy365 in https://github.com/labring/sealos/pull/2517 +* bug: optimize installApp by @lingdie in https://github.com/labring/sealos/pull/2519 +* fix: secret name bug by @xiaohan1202 in https://github.com/labring/sealos/pull/2536 +* fix: ignore cancel error by @fengxsong in https://github.com/labring/sealos/pull/2543 +* fix: sealos download by @xiaohan1202 in https://github.com/labring/sealos/pull/2542 +* fix sealos version err return bug by @cdjianghan in https://github.com/labring/sealos/pull/2545 +* feat: support arm arch && new version cmd by @xiaohan1202 in https://github.com/labring/sealos/pull/2546 +* make `getGuestCmd` handle RootfsImage by @dinoallo in https://github.com/labring/sealos/pull/2550 +* docs(main): fix deploy for user,account,terminal by @cuisongliu in https://github.com/labring/sealos/pull/2537 +* fix: inspect function support resolving shortname by @24sama in https://github.com/labring/sealos/pull/2551 +* feat: add compatible Aliyun interface by @xiaohan1202 in https://github.com/labring/sealos/pull/2552 +* remove duplicate pull image by @huiwq1990 in https://github.com/labring/sealos/pull/2492 + +## New Contributors +* @ShuaoZhang made their first contribution in https://github.com/labring/sealos/pull/2493 +* @dinoallo made their first contribution in https://github.com/labring/sealos/pull/2550 +* @24sama made their first contribution in https://github.com/labring/sealos/pull/2551 +* @huiwq1990 made their first contribution in https://github.com/labring/sealos/pull/2492 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5-alpha2...v4.1.5-rc1 + + + diff --git a/CHANGELOG/CHANGELOG-4.1.5-rc2.md b/CHANGELOG/CHANGELOG-4.1.5-rc2.md new file mode 100644 index 000000000000..9117ec3f8df9 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5-rc2.md @@ -0,0 +1,84 @@ +- [v4.1.5-rc2](#v415-rc2) + - [Downloads for v4.1.5-rc2](#downloads-for-v415-rc2) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5-rc2](#changelog-since-v415-rc1) + - [NewContributors](#new-contributors) + + +# [v4.1.5-rc2](https://github.com/labring/sealos/releases/tag/v4.1.5-rc2) + +## Downloads for v4.1.5-rc2 + + +### Source Code + +filename | +-------- | +[v4.1.5-rc2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5-rc2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5-rc2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc2/sealos_4.1.5-rc2_linux_amd64.tar.gz) | +[sealos_4.1.5-rc2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc2/sealos_4.1.5-rc2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc2/sealos_4.1.5-rc2_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5-rc2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc2/sealos_4.1.5-rc2_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5-rc2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.5-rc1 + +### What's Changed +* docs(main): add action and images to link by @cuisongliu in https://github.com/labring/sealos/pull/2555 +* docs: update develop guide by @aFlyBird0 in https://github.com/labring/sealos/pull/2556 +* Add roadmap link in readme by @fanux in https://github.com/labring/sealos/pull/2554 +* docs(main) add changelog by @GaoQun825 in https://github.com/labring/sealos/pull/2557 +* feat. add repo public for hubauth svc. by @lingdie in https://github.com/labring/sealos/pull/2518 +* Feat. add downloadCount in repo status by @lingdie in https://github.com/labring/sealos/pull/2563 +* refactor: adjust the order of merging kubeadm configs by @fengxsong in https://github.com/labring/sealos/pull/2567 +* Ci e2e metering by @xiao-jay in https://github.com/labring/sealos/pull/2572 +* ci(main): fix pr controllers build by @cuisongliu in https://github.com/labring/sealos/pull/2564 +* ci(main): fix lic,converage,format-code ci by @cuisongliu in https://github.com/labring/sealos/pull/2573 +* Update controllers module usage. by @zzjin in https://github.com/labring/sealos/pull/2574 +* remove olg unused `pkg/infra` by @zzjin in https://github.com/labring/sealos/pull/2581 +* ci(main): fix test ci for inspect by @cuisongliu in https://github.com/labring/sealos/pull/2582 +* fix: error in inspect function when the image is in format "docker-archive:./xxx.tar" by @24sama in https://github.com/labring/sealos/pull/2579 +* Fix: cluster updateStatus. by @lingdie in https://github.com/labring/sealos/pull/2588 +* fix: mark buildah's flags as hidden to prevent incorrect use by @fengxsong in https://github.com/labring/sealos/pull/2590 +* docs image tag error by @xiao-jay in https://github.com/labring/sealos/pull/2591 +* feat: add Failed status for cloud provider by @xiaohan1202 in https://github.com/labring/sealos/pull/2566 +* init status modify condition by @lingdie in https://github.com/labring/sealos/pull/2597 +* Feat: add command conditions in cluster status. by @lingdie in https://github.com/labring/sealos/pull/2592 +* add cluster concurrent reconciles opts by @ghostloda in https://github.com/labring/sealos/pull/2601 +* feat(main): fix docker calico image by @cuisongliu in https://github.com/labring/sealos/pull/2613 +* docs(main): add docker pulls by @cuisongliu in https://github.com/labring/sealos/pull/2610 +* feat(main): fix merge for cri and socket by @cuisongliu in https://github.com/labring/sealos/pull/2616 + +## New Contributors +* @aFlyBird0 made their first contribution in https://github.com/labring/sealos/pull/2556 +* @GaoQun825 made their first contribution in https://github.com/labring/sealos/pull/2557 +* @ghostloda made their first contribution in https://github.com/labring/sealos/pull/2601 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5-rc1...v4.1.5-rc2 + + + + diff --git a/CHANGELOG/CHANGELOG-4.1.5-rc3.md b/CHANGELOG/CHANGELOG-4.1.5-rc3.md new file mode 100644 index 000000000000..84ca8755112d --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5-rc3.md @@ -0,0 +1,70 @@ +- [v4.1.5-rc3](#v415-rc3) + - [Downloads for v4.1.5-rc3](#downloads-for-v415-rc3) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5-rc3](#changelog-since-v415-rc2) + + +# [v4.1.5-rc3](https://github.com/labring/sealos/releases/tag/v4.1.5-rc3) + +## Downloads for v4.1.5-rc3 + + +### Source Code + +filename | +-------- | +[v4.1.5-rc3.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5-rc3.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5-rc3_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc3/sealos_4.1.5-rc3_linux_amd64.tar.gz) | +[sealos_4.1.5-rc3_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5-rc3/sealos_4.1.5-rc3_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc3/sealos_4.1.5-rc3_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5-rc3_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5-rc3/sealos_4.1.5-rc3_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5-rc3_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.5-rc2 + +### What's Changed +* feat(main): add changelog by @cuisongliu in https://github.com/labring/sealos/pull/2618 +* feat(main): add logger for registry and remote shell by @cuisongliu in https://github.com/labring/sealos/pull/2617 +* fix(auth): replace user id with nanoId by @Abingcbc in https://github.com/labring/sealos/pull/2443 +* Fix image pull policy by @fengxsong in https://github.com/labring/sealos/pull/2623 +* feat(main): add registry imageID by @cuisongliu in https://github.com/labring/sealos/pull/2608 +* Drop semgrep lint text/template for use. by @zzjin in https://github.com/labring/sealos/pull/2626 +* Drop semgrep 2 lint for common usage. (#2626) by @zzjin in https://github.com/labring/sealos/pull/2628 +* chore(deps): bump kubernetes version by @fengxsong in https://github.com/labring/sealos/pull/2629 +* feat: app refactor by @lingdie in https://github.com/labring/sealos/pull/2633 +* remove "list namespace" privileges from clusterroles created by the user controller by @dinoallo in https://github.com/labring/sealos/pull/2637 +* Feat: init app, add app data, add workflow, add app to go.work by @lingdie in https://github.com/labring/sealos/pull/2638 +* feat(main): delete upx ci by @cuisongliu in https://github.com/labring/sealos/pull/2640 +* Fix: fix app deploy.yaml, set rep=0. by @lingdie in https://github.com/labring/sealos/pull/2642 +* feat: refactor imagecrbuilder, add cr-option flag. by @lingdie in https://github.com/labring/sealos/pull/2620 +* ci(main): fix note using sealos.io by @cuisongliu in https://github.com/labring/sealos/pull/2647 +* fix: inspect with id #2635 by @fengxsong in https://github.com/labring/sealos/pull/2639 +* ci(main): fix scp error by @cuisongliu in https://github.com/labring/sealos/pull/2636 + + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5-rc2...v4.1.5-rc3 + + diff --git a/CHANGELOG/CHANGELOG-4.1.5.md b/CHANGELOG/CHANGELOG-4.1.5.md new file mode 100644 index 000000000000..e619fa77ad1d --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.5.md @@ -0,0 +1,75 @@ +- [v4.1.5](#v415httpsgithubcomlabringsealosreleasestagv415) + - [Downloads for v4.1.5](#downloads-for-v415) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5](#changelog-since-v415-rc3) + - [NewContributors](#new-contributors) + + +# [v4.1.5](https://github.com/labring/sealos/releases/tag/v4.1.5) + +## Downloads for v4.1.5 + + +### Source Code + +filename | +-------- | +[v4.1.5.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.5.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.5_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5/sealos_4.1.5_linux_amd64.tar.gz) | +[sealos_4.1.5_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.5/sealos_4.1.5_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5/sealos_4.1.5_linux_amd64.tar.gz && tar -zxvf sealos_4.1.5_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.5/sealos_4.1.5_linux_arm64.tar.gz && tar -zxvf sealos_4.1.5_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.5-rc3 + +### What's Changed +* ci(main): fix merge error by @cuisongliu in https://github.com/labring/sealos/pull/2648 +* Update false semgrep of `missing-user` by @zzjin in https://github.com/labring/sealos/pull/2650 +* feat desktop supports app install by @zjy365 in https://github.com/labring/sealos/pull/2645 +* feat: desktop app SDK and sdk using demo #2547 by @c121914yu in https://github.com/labring/sealos/pull/2549 +* ci(main): add changelog by @cuisongliu in https://github.com/labring/sealos/pull/2649 +* Fix ENTRYPOINT using exec form by @zzjin in https://github.com/labring/sealos/pull/2652 +* Fix image command error by @yangchuansheng in https://github.com/labring/sealos/pull/2656 +* Replace `github.com/pkg/errors` with builtin `errors` by @zzjin in https://github.com/labring/sealos/pull/2658 +* Remove not used package by @zzjin in https://github.com/labring/sealos/pull/2659 +* feat: implement aliyun interface by @xiaohan1202 in https://github.com/labring/sealos/pull/2627 +* fix: same issue #2623 again by @fengxsong in https://github.com/labring/sealos/pull/2666 +* Update module(special for service/hub) by @zzjin in https://github.com/labring/sealos/pull/2664 +* fix version typo by @zzjin in https://github.com/labring/sealos/pull/2665 +* update pravite registry docs by @willzhang in https://github.com/labring/sealos/pull/2672 +* fix: delete local file by @c121914yu in https://github.com/labring/sealos/pull/2662 +* fix(main): fix merge and hidden flags by @cuisongliu in https://github.com/labring/sealos/pull/2667 +* Use lock to ensure safe build configuration under goroutine. by @bxy4543 in https://github.com/labring/sealos/pull/2673 +* better sealos version by @cdjianghan in https://github.com/labring/sealos/pull/2679 +* fix(main): delete pkg/infra code by @cuisongliu in https://github.com/labring/sealos/pull/2674 + +## New Contributors +* @bxy4543 made their first contribution in https://github.com/labring/sealos/pull/2673 + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5-rc3...v4.1.5 + + diff --git a/CHANGELOG/CHANGELOG-4.1.7.md b/CHANGELOG/CHANGELOG-4.1.7.md new file mode 100644 index 000000000000..fc1dc21910c1 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.1.7.md @@ -0,0 +1,55 @@ +- [v4.1.7](#v417) + - [Downloads for v4.1.7](#downloads-for-v417) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.7](#changelog-since-) + - [NewContributors](#new-contributors) + + +# [v4.1.7](https://github.com/labring/sealos/releases/tag/v4.1.7) + +## Downloads for v4.1.7 + + +### Source Code + +filename | +-------- | +[v4.1.7.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.1.7.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.1.7_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.7/sealos_4.1.7_linux_amd64.tar.gz) | +[sealos_4.1.7_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.1.7/sealos_4.1.7_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.7/sealos_4.1.7_linux_amd64.tar.gz && tar -zxvf sealos_4.1.7_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.1.7/sealos_4.1.7_linux_arm64.tar.gz && tar -zxvf sealos_4.1.7_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + + +## Changelog since v4.1.7 + +### What's Changed +* fix(main): delete node not exec clean.sh by @cuisongliu in https://github.com/labring/sealos/pull/2759 + + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.1.5...v4.1.7 + + diff --git a/CHANGELOG/CHANGELOG-4.2.0-alpha1.md b/CHANGELOG/CHANGELOG-4.2.0-alpha1.md new file mode 100644 index 000000000000..baba0981107b --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.0-alpha1.md @@ -0,0 +1,133 @@ +- [v4.2.0-alpha1](#v420-alpha1) + - [Downloads for v4.2.0-alpha1](#downloads-for-v420-alpha1) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.1.5](#changelog-since-v415) + +# [v4.2.0-alpha1](https://github.com/labring/sealos/releases/tag/v4.2.0-alpha1) + +## Downloads for v4.2.0-alpha1 + +### Source Code + +filename | +-------- | +[v4.2.0-alpha1.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.2.0-alpha1.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.2.0-alpha1_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.2.0-alpha1/sealos_4.2.0-alpha1_linux_amd64.tar.gz) | +[sealos_4.2.0-alpha1_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.2.0-alpha1/sealos_4.2.0-alpha1_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0-alpha1/sealos_4.2.0-alpha1_linux_amd64.tar.gz && tar -zxvf sealos_4.2.0-alpha1_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0-alpha1/sealos_4.2.0-alpha1_linux_arm64.tar.gz && tar -zxvf sealos_4.2.0-alpha1_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +## Changelog since v4.1.5 + +### What's Changed +* af1bae9b - Jiahui - Merge pull request https://github.com/labring/sealos/pull/#2794 from cuisongliu/delete_node +* 07e2bcea - cuisongliu - feature(main): delete node if failed +* ebefb194 - cuisongliu - feature(main): registry list repo fix (https://github.com/labring/sealos/pull/#2821) +* 7a89ce36 - 中弈 - update zh readme (https://github.com/labring/sealos/pull/#2819) +* 5c57670a - cuisongliu - feature(main): delete shim images dir (https://github.com/labring/sealos/pull/#2817) +* 9a419650 - fengxsong - Regenerate types (https://github.com/labring/sealos/pull/#2815) +* 044abe05 - Mihai Țimbota-Belin - fix concurrent map access in clusterClient (https://github.com/labring/sealos/pull/#2812) +* 3cd3ddc4 - zzjin - Update module (https://github.com/labring/sealos/pull/#2813) +* 45e62bae - cuisongliu - feature(main): check scp md5sum to config cmd (https://github.com/labring/sealos/pull/#2806) +* 299ef1c4 - cuisongliu - feature(main): add cri cgroup-driver (https://github.com/labring/sealos/pull/#2805) +* d3b6cdf7 - will - update offline-docs (https://github.com/labring/sealos/pull/#2804) +* 3344780c - yy - feat: add default time to image cr. (https://github.com/labring/sealos/pull/#2774) +* 60e3d636 - yy - feat: refactor auth, move dir. (https://github.com/labring/sealos/pull/#2796) +* 0bd7639e - 晓杰 - fix(account_controller.go): will lead to Repeat recharge when in recharge (https://github.com/labring/sealos/pull/#2763) +* 80c8af28 - fengxsong - refactor: support setting ssh field for each subset of hosts (https://github.com/labring/sealos/pull/#2795) +* 8512a5a8 - Jiahui - Fix println built-in function output to standard error (https://github.com/labring/sealos/pull/#2798) +* 3dffefe8 - Archer - feat: prevent refresh page from keyboard;mock app in 'dev env' (https://github.com/labring/sealos/pull/#2793) +* a22a7cf3 - Jiahui - fix `GetRootfsImage` function cause panic (https://github.com/labring/sealos/pull/#2799) +* e0399833 - zhujingyang - feat modify request header & delete infra (https://github.com/labring/sealos/pull/#2791) +* 6c8f7290 - zhujingyang - Fix iframe copy issue (https://github.com/labring/sealos/pull/#2790) +* 8ac16751 - cuisongliu - feature(main): add system config to sealos (https://github.com/labring/sealos/pull/#2792) +* 350ba2e9 - Meteorite - feat: add infra concurrent opts & optimize delete instances (https://github.com/labring/sealos/pull/#2789) +* 505c6ec0 - cuisongliu - feature(main): add compressed to registry/docker (https://github.com/labring/sealos/pull/#2786) +* e632ccbe - cuisongliu - fix(main): add CHANGELOG-4.1.7.md (https://github.com/labring/sealos/pull/#2787) +* 1521f9cc - zhujingyang - feat Obtain desktop applications through app crd (https://github.com/labring/sealos/pull/#2751) +* 52f954a9 - cuisongliu - fix(main): fix kubeadm init func (https://github.com/labring/sealos/pull/#2756) +* 5e5ac7f5 - cuisongliu - ci(main): add ci for merge (https://github.com/labring/sealos/pull/#2785) +* a22e98e6 - yy - feat: add app cr. (https://github.com/labring/sealos/pull/#2760) +* 97c45eb2 - gitccl - get ingress secret from env (https://github.com/labring/sealos/pull/#2768) +* 201f9d25 - cuisongliu - ci(main): delete rebase (https://github.com/labring/sealos/pull/#2780) +* 429acc6d - Archer - feat: desktopSDK0.1.11 (https://github.com/labring/sealos/pull/#2778) +* 25a9e54f - fengxsong - fix: share the same systemContext in build command (https://github.com/labring/sealos/pull/#2770) +* 9cf172a0 - cuisongliu - ci(main): update COLLABORATOR (https://github.com/labring/sealos/pull/#2779) +* cb2a168a - sealos-ci-robot - Merge pull request https://github.com/labring/sealos/pull/#2777 from cuisongliu/print +* 3e9987d1 - cuisongliu - ci(main): add debug.yml +* d57fc561 - sealos-ci-robot - Merge pull request https://github.com/labring/sealos/pull/#2776 from cuisongliu/print +* 9741bace - cuisongliu - ci(main): add debug.yml +* 44d10a67 - cuisongliu - ci(main): add github.event.comment.author_association (https://github.com/labring/sealos/pull/#2775) +* a31b6b8d - cuisongliu - ci(main): add labels size to .github (https://github.com/labring/sealos/pull/#2773) +* 3875a8a8 - cuisongliu - ci(main): add github.event.comment.author_association (https://github.com/labring/sealos/pull/#2772) +* 7afb70ec - cuisongliu - ci(main): replace GH_TOKEN,GITHUB_TOKEN to GH_PAT (https://github.com/labring/sealos/pull/#2771) +* 3061ca6e - cuisongliu - ci(main): add rebase (https://github.com/labring/sealos/pull/#2769) +* 16998446 - zzjin - Change terminal image to self-host hub (https://github.com/labring/sealos/pull/#2762) +* 62f88352 - cuisongliu - fix(main): delete node not exec clean.sh (https://github.com/labring/sealos/pull/#2765) +* 4e921fe2 - fengxsong - fix: avoid invoking default function before complete merging configs (https://github.com/labring/sealos/pull/#2758) +* 1d6f5f81 - zzjin - Ignore frontend changes from ci docker (https://github.com/labring/sealos/pull/#2761) +* 59c9a394 - fengxsong - feat: add diff subcommand (https://github.com/labring/sealos/pull/#2753) +* dd24d6cc - Xin He - BUG: custom certSANs should append to the default ClusterConfiguration certSANs (https://github.com/labring/sealos/pull/#2752) +* 11c2f8fd - cuisongliu - fix(main): bootstrap registry before all step (https://github.com/labring/sealos/pull/#2742) +* 3fae3c42 - 晓杰 - fix: user limit memory too small lead to pod oom (https://github.com/labring/sealos/pull/#2750) +* 0eb07c46 - yy - add app rbac to user ns. (https://github.com/labring/sealos/pull/#2730) +* debc6c49 - zzjin - Fix readme typo. (https://github.com/labring/sealos/pull/#2748) +* 4a5149e5 - zzjin - Delete unused docs&files. (https://github.com/labring/sealos/pull/#2747) +* 076cf70c - zzjin - Fix Readme assets, using absolute path. (https://github.com/labring/sealos/pull/#2745) +* 3b9ab158 - Meteorite - debug: hostname too long (https://github.com/labring/sealos/pull/#2743) +* 3c29adb4 - fengxsong - fix: provide a common way to wrap bash (https://github.com/labring/sealos/pull/#2739) +* 5418faf0 - zzjin - Add Temp pre-installed deploy-manager (https://github.com/labring/sealos/pull/#2737) +* 3c362ffc - zzjin - rebase doc (https://github.com/labring/sealos/pull/#2735) +* 87b502ce - cuisongliu - fix(main): alt-names empty check (https://github.com/labring/sealos/pull/#2733) +* eafdc1c7 - zzjin - Fix en Intro.md images. (https://github.com/labring/sealos/pull/#2732) +* bd51db4a - Meteorite - add new driver interface and fix some bugs (https://github.com/labring/sealos/pull/#2720) +* 2b51d7bb - cuisongliu - fix(main): delete ipvs ip link (https://github.com/labring/sealos/pull/#2729) +* 32810dc4 - cuisongliu - fix(main): shell Wrapper from env (https://github.com/labring/sealos/pull/#2711) +* d4c0398f - yy - add imagehub frontend deploy.yaml (https://github.com/labring/sealos/pull/#2727) +* d774d064 - yy - feat: add ctime to image detail. (https://github.com/labring/sealos/pull/#2726) +* 630caa69 - zhujingyang - docs how to deploy the application to desktop (https://github.com/labring/sealos/pull/#2718) +* 082923cb - zhujingyang - sealos-image-hub (https://github.com/labring/sealos/pull/#2700) +* 669fba99 - Carson Yang - Update README (https://github.com/labring/sealos/pull/#2724) +* b1b01df4 - 晓杰 - user add clusterrole rbac (https://github.com/labring/sealos/pull/#2722) +* f38c6cac - gitccl - get terminal domain from env (https://github.com/labring/sealos/pull/#2717) +* 7b01f946 - Jiahui - Fix when no app image is specified, run error but the command condition still returns success (https://github.com/labring/sealos/pull/#2715) +* 6b5b2bd8 - Xin He - BUG: setting external etcd address does not take effect (https://github.com/labring/sealos/pull/#2704) +* 235a611a - Jiahui - fix `runtime error: slice bounds out of range [:19] with length 0` (https://github.com/labring/sealos/pull/#2713) +* cf6df12c - 晓杰 - add account balance when account is new user (https://github.com/labring/sealos/pull/#2710) +* 35dc0003 - cuisongliu - fix(main): set short for create feature (https://github.com/labring/sealos/pull/#2708) +* e369f67d - Meteorite - feat: modify infra CRD and support aliyun (https://github.com/labring/sealos/pull/#2682) +* 38fd41b2 - yy - delete cmd, use image cmd. (https://github.com/labring/sealos/pull/#2703) +* c11539ca - zhujingyang - modify the account and add the iframe imagehub (https://github.com/labring/sealos/pull/#2698) +* 16c23bd6 - yy - fix authz logic. (https://github.com/labring/sealos/pull/#2701) +* 50e447f6 - zzjin - Add actions inputs (https://github.com/labring/sealos/pull/#2697) +* 7958c0f0 - zhujingyang - Move desktop to frontend (https://github.com/labring/sealos/pull/#2694) +* 76c63491 - zzjin - Update mod typo (https://github.com/labring/sealos/pull/#2696) +* a612d648 - 榴莲榴莲 - Fix double reconcile by status updates (https://github.com/labring/sealos/pull/#2676) +* 6fc4d4ab - zzjin - Fix errors pkg nil checker (https://github.com/labring/sealos/pull/#2695) +* 867b0f17 - zhihui - bugfix: change CRI version (https://github.com/labring/sealos/pull/#2680) +* 445139dc - Xin He - feature: single host should allow scheduling to master (https://github.com/labring/sealos/pull/#2654) +* 7212f4ad - cuisongliu - fix(main): add changelog (https://github.com/labring/sealos/pull/#2683) +* 688743f3 - 晓杰 - rolebing.subject.naemspace should set user-system (https://github.com/labring/sealos/pull/#2681) \ No newline at end of file diff --git a/CHANGELOG/CHANGELOG-4.2.0-alpha2.md b/CHANGELOG/CHANGELOG-4.2.0-alpha2.md new file mode 100644 index 000000000000..979fffa6a356 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.0-alpha2.md @@ -0,0 +1,72 @@ +- [v4.2.0-alpha2](#v420-alpha2) + - [Downloads for v4.2.0-alpha2](#downloads-for-v420-alpha2) + - [Source Code](#source-code) + - [Client Binaries](#client-binaries) + - [Usage](#usage) + - [Changelog since v4.2.0-alpha1](#changelog-since-v420-alpha1) + +# [v4.2.0-alpha2](https://github.com/labring/sealos/releases/tag/v4.2.0-alpha2) + +## Downloads for v4.2.0-alpha2 + +### Source Code + +filename | +-------- | +[v4.2.0-alpha2.tar.gz](https://github.com/labring/sealos/archive/refs/tags/v4.2.0-alpha2.tar.gz) | + +### Client Binaries + +filename | +-------- | +[sealos_4.2.0-alpha2_linux_amd64.tar.gz](https://github.com/labring/sealos/releases/download/v4.2.0-alpha2/sealos_4.2.0-alpha2_linux_amd64.tar.gz) | +[sealos_4.2.0-alpha2_linux_arm64.tar.gz](https://github.com/labring/sealos/releases/download/v4.2.0-alpha2/sealos_4.2.0-alpha2_linux_arm64.tar.gz) | + +## Usage + +amd64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0-alpha2/sealos_4.2.0-alpha2_linux_amd64.tar.gz && tar -zxvf sealos_4.2.0-alpha2_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +arm64: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0-alpha2/sealos_4.2.0-alpha2_linux_arm64.tar.gz && tar -zxvf sealos_4.2.0-alpha2_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +## create a cluster for sealos +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 --passwd your-own-ssh-passwd +``` + +## Changelog since v4.2.0-alpha1 + +### What's Changed +* 22baae1a - cuisongliu - feature(main): delete registry code from fork (https://github.com/labring/sealos/pull/#2875) +* 41d2092f - Zihan Li - feat: update RunNewImages based on image diffs (https://github.com/labring/sealos/pull/#2856) +* afb274c7 - zhujingyang - feat desktop notification system (https://github.com/labring/sealos/pull/#2809) +* 7c1d411a - 晓杰 - fix metering e2e ci (https://github.com/labring/sealos/pull/#2873) +* 6c45ffed - 晓杰 - feat: add Notification CRD (https://github.com/labring/sealos/pull/#2580) +* fb01faca - 晓杰 - refactor:Strip billing function to reosurce-controller (https://github.com/labring/sealos/pull/#2811) +* 75dd26e7 - cuisongliu - feature(main): add token config for token cmd (https://github.com/labring/sealos/pull/#2869) +* 0ba7843d - cuisongliu - feature(main): replace name ref from registry (https://github.com/labring/sealos/pull/#2834) +* 6c49e19e - 榴莲榴莲 - Add rateLimiter opts for controllers (https://github.com/labring/sealos/pull/#2831) +* b338add0 - 晓杰 - Feat: add Debt CRD, limit user rbac when in arrears (https://github.com/labring/sealos/pull/#2634) +* 26df5bfb - yy - feat. registry add pull limit. (https://github.com/labring/sealos/pull/#2857) +* 7593a1dd - cuisongliu - feature(main): using bootstrap undo (https://github.com/labring/sealos/pull/#2867) +* 339adfa1 - Xinwei Xiong - fix: Password does not add single quotes will identify environment va… (https://github.com/labring/sealos/pull/#2862) +* ec97abfb - fengxsong - feat: add auto answer sudo prompt in ssh command (https://github.com/labring/sealos/pull/#2840) +* a02ebcaf - fengxsong - fix: ensure response body has to be read fully and close correctly; also use a sharedclients (https://github.com/labring/sealos/pull/#2855) +* 69bd0edb - fengxsong - feat: register all versions of the ImageServiceServer (https://github.com/labring/sealos/pull/#2853) +* cc8027c7 - fengxsong - fix: avoid rendering template repeatedly (https://github.com/labring/sealos/pull/#2852) +* 6071e10a - 晓杰 - fix(): cluster make build fail and controller ci fail (https://github.com/labring/sealos/pull/#2847) +* 56a2e78f - cuisongliu - feature(main): add env image test (https://github.com/labring/sealos/pull/#2845) +* 373c1f7f - zhujingyang - fix pgsql create default state (https://github.com/labring/sealos/pull/#2843) +* 15bcbc31 - zhujingyang - fix the request header problem (https://github.com/labring/sealos/pull/#2846) +* 7836f557 - fengxsong - fix(ci): golangci-lint error (https://github.com/labring/sealos/pull/#2844) +* 111e7ec4 - zhujingyang - Request failed to switch config (https://github.com/labring/sealos/pull/#2835) +* de1a400f - zhujingyang - Change 1 RMB equals 10000 (https://github.com/labring/sealos/pull/#2839) +* abc607f4 - fengxsong - fix: json tag of ssh field (https://github.com/labring/sealos/pull/#2830) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.0-alpha1...v4.2.0-alpha2 diff --git a/CHANGELOG/CHANGELOG-4.2.0-alpha3.md b/CHANGELOG/CHANGELOG-4.2.0-alpha3.md new file mode 100644 index 000000000000..51e526fda704 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.0-alpha3.md @@ -0,0 +1,150 @@ +# Sealos v4.2.0-alpha3 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.0-alpha3 🎉🎉! + +## Sealos Cloud: Powerful Cloud Operating System Distribution + +Sealos Cloud is a cloud operating system distribution with Kubernetes at its core. Users can directly use Sealos Cloud or run Sealos in their private environment to have the same capabilities as Sealos Cloud. Sealos Cloud offers a range of advantages, including a sleek product experience, fully open-source architecture, consistent public and private cloud experiences, cross-platform compatibility, and highly competitive pricing. + +### Sealos Cloud Usage Guide + +Sealos Cloud offers you exceptional public cloud services for cloud-native applications, making it easy to manage cloud-native applications. Sealos Cloud provides two ways to use: cloud access and private access. The online mode is now officially launched, and offline mode will be introduced in future releases. + +#### Cloud Access + +Cloud access is provided by directly accessing the cloud services provided by Sealos Cloud. Just enter the following link in your browser to start using the powerful features of Sealos Cloud immediately: + +``` +https://cloud.sealos.io +``` + +Cloud access allows you to access and manage your cloud-native applications anytime, anywhere, without any additional configuration and deployment. This makes the online mode an ideal choice for quickly getting started with Sealos Cloud. + +### Component Introduction + +Sealos Cloud's main features are divided into the frontend interface, backend API services, and Kubernetes Operator, working together to provide a complete cloud-native application management experience. + +#### Main Components + +- **Auth-Service**:Provides authentication services using casdoor, ensuring the security and accuracy of user identities. +- **Image Hub**:Sealos image repository-related services, making it easy for users to manage and use images. +- **Desktop**:Public cloud frontend service, providing a friendly user interface and operation experience. +- **User**:User and user group management services, making it easy for administrators to assign and manage user permissions. +- **Account** & **Metering**:Provides billing and account capabilities, helping users to reasonably control and plan costs. +- **App**:Provides the Sealos Cloud desktop application, making it easier for users to use and manage cloud services. +- **Infra**:Provides basic settings, currently supports AWS and Alibaba Cloud, and may support more cloud service providers in the future. +- **Cluster**:One-click cluster startup on Sealos Cloud, simplifying the cluster deployment and management process. +- **Terminal**:Terminal services on Sealos Cloud, providing convenient access and management of cloud services. + +For more detailed documentation about Sealos Cloud, please visit `https://sealos.io/docs/cloud/Intro`. + + +## Sealos Boot: Professional Cloud-Native Application Management Tool + +Sealos Boot is the core component of Sealos, mainly responsible for the lifecycle management of clusters, downloading and deploying OCI-compatible distributed applications, and customizing distributed applications. + + +### How to Install + +#### Binary Installation + +```shell + curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.2.0-alpha3/scripts/install.sh \ + | sh -s v4.2.0-alpha3 labring/sealos +``` + + + +### Component Introduction + +Sealos provides two Docker containers: sealos and lvscare, as well as two binary files: sealctl and image-cri-shim. Below is a brief introduction to these components: + +- Sealos Container: This container is the core component of the Sealos project, responsible for deploying and managing Kubernetes clusters and distributed applications. It offers a range of command-line tools to help users quickly build and maintain clusters. +- lvscare Container: This container is used to support load balancing management within Kubernetes clusters. It can monitor node status in real-time, ensuring that the load balancer always routes traffic to available nodes. +- sealctl Binary: This is a command-line tool provided by the Sealos project, used to simplify cluster management tasks such as certificate management, IPVS, hosts, and cluster certificates. +- image-cri-shim Binary: This component is a CRI adapter for the Sealos project, supporting different container runtimes (such as Docker and containerd). It allows Sealos to seamlessly integrate with multiple runtimes, enhancing the project's flexibility and scalability. + +### Quick Start + +```shell +# Create a cluster +sealos run labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 \ + --passwd your-own-ssh-passwd +``` + + +## Changelog +### New Features +* 4131325b32ce4ba7445b1ca2202d02961406612a: feat(debt): Debt-controller should delete user resources over 7 days in arrears (#2899) (@xiao-jay) +* 6c0a2163da5c3b73c04c1e3656ef23090d5f7910: feat: add resource group (#2905) (@xiaohan1202) +* 6ef4df2f9048cee482235840f0df077552f7286f: feat: grouping subcommands (#2920) (@fengxsong) +### Bug fixes +* 5bb06f4e82b1facc0e4d58358f586ed1690686a4: bugfix: the failure to add master after updating the certificate (#2942) (@bxy4543) +* f7bb82c36605e341d9a74bd1734e2a5a162a4ffe: fix(ci): fix metering e2e install account error (#2901) (@xiao-jay) +* 0c4a3cf71228a1a304019019126bdd8dfc154590: fix: check sg & vswitch existence (#2937) (@xiaohan1202) +* 3de1dc66a4b6cfbd5a9f200ab071fabce5787eac: fix: fix always return the insecure registry (#2927) (@fengxsong) +* 472d25c59ef7bab4bbd3acedbcae0d8895183d0a: fix: make pre-deploy error (#2904) (@xiao-jay) +* 884f1d9d0ff23d96e17a43980dc37f107afc7fd4: fix: return image name if oci archive file has names instead of id (#2887) (@fengxsong) +* 09aa95f0c36e11ade2851c2b328e70e704fa501c: fix: rm master taint in single model (#2860) (@mixinkexue) +* ddabe65678f86343b40687d1059d9b87136a3d1c: fix: use assertion, avoid patching external interface (#2891) (@fengxsong) +### Other work +* 89a519b55ecf9c266f55953d5a84ed0270d1fda3: E2e/apply test workflow (#2952) (@bxy4543) +* 31e40627dc8db35dfa2681804b2b895c653bcc1e: Fix login ingress deploy (#2909) (@zzjin) +* 9b7fd80b7659f57e87b2b9f09e8e103e575a2e14: Move deploy doc. (#2951) (@zzjin) +* cdacf36cb0918462e6c517a342b00d40bd61295a: Reduce terminal limits a little. (#2915) (@zzjin) +* 5efa4b1c2d6748b8542ff3c6f958f7527350496b: Title: Update auto-release logic to skip deb and rpm packages for non-production releases (#2882) (@cuisongliu) +* 9725dedb5dde50e8bb6b04f975d17a1a97bce54d: add bytebase controller (#2841) (@dinoallo) +* 7ce51bb3074fbb8b2f4d9b8bd9c1ec20306c3f90: add e2e apply test (#2936) (@bxy4543) +* fba6b37728e840b15e41b5713d19d7438bcae479: add fetch Kubeadm Config: (#2943) (@bxy4543) +* b2ed4c7b9754091a4bf31311e8bda970101e8263: add image-cri-image test (#2849) (@bxy4543) +* 21cdb8bc7a3b9648e97a9ce006d47b3754af5541: add local deplopment docs. (#2945) (@lingdie) +* 8a67f568a58287dbe2d0108fbb0c9d20598cb00d: ci,docs: add account e2e test and docs (#2935) (@xiao-jay) +* 2d8d793e7eaa338aa8261075d6b16596cb06f1cd: docs(main): Refactor Changelog generation script and update usage guide (#2884) (@cuisongliu) +* 72a58a94031eb8ceadcd35aca3c67df43bbf8cea: docs(main): add changelog full to shell (#2885) (@cuisongliu) +* 53974f5e7498593af7384c0a77062f551311711a: docs(main): add debug for merge logger (#2886) (@cuisongliu) +* a100d1a5d4136369b49dfefb02e1dda58203ceba: docs(main): add docs for sealctl (#2922) (@cuisongliu) +* c781a29e50c45a57254c66eb7a09c25013d9ffd9: docs(main): add docs for user (#2929) (@cuisongliu) +* e5a204154f236622aebcccc51ba10df5f32d84b0: docs(main): add image-cri-shim docs (#2880) (@cuisongliu) +* de11f2b790efa907a4c62c4e934ba16a60e594a0: docs(main): add robot for sealos (#2959) (@cuisongliu) +* 42e06f9ea34e7db1edee1277f2050601ffd5940b: docs(main): support v1.27 docs (#2939) (@cuisongliu) +* 60b038828da1ac2b0e915789b311104e05149d6c: docs(main): support v1.27 k8s (#2938) (@cuisongliu) +* 8c4a998bb45d48396878a28ff690c286a95720d2: docs(main): sync code user and email (#2900) (@cuisongliu) +* 18b344744a4df5c387c7323818b3c655ad805bd8: docs(main): update gorelease config for new release (#2953) (@cuisongliu) +* a929e01b639a409d03c9db06694f9a01e0fcb936: docs(main): upgrade gomod (#2894) (@cuisongliu) +* 696c7415e2f196dd40722fb8ee4bfc53df5924e7: docs: how to write reousrce-controller (#2941) (@xiao-jay) +* e43c77cbf38e1d6fb4bd591621323a40e0e6a51e: docs: translate metering design from CN to EN (#2746) (@xiao-jay) +* b49983dc993b0ffe0ac33b7f84a3589ec9c9afd4: feat add bytebase application (#2925) (@zjy365) +* c93c368967bf38061d3ff7bf8b5371554a75414e: feature: support checking cluster's no-odd number of master node (#2842) (@pixeldin) +* 9ca8b5e54d592c69aa33676f1540d575ee055c5a: feature: support pulling multiple images at the same time (#2931) (@dhanusaputra) +* 9256d27ef8f0d840f44f02526f64e46437b74695: fix len(deleteInstancesByOption.instanceIds) always gt 0 (#2934) (@bxy4543) +* 3018f694ebfdb918f1a577d9dffe59afd03cc578: fix multi infra apply (#2960) (@bxy4543) +* 7fd1373459b7e1172f1f6f6fe2281271b79c914f: fix notification (#2895) (@zjy365) +* 2fc130bec22217eb015b777ab48f971f97838834: fix pgsql status null (#2924) (@zjy365) +* a26403fc794a015d97f89996e0431b3f7219388f: fix stop kube-apiserver pod cause `DeadlineExceeded` when update cert. (#2918) (@bxy4543) +* 86e8983b62d5296c3908ad3e4b8bc252cd620716: fix sync docs error (#2944) (@xiao-jay) +* be282f8d12d25570ea8bb4171300889ee240cf80: format(account): format webhook name (#2928) (@xiao-jay) +* 168319f56bfecffbbf819b32c97d5fa487eaf691: optimize reset node `ip link delete` command. (#2947) (@bxy4543) +* 7c66fd20cf6c424c4cc492bf119c38115d72c141: refactor: add back auto build and auto save abilities in `diff` command (#2906) (@fengxsong) +* 187be92fc0d62898683db58725a6a9a79cf7bb76: refactor: diff command (#2879) (@fengxsong) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.0-alpha2...v4.2.0-alpha3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +## Roadmap + +In the future development plan, Sealos Boot and Sealos Cloud will continue to expand their capabilities to meet the needs of more users. Our Roadmap includes the following key plans: + +1. Sealos Cloud Private Deployment: We will introduce a private deployment solution for Sealos Cloud, allowing users to deploy and run Sealos Cloud in their own data centers or private cloud environments, achieving consistency between public and private cloud experiences. +2. Heterogeneous Deployment Support: Sealos Boot will support the deployment of Kubernetes clusters on various hardware platforms and operating systems, achieving broader compatibility and flexibility. +3. K3s and K0s Integration: We plan to integrate the lightweight Kubernetes distributions K3s and K0s into Sealos Boot, allowing users to choose different Kubernetes distributions for deployment according to their needs. + +Please look forward to the launch of these new features. We will continue to work hard to provide users with better services and experiences. + +We are very proud to introduce the two major functional modules of Sealos Cloud and Sealos Boot, which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.0.md b/CHANGELOG/CHANGELOG-4.2.0.md new file mode 100644 index 000000000000..c5d22d946281 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.0.md @@ -0,0 +1,146 @@ +# Sealos v4.2.0 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.0 🎉🎉! + +## Sealos Cloud: Powerful Cloud Operating System Distribution + +Sealos Cloud is a cloud operating system distribution with Kubernetes at its core. Users can directly use Sealos Cloud or run Sealos in their private environment to have the same capabilities as Sealos Cloud. Sealos Cloud offers a range of advantages, including a sleek product experience, fully open-source architecture, consistent public and private cloud experiences, cross-platform compatibility, and highly competitive pricing. + +### Sealos Cloud Usage Guide + +Sealos Cloud offers you exceptional public cloud services for cloud-native applications, making it easy to manage cloud-native applications. Sealos Cloud provides two ways to use: cloud access and private access. The online mode is now officially launched, and offline mode will be introduced in future releases. + +#### Cloud Access + +Cloud access is provided by directly accessing the cloud services provided by Sealos Cloud. Just enter the following link in your browser to start using the powerful features of Sealos Cloud immediately: + +``` +https://cloud.sealos.io +``` + +Cloud access allows you to access and manage your cloud-native applications anytime, anywhere, without any additional configuration and deployment. This makes the online mode an ideal choice for quickly getting started with Sealos Cloud. + +### Component Introduction + +Sealos Cloud's main features are divided into the frontend interface, backend API services, and Kubernetes Operator, working together to provide a complete cloud-native application management experience. + +#### Main Components + +- **Auth-Service**:Provides authentication services using casdoor, ensuring the security and accuracy of user identities. +- **Image Hub**:Sealos image repository-related services, making it easy for users to manage and use images. +- **Desktop**:Public cloud frontend service, providing a friendly user interface and operation experience. +- **User**:User and user group management services, making it easy for administrators to assign and manage user permissions. +- **Account** & **Metering**:Provides billing and account capabilities, helping users to reasonably control and plan costs. +- **App**:Provides the Sealos Cloud desktop application, making it easier for users to use and manage cloud services. +- **Infra**:Provides basic settings, currently supports AWS and Alibaba Cloud, and may support more cloud service providers in the future. +- **Cluster**:One-click cluster startup on Sealos Cloud, simplifying the cluster deployment and management process. +- **Terminal**:Terminal services on Sealos Cloud, providing convenient access and management of cloud services. + +For more detailed documentation about Sealos Cloud, please visit `https://sealos.io/docs/cloud/Intro`. + + +## Sealos Boot: Professional Cloud-Native Application Management Tool + +Sealos Boot is the core component of Sealos, mainly responsible for the lifecycle management of clusters, downloading and deploying OCI-compatible distributed applications, and customizing distributed applications. + + +### How to Install + +#### Binary Installation + +```shell + curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.2.0/scripts/install.sh \ + | sh -s v4.2.0 labring/sealos +``` + + + +#### Yum Repository Installation + +Add the following content to the `/etc/yum.repos.d/sealos.repo` file: + +``` +[sealos] +name=Sealos Repository +baseurl=https://yum.repo.sealos.io/ +enabled=1 +gpgcheck=0 +``` + +Then run: + +```shell +yum install sealos +``` + +#### APT Repository Installation + +Add the following content to the `/etc/apt/sources.list.d/sealos.list` file: + +``` +deb [trusted=yes] https://apt.repo.sealos.io/ / +``` + +Then run: + +```bash +apt-get update +apt-get install sealos +``` + + + +### Component Introduction + +Sealos provides two Docker containers: sealos and lvscare, as well as two binary files: sealctl and image-cri-shim. Below is a brief introduction to these components: + +- Sealos Container: This container is the core component of the Sealos project, responsible for deploying and managing Kubernetes clusters and distributed applications. It offers a range of command-line tools to help users quickly build and maintain clusters. +- lvscare Container: This container is used to support load balancing management within Kubernetes clusters. It can monitor node status in real-time, ensuring that the load balancer always routes traffic to available nodes. +- sealctl Binary: This is a command-line tool provided by the Sealos project, used to simplify cluster management tasks such as certificate management, IPVS, hosts, and cluster certificates. +- image-cri-shim Binary: This component is a CRI adapter for the Sealos project, supporting different container runtimes (such as Docker and containerd). It allows Sealos to seamlessly integrate with multiple runtimes, enhancing the project's flexibility and scalability. + +### Quick Start + +```shell +# Create a cluster +sealos run labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 \ + --passwd your-own-ssh-passwd +``` + + +## Changelog +### New Features +* ef9d393890ffad3eacddf03b736324bb5ac0a4d0: feat: add infra metering implementation with e2e test (#2797) (@xiaohan1202) +### Other work +* aa72e131e7a0dc28e0ed3d096489709cc6c4b2ae: Automated Changelog Update: Update directory for v4.2.0-alpha3 release (#2964) (@sealos-ci-robot) +* e26332d3fedc8c684481515e64d7a3fce7b2be9f: docs(main): delete build sealos image (#2975) (@cuisongliu) +* f696a621e5a91ce771b725e8b465f7f84f2aaa13: docs(main): fix bot config (#2976) (@cuisongliu) +* 8a521506e2c8b335225af8f24c3e61c5aca9d0a2: docs(main): fix change owner (#2973) (@cuisongliu) +* 967baa6b081135e7579a74d9b45e82247401227c: docs(main): fix change owner (#2974) (@cuisongliu) +* 8679b13f08dce0c72cb1ae5bd66e05e0ca1474a0: docs(main): fix changelog for sealos (#2963) (@cuisongliu) +* 908039e028e6128a7ff62f5a1099fd251d97599f: docs(main): fix ci patch images (#2971) (@cuisongliu) +* fa1776069c2633e02f072eb814a225ff02c737d6: docs(main): fix image0 to scratch (#2969) (@cuisongliu) +* 8e1d6afebc1e816412ad98382d8abc194bb2f9e5: docs(main): fix run workflow to test-run (#2970) (@cuisongliu) +* fa063c1fa9c68c473a395d94f12b17d3270d8737: docs: upgrade bot config (#2968) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.0-alpha3...v4.2.0 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +## Roadmap + +In the future development plan, Sealos Boot and Sealos Cloud will continue to expand their capabilities to meet the needs of more users. Our Roadmap includes the following key plans: + +1. Sealos Cloud Private Deployment: We will introduce a private deployment solution for Sealos Cloud, allowing users to deploy and run Sealos Cloud in their own data centers or private cloud environments, achieving consistency between public and private cloud experiences. +2. Heterogeneous Deployment Support: Sealos Boot will support the deployment of Kubernetes clusters on various hardware platforms and operating systems, achieving broader compatibility and flexibility. +3. K3s and K0s Integration: We plan to integrate the lightweight Kubernetes distributions K3s and K0s into Sealos Boot, allowing users to choose different Kubernetes distributions for deployment according to their needs. + +Please look forward to the launch of these new features. We will continue to work hard to provide users with better services and experiences. + +We are very proud to introduce the two major functional modules of Sealos Cloud and Sealos Boot, which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1-rc1.md b/CHANGELOG/CHANGELOG-4.2.1-rc1.md new file mode 100644 index 000000000000..3733f737cad6 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1-rc1.md @@ -0,0 +1,136 @@ +# Sealos v4.2.1-rc1 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.1-rc1 🎉🎉! + +## Sealos Cloud: Powerful Cloud Operating System Distribution + +Sealos Cloud is a cloud operating system distribution with Kubernetes at its core. Users can directly use Sealos Cloud or run Sealos in their private environment to have the same capabilities as Sealos Cloud. Sealos Cloud offers a range of advantages, including a sleek product experience, fully open-source architecture, consistent public and private cloud experiences, cross-platform compatibility, and highly competitive pricing. + +### Sealos Cloud Usage Guide + +Sealos Cloud offers you exceptional public cloud services for cloud-native applications, making it easy to manage cloud-native applications. Sealos Cloud provides two ways to use: cloud access and private access. The online mode is now officially launched, and offline mode will be introduced in future releases. + +#### Cloud Access + +Cloud access is provided by directly accessing the cloud services provided by Sealos Cloud. Just enter the following link in your browser to start using the powerful features of Sealos Cloud immediately: + +``` +https://cloud.sealos.io +``` + +Cloud access allows you to access and manage your cloud-native applications anytime, anywhere, without any additional configuration and deployment. This makes the online mode an ideal choice for quickly getting started with Sealos Cloud. + +### Component Introduction + +Sealos Cloud's main features are divided into the frontend interface, backend API services, and Kubernetes Operator, working together to provide a complete cloud-native application management experience. + +#### Main Components + +- **Auth-Service**:Provides authentication services using casdoor, ensuring the security and accuracy of user identities. +- **Image Hub**:Sealos image repository-related services, making it easy for users to manage and use images. +- **Desktop**:Public cloud frontend service, providing a friendly user interface and operation experience. +- **User**:User and user group management services, making it easy for administrators to assign and manage user permissions. +- **Account** & **Metering**:Provides billing and account capabilities, helping users to reasonably control and plan costs. +- **App**:Provides the Sealos Cloud desktop application, making it easier for users to use and manage cloud services. +- **Infra**:Provides basic settings, currently supports AWS and Alibaba Cloud, and may support more cloud service providers in the future. +- **Cluster**:One-click cluster startup on Sealos Cloud, simplifying the cluster deployment and management process. +- **Terminal**:Terminal services on Sealos Cloud, providing convenient access and management of cloud services. + +For more detailed documentation about Sealos Cloud, please visit `https://sealos.io/docs/cloud/Intro`. + + +## Sealos Boot: Professional Cloud-Native Application Management Tool + +Sealos Boot is the core component of Sealos, mainly responsible for the lifecycle management of clusters, downloading and deploying OCI-compatible distributed applications, and customizing distributed applications. + + +### How to Install + +#### Binary Installation + +```shell + curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.2.1-rc1/scripts/install.sh \ + | sh -s v4.2.1-rc1 labring/sealos +``` + + + +### Component Introduction + +Sealos provides two Docker containers: sealos and lvscare, as well as two binary files: sealctl and image-cri-shim. Below is a brief introduction to these components: + +- Sealos Container: This container is the core component of the Sealos project, responsible for deploying and managing Kubernetes clusters and distributed applications. It offers a range of command-line tools to help users quickly build and maintain clusters. +- lvscare Container: This container is used to support load balancing management within Kubernetes clusters. It can monitor node status in real-time, ensuring that the load balancer always routes traffic to available nodes. +- sealctl Binary: This is a command-line tool provided by the Sealos project, used to simplify cluster management tasks such as certificate management, IPVS, hosts, and cluster certificates. +- image-cri-shim Binary: This component is a CRI adapter for the Sealos project, supporting different container runtimes (such as Docker and containerd). It allows Sealos to seamlessly integrate with multiple runtimes, enhancing the project's flexibility and scalability. + +### Quick Start + +```shell +# Create a cluster +sealos run labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 \ + --passwd your-own-ssh-passwd +``` + + +## Changelog +### New Features +* 90b9a163eafe28c185a3e752cacda171872a9d57: feat: Add support for single-node mode without hosts in Clusterfile (#3024) (@LZiHaN) +* 1c6dd12c36754ec4cc75c9889fd852e45fdf0336: feat: open other app by sdk (#3011) (@c121914yu) +### Bug fixes +* 16ab2d2c81a3d1312937aa21538763f9080ea2b6: bugfix: fix duplicate print prompts (#3020) (@nowinkeyy) +### Other work +* 8783311762ac938eb035a5c6ef15baf3aecffd72: Automated Changelog Update: Update directory for v4.2.0 release (#2977) (@sealos-ci-robot) +* 9aa43c8b4f15a14c60153d7752a1141363b86772: Cluster image desktop (#3028) (@lingdie) +* 3cccfbaab4a99b023d28e2b684fec8b65172beb5: Replace all `math/rand` to safer `crypto/rand`. (#3031) (@zzjin) +* ff35c3e6ef8415067dace67c2256dd0750457a12: Stream log (#3018) (@c121914yu) +* 2d39bc86e7249a627ad01a7cdc799ea4a706bb19: Terminal controller support cors. (#2991) (@zzjin) +* 015f9c54c4074ac18e8541fb59c2593fdb823b42: Update README, add the new version UI showcase. (#3040) (@fanux) +* d528d6be713b9b9cf92169e5822d354d29fffb9d: add auth cluster image and update workflow (#2981) (@lingdie) +* df615299d952dac6ad03d59cadf3775088ff86e8: add systemDB design (#2955) (@fanux) +* 6f1bbae0a14f0fe9beb5c0a8b14db6e4f0ab1131: change cert env (#3033) (@lingdie) +* 6f0accc78c98843c171f1c2e6c725731fb8d0175: docs(main): add changelog action (#2990) (@cuisongliu) +* 6406b475674ffc030579e61c17ea71d99fa2281d: docs(main): add default image config.yml to config patch image (#3006) (@cuisongliu) +* c8d475710ab305cc20a272fe2010cfec284dd846: docs(main): add e2e test images (#3013) (@cuisongliu) +* a6e33bc799c2b760cad3ff38ad55afc02212f84b: docs(main): add image guide docs (#2992) (@cuisongliu) +* c552e68b0097c256202ac0566f2a6ec9b3037e9f: docs(main): e2e for core sealos (#3030) (@cuisongliu) +* 9f8189aa7a3131c716ed6a8d2887d1fad347c863: docs(main): e2e for image shim for sealos (#3034) (@cuisongliu) +* bb7e82da0380547aae9c456f3fa0aa1cd2a305bb: docs(main): fix bot config (#3021) (@cuisongliu) +* 453218412cac23039c9cbf74b61924d2dedbad54: docs(main): fix release comment (#2982) (@cuisongliu) +* 0d2588faf32e51c4467c66b106c4018276d73af6: docs(main): replace lookup env to apply (#3039) (@cuisongliu) +* 884dfb0845d00f65f49782c50127e321d85eb1fb: docs(main): save sealos for controllers (#3026) (@cuisongliu) +* e758ed41264cc71d99e366185f7d2d572aa3653c: docs(main): save sealos for controllers,front,service (#3029) (@cuisongliu) +* 4b5f9d794435f9f9835bf43ed74ccb4670ef2c55: docs(main): upgrade bot version (#3022) (@cuisongliu) +* 6e1db7b96a2d7631777677d0f728db3b03ad9b42: feat terminal add env ttyd image (#3012) (@zjy365) +* f8269fb07b215c76eee37ed6d08fd47ee4d14d34: feat terminal cors & decodeURIComponent url command (#2988) (@zjy365) +* 147ed6d372d3281f8a6e666db61572b07a46c5f3: feat web terminal frontend (#2972) (@zjy365) +* 22bdba549f669086556bf06e8080c8da1fa6d535: feat. build frontend cluster image (#3016) (@lingdie) +* 3a7970cc1b3512517020627bbaf4fdc2e5ab1af4: feature: using promptui replace confirm (#2993) (@nowinkeyy) +* 5feb91b71a25eed7ef534e87963a88bc99c2dddd: fix GetHostArch without use args port (#3042) (@ghostloda) +* e71f980be1d586648b757d6938c8113e7ad1897e: fix sha len not same (#3025) (@lingdie) +* d4f5eb26752a5cdc291bc36b992147715c6feffa: fix sync tag name (#3015) (@lingdie) +* b68437084ba4a3b8de520b258332469c2dfeaa17: fix. cluster image controller (#3017) (@lingdie) +* d57b729566c85aee12065a3d0f0388b4424c021c: new app: launchpad (#2987) (@c121914yu) +* f8774d06b944655a16fbcc1f65f31f1c5d6c4c5e: update controller and service cluster image build (#3001) (@lingdie) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.0...v4.2.1-rc1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +## Roadmap + +In the future development plan, Sealos Boot and Sealos Cloud will continue to expand their capabilities to meet the needs of more users. Our Roadmap includes the following key plans: + +1. Sealos Cloud Private Deployment: We will introduce a private deployment solution for Sealos Cloud, allowing users to deploy and run Sealos Cloud in their own data centers or private cloud environments, achieving consistency between public and private cloud experiences. +2. Heterogeneous Deployment Support: Sealos Boot will support the deployment of Kubernetes clusters on various hardware platforms and operating systems, achieving broader compatibility and flexibility. +3. K3s and K0s Integration: We plan to integrate the lightweight Kubernetes distributions K3s and K0s into Sealos Boot, allowing users to choose different Kubernetes distributions for deployment according to their needs. + +Please look forward to the launch of these new features. We will continue to work hard to provide users with better services and experiences. + +We are very proud to introduce the two major functional modules of Sealos Cloud and Sealos Boot, which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1-rc3.md b/CHANGELOG/CHANGELOG-4.2.1-rc3.md new file mode 100644 index 000000000000..6aebe98ceadc --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1-rc3.md @@ -0,0 +1,105 @@ +# Sealos v4.2.1-rc3 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.1-rc3 🎉🎉! + +## Sealos Cloud: Powerful Cloud Operating System Distribution + +Sealos Cloud is a cloud operating system distribution with Kubernetes at its core. Users can directly use Sealos Cloud or run Sealos in their private environment to have the same capabilities as Sealos Cloud. Sealos Cloud offers a range of advantages, including a sleek product experience, fully open-source architecture, consistent public and private cloud experiences, cross-platform compatibility, and highly competitive pricing. + +### Sealos Cloud Usage Guide + +Sealos Cloud offers you exceptional public cloud services for cloud-native applications, making it easy to manage cloud-native applications. Sealos Cloud provides two ways to use: cloud access and private access. The online mode is now officially launched, and offline mode will be introduced in future releases. + +#### Cloud Access + +Cloud access is provided by directly accessing the cloud services provided by Sealos Cloud. Just enter the following link in your browser to start using the powerful features of Sealos Cloud immediately: + +``` +https://cloud.sealos.io +``` + +Cloud access allows you to access and manage your cloud-native applications anytime, anywhere, without any additional configuration and deployment. This makes the online mode an ideal choice for quickly getting started with Sealos Cloud. + +### Component Introduction + +Sealos Cloud's main features are divided into the frontend interface, backend API services, and Kubernetes Operator, working together to provide a complete cloud-native application management experience. + +#### Main Components + +- **Auth-Service**:Provides authentication services using casdoor, ensuring the security and accuracy of user identities. +- **Image Hub**:Sealos image repository-related services, making it easy for users to manage and use images. +- **Desktop**:Public cloud frontend service, providing a friendly user interface and operation experience. +- **User**:User and user group management services, making it easy for administrators to assign and manage user permissions. +- **Account** & **Metering**:Provides billing and account capabilities, helping users to reasonably control and plan costs. +- **App**:Provides the Sealos Cloud desktop application, making it easier for users to use and manage cloud services. +- **Infra**:Provides basic settings, currently supports AWS and Alibaba Cloud, and may support more cloud service providers in the future. +- **Cluster**:One-click cluster startup on Sealos Cloud, simplifying the cluster deployment and management process. +- **Terminal**:Terminal services on Sealos Cloud, providing convenient access and management of cloud services. + +For more detailed documentation about Sealos Cloud, please visit `https://sealos.io/docs/cloud/Intro`. + + +## Sealos Boot: Professional Cloud-Native Application Management Tool + +Sealos Boot is the core component of Sealos, mainly responsible for the lifecycle management of clusters, downloading and deploying OCI-compatible distributed applications, and customizing distributed applications. + + +### How to Install + +#### Binary Installation + +```shell + curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.2.1-rc3/scripts/install.sh \ + | sh -s v4.2.1-rc3 labring/sealos +``` + + + +### Component Introduction + +Sealos provides two Docker containers: sealos and lvscare, as well as two binary files: sealctl and image-cri-shim. Below is a brief introduction to these components: + +- Sealos Container: This container is the core component of the Sealos project, responsible for deploying and managing Kubernetes clusters and distributed applications. It offers a range of command-line tools to help users quickly build and maintain clusters. +- lvscare Container: This container is used to support load balancing management within Kubernetes clusters. It can monitor node status in real-time, ensuring that the load balancer always routes traffic to available nodes. +- sealctl Binary: This is a command-line tool provided by the Sealos project, used to simplify cluster management tasks such as certificate management, IPVS, hosts, and cluster certificates. +- image-cri-shim Binary: This component is a CRI adapter for the Sealos project, supporting different container runtimes (such as Docker and containerd). It allows Sealos to seamlessly integrate with multiple runtimes, enhancing the project's flexibility and scalability. + +### Quick Start + +```shell +# Create a cluster +sealos run labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 \ + --passwd your-own-ssh-passwd +``` + + +## Changelog +### New Features +* 99156c4bc06da2d462feb96eab728df97fb38470: feat: add withcontext functions in ssh interface (#3129) (@fengxsong) +### Other work +* 869eed0baae92a83fd8ae3380bd33e9179b32d82: Add type status hide/show system app. (#3127) (@zzjin) +* f010bc2f707675fa47b9591f4aa66df297242145: Move apps.displayType to spec. (#3133) (@zzjin) +* 8438a3fd3eacc97cca798d310e9e657270448eb7: fix ci error (#3128) (@bxy4543) +* ac62c85585d024d7204a489117da4ad6b1a2d04b: update release error (#3132) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1-rc2...v4.2.1-rc3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +## Roadmap + +In the future development plan, Sealos Boot and Sealos Cloud will continue to expand their capabilities to meet the needs of more users. Our Roadmap includes the following key plans: + +1. Sealos Cloud Private Deployment: We will introduce a private deployment solution for Sealos Cloud, allowing users to deploy and run Sealos Cloud in their own data centers or private cloud environments, achieving consistency between public and private cloud experiences. +2. Heterogeneous Deployment Support: Sealos Boot will support the deployment of Kubernetes clusters on various hardware platforms and operating systems, achieving broader compatibility and flexibility. +3. K3s and K0s Integration: We plan to integrate the lightweight Kubernetes distributions K3s and K0s into Sealos Boot, allowing users to choose different Kubernetes distributions for deployment according to their needs. + +Please look forward to the launch of these new features. We will continue to work hard to provide users with better services and experiences. + +We are very proud to introduce the two major functional modules of Sealos Cloud and Sealos Boot, which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1-rc4.md b/CHANGELOG/CHANGELOG-4.2.1-rc4.md new file mode 100644 index 000000000000..33d005ea1aad --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1-rc4.md @@ -0,0 +1,170 @@ +# Sealos v4.2.1-rc4 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.1-rc4 🎉🎉! + +## Sealos Cloud: Powerful Cloud Operating System Distribution + +Sealos Cloud is a cloud operating system distribution with Kubernetes at its core. Users can directly use Sealos Cloud or run Sealos in their private environment to have the same capabilities as Sealos Cloud. Sealos Cloud offers a range of advantages, including a sleek product experience, fully open-source architecture, consistent public and private cloud experiences, cross-platform compatibility, and highly competitive pricing. + +### Sealos Cloud Usage Guide + +Sealos Cloud offers you exceptional public cloud services for cloud-native applications, making it easy to manage cloud-native applications. Sealos Cloud provides two ways to use: cloud access and private access. The online mode is now officially launched, and offline mode will be introduced in future releases. + +#### Cloud Access + +Cloud access is provided by directly accessing the cloud services provided by Sealos Cloud. Just enter the following link in your browser to start using the powerful features of Sealos Cloud immediately: + +``` +https://cloud.sealos.io +``` + +Cloud access allows you to access and manage your cloud-native applications anytime, anywhere, without any additional configuration and deployment. This makes the online mode an ideal choice for quickly getting started with Sealos Cloud. + +### Component Introduction + +Sealos Cloud's main features are divided into the frontend interface, backend API services, and Kubernetes Operator, working together to provide a complete cloud-native application management experience. + +#### Main Components + +- **Auth-Service**:Provides authentication services using casdoor, ensuring the security and accuracy of user identities. +- **Image Hub**:Sealos image repository-related services, making it easy for users to manage and use images. +- **Desktop**:Public cloud frontend service, providing a friendly user interface and operation experience. +- **User**:User and user group management services, making it easy for administrators to assign and manage user permissions. +- **Account** & **Metering**:Provides billing and account capabilities, helping users to reasonably control and plan costs. +- **App**:Provides the Sealos Cloud desktop application, making it easier for users to use and manage cloud services. +- **Infra**:Provides basic settings, currently supports AWS and Alibaba Cloud, and may support more cloud service providers in the future. +- **Cluster**:One-click cluster startup on Sealos Cloud, simplifying the cluster deployment and management process. +- **Terminal**:Terminal services on Sealos Cloud, providing convenient access and management of cloud services. + +For more detailed documentation about Sealos Cloud, please visit `https://sealos.io/docs/cloud/Intro`. + + +## Sealos Boot: Professional Cloud-Native Application Management Tool + +Sealos Boot is the core component of Sealos, mainly responsible for the lifecycle management of clusters, downloading and deploying OCI-compatible distributed applications, and customizing distributed applications. + + +### How to Install + +#### Binary Installation + +```shell + curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.2.1-rc4/scripts/install.sh \ + | sh -s v4.2.1-rc4 labring/sealos +``` + + + +### Component Introduction + +Sealos provides two Docker containers: sealos and lvscare, as well as two binary files: sealctl and image-cri-shim. Below is a brief introduction to these components: + +- Sealos Container: This container is the core component of the Sealos project, responsible for deploying and managing Kubernetes clusters and distributed applications. It offers a range of command-line tools to help users quickly build and maintain clusters. +- lvscare Container: This container is used to support load balancing management within Kubernetes clusters. It can monitor node status in real-time, ensuring that the load balancer always routes traffic to available nodes. +- sealctl Binary: This is a command-line tool provided by the Sealos project, used to simplify cluster management tasks such as certificate management, IPVS, hosts, and cluster certificates. +- image-cri-shim Binary: This component is a CRI adapter for the Sealos project, supporting different container runtimes (such as Docker and containerd). It allows Sealos to seamlessly integrate with multiple runtimes, enhancing the project's flexibility and scalability. + +### Quick Start + +```shell +# Create a cluster +sealos run labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 \ + --passwd your-own-ssh-passwd +``` + + +## Changelog +### New Features +* ff4ef1b86cf6dbde55aee3d972758902ac33d29e: feat: add max limit in podline chart (#3140) (@moonrailgun) +* 6f913384a9d856a58ad2e910a95a2daf3daa4a82: feat: add more verbose output while syncing registry (#3181) (@fengxsong) +* 1b97f79ccbb2daf5a35c587e86efb877bf064c67: feat: add registry sync command (#3173) (@fengxsong) +* be1f9864f23532c9e6b694d85c52c8e27227890f: feat: make manifest command visible (#3159) (@fengxsong) +* 138521704d508d793e58be0b36aae621de975148: feat: more apps and hover ball (#3126) (@zjy365) +### Bug fixes +* 96145f888cfbefb08218692847a364e1764126da: fix: can't find variable structuredClone in safari (#3158) (@zjy365) +* f8a17787822714c5fdf21f2a75cc86fadb88adfa: fix: commandline usage output (#3219) (@fengxsong) +* c3744ed64658b7020e531d839f6f81f7874cc2f6: fix: correct descriptions of env settings (#3164) (@fengxsong) +* 2a7380b370f7c1d234ff15b729eb0f8f4714dae0: fix: fix loop call which occur in throw error in onmessage callback (#3150) (@moonrailgun) +* 7ab33208c638b0f062f9b677ce82718cf66d72e3: fix: if a command requires buildah module, call SetRequireBuildahAnnotation explicitly (#3146) (@fengxsong) +* 7bcc3bca5632794a8bec4953a12f6c536c0cfa0a: fix: name of BUILDAH_FORMAT env key (#3145) (@fengxsong) +* c34fbee89d637d2e2aec36fa1f92f0d595551249: fix: window bug & notification field & modify terminal color (#3147) (@zjy365) +### Other work +* 4d33254a1403af2ff4e8f10c0700798b0d05a640: Added Hyperlink for twitter in README.md (#3153) (@Harshad112) +* a4e9c1c79139ed0ffb2eba4141a7621598267b3b: Added documentation for quickly deploying a database and wordpress with sealos (#3178) (@ElonMuskkkkkk) +* 07a7f70a408861b3b51efafbe6446f491756b280: Added documentation for quickly deploying a database and wordpress with sealos (#3194) (@ElonMuskkkkkk) +* 58858354d566d81a852639ce689a2a5f51bcf33d: Auto translate Intro.md (#3180) (@zzjin) +* cbfaea21f2dac9251a7708df8ca58a8f3a5c727e: Fix missing intro endpoint. (#3171) (@zzjin) +* 491dd2f48b0a46321cfc8c990eba613fdcef93d0: Fix typo. (#3185) (@zzjin) +* da8afb066e0365b37657d57ebc490be4a9f70e70: Frontend perf (#3225) (@c121914yu) +* 0b86ceb5f84e11c7ee6de2f8805705bdd5207e5f: Manual change position (#3212) (@zzjin) +* 27d6abbc26c7a50aa8e80a06271e2978d5f9413a: Minor updates: (#3148) (@zzjin) +* aa14645b4cfa28ef0ce7f99edc8e0c566d0ce496: Move doc sidebar config into main repo. (#3184) (@zzjin) +* 606bc14acf31b9acedbcd38e1d8280890804763b: Optimize the title (#3214) (@nowinkeyy) +* 71318a4c0a4eca71536458eb2dedf4343f6114ff: Refactor docs dir. (#3170) (@zzjin) +* 1f7ecfe1c62a66d34abcad2ff90c8c17ccaa238e: Restructure the directory structure (#3203) (@ElonMuskkkkkk) +* bd16b9e1b2a5b5e691731ddfd1183c0a77392662: Restructure the directory structure (#3207) (@ElonMuskkkkkk) +* f9ab4172dd345bfd5a8449d0afdab4f8813d9d84: Restructure the directory structure (#3210) (@ElonMuskkkkkk) +* 55bbb66efc1fcde7db0611ce2dec7e69c6ef602d: Update Intro.md (#3142) (@Yvan-code) +* 9d03c72600935fe3cb3cc5a8a4f13e4671e88c5a: Update Intro.md (#3179) (@yangchuansheng) +* 6ebe11b92f221ace41054b0ff79215ece4105c22: Update adminer provider. (#3162) (@zzjin) +* 655196af17c7f87f99e8bb160c9d133bcdb82847: Update app cr displayType (#3152) (@zzjin) +* 96d24582212705d25ed5fa00e17fa51512d82ff7: Update docs, remove `docs/4.0/apis`. (#3163) (@zzjin) +* 0007374f776b70ce02b3279d3b72a10e14ccb8d7: Update icon&fix apply (#3135) (@zzjin) +* 8242d6c808ae970b37750d9afbe9eb6f0f59f4b5: Update sidebar position. (#3215) (@zzjin) +* 3800df71c14c6cd4f55b82829d69171c5b468ce5: [ImgBot] Optimize images (#3200) (@imgbot[bot]) +* b71600e300cdfb446526c5775828f14bcc286bed: add additional exemplars: halo && uptime kuma (#3174) (@bxy4543) +* 21d6af7afd3892bfcdf835e8100d8a85ebf83665: add docs for bytebase in both en_US and zh_Hans (#3172) (@dinoallo) +* 5d787233b9a97d98112236b230d9db3cdc3dd1b2: add low-code-platform (#3211) (@nowinkeyy) +* 8299dfcefdde2a6b327f6fa19d2119fb4e3e37c1: correct the title (#3217) (@nowinkeyy) +* 8226a823298dba1250704b7b98ab149a2cc271a5: docs(main): add registry sync docs for build (#3222) (@cuisongliu) +* 576679db8b2bd244e086ea07612d49315fff2248: docs: Add Chinese (Simplified) quick-start documentation with new images and instructions. (#3182) (@yangchuansheng) +* 722acdbafd81fbb0400c24649c924b8d1ac1624d: docs: Add Kubernetes deployment tutorial with terminal (#3198) (@yangchuansheng) +* a204b3ec1450ac60c0bc2bf8495083302d7fa31c: docs: Add new images and update file names in quick-start guide. (#3193) (@yangchuansheng) +* ddab1c56aeb2a86f0dd9f5643f887d46e32f9dcd: docs: Improve Sealos introduction (#3189) (@yangchuansheng) +* 4fb261f3f7faa58f9fda4276bbe405782dae6c18: docs: Refactor YAML conventions and code block language specifiers (#3199) (@yangchuansheng) +* 72ab0eb92554c7dd7365d8e8f2cd683cdcc939bb: docs: Remove `docs/4.0/docs/quick-start/index.md` file (#3197) (@yangchuansheng) +* e4da55fdbe63238c4af9487ffa2d869509f7623d: docs: Remove deprecated features and unused images from README and docs directory. (#3218) (@yangchuansheng) +* 7027fb1a50e085746d9dc1f39a586948f0673f66: docs: add examples pageplug.md (#3176) (@nowinkeyy) +* 49e641decc70bc6b4bddcbaf030ced40ab323671: docs: applaunchpad dbprovider install-fastgpt (#3195) (@c121914yu) +* f943178149517e5a9f42135c04531d4701f05a02: docs: cp docs (#3208) (@c121914yu) +* cb58670e97ad2f204f590a0c41595bf9f3180b90: make bytebase request less resource (#3157) (@dinoallo) +* 833a6affe9bc7ebd18a0f9ab274786752f19580a: mv docs and pics path (#3206) (@nowinkeyy) +* ca838639dd05d684043b296bc66fd647612f1f28: mv docs image path (#3204) (@bxy4543) +* 1aaa0700b1b20c2c3d2f72e875d4a764c8890d4b: mv pics path (#3209) (@nowinkeyy) +* 28f6e5493118cd02c6e73febbc85a63bd002c9a5: optimize docs (#3190) (@bxy4543) +* 43e7e2e058bc55fe029b3c3639743ad836851b4c: optimize the title (#3213) (@nowinkeyy) +* aab8beffe2a8f6b2156af6a8e696638e523aac90: perf: compress default desktop background size. (#3149) (@moonrailgun) +* b37ad2c269dd5aa29f00858a47169e0902becc29: refactor(main): add lifecycle mardown (#3188) (@cuisongliu) +* 5d27f955d8405031df11d2e3a100b65b80de5bdf: refactor(main): add lifecycle mardown (#3177) (@cuisongliu) +* 88dfd8f91de0e33de1648204d0d73520182e92fa: refactor(main): add lifecycle mardown (#3202) (@cuisongliu) +* 6cc595e830b932566c9cbe35687654526c18bea6: refactor(main): add lifecycle mardown for cri-image-shim docs (#3183) (@cuisongliu) +* ed93ee16ce7b05fe94fd732d26b33effbc0e46f6: refactor(main): fix ci error (#3139) (@cuisongliu) +* 66271244a1eaaf3977412ab10b2f4cfed1c22d1c: refactor(main): remove registry compression (#3166) (@cuisongliu) +* 22289636a79c266ec44e211230084e7a35c75017: refactor(main): using registry mode build save images (#3154) (@cuisongliu) +* efb36bd1ec51a1e723952bc24b244dfc8f6260e4: refactor: add sync mode impl (#3113) (@fengxsong) +* 3818b78064358c83200d17ead2492f47a649dff3: revert: remove easter eggs in diff command (#3186) (@fengxsong) +* ac7e2e0b11be16a8e3630efc4941a220d31bd9d6: style: return error instead of bool type (#3169) (@fengxsong) +* 2b964edf8e6e6b71f606058d4b9adfe93cf75411: update doc. (#3196) (@zzjin) +* 9c20dacb539d0063053d4665116d1f21b42edb44: 🤖 add release changelog using rebot. (#3134) (@sealos-release-rebot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1-rc3...v4.2.1-rc4 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +## Roadmap + +In the future development plan, Sealos Boot and Sealos Cloud will continue to expand their capabilities to meet the needs of more users. Our Roadmap includes the following key plans: + +1. Sealos Cloud Private Deployment: We will introduce a private deployment solution for Sealos Cloud, allowing users to deploy and run Sealos Cloud in their own data centers or private cloud environments, achieving consistency between public and private cloud experiences. +2. Heterogeneous Deployment Support: Sealos Boot will support the deployment of Kubernetes clusters on various hardware platforms and operating systems, achieving broader compatibility and flexibility. +3. K3s and K0s Integration: We plan to integrate the lightweight Kubernetes distributions K3s and K0s into Sealos Boot, allowing users to choose different Kubernetes distributions for deployment according to their needs. + +Please look forward to the launch of these new features. We will continue to work hard to provide users with better services and experiences. + +We are very proud to introduce the two major functional modules of Sealos Cloud and Sealos Boot, which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1-rc5.md b/CHANGELOG/CHANGELOG-4.2.1-rc5.md new file mode 100644 index 000000000000..508c821b81df --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1-rc5.md @@ -0,0 +1,45 @@ +# Sealos v4.2.1-rc5 🎉🎉 + +We are excited to announce the official release of Sealos v4.2.1-rc5 🎉🎉! + +## Sealos : A Cloud Operating System designed for managing cloud-native applications + +Sealos is a cloud operating system distribution based on the Kubernetes kernel. It adopts a cloud-native approach, discarding traditional cloud computing architecture, and shifting towards a new architecture with Kubernetes as the cloud kernel. This allows enterprises to use the cloud as effortlessly as they would use a personal computer. + +## Deploy your app on Sealos + +[Try online demo](https://cloud.sealos.io) + +## Core features + +✅ **Instant Kubernetes Usage Upon Login**: There is no need to install a Kubernetes cluster. Sealos provides the ability for **multiple tenants** to share a Kubernetes on the public internet. It also offers strong isolation capabilities to ensure the data safety of each tenant. + +✅ **Swiftly deploy any application on Sealos**: With the in-built App Launchpad, effortlessly deploy any application in an astonishingly short span of time. + +✅ **Resource Saving and Cost Reduction**: You only pay for the container. The automatic scaling function fundamentally solves the problem of resource wastage, saving between 10% to 40% in costs. + +✅ **Easy Public Network Access**: Sealos automatically assigns a secondary domain name to your business, helping you achieve easy public network access. It also supports custom domain name binding. + +✅ **Efficient Database Service**: Sealos offers a service that allows businesses to create high-availability databases within seconds. Through service discovery, the internal network DNS can connect directly to the database. + +✅ **User-Friendly Operating Experience**: Sealos has an in-built terminal that supports command-line operation of the Kubernetes cluster, and also has in-built App Launchpad, allowing you to have a good user experience on Sealos, regardless of your familiarity with cloud-native technology. + +For more detailed documentation about Sealos , please visit `https://sealos.io/docs/Intro`. + + + +## Changelog +### Other work +* 5138733c3bc2d816873e50eccfc3a1feed6741db: refactor(main): fix release notes (#3224) (@cuisongliu) +* 65129e0b8128381dc6daec75042305a30bb16f06: 🤖 add release changelog using rebot. (#3227) (@sealos-release-rebot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1-rc4...v4.2.1-rc5 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + + +We are very proud to introduce the functional modules of Sealos , which we believe will bring more convenience and efficiency to your cloud-native application management. We look forward to your feedback and suggestions so that we can continue to improve and provide better products and services. + +Thank you for your support of Sealos🎉🎉. + +If you encounter any problems during use, please submit an issue in the [GitHub repository](https://github.com/labring/sealos) , and we will solve your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1-rc6.md b/CHANGELOG/CHANGELOG-4.2.1-rc6.md new file mode 100644 index 000000000000..7bd6bf97c63c --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1-rc6.md @@ -0,0 +1,75 @@ +Welcome to the v4.2.1-rc6 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* b567669d5cc49681c75730901062b28baba9e51a: feat(desktop):login (#3276) (@xudaotutou) +* 134904e55a7b80f2c8eb41574fee3dcf88ee524b: feat: app launchpad i18n (#3235) (@zjy365) +* 0d9ea570595efa7456cc5b5dd05e1125ccce1538: feat: apply limitrange;perf: db min cpu and memory;docs (#3234) (@c121914yu) +* 32572c676bad95f1e29337111b8f66eac24b0924: feat: desktop i18n (#3221) (@zjy365) +* b08fc64e0a0a44359202179dd09c5cdec8d2b099: feat: remove the i18n language prefix (#3274) (@zjy365) +* 49dd20305843b4ecfa40925470b9eeeb6343ce6b: feat: terminal connection stability (#3263) (@zjy365) +### Bug fixes +* 1029bfd50fb7efd93eddd86d0027bf08655d0b4e: fix: add default configs in storage.options.overlay section (#3236) (@fengxsong) +* f4382af21e13b93dd5c47c0c551b64f74eb9b446: fix: name regx (#3254) (@c121914yu) +* f9b4da648b336c375c641a0146d535fe4f9d9b00: fix: return early to avoid calling imagesaver (#3288) (@fengxsong) +* f150ecb9001a1ec587508e6510a64b3b654e052f: fix: router back invalid; perf: database ux (#3241) (@c121914yu) +* 469d6f7c4b9a4799a9b5dfc9da4508610f23dc1d: fix: set default storage config path for root users (#3293) (@fengxsong) +### Other work +* be23cefe06af6c36bbbab3c4e4b2540758759d79: Add ARCH doc. (#3262) (@zzjin) +* a7a1765730ffd0ac808e70750ab290f4651e2317: Add adminer status Check to adminer. (#3295) (@zzjin) +* 0508e4da612aefa3f3562f655cecec9e399f9444: Added an example of how to connect to the database in the WordPress (#3270) (@ElonMuskkkkkk) +* 253cd3200bf7c0dd11f881cd6fbbf5d72765ddcc: Adjust documentation example structure (#3261) (@bxy4543) +* 053fb5eb47cf73e5d8b50e6380c6ab0db7c52f4e: Delete database-launch.png (@sakcer) +* 146b329ad8773fa7e5870761ce453f61e74ba99f: Fix adminer connect driver name. (#3230) (@zzjin) +* 1544a8955647f992063cd1f160151838174d11a2: Fix ass go-lint-ci version. (#3244) (@zzjin) +* 16cdabf7889283613eb38ed9f4b33acd8fd9236e: Fix back (#3253) (@zzjin) +* 484275079b5f542d70c612b3e858ddaea97d8a79: Fix life-circle sidebar (#3290) (@zzjin) +* 7bbba06f54b3e18da48685b3c7b73054a02fc2ca: Remove files with no more needed. (#3285) (@zzjin) +* 99e6accf65e9e6564b7ee04813c72aadf6277cb8: Revoke lint version 1.53 bug. (#3242) (@zzjin) +* ce700f48b40faa9bc6eec2f8470a3d7e505d8890: Update aap cr, support i18n. (#3243) (@zzjin) +* 522aefa768639aa9ca5d6534225a5bab8bee26af: Update doc links. (#3284) (@zzjin) +* 53c788e852ace030b85f260a5b2b4c9efef2d7d2: Update install-db-with-database.md (@sakcer) +* 77ffa22b3f16a65708c4f504507db87c0d582e01: Update install-db-with-database.md (@sakcer) +* 159cd00ef3787dbf4c7de6f8664b3390ce69bbe8: Update install-db-with-database.md (@sakcer) +* d1051809083aed3d25e40d0a3c366e699ac6c13c: add doc install-db-with-database (#3239) (@sakcer) +* ee3c5c651510d9f1f9e120b24eb9909d424efd2a: add some examples in README (#3281) (@fanux) +* ca49ff88ce59a4bec3cb7c356ae71dbc60f074ed: change database-launch to chinese version (@sakcer) +* 1b68ca80b652d4de6011550d488f558053689963: delete how-to-deploy-postgresql-with-kubeblocks-on-sealos-cloud.md (#3282) (@sakcer) +* e8f3d01afb8cb052f859fcc810ace2a4ad31e502: delete the old version of markdown about database (#3249) (@ElonMuskkkkkk) +* 3980d37399a15c977104c525a48ad3e2bb634f98: docs (#3255) (@c121914yu) +* 6c5fb2bcbbf2e711008bd0c6644b890c020b8a47: docs(main): fix lifecyle-management from intro (#3256) (@cuisongliu) +* 163bcfcce2dc9d9a04ce9d8777bbbaf69f305d0f: docs(main): add docs issue template (#3278) (@cuisongliu) +* 1f42cdbf24abf0a1496602c5fb0a5cdb330b0f8f: docs(main): add tips for sealos buildah rm (#3291) (@cuisongliu) +* 0c8d89aa6646d0fe404ec41ede611f5de7a31c93: docs: Add Desktop image for Intro documentation (#3252) (@yangchuansheng) +* 73b168edb57cc82199645ea240e83106c351b2cb: docs: Add terminal documentation and images for platform components (#3265) (@yangchuansheng) +* 56f2e39cbffc625c20f7f66bbcbd0f218f988352: docs: Update App Launchpad quick start guide (#3248) (@yangchuansheng) +* d73fcb1c550076e87858ffe950c169e9f2a5e3f3: docs: Update README (#3246) (@yangchuansheng) +* 6090210673f3330b54db88b3178911f0ad3cdf02: docs: fix part of the link failure (#3267) (@nowinkeyy) +* 019d321b26024138476eeb59e6579d95af26a46d: docs: fix part of the link failure (#3271) (@nowinkeyy) +* ce99fdfd72fde6f8ef5805e21eeac7013487dd18: docs: modifying docs format (#3268) (@BanTanger) +* 0e21d6085cdc993703cf8b948e3690cc8376a2d6: feature(main): add buildah labeler (#3289) (@cuisongliu) +* 9feb721d17bc79c7f9f276fefc5ff9b6c3e2e46e: feature(main): add docs for sealos run (#3292) (@cuisongliu) +* 6b228fe9d64946b83f3b70ed890919c02a471ca1: feature: recharge extra gift amount (#3296) (@nowinkeyy) +* 5a536fe5a17f8a4197492b90f3e06fe3e187b5ff: fix docs img link (#3286) (@bxy4543) +* 62e25cb1e5f0ad1f7c7e3189cb36aad4747bf19d: fix readme (#3297) (@bxy4543) +* fa97893c8f9b7fc8c2d0dab9c6bb15a6be11e34d: new app: DB Provider (#3232) (@c121914yu) +* 534a5924b6d18ef6a5f906a58f002f40ac3466d3: refactor(main): add docs for registry sync solution (#3226) (@cuisongliu) +* 8e4dd4a906edebddaf7057755b5f02645b6592a5: refactor(main): add docs for lvscare (#3233) (@cuisongliu) +* d029b711fb6cf6e56da0e5cad9f7bef524a8cef7: refactor(main): add labeler for ci (#3259) (@cuisongliu) +* 78b250996dbe78b10945fdee24d58b0a1e3e8f94: refactor(main): add master to qa (#3283) (@cuisongliu) +* 2e144779753500c3cf3e31bfc9df9eabacfd43dc: refactor(main): add qa for docs (#3250) (@cuisongliu) +* a4661fe54b023088a51328c53e7609eded8e453c: refactor(main): add qa for main docs (#3223) (@cuisongliu) +* f4049753559f4f6f32996539d9fbd4649d759860: refactor(main): add skip error for sync (#3238) (@cuisongliu) +* bfe872050dcc4b842a97df896e7ca1771e64f828: refactor(main): add translator for ci (#3264) (@cuisongliu) +* ef61e0952f1654caa82976d19983f450b0c8537d: refactor(main): release docs (@cuisongliu) +* a5b21e22b9dea74fa18f10652a90933e959e04ee: 🤖 add release changelog using rebot. (#3229) (@sealos-release-rebot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1-rc5...v4.2.1-rc6 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.1.md b/CHANGELOG/CHANGELOG-4.2.1.md new file mode 100644 index 000000000000..d68fa46ba785 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.1.md @@ -0,0 +1,58 @@ +Welcome to the v4.2.1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 03e43356c73c5ef07b8e0fc3e6d50fe62ef25fdb: feat: desktop communication app change i18n (#3324) (@zjy365) +* 0a45fe75ecfe5bed0ac1ea74c4261e63fd2db173: feat: js-cookie (#3322) (@zjy365) +* 2de6e52812e87b02b0a65ef012e316f3324992f4: feat: support override platform flags for inspect command (#3304) (@fengxsong) +* 0866fbbe051ee06c8fdf6fa0452878cc520c2706: feat:costcenter (#3326) (@xudaotutou) +### Bug fixes +* 2114a8744b5b262928563ec7a246e838f06dade3: bugfix: fix the wrong unit (#3313) (@nowinkeyy) +* dfa71f933e986473da3461cb8fe8e116fe6e4273: fix: avoid fetching local repos repeatly (#3336) (@fengxsong) +* bc26a4ae7b525c1460f44e3c15f2b127988dee62: fix: i18n and db opsrequest (#3348) (@c121914yu) +* aa9be9451d6f17f53632e4482c47a7b3c618548e: fix: launchapad i18n & update app way (#3321) (@c121914yu) +* 2a0e3e867df8756ff80f6c1ae57538333abf3c70: fix: launchpad i18n init;fix: do not split command (#3329) (@c121914yu) +* 502bee1437d919cbf64b974556cac2fc17990a76: fix: modify cookie policy (#3325) (@zjy365) +* 5eeb554e5b510abedfa168612327b86a2319c2d0: fix: retry when error contains internal server error message (#3347) (@fengxsong) +* 807a736100fba5b20efa7970237d099afec47e0a: fix:fix version comparison issue (#3328) (@overstarry) +### Other work +* a4c62adf98682970049ab98a5ee26719e0f427ca: Add adminer prints. (#3346) (@zzjin) +* e2514f060e60d3dddd1a87dd84f413847b1121ce: Fix desktop login error (#3320) (@xudaotutou) +* 0118f9fe56c6268d6ec2a90fdd0106dd24c27f76: SDK enhances communication, app supports event monitoring (#3311) (@zjy365) +* 261d85cc8cfd465a3cad7b252bef01a772a94ee2: Try using probe to check pod ready. (#3345) (@zzjin) +* fb191f4fbfb97b9692ecfb9b0e150d61dc249704: Update README (#3341) (@yangchuansheng) +* ae21df79204adeecde7eaf913cf8237628d5bd58: Update format linter. (#3317) (@zzjin) +* 8e1ead6a1ea1b3476980401f59bf7a147c628379: Update reflector namespaces. (#3351) (@zzjin) +* 29e86d2971dc4559ca6ad30f207f7600c1985d9d: debt controller implement (#3160) (@bxy4543) +* ff8d2dd74395ee278727df120642d3b03f5c11f4: docs(main): fix link for docs (#3309) (@cuisongliu) +* 475a87bb8d2d047cd387c32f64a687d92b24dd55: docs(main): fix link for docs (#3310) (@cuisongliu) +* 7eae15a8bbdfd03e421b36e9adae99da6718acbb: fix resources controller Kubefile (#3332) (@bxy4543) +* 7185ae7057e62027edbb5c5dc29d8c44caca85b8: fix resources controller Kubefile (#3333) (@bxy4543) +* fe6da0a030e6883f3b4447a66ca771cf8bf4309a: fix(cert sign): not get the correct first svc ip from cidr (#3308) (@yldian) +* 46f3003b8dd21310fab7008be43ef2046ab05655: fix(fronted desktop): Cookies are not set by default (#3301) (@xudaotutou) +* b2bba64c0b3a780b76651fda2721ed44a40ebe58: fix(frontend/costcenter): fix i18n (#3330) (@xudaotutou) +* b0a0461b6cba7215fce272a7eb24b5437c7df242: fix(frontend/costcenter):fix i18n (#3335) (@xudaotutou) +* 86e80ed3690bccee81def11e8a5dd313f3721b05: improve cloud deploy (#3339) (@lingdie) +* 49e9981cdf7410e1e2fa6c36292d249315ee9a57: realize timing monitor resources;add mointor, metering to automatically build container image & cluster-image. (#3240) (@bxy4543) +* 4fda7eaec6e19e8d718f79473218765c5d48baf1: refactor(main): add ednpoint check for adminer (#3300) (@cuisongliu) +* 4e1f1f79fdd29ed2ae83f15a96d7afa1eb4bbfd9: refactor(main): add ednpoint rbac (#3303) (@cuisongliu) +* 456b0991df4044acefe11085c4beba5da1e85b60: refactor(main): add link and report (#3306) (@cuisongliu) +* 837234467679471f730bbe38cdc9e74a939898d8: refactor(main): add pull_request_target for checkout (#3314) (@cuisongliu) +* 9101369cd8b00a115cfa53fd034ce8714c5e1a59: refactor(main): add qa for loggers (#3307) (@cuisongliu) +* 18820d0f6d0b963a3eddb9ed8801f68645ef9ad5: refactor(main): fix ci code error (#3323) (@cuisongliu) +* b6d1417ac0ede86eeb8f112a724384bcc68d7982: refactor(main): update link pr action (#3315) (@cuisongliu) +* 33c69299e7e50f567258f1586c0c9f50e7a495f6: refactor(main): update registry when node is update error (#3312) (@cuisongliu) +* daf455b9dda41f610969d18ac7c4b65628eea64a: refactor: registry command with exampleprefix var explicitly (#3305) (@fengxsong) +* e11903e6bc8ed6937e2e91b664a80346504c92e8: update app crd (#3340) (@lingdie) +* 43d8190ef911b345d35052db840ba3d3cd14690c: update issue num (#3343) (@lingdie) +* 8f868d53a9222ea5e0ab488fd0f6f9f2310c03f6: 🤖 add release changelog using rebot. (#3299) (@sealos-release-rebot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1-rc6...v4.2.1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.2.md b/CHANGELOG/CHANGELOG-4.2.2.md new file mode 100644 index 000000000000..b6e582b22beb --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.2.md @@ -0,0 +1,15 @@ +Welcome to the v4.2.2 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* 492e9091e840efcddb91290be80995511ac2cecd: revert: rollback mergo version (#3364) (@fengxsong) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1...v4.2.2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.2.3.md b/CHANGELOG/CHANGELOG-4.2.3.md new file mode 100644 index 000000000000..800471ad0aff --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.2.3.md @@ -0,0 +1,23 @@ +Welcome to the v4.2.3 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 77e312e408053e483c4b17e40dd12c6cf1115f8f: feat: support override platform in diff command (#3376) (@fengxsong) +### Bug fixes +* 450b52aa90a0193cf49898b76d50c09dc89a578a: fix: error while unmount container mountpoint (#3385) (@fengxsong) +### Build process updates +* 4e0e84b2a7af0a35e6d9a6dcbeeae4477055c761: build(main): add sync patch image (#3398) (@cuisongliu) +* 0ce9c15ef92c6ae820f1f4a3b43ad0499e318ebc: build(main): auto renew issue and auto comment body (#3393) (@cuisongliu) +### Other work +* 7413c81f125fb16e99c44925c41d76857d51ef3d: docs(main): upgrade version for kubeadm (#3371) (@cuisongliu) +* 2b42496c3c0afb766d8d55a3fd98177b4b155bca: restore easter egg feature in diff command (#3388) (@fengxsong) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.2...v4.2.3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.0-rc1.md b/CHANGELOG/CHANGELOG-4.3.0-rc1.md new file mode 100644 index 000000000000..967b63ac606b --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.0-rc1.md @@ -0,0 +1,24 @@ +Welcome to the v4.3.0-rc1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 0a6ea4f44d87d42a80e9771218aff77bd5bb5b34: feat: add host ssh and sftp client map cache for sftp (#3450) (@ArenaSu) +### Bug fixes +* 75231c231766f620c7293155a2e67ddd37cde0d7: fix(main): fix merge support $() (#3427) (@cuisongliu) +* 9da41c56873f1342d05e2053b03afe9113af9fc8: fix: upgrade k8s version failed on non default ssh port (issue #3069) (#3434) (@yangxggo) +### Other work +* 472c7b50b31062dcfb4aad89cafcd2baea7d2149: Feature:sudo support for scp (#3421) (@ghostloda) +* 0567195db44fd25469094fbe90a90b0a7973b125: refactor(main): auto cherry pick (#3412) (@cuisongliu) +* 81cd5f7023e598e4dc19306a1a594c5117b46964: refactor(main): delete node for check (#3405) (@cuisongliu) +* 933bc8a6589164f9bc25e9d2909bd76031c3fa6a: refactor: save and load subcommand support multiple images in a single tar… (#3442) (@fengxsong) +* 27ea418989bb99cbbc1f52d5a312a68d99c60617: support old version of kubelet-config (#3447) (@yangxggo) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.3...v4.3.0-rc1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.0.md b/CHANGELOG/CHANGELOG-4.3.0.md new file mode 100644 index 000000000000..1757bd094384 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.0.md @@ -0,0 +1,21 @@ +Welcome to the v4.3.0 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* aaafe9a49853b596536c8c1de10e1d682cc3ceed: feat: support dual-stack (#3509) (@ghostloda) +### Build process updates +* f3415f4403880cef422a20913fa41fae09d93a36: build(main): add ci for latest (#3513) (@cuisongliu) +* 7ee53f1d67d62018b301cd11e4097bc4cb34b298: build(main): add ci for latest (#3516) (@cuisongliu) +### Other work +* 9259b2d21bf6507d9ced75a15e47b0742b72c28c: feature(main): add commitor for release (#3487) (@cuisongliu) +* 3a07652bcf09c002a449018fbdc5147d18e31993: refactor: cri_server multiple versions of AuthConfig (#3507) (@muicoder) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.0-rc1...v4.3.0 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.1-rc1.md b/CHANGELOG/CHANGELOG-4.3.1-rc1.md new file mode 100644 index 000000000000..3e74fde0dec5 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.1-rc1.md @@ -0,0 +1,27 @@ +Welcome to the v4.3.1-rc1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* f7d140bf09aa7c32c421f69b308a328fc83694d6: feat: add and delete master or worker nodes with different ssh settings (#3525) (#3571) (@sealos-ci-robot) +### Bug fixes +* a40ff9177059aa47811cab5f15f1c8698a37ca0e: fix: check hostname unique in creating new cluster (#3569) (#3570) (@sealos-ci-robot) +* bbf88d0419ec3fff3e3a1f87bd78a5d34b3ed916: fix: if the root user is not specified,sudo should be disabled. (#3598) (#3599) (@sealos-ci-robot) +* aed2a4623f574201cf78d67c0acbcf4f33b2b01d: fix: upgrade cluster (#3588) (#3590) (@sealos-ci-robot) +### Other work +* 2ef981b614dacb363671be197f564c455f251945: feature(v4.3): delete workflow auto build controllers (#3557) (@cuisongliu) +* b68c4b8170d2ee29c36c820d69617a14841dd706: feature(v4.3): delete workflow link check (#3558) (@cuisongliu) +* 8ae61fb94656eb376d950917fc15302f095ccc13: feature(v4.3): default support tmp registry build image (#3535) (#3536) (@cuisongliu) +* 0a45544e190ecedebc6271718403c004394e71e0: fix cluster status (#3572) (#3574) (@sealos-ci-robot) +* 6c156c7ea2dc593b10582a87a90b79f2089eccca: fix e2e apply test with infra && fix wrong ssh.fetch interface method implementation (#3560) (#3567) (@sealos-ci-robot) +* 65b7a43da4fbe3103fe02db0780bfb3491ecc1dc: fix the current cluster does not exist, but the creationTimestamp of the expected cluster is not zero. (#3549) (@bxy4543) +* 98719a4e3ed6c9fc4e2b3a017aa2fc670ffc451a: refactor: remove versionutils (#3592) (#3593) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.0...v4.3.1-rc1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.1-rc2.md b/CHANGELOG/CHANGELOG-4.3.1-rc2.md new file mode 100644 index 000000000000..164063ffd849 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.1-rc2.md @@ -0,0 +1,27 @@ +Welcome to the v4.3.1-rc2 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 296db1aeb9135a3e7333a735a6869af7f6a9d69a: fix(main): fix: skip pulling app image (#3649) (#3650) (@sealos-ci-robot) +* 7f78d1748ededc997d7116e5e5da1fef47d33bbe: fix: avoid concurrent map writes (#3729) (#3730) (@sealos-ci-robot) +* 9d017cb3449472b53373859ae273e95ca6b75818: fix: install latest sealos docs. (#3646) (#3647) (@bxy4543) +* ccc09f28aafacb353a99c1cfaf50032c6e161f37: fix: remove unexpected mergo options (#3682) (#3683) (@sealos-ci-robot) +### Build process updates +* 9bfa1ef4d4a23b88a937978dc18f24370bcc3780: build(main): add test docker image (#3693) (@cuisongliu) +### Other work +* 139e4037fbc32460ddf00c379c2eff2292dbff1c: Sweep: run guest command one by one and support run patch-type image (#3677) (#3704) (@sealos-ci-robot) +* cc43ac5c7b13729436f2b9485a2825e16a4a1323: feature(main): delete registry feature using sreg code replace (#3714) (#3720) (@cuisongliu) +* 951ec11e49b9d99c4ee891799f89d205fc792a2a: optimize docs: install latest sealos version (#3638) (#3639) (@cuisongliu) +* a40c5ed1d993af0e7f0cf0e5e790a2ce1c5c3630: refactor(main): ssh interface instance func (#3654) (#3660) (@sealos-ci-robot) +* 8f63707b64b9f036b7700e02ba4e91676625a115: refactor(main): using new runtime interface (#3691) (#3732) (@sealos-ci-robot) +* da01479f408408da58c0fe98860886408b6397b3: refactor: use http mode as default to sync registry contents (#3659) (#3661) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.1-rc1...v4.3.1-rc2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.1.md b/CHANGELOG/CHANGELOG-4.3.1.md new file mode 100644 index 000000000000..11329a4931b4 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.1.md @@ -0,0 +1,17 @@ +Welcome to the v4.3.1 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 5f62b0cd4ad03053bcb8a65e6cc0219c5c88b16d: fix: ignore signal killed when context cancelled (#3748) (#3749) (@sealos-ci-robot) +### Other work +* 2a9e4c8864de93eaaee0ef7ea16511e41693dd92: update dockerfile base image to ubuntu22.04 (#3740) (#3746) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.1-rc2...v4.3.1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.2.md b/CHANGELOG/CHANGELOG-4.3.2.md new file mode 100644 index 000000000000..bbaaf483246b --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.2.md @@ -0,0 +1,15 @@ +Welcome to the v4.3.2 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* 3d1cbf6cdd1bdbac912d6aa079557723620e49b7: feature(main): using 22 default port (#3759) (#3760) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.1...v4.3.2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.3.md b/CHANGELOG/CHANGELOG-4.3.3.md new file mode 100644 index 000000000000..ac1f6933d1cb --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.3.md @@ -0,0 +1,19 @@ +Welcome to the v4.3.3 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 9067cecf01deb430912931e9ede7e35e1f6517d4: fix: panic when resetting a non-existent cluster (#3782) (#3788) (#3790) (@sealos-ci-robot) +* 68a9aa6de6a2fdf26b5040a2d1d79653db7a720a: fix: setup logger after setting runtimeRootDir (#3793) (#3795) (@sealos-ci-robot) +### Other work +* b923bf085aacfb50ae77d219967e48943dcbc5b5: feature(main): remove docker and containerd ci (#3765) (#3766) (#3767) (@sealos-ci-robot) +* d27f4fbc84181622bb48cb569fbdfdb31f1ae0a4: feature(main): sealos run uing defualt ssh port (#3791) (#3801) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.2...v4.3.3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.4.md b/CHANGELOG/CHANGELOG-4.3.4.md new file mode 100644 index 000000000000..017a574157ee --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.4.md @@ -0,0 +1,17 @@ +Welcome to the v4.3.4 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 6cd5d782435dc93e2be1f25c5dc118c749f499bb: fix: remove copying kubeconfig file (#3809) (#3813) (@cuisongliu) +### Other work +* 047cb2635330c33cb9b78990e659428276ac84b5: merge from 4.4 (#4016) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.3...v4.3.4 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.5.md b/CHANGELOG/CHANGELOG-4.3.5.md new file mode 100644 index 000000000000..1f5f8e81612b --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.5.md @@ -0,0 +1,19 @@ +Welcome to the v4.3.5 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 715a648081df2aa150d88baf1b8a44d40d1df12f: fix: scp files (#4041) (#4042) (#4043) (@sealos-ci-robot) +* 7ad66b4e8055eaf7ad14acf60de605e8af987a47: fix: sync cert failed on non default port (#4034) (@cuisongliu) +### Other work +* ba3d7c932e5461528c292126205a3cbc35cb4e5f: feature(main): using sreg 0.1.4 build image (#4023) (#4028) (#4029) (@sealos-ci-robot) +* 881c10cbb35d03950f4de03c12f5c4e2df33d453: fix(4.3): revert cert feature (#4053) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.4...v4.3.5 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.6.md b/CHANGELOG/CHANGELOG-4.3.6.md new file mode 100644 index 000000000000..f579f0772a4a --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.6.md @@ -0,0 +1,19 @@ +Welcome to the v4.3.6 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 34236b4ef946e09ebf695cc7640fb33cf87f5353: fix: create new container mount when force override (#4068) (#4072) (#4079) (@cuisongliu) +### Other work +* a2719848e02e5a00ffa7718243bfaf8801d77669: :bug: set default http sync registry (#4120) (@cuisongliu) +* ff6ac8ce09d66a4da123d714df8c8c13648487a4: Fix conflicts (#4065) (@muicoder) +* b5efb5997988d7be74a042781e7a8008bfcecab9: fix sync image (#4119) (@bxy4543) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.5...v4.3.6 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.7-rc1.md b/CHANGELOG/CHANGELOG-4.3.7-rc1.md new file mode 100644 index 000000000000..c36652e0e026 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.7-rc1.md @@ -0,0 +1,15 @@ +Welcome to the v4.3.7-rc1 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* fef14bea9df8b36ec124e1a66a950fd12d2ae462: :bug: support env for export (#4135) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.6...v4.3.7-rc1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.3.7.md b/CHANGELOG/CHANGELOG-4.3.7.md new file mode 100644 index 000000000000..fdfae3fb3aa9 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.3.7.md @@ -0,0 +1,19 @@ +Welcome to the v4.3.7 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* 311213aa175c71120f6f91ce616c74e8ab05661d: :bug: fix env merge for sealos run (#4144) (@cuisongliu) +* 5cd471632c42fd1535dcfe542c9faa148a7b4ec3: feature(main): support crun youki gvisor kata (#4189) (#4191) (#4192) (@sealos-ci-robot) +* 4057aacb494206704dcf23441bd0ec9d4e653235: refactor(main): fixed golang lint version (#4141) (#4142) (#4143) (@sealos-ci-robot) +* 579a0163baae48f4547b394530e8bc31196da8ef: refactor(main): skip success exec (#4162) (#4170) (#4175) (@sealos-ci-robot) +* f39b23394b9d06774cf096de7d342201d4b264ca: refactor(main): unmount override container (#4161) (#4208) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.7-rc1...v4.3.7 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.4.0-alpha1.md b/CHANGELOG/CHANGELOG-4.4.0-alpha1.md new file mode 100644 index 000000000000..885ffbc8f4aa --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.4.0-alpha1.md @@ -0,0 +1,39 @@ +Welcome to the v4.4.0-alpha1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 9f656f5a149f866271e8d2fb32ac213957f01ac7: feat: deserialize object from multiple yaml documents (#3862) (#3866) (@sealos-ci-robot) +* 65d998613b9d4e623d2fc53b64195b2e6da4d872: feat: k3s runtime (#3787) (#3814) (@sealos-ci-robot) +* 96b4f2de3dbef81d3fb32e6b42a3802341efa4bd: feat: support rendering template files on target host with sealctl (#3872) (#3874) (@sealos-ci-robot) +### Bug fixes +* ce8f642b29dec17cb31da8cc323db68278441983: fix: default config for server/agent (#3842) (#3845) (@sealos-ci-robot) +* c60b2fd8375a06bb8e7388efc30c81ec1172af66: fix: dnsDomain does not take effect in kubelet (#3834) (#3835) (@sealos-ci-robot) +* 2d4d78b6295c559153a63fce3554e43b6dd6e758: fix: ignore http server close error (#3854) (#3857) (@sealos-ci-robot) +* 86ba1e46e8ca5afa995bcbe09f385165b4f6fc2c: fix: merge config order (#3847) (#3849) (@sealos-ci-robot) +* 012863eebb7bda3bdd67eb4a01750b2512580460: fix: panic when resetting a non-existent cluster (#3782) (#3788) (@sealos-ci-robot) +* 63ace17fff2342ec59500766f2c30c90f69edf0c: fix: remove copying kubeconfig file (#3809) (#3812) (@sealos-ci-robot) +* 726118ae8b24696977ece7066882ce4ff530f33e: fix: setup logger after setting runtimeRootDir (#3793) (#3795) (#3796) (@sealos-ci-robot) +### Other work +* 3edfd1cab9b775199b8a8ea98ffa69d7911255fc: K3s runtime (#3837) (#3838) (@sealos-ci-robot) +* d29b03e26d67af8fbbcfd147ea670918b95475a6: feature(main): add initsystem cmd and so on (#3841) (#3861) (@sealos-ci-robot) +* 116c72d7de61ccdcf70b3ed9e5bdbf0294d73b06: feature(main): add spec for init-cri and clean-cri (#3840) (#3844) (@sealos-ci-robot) +* 1efbb04aca46edca077f48f8fd3c02c6efddad93: feature(main): remove docker and containerd ci (#3765) (#3766) (@sealos-ci-robot) +* eeee7ff4cb8d92e4374227cdfa509002d3e90c82: feature(main): sealos run uing defualt ssh port (#3791) (#3801) (#3805) (@sealos-ci-robot) +* 6561bded49303c996fd40f16d98dba8dbf766db7: feature(main): using 22 default port (#3759) (#3760) (#3761) (@sealos-ci-robot) +* fecb3d14bf56751ae4eee4661c5a0ca6e71ceb22: refactor(main): add spec for image label v2 (#3816) (#3827) (@sealos-ci-robot) +* efe8258b0ce0665fc2c57138388efc6522cd2f77: refactor(main): k3s runtime hosts fix (#3865) (#3873) (@sealos-ci-robot) +* 50acaece505a6c135594e24bece28fefb508d11e: refactor: remove redundant nil check (#3856) (#3858) (@sealos-ci-robot) +* 16ba0e796bd2d0c6e5c69b8fcc678c82693a3e7f: refactor: separate runtime into different modules for better directory structure (#3741) (#3757) (@sealos-ci-robot) +* 5a65c83aa31c78e5e7922edcfc40d3f68a5955c0: style: remove unused utils (#3830) (#3833) (@cuisongliu) +* 2a265f1f3f9c145e2bebc07f8a23aca9eadd8a44: style: rename some functions and constants related with runtime implementation (#3763) (#3779) (@sealos-ci-robot) +* 5c2bca39fbda60813314399292c90b2400d0e2c8: test(main): add test for k3s (#3875) (#3878) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.3.1...v4.4.0-alpha1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.4.0-alpha3.md b/CHANGELOG/CHANGELOG-4.4.0-alpha3.md new file mode 100644 index 000000000000..cc9a5ba02305 --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.4.0-alpha3.md @@ -0,0 +1,15 @@ +Welcome to the v4.4.0-alpha3 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* 050d70b43fae2c919e5255fcb84e760806b9bb63: feature(main): add lvscare gomod (#3995) (@cuisongliu) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.4.0-alpha2...v4.4.0-alpha3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.4.0-beta1.md b/CHANGELOG/CHANGELOG-4.4.0-beta1.md new file mode 100644 index 000000000000..c17031d17d9a --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.4.0-beta1.md @@ -0,0 +1,24 @@ +Welcome to the v4.4.0-beta1 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* cb75187f249f142c9e96a2543dbf60efdbb6c37a: fix(main): delete clean_unused_func (#4017) (#4018) (@sealos-ci-robot) +* 4242e7c3dcb19edb1da80c7119f8ceca3965b4b8: fix: create new container mount when force override (#4068) (#4072) (@sealos-ci-robot) +* 105a74c48f726df599bef29d718377219dcbf3bc: fix: if string addr has not colon (#4045) (#4046) (@sealos-ci-robot) +* 023045510ae93691361dc7ef94db634a75ddcb07: fix: make the usage of '--env' flag more precisely (#4060) (#4063) (@sealos-ci-robot) +* 06f2cf7945d20470a853d837ee420fbe883875ba: fix: scp files (#4041) (#4042) (@sealos-ci-robot) +* c86e7c751c440916cad2ee01115ba8c99cdb228f: fix: sync cert failed on non default port (#4031) (#4032) (@sealos-ci-robot) +### Other work +* 3f4d1005326d93f239433147f730f6029ec16962: feature(main): reset cluster not delete nodes (#3991) (#4022) (@sealos-ci-robot) +* 13b303669565694d22df284a3ef0c73a7b3f853e: feature(main): using sreg 0.1.4 build image (#4023) (#4028) (@cuisongliu) +* b8affb05bc7436515abb4e6933553176c45ace59: refactor env keys (#4066) (#4076) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.4.0-alpha3...v4.4.0-beta1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.4.0-beta2.md b/CHANGELOG/CHANGELOG-4.4.0-beta2.md new file mode 100644 index 000000000000..46a7f92ae90c --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.4.0-beta2.md @@ -0,0 +1,17 @@ +Welcome to the v4.4.0-beta2 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* 62e8e007558ebbe2000a8ac55bd27771eef2a1d2: fix: #4081 (#4083) (#4085) (@sealos-ci-robot) +* 33aee733b0586427b5b41963c8826a1e1f598ab8: fix: merge cluster.spec.env (#4100) (#4105) (@sealos-ci-robot) +* 6ff39a8a812f1576290c49771e8087a38fa7eab4: fix: remove default vip (#4087) (#4093) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.4.0-beta1...v4.4.0-beta2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-4.4.0-beta3.md b/CHANGELOG/CHANGELOG-4.4.0-beta3.md new file mode 100644 index 000000000000..930c1f62987c --- /dev/null +++ b/CHANGELOG/CHANGELOG-4.4.0-beta3.md @@ -0,0 +1,26 @@ +Welcome to the v4.4.0-beta3 release of Sealos!🎉🎉! + + + +## Changelog +### Bug fixes +* e1a099e9a630af942dec4050e6466af40bb60086: fix: --env not work during sealos run (#4140) (#4145) (@sealos-ci-robot) +* fa887779be7e307b130ac5a6a4b39fea8c62c5d4: fix: return converted component configs (#4121) (#4126) (@sealos-ci-robot) +* 0de99c5a6866b97de81afd0fbd5a5c84d132ede5: fix: ssh sync registry contents to target dir (#4123) (#4124) (@sealos-ci-robot) +* c8f7a9c8fe82a10bbbea4d36ba49c09d942d1fbb: fix: sync registry contents via ssh (#4106) (#4109) (@sealos-ci-robot) +### Other work +* 1b96443449e51145853aaacd1963312acc9c7592: cherry-pick #4281. (#4286) (@lingdie) +* 86eb9e07c484d1a37a2b14efba449b0b1a2e9e5c: docs: upgrade sreg gomod for release-v4.4 (#4283) (@LZiHaN) +* 02cd978e25cb7459ce5814ec6225d3f200faf227: feature(main): support crun youki gvisor kata (#4189) (#4191) (@sealos-ci-robot) +* e332192ed46a9e43b500ddaab0acd7beef0d6f9c: feature: `sealos create` support for environment variable based template rendering (#4222) (#4232) (@sealos-ci-robot) +* 04c81fe9551f3837356ae6b9f2c0e2e2837ac88b: refactor(main): fixed golang lint version (#4141) (#4142) (@sealos-ci-robot) +* e8fd12c95099a30138a6277ce0afdcda1f7efa61: refactor(main): skip success exec (#4162) (#4170) (@sealos-ci-robot) +* 286c18103b5d4ee8299719e5394cc79fe886c662: refactor(main): unmount override container (#4161) (#4205) (@sealos-ci-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.4.0-beta2...v4.4.0-beta3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-alpha1.md b/CHANGELOG/CHANGELOG-5.0.0-alpha1.md new file mode 100644 index 000000000000..a8e83d948b55 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-alpha1.md @@ -0,0 +1,15 @@ +Welcome to the v5.0.0-alpha1 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* c0cc8e78f3e52fb4ae4d5e8fd04147fff0c8cfb5: 🤖 add release changelog using rebot. (#3352) (@sealos-release-rebot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v4.2.1...v5.0.0-alpha1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-alpha2.md b/CHANGELOG/CHANGELOG-5.0.0-alpha2.md new file mode 100644 index 000000000000..ebc97a9f8725 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-alpha2.md @@ -0,0 +1,505 @@ +Welcome to the v5.0.0-alpha2 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* f98921b205a9adeb087867ef43441b35ff5d5946: feat(costcenter): supported for network (#3870) (@xudaotutou) +* bf46d395d3fd1afcc72e5481ea4f74b6050dbea3: feat(costcenter):measure by app (#3983) (@xudaotutou) +* 6d047b7a0aeeb1fbdf8b1e6434e9fa7c7bc1dd0a: feat(terminal):auto switch namespace (#3959) (@xudaotutou) +* 6d52319b7d461a604e9ff8dacb1c612383598cdd: feat(terminal):auto switch namespace (#3961) (@xudaotutou) +* 0599f60faf0dfb537f8fb6c3bdad7b67f6cc7dfe: feat(user_controller): create user -> sync three roles (#3555) (@geniuxy) +* 4efb22a4f1f87ef870cdf5434eacd38278e831b5: feat: adapt license for community edition (#3644) (@yxxchange) +* 796bbebb74e0033d5ffe8646fdc463144f0c50a0: feat: Anticipated Price (#3357) (@c121914yu) +* a17d93205c524f3d0127687d1f90b74c6392fc46: feat: BillingRecordQueryStatus add TotalCount (#3686) (@bxy4543) +* 9c3d1a9ed83c5660d4ae349b3af7f431e3d351e7: feat: adapt aio cluster image fot changeable port. (#3674) (@lingdie) +* 9a013cea2570c4ea27b8f45c09b104c154af0f06: feat: adapt for gpu cost (#3596) (@bxy4543) +* b695e9978b951a1629cb4ef311cc9c8fecfdbfc9: feat: adapt license for poc (#3594) (@yxxchange) +* 3a0dc73f522fb96c1ac1af45edfde4fdaf7dff6f: feat: add and delete master or worker nodes with different ssh settings (#3525) (@yangxggo) +* 581f5fa30bcc54476ef6ba507ea2108593f62fb8: feat: add cloud cluster image (#3462) (@lingdie) +* b458c8237d6165c18d67d2f39256e25d6f54b733: feat: add cloud install script. (#4030) (@lingdie) +* b20966b33da2694f64f562addd346440633d6db2: feat: add database monitor (#3934) (@sakcer) +* cd47600d9470067139d3a2824d54644dd1aac16a: feat: add debt message code (#3724) (@bxy4543) +* 40babea9d5fe17c08783e36931e0fa5632ac5a1d: feat: add default limitrange (#3603) (@bxy4543) +* cf8ba36a48e5f6b45e16517bc3779d61e131cbe3: feat: add host ssh and sftp client map cache for sftp (#3450) (@ArenaSu) +* 2c6d7df387fefc7a30439db52a089e44b3435bb6: feat: add icp query for ingress. (#4069) (@lingdie) +* 759509ab17a47db8c500ded82f704d6a6eae3094: feat: add imageshim for terminal and applaunchpad cluster image (#3458) (@lingdie) +* 205a5bb4fdeeb05d8ca27bd25cda37b8eeddb8b1: feat: add initGPUInfoCM func (#3633) (@nowinkeyy) +* 7df69f23cf520b0d171e7223f1fe4f1cb7487750: feat: add latest cluster image and update deploy docs. (#3484) (@lingdie) +* 67c4381d63d8930edac31c62f9fc544ecf60cacd: feat: add mongo version env for sealos aio cluster image (#3893) (@lingdie) +* db209d41bed77ac021b6d8f18bfff026c40843c5: feat: add operationrequest for multi-namespace and update user controller. (#3595) (@lingdie) +* c3d29736c6dbb949701dd2b4abb2d2a8d0495b86: feat: add owner check. (#3972) (@lingdie) +* 6b5875687f7b57874ebf778c9cb67f2a7c99dcd6: feat: add sealos cloud install script and docs. (#4035) (@lingdie) +* cb1fefe72671a7fd16cff173b9e0f57a9014ac9a: feat: add update operation request (#3802) (@lingdie) +* 1f8dc525f3ff401b7ee2f6491da9aa1261397bd8: feat: admission webhook for sealos (#3678) (@lingdie) +* 72e5d94f402fdd8ed94968c99ca769b010f489b2: feat: change baseCount to common module for account charge (#3673) (@yxxchange) +* c4baed2f6dc305af71c75faaedfaed16adf01d19: feat: custom domain port (#3635) (@c121914yu) +* 20072ad0153f0649fc9632cf09e007d3840c74ca: feat: database env set storageName (#3491) (@c121914yu) +* 49aaf135df95bc607bfe4e2a57b8220926c2dc81: feat: dbprovider add monitor (#3604) (@zjy365) +* 48a1d690f17cfa0b7c6de27a79755bfb44c529ad: feat: deserialize object from multiple yaml documents (#3862) (@fengxsong) +* 0b2dd6da78e369cc08b51708d50f94752c3dcae7: feat: develop payment interface (#3610) (@geniuxy) +* 78926338524f678a3fab99b3a0f243cb74b77bbc: feat: docs add theme classic navbar.json (#3694) (@zjy365) +* 1c73745ef2c817af267f136183ca491f43b9b963: feat: encrypte default ammount for account (#3681) (@bxy4543) +* f303c392495ebdc725a97dbcf1de2835e6a1906e: feat: expose mongo uri. (#3586) (@lingdie) +* f03f37a3c058f82b73c5f31063baad0bd67d1542: feat: fix license deploy in aio cluster image (#3684) (@yxxchange) +* 10e5aa252316cb597a16b86123b3a372b96ac18d: feat: init helm chart for sealos desktop (#3642) (@lingdie) +* e95d2cb4d09dee398f4e86461d06f58fc3f3598a: feat: k3s runtime (#3787) (@fengxsong) +* b6ef21704fd8b59a9b4cb49f733e1bdffc5fd65e: feat: language automatic detection (#4114) (@zjy365) +* 289994dc4cc198417aa5f64912c58b60e545820f: feat: launchpad multiple port (#3871) (@c121914yu) +* 7aaad32a641ba96ba27773b6a0efcbc3d4a43872: feat: license controller. (#4111) (@lingdie) +* c39b6a2c4f5ee89a3c899f7327670cd5ad744b68: feat: license desktop page (#3969) (@zjy365) +* 50e908ed417b46e481fe69458d8213bbf5335ea1: feat: quota for costcenter (#3734) (@xudaotutou) +* 15add6d6c89297d2ea72c81cca5561915447942c: feat: redis and dyminic dbversion (#3449) (@c121914yu) +* 076c77c00d1809963245787584f529bb71cbde84: feat: refactor license and preset admin user (#3712) (@yxxchange) +* 2fb77c2fdf5b042a898a5e3cf1bc97c67f3c0067: feat: stripe return url (#3652) (@bxy4543) +* d3e345664850d15632058c38704d0e8bdea7f433: feat: support dual-stack (#3509) (@ghostloda) +* ef612ecb1885ed4ddb5c3a1404312c010a989bae: feat: support override platform in diff command (#3376) (@fengxsong) +* 4f06cbab8fd9b0d88be8774b807bc3111c0d150f: feat: support rendering template files on target host with sealctl (#3872) (@fengxsong) +* a27db611db3b1dea44df2c8310f826204099c5fe: feat: support timeout setting for lvscare http prober (#3901) (@fengxsong) +* 6ee591101fe8043923318b0a9fa383d941977dd4: feat: template offline environment (#3936) (@zjy365) +* cf47ac7383aa5a450f0fa9ee8be03a6ef2860671: feat: template other resource (#4098) (@zjy365) +* a7e229ef3e8ec05a3ed69ef88b8d08ca69cbdfab: feat: transfer account balance (#3408) (@bxy4543) +* 2683778d3b8ca5393df65da1f59c9653d18ef1c8: feat: update sealos cloud cluster image (#3448) (@lingdie) +* 4ebacd88f05367c7cedc747cdd6796425ff6b1b9: feat: use cert-manager to gen tls cert and secret. (#3540) (@lingdie) +* f3a24d60512c96c36b8fa3ea7900f2e2cbe2ed29: feat:cronjob deploy & desktop background (#3776) (@zjy365) +* b837bd371c42e2fdc3f6a0ea9ab3fe8e8d1d70d6: feat:cronjob detail page (#3914) (@zjy365) +* 3c5445c27d26e301f30cbeeaedc94afed128ca30: feat:cronjob timezone (#3948) (@zjy365) +* 7bc47733ad4aaadd41a0ed4cf76470833c4c0f89: feat:db add quota (#4055) (@zjy365) +* 6ed0859dca8c0b26e5e17778376e58fe6cafede7: feat:dbprovider modify the monitoring disk unit (#3637) (@zjy365) +* 3be0e8fdd28efa20860aa70e619e8aac8e139aef: feat:desktop add umami (#3676) (@zjy365) +* 24bd6982dc184a4f401330f8de905ca5f4df1a78: feat:desktop background layout (#3785) (@zjy365) +* 19f10b168cfa6087abceef943a41abc93933d78d: feat:desktop get env (#3623) (@zjy365) +* a6f44ed01bb0b638a83a8d9609b556aee72ac61e: feat:desktop support theme (#3800) (@zjy365) +* 79d38e4b7007061ad28240f7175267a6fb07b55b: feat:docs add website & github page (#3675) (@zjy365) +* 99b150043fd05ef35eb6e5c892d0b8330531b972: feat:docs monitoring data (#3810) (@zjy365) +* 143bdc58eebc35bc4afc79f99a437f61d242ff44: feat:license app (#3949) (@zjy365) +* bff3b0346bd70ab9106c7e0765603b075d48a383: feat:license deploy (#4002) (@zjy365) +* 4cbb449522b1ca1d0dbcbad82fd1b5d08d7327b7: feat:modify the format of the exported file (#4040) (@zjy365) +* a62daa6824f3d23577b63928a6eb93ec55351ba2: feat:providers add template provider (#3658) (@zjy365) +* c0e2e601d8c3ca15fe6b3edbb3a4f7e9f816bfac: feat:providers cronjob app (#3770) (@zjy365) +* 5f6c39b2230e2ff1847d1ff7ac715624b5d5f616: feat:template develop dryrun deploy (#3778) (@zjy365) +* b02cf53011b2c5f329770d0a05f79606a722fef2: feat:template develop envs & links (#4073) (@zjy365) +* 91ad2fd2e6b87d5fa99badd32afe631134439465: feat:template draft & default random (#3973) (@zjy365) +* 65797c49dbd89a85ebbc817e83d0fceca63273b7: feat:template git err & terminal insufficient balance (#3755) (@zjy365) +### Bug fixes +* 0a45da3c74cd1511e5cb7bd258516e50c305de7c: fix(costcenter): fix get price error (#4007) (@xudaotutou) +* cd43d4b53a868663fe793f3a778dac79af6c64fd: fix(costcenter): transfer error (#4010) (@xudaotutou) +* 094ce77bdbbae1a29a12fddf599f6ca0e7587741: fix(desktop): When a team member is kicked out, there is inconsistency between the front-end and back-end states. (#3930) (@xudaotutou) +* bfcdbe2947497d66cacf3f7a7288a522f5c991f9: fix(desktop):fix get amount (#3699) (@xudaotutou) +* b90a17e6f52e7b9e8e474697968c632856e23a17: fix(desktop):invite & modify role (#4005) (@xudaotutou) +* 3f80a814311950f945ec1f21447496c7b99d9115: fix(main): delete .kube dir for all nodes (#3911) (@cuisongliu) +* e3fbf75a95ceb3ca0f0c687a310cd6f70cdabdff: fix(main): delete clean_unused_func (#4017) (@cuisongliu) +* 8cfe0e96d3d5f32c24959b6100b3a16c3aee54cc: fix(main): fix merge support $() (#3427) (@cuisongliu) +* 81609b2b01fbe9471fcbf30d1f010393c50d202b: fix(main): fix: skip pulling app image (#3649) (@cuisongliu) +* cbaf975a40d936c4d2752a6ca2936447e8fd4c2a: fix(main): sync cert for cert cmd (#3891) (@cuisongliu) +* 6b01bf2ca3a6783e3df12b6cce257b5953a6867b: fix: #4081 (#4083) (@fengxsong) +* 93961f75fb5b16e72a51fae3fc191173f0034205: fix: --env not work during sealos run (#4140) (@fengxsong) +* 47c90a42d7b8c280c5d579f01c37cce321310601: fix: HPA apiversion (#3922) (@c121914yu) +* 5785d7b1d42afc28c0cc68d107bb4cdbeaa035d7: fix: add free license and update kb version (#4159) (@lingdie) +* 5b3b444986e2666b25d3923cc899da6f1b8b879e: fix: add timeout for command execution (#3980) (@fengxsong) +* cf4242de24c674f5c37599acd379eb041245212d: fix: avoid concurrent map writes (#3729) (@fengxsong) +* c5ae85a48f2de86ac274dd539bb5373897310b80: fix: change SyncPeriod to setable to avoid too many users need sync at one time. (#3508) (@lingdie) +* c530333bba1628b60a756d247653431fd0aa8e39: fix: check hostname unique in creating new cluster (#3569) (@yangxggo) +* c79c1b20342c45987c6e48adb9fa97f87fdb09e7: fix: create new container mount when force override (#4068) (@fengxsong) +* fc1997cb839d41acd2b7928272ff38d92305b5ef: fix: default config for server/agent (#3842) (@fengxsong) +* 15451ea2f09290b1f92ae62f226996655c5c3320: fix: desktop open app & add number of notifications (#3478) (@zjy365) +* c7b1956221c31c81248c59479ea7b8d459d1fd97: fix: disable scp checksum by default (#3913) (@fengxsong) +* bca34d0d33aa450d03cf9bfe858f734559bbd93e: fix: dnsDomain does not take effect in kubelet (#3834) (@yangxggo) +* 610e5d3fd25467f539df9abffef90ecaab52fdf5: fix: error while unmount container mountpoint (#3385) (@fengxsong) +* bccd0cb99510c81ecd74d56ab959e3c6f525b51b: fix: fastgpt docs link error (#3643) (@c121914yu) +* 483280b7b97178c4bc4b369fa2fa3bcafd151b1d: fix: get remote kubeconfig path with $HOME variable (#3906) (@fengxsong) +* 2f00889e37b074d710c96052d8be803ce3d8bc2d: fix: gpu select ui and read file config (#3627) (@c121914yu) +* 25e08fd3dc56669586cb7a95a3c5483e8782e0ad: fix: if string addr has not colon (#4045) (@fengxsong) +* 493328777d7c8607f4eeda7d48b953502fa6e3af: fix: if the root user is not specified,sudo should be disabled. (#3598) (@ghostloda) +* b0f6c597ad744369dc460ded0bf4b18c215ad68c: fix: ignore http server close error (#3854) (@fengxsong) +* 4240435df17bc4e9ba2ed571b045ddbb6b965bed: fix: ignore signal killed when context cancelled (#3748) (@fengxsong) +* 5e83e76d2c6eb26362021bfe1e5c4f6e7da62da4: fix: ignore stderr output of sealctl render command (#3881) (@fengxsong) +* 1eddd2d25b70ffbaffb66acf810604f3035c1930: fix: imagePullPolicy (#3768) (@c121914yu) +* b33f00b6aaa6c209d2d96c26fe4aa6ac19c1f040: fix: init job for user with a uuid label. (#4147) (@lingdie) +* dc76a9c1d7f8eddd2f96d2d10d5baac148b04b9c: fix: install latest sealos docs. (#3646) (@bxy4543) +* 14aee6ea303e8b849aea3e5252ba9b77cb22fbe1: fix: launchapd main info ui (#3882) (@c121914yu) +* f8764c878374832fcb93a02fbc346cc9268b65be: fix: make the usage of '--env' flag more precisely (#4060) (@fengxsong) +* 3d1cd305d7eb997319a16e21e74566d764482bba: fix: merge cluster.spec.env (#4100) (@fengxsong) +* 8d6cb010e4ecf7aee149d4d9aa9d200dfe7b4268: fix: merge config order (#3847) (@fengxsong) +* 889b0bfd5c0e05920eece3cc6e074f616e3c9994: fix: panic when resetting a non-existent cluster (#3782) (@fengxsong) +* e2fde2e5da2998a2799ded7724a133563abcf2f4: fix: payment amount (#3621) (@geniuxy) +* f104b5f7e53285a272ae072739c688627ebb29bc: fix: payment error (#3641) (@bxy4543) +* 9488cdcd6ace12e2e866dc06d8ce22c285bf9194: fix: remove copying kubeconfig file (#3809) (@fengxsong) +* 17a0e3ea0d29528c0d9a9cc4212f4c38efb02a94: fix: remove default vip (#4087) (@cuisongliu) +* 500fbac0e432098d26c925849678711e72ab199e: fix: remove template files to avoid rendering templates repeatedly (#3916) (@fengxsong) +* 570ae9075fb481d7bda55d3db8cef5ae6bc7d00c: fix: remove unexpected mergo options (#3682) (@fengxsong) +* 15b094a3b2863ca98d8557c5411204cd28ec85d3: fix: return converted component configs (#4121) (@fengxsong) +* 541bf7c70a73e2f928c9acac2daf8efefbb905aa: fix: scp files (#4041) (@ghostloda) +* 63d5423fa4673264efa3b57f66c7a99d15782211: fix: sealos aio cluster image arm64 (#3786) (@lingdie) +* 7669308433aaf512f6d1042077bc426a0e822800: fix: sealos logout cmd documentation error (#3839) (@LZiHaN) +* de1ccb63018f0f25b1db41f8f2be552657653d6d: fix: service targetport update (#4011) (@c121914yu) +* 6e9afb76ae694cd8308361db616d3b1b3ba57724: fix: set desktop secret optional as env (#3583) (@lingdie) +* 84bcff3ab3d7ef341d851d5fb93c134e8ab9a828: fix: set runmode env for each host before rendering templates (#3886) (@fengxsong) +* 27fa72e59ec52f7d2719467544b940d9d41cbff3: fix: setup logger after setting runtimeRootDir (#3793) (@fengxsong) +* 51f43a6b239adf3b240637d2164e55722ebe648b: fix: skip same path (#3898) (@ghostloda) +* 2a8eca1a02694ca16a5264b138b8d58fdba279d7: fix: ssh sync registry contents to target dir (#4123) (@fengxsong) +* 7ed76db14b7a51d44b1cb29f18e12d43b6a4c85b: fix: sync cert failed on non default port (#4031) (@yangxggo) +* 92af429384c4620307a17b01f0f2fdb2c0dd4ae0: fix: sync registry contents via ssh (#4106) (@fengxsong) +* d2047078218a00e7d0e28297a8898a01a07e66f2: fix: sync sealos work dir (#3975) (@ghostloda) +* fa091a87721b85ab41cafa76cf6a5851b0d0f35e: fix: sync sealos work dir (#3987) (@ghostloda) +* 36a2678282db4bd7e24d72df31c24174b4d8e0a8: fix: upgrade cluster (#3588) (@fengxsong) +* 02ce79b9a613507c5c0c41dd4b8145724965da41: fix: upgrade k8s version failed on non default ssh port (issue #3069) (#3434) (@yangxggo) +* 344296e9334639719cc22f307d8e659a19fe583d: fix: using extra valid status codes when response status code greater than 400 (#3986) (@fengxsong) +* c1399981820d4e4bab5eea5fa0af3573298842ec: fix: volumn auto name and pod status (#3418) (@c121914yu) +* 1866a98cf6c01875a2d18902bdc648da607ba0fc: fix: write ip without port to hosts file (#3892) (@fengxsong) +* 494e48bb253dddfc278a6c6ce2d211dc2b939bc2: fix:desktop message notification field (#3499) (@zjy365) +* 90852b060b6bac101a50739b79c931d38f19e6c3: fix:docs github svg (#3817) (@zjy365) +* 77f88ceeeef866f04e734a3f0ecba0999c3ecae6: fix:docs link not exist (#3771) (@zjy365) +* 468aa11937dd8b58a51fcb7bf079aa3dbd36986f: fix:template multiple instances of @codemirror/state (#3931) (@zjy365) +* 64ff087b6b6377ae7588e2ac931fd0857ce1154f: fix:template default value (#3977) (@zjy365) +* cebb6578aa6e32a80ae4614813a0e4820c0bff6c: fix:template file export (#3851) (@zjy365) +* 3686c3dd915b5a1504e991d1884753e35e516079: fix:template multiple names (#4047) (@zjy365) +### Build process updates +* 4d4f7bbd51e4dd78101964355c1f68dbf9da0202: build(main): add ci for latest (#3514) (@cuisongliu) +* 118a4509e88dd3e8c37b388ee4fb461b4cec645c: build(main): add ci for latest (#3515) (@cuisongliu) +* 432d7172e1e511e1f0e84f2708478f5f278db1b6: build(main): add sync patch image (#3398) (@cuisongliu) +* 5857a39610e273ec8ca83d4a0a54ccc6902c791b: build(main): add test docker image (#3395) (@cuisongliu) +* ae8a86780325f9bbdb4552ee291fdb239572f6e1: build(main): auto renew issue and auto comment body (#3393) (@cuisongliu) +* 0c00b99f2be5e186c606657a74fea9f0f0b4f5f9: build(main): dev to latest (#3482) (@cuisongliu) +* 9b2ecedf56b6006b1b64e39b5be79ea135b99262: build(main): target pull registry checkout code (#3877) (@cuisongliu) +### Other work +* 6f9d5dbc693ed47b80cd124eefb667bfa3389d31: Adapt refactor user controller #3359 (#3394) (@bxy4543) +* 60682248975f04bfbc8ac728a81ac847840b0205: Add an example of how to fast deloy Gitea on Sealos (#3338) (@a497625414) +* de953956b189fac341e6f1fa3722441115a4e92f: Add app template draft spec. (#4044) (@zzjin) +* b2babef751193215d6f0d43574d2a103a076c445: Add app.sealos.io support template CRD. (#3957) (@zzjin) +* d9fb384eb2db9b151082848b14c1ffba13d2c024: Add cloud account (#3587) (@bxy4543) +* 98f6c0a1d2944fd42150b393c791a34138ef4047: Add docs of installing anki (#3506) (@yangchuansheng) +* e8baa140cf8b597b3437fd44ae60b07618268c90: Add links for sealos (#3505) (@yangchuansheng) +* 588a495ed95934f0ecf85dc39c65b68694f22015: Add the Service to automatically build a cluster-image. (#4096) (@bxy4543) +* 13b2a2948a866d762c0c1d6806e47eb93cfc5f2d: Add transfer notification (#3522) (@bxy4543) +* 2f9eedd8366f3428acc919b12cec41abe5334a02: Added a cloud-based module, responsible for message notifications, license issuance and validation, information collection, and monitoring functions. (#3409) (@yxxchange) +* 7780d42cbc2bd216b22a24b4b55cbc9f6ef5dfdb: Added a network probe to sense the network environment and adjust the license policy. (#3764) (@yxxchange) +* 384e84549d60763c61b3291b20ce607200895820: Adjusted the resource configuration for community edition (#3607) (@yxxchange) +* 53425971e2de9b6ee9713f0d51ef3b4e68d610d6: Change Doc Name (#3438) (@a497625414) +* 17e718404a3647d81f657980158766da7ece8ad7: Change Doc Name (#3440) (@a497625414) +* b78d365df5a324890dc920691b87e1ec4df2ab5d: Change Example Doc Images (#3435) (@a497625414) +* f18e9ff85b33f26523ee400af008b9e87c8d6cba: Cherry pick (#3565) (@cuisongliu) +* 07f1c712e6635c67841e9b1975ed9284e09f0843: Costcenter gpu (#3609) (@xudaotutou) +* 9953055f66c0a1a382e8d2ee9865f58df5e7b53c: Count pods that not in succeeded & watch gpu changes on nodes (#3754) (@nowinkeyy) +* c2df6ef12aebb7cf73b6e0dc933a1656ef6e4b59: Currency (#3671) (@xudaotutou) +* d3467d251018e3034d01832eb58aa992ca2c9e9f: Delete auth (#3925) (@lingdie) +* 8a7a1b50019b9ea13aabd72f50116b26269d20bc: Dev adminer (#4165) (@zzjin) +* 6f3c32c353bec722fcf1ab1fe1421887cba764cb: Dev template (#3960) (@zzjin) +* ffd13b322e8ee50170da54ad289a198ee617b598: Dev template (#3968) (@zzjin) +* 497040597ac0efc73ef6b440c50f1dbfda85c20e: Dev yjl (#3713) (@c121914yu) +* ba38e331cac1728c646aecae3ce7caca396f3c02: Docs: add "Add a domain" (#4075) (@yangchuansheng) +* fc85f6b926d0d23dcc5b5b481057840ae923406e: Docs: add "Expose multiple ports" (#4091) (@yangchuansheng) +* 130099ecdc65c4bd1c62f9531583d4fd6efb2c06: Docs: add "Update app" (#4088) (@yangchuansheng) +* 075a989f7b00464142de414f2c249ced34257732: Docs: change sidebar structure (#4056) (@yangchuansheng) +* f130bdad092c33c1e0c6ad09af8d8d70ceb02df7: Docs: fix typo (#4061) (@yangchuansheng) +* 35b1d244d182bbed2faa42b9b56fd979cd8abcd2: Docs: fix typo (#4089) (@yangchuansheng) +* 9ebba04d7c3bb880eccaddfafc795ed9ceef16e9: Docs: fix typo for sync_docs workflow (#4057) (@yangchuansheng) +* 4809993ef1070c64fac05eb67498e3d223fcaf6a: Docs: update Quick Start (#3970) (@yangchuansheng) +* 1fc97cb3bdcc294aeeb0e0b67d148dfb490b2ab9: Docs: update link for questionnaire (#4026) (#4013) (@xudaotutou) +* 6c558dbb27f15fcad4d22e985b4a1e1916a5ec08: Docs: update link for questionnaire (#4026) (@yangchuansheng) +* 78daa42214a9da3b8ef5bcd82fac5d4daafc6e6a: Docs: update sidebar (#4078) (@yangchuansheng) +* a3728eccf1a7dc579210adcbc8fe2cbeac2f28d1: Feat/traffic proto (#4095) (@bxy4543) +* 6a2d9f7e8358396f239398ca813b3aef71f2ccb8: Feat: adapt multi namespace account (#3828) (@bxy4543) +* 6ac3aa3542b09a8efb67326cf2924e2cd31197a9: Feat: add gpu prices alias (#3747) (@bxy4543) +* 4a57463fa38104d603870e0ba5e168d46cc64e65: Feat: gpu quota (#3751) (@bxy4543) +* 9c1690367f9d3c0b5d5a10ba28783529b520b687: Feat: init frontend costcenter helm chart (#3852) (@bxy4543) +* 69f2fdd742c6a0444716c4e74dbeae9ee322425c: Feature:sudo support for scp (#3421) (@ghostloda) +* 619c493786a5072d3cfcd7d59f054098913395bb: Fix Image Path (#3441) (@a497625414) +* b609e22b39ae611b635c70c3fe57f105262169c0: Fix adminer docker image url. (#3888) (@zzjin) +* 4a033f48dc1b70a3ce863ea3f124326d0f7d3688: Fix adminer&terminal. (#3664) (@zzjin) +* a0b021c3eefddd0965a33b467c94326bba484d33: Fix deploy doc link error. (#3742) (@zzjin) +* a636615ad199d8b59c75aea08d964cf6aec7a61b: Fix doc deprecated "#gh-dark-mode-*" fragment. (#3981) (@zzjin) +* 691925938b50e3b597154a71f73233e9955b47c9: Fix license (#4129) (@lingdie) +* a0dd4b1fab8fc40ec3b34f39d3b0ca147b030516: Fix license (#4131) (@lingdie) +* f1b3b6466569e04475655f9d4e8668a4cb353e10: Fix poc (#3695) (@xudaotutou) +* 4f8e05df3d5aafcdb2d139ceb091df49a68a28e1: Fix registry link. (#4133) (@zzjin) +* 92703e34a5b5349b333d9f65a2c5ca59e79c3f57: Fix tag typo. (#3985) (@zzjin) +* 7995c162e6c80932cf9a56aa2ce0341643c05f7b: Fix tlsEnabled default value. (#4164) (@zzjin) +* 4ad864be2c60731269af80f996093d8f809fde10: Fix update (#3696) (@xudaotutou) +* 513162a5684abc63990884f41c7724b37649b829: Fix/cloud init scripts (#4084) (@bxy4543) +* 00726c66bb59be88fe8bcc41a844e1ab3ab50cea: Fix/cloud scripts (#4115) (@bxy4543) +* 40eb424ee897608d03d6b006406777c7e2694455: Fix/debt webhook config (#3777) (@bxy4543) +* b85926f5952ba3b27abb95e852f6e647aa5c5a64: Fix/multi ns account (#3978) (@bxy4543) +* cb91077c589656190ab576fac1dfbed6f3dc9d40: Fix: billing error (#4006) (@bxy4543) +* 5558cac710f8959500029e8acdabfaec23455a6c: Get payment information from payment response status (#3370) (@bxy4543) +* 987fb122712d833795b9e50d109c4aca839197bf: Google oauth (#3884) (@xudaotutou) +* 490cef183db3454d3f36869191b6b446cada6139: How to Run Your First Code on Sealos (#3391) (@a497625414) +* d6f509a733e442fd342f5b06487028c3228e0d6b: Ignore semgrep github-actions check (#3534) (@zzjin) +* ce7cb738dd315df5f383074aea4b2e19cd081a00: Increase adminer proxy-body size. (#3663) (@zzjin) +* 510864813d5f6531cda4d7ec48a17bfbe9e96436: K3s runtime (#3837) (@fengxsong) +* 4027c491b3acc3db7c9f2faae0ad03d4780fa04d: Launchpad GPU (#3619) (@c121914yu) +* bb5abee999dcdf154c2ba6325c335155ede433b6: Optimize BillingRecordQuery to query separate recharge and arrival (recharge + gift) amounts (#4167) (@bxy4543) +* f45e6a9679f26641ad84c74f373fff4957f2362a: Optimize/cloud scripts (#4169) (@bxy4543) +* f09ff8288a6d5d7e88e5c8bc5c08dd0e27c51334: Optimized the details of the community edition, decoupled multi-processes, enhanced idempotent capabilities, and increased configurability. (#3726) (@yxxchange) +* 4ad2acb6a764089bc93ba1cfdb3c00fe1d7e213c: Rebase #3867 (#4092) (@bxy4543) +* 07da09a96c50bc5c2414c2803de722db83684740: Rebase docusaurus doc fragment. (#3993) (@zzjin) +* ffba35047dfb1b5b47a8437a33e3e546c08c2dfe: Rename adminer label. (#4112) (@zzjin) +* ba7f8c39a0ff00718f95441012e7e0d40e4978c5: Set auto hide adminer icon to more. (#4166) (@zzjin) +* fea147d86bc084451cc0dbf27183f6004dbc3368: Stripe (#3640) (@xudaotutou) +* 95493cfa3ea4ec53bafbfb953618cb572dda1da5: Sweep: run guest command one by one and support run patch-type image (#3677) (@fengxsong) +* 763970344989d0cb521930896a70560f6f1ff122: Try update terminal safety. (#3645) (@zzjin) +* 0d73414c5e4422d1e23f190789d1ad680d3d8744: Update Database doc (#3803) (@yangchuansheng) +* 61079e90451ed0aa1aa97079b59fee946bc1a911: Update Intro (#3401) (@yangchuansheng) +* c54da85e6ceae43eecfa634058ad15e2fcd9e375: Update README (#4110) (@yangchuansheng) +* 9dd035bbb0cfd5fc3c28562994319f93e8a1f473: Update README.md (#3354) (@fanux) +* dbde6e1b02a49284f31a847492c8100ca7ebdad9: Update README.md (#3374) (@fanux) +* 4f8d6a5ae5ac7435d8abc3cc0ae1c60caa043f2e: Update README.md (#3552) (@balainit) +* c6ce93123aeddf792ea99fc1ce9ae20391147759: Update README.md (#3918) (@zuoFeng59556) +* a44018341f783a9ded3500140612b605bf853bf0: Update README_zh.md (#3360) (@fanux) +* 63cc5cc3031ec159b58f17eba93c25d8e5b52d76: Update adminer frontend to support 404&503 (#3361) (@zzjin) +* 7dbf9edc548e768f4621068941c46be2224ce696: Update cloud.yml (#3688) (@lingdie) +* 32f3843470323a75de36da7764c677680c5c7804: Update deploy.yaml.tmpl (#3710) (@lingdie) +* 8705cbe8be7fd2be122a878f9b9daba466c42950: Update discord link (#3519) (@yangchuansheng) +* db2953dc1b1c537da6b60e7236f41c0f6d05d640: Update frontend deploy update strategy. (#3390) (@zzjin) +* 6c91144e7d11d0bea0693c82951dd386aab6d095: Update install-illacloud.md (#3378) (@JessicaHu03) +* e261ec4cc35fd40be02b681e9cb807c4ec22651e: Update wechat qr code (#3520) (@yangchuansheng) +* e7d40dd759c990c5e8da1eeba5d20a7c19de80df: [ImgBot] Optimize images (#3554) (@imgbot[bot]) +* a8881c5b9710db543671c8689b5fcf4205d9a8de: adapt changeable port. (#3626) (@lingdie) +* d5e48baf350a4b949ef6eb77decd8b72522e3c6f: adapt mongo4 not support Time Series Collections (#3815) (@bxy4543) +* 9e8b703e1ebfc1fd993d75f654714e9e13dafde5: add Kubefile and update controllers.yml (#3612) (@nowinkeyy) +* e0f638ba1ba58f6cddf5c8011ebf89c8aed3fd1c: add a markdown for fastgpt AI (#3452) (@geniuxy) +* 84eaa2266735ee57891e5e5f5901ef5f796c61d7: add a support for community edition simply (#3622) (@yxxchange) +* 6ac6bf88ea8bcceee6029d122e2f50d522897680: add account webhook namespaceSelector matchExpressions with user ns label (#3996) (@bxy4543) +* 7254d5fb68a06729dbd8eb602e7681f184e6328d: add admission controller clusterimage (#3702) (@lingdie) +* d7c046cc4307f74362c54ee38fb83c29abde503f: add anno and label for user, ns and so on. (#3544) (@lingdie) +* 9e83f02b805ee7a882725a9f0bca2a0a53ae2027: add base64 for public key (#3470) (@yxxchange) +* 75035be377aa5305169e70fbc5361516a88e3324: add cherry pick action (#3564) (@cuisongliu) +* cb641b652bb923212c004b0072768a2bf2350a5d: add code for ingress webhook. (#3824) (@lingdie) +* c236f8cf7a1bf6cd252da2f52bca87f13b651a76: add controllers license to Makefile: `make license.controller` (#3707) (@bxy4543) +* 9673b605abedf46511724a654cfb54b366cb1fb2: add cost-center env. (#3468) (@lingdie) +* 36f8008168b5c138381a88ddac04e4b8ba65b307: add debt-ctr backoff version func (#3615) (@nowinkeyy) +* 80fba6fb00456bfb15b23ddc12a004a7f5a159b1: add default install of metrics-server (#3876) (@zzjin) +* d1299119e61a97aa8c45699e2c75626ffc810ae0: add default nodeport count limit. (#3808) (@bxy4543) +* 8110c7735038bd636f1704bb517e8403dae6915d: add en docs (#3475) (@cuisongliu) +* 2bbd1dda76e1b5452ff0a9343f37263b165b1561: add ldflags to go build binary (#3679) (@bxy4543) +* 0adc21d334d46c76bc8d8792456be4e4960e61d0: add licenseissuer to cluster image (#3584) (@yxxchange) +* c47fa85153d28dcf0bbffb7a95648abb7ee34bb7: add memory record for license (#3860) (@yxxchange) +* 03623923dffc01b2519d863fdf4001407c6306cb: add mod file for controller pkg (#4062) (@lingdie) +* 84b39f8183316125e591e5b75eae3cfc69ef5480: add namespace billinghistory query for costcenter (#3902) (@bxy4543) +* 166654da9096a489e7bef4e498095870ba108587: add node-gpu controller to get GPU info from node and pod (#3601) (@nowinkeyy) +* b194262061223524ac7bf2bdae2e9f3027597277: add rbac for dbprovider (#3431) (@sakcer) +* 8f4db16e0aeafa4d66a09b950d7ac54b92132fef: add rbac for desktop. (#3825) (@lingdie) +* 460e44c4c79fd712061d37620be36bac3aa9114a: add sealos cloud arm64 cluster image (#3784) (@lingdie) +* 52409081671906ade65b947c8e0388307340cdad: add stripe payment (#3541) (@bxy4543) +* 056cf9203e9ebee7611f13bb21e51c8cd14b0e9f: add support for kubeblocks to bytebase controller (#3419) (@dinoallo) +* 2b943285da06c235a441b792553e735e82e23673: add template to aio cluster image. (#3722) (@lingdie) +* 22b0d6ce64696fff196ec9d9197af3b295d532c3: add user env (#3469) (@lingdie) +* fa3f2dba848e53315b31f743a57362ab5bca710d: addDirectory (#3433) (@a497625414) +* b695d971403eca2ae04492b1ecf47fa47e90e192: auto backup (#3486) (@c121914yu) +* f1223aa933a71980341f6a2fb39e03c30fcbb07c: backup buildah image cr build logic to deprecate (#3955) (@lingdie) +* fc3e8cdbcfdaec4cee3955b6d17fc6b030af95e0: change app rbac for default user. (#3423) (@lingdie) +* d5c2a6ebb600901ff96194425d027d05590eb1ea: change name to admin (#4039) (@lingdie) +* f7b947c99cf0df5878049117ccd8ec86df52965d: change the version of terminal image (#3822) (@yxxchange) +* c447626e1dbcc3c78fd1d3648ea77834cb88642d: change the version of terminal image (#3836) (@yxxchange) +* b890eb75ec01c99b1b38cd45ef68eb6f283536e1: change user annotation for user owner. (#3850) (@lingdie) +* 9904f62e822f86b6768d4684f729f15737d3c630: ci: add frontend arm64 docker image (#3789) (@lingdie) +* cd9e5d1982dce2a0b3161743d45336a0eaa18adb: clean terminal switch message cmd history (#3967) (@bxy4543) +* 9d234f6430b644c918a842e4c502909983f2aed6: crypto prices && add default prices (#3575) (@bxy4543) +* 87166c3bdece0c8b6e47c31b97c9d4fdb444cb25: database backup and pricebox (#3396) (@c121914yu) +* 3161cbcf6aceea089d5bee41ac651dd10a1629fc: database i18n (#3384) (@c121914yu) +* df817021de4113cf4ed622f30926fabd52ad2650: delete and mv pkg for controllers. (#3938) (@lingdie) +* 72b58791f0b334995b2ef3f7a352ec82faaa2006: delete auth service (#3924) (@lingdie) +* 22038a199952e7065f2ee16c4870dcb2f54e17fa: delete cr build for buildah (#3953) (@lingdie) +* f6692ac8bcecd213cf40113980ac25f089e3b14c: delete deprecated cloud deploy (#3496) (@lingdie) +* c4f1da2491022125a2f634a3c5320927a5be875b: delete license issuer. (#4101) (@lingdie) +* 4984d4280bbe3312276b8a70e7760d965f85a62d: docs (#3510) (@c121914yu) +* cd5c05e7042517574884bcdc66e90af124578a3d: docs(main): remove cnblogs link (#3362) (@cuisongliu) +* 0b07a8e78bd8a079d18bee7bc15dfa665cc67bf7: docs(main): upgrade version for kubeadm (#3371) (@cuisongliu) +* ae1e069fc1d6cbc2e9fd91f51cd4966338d94723: docs(v4.3): fix save and load docs (#3464) (@cuisongliu) +* 6b0a03ab05a93dd727c5dbba334c610d0a954c85: docs: Add menu for fireboom example (#3956) (@AnsonCode) +* 9fdc27de555bf45d1677f94218d47286a76d4c47: docs: Automated Changelog Update for v4.2.2 (#3372) (@) +* a80b7cfd9406a7f6a35c85f5cde24eb934417a88: docs: Automated Changelog Update for v4.2.3 (#3404) (@) +* 1d80ff9b3dfe8907d5d05408a181681c57ce2f4d: docs: Automated Changelog Update for v4.3.0 (#3518) (@sealos-release-robot) +* 00e913e37a33d33f3c18bce940265e61b8ac87e9: docs: Automated Changelog Update for v4.3.0-rc1 (#3456) (@) +* 60cbe612e6b63149cd55b6ab45d6f19ee4a57db3: docs: Automated Changelog Update for v4.3.1 (#3756) (@sealos-release-robot) +* c47327e47d8de2c8aba5369ae2e8d8db21b68f84: docs: Automated Changelog Update for v4.3.1-rc1 (#3636) (@sealos-release-robot) +* 4ee5d0eda69e766821a3a482d95c3c5f95012bb9: docs: Automated Changelog Update for v4.3.1-rc2 (#3739) (@sealos-release-robot) +* 74e550b1d78af276c7d4682be186493bb3a7acb6: docs: Automated Changelog Update for v4.3.2 (#3762) (@sealos-release-robot) +* a9ff21a3e748276e79fbf57c5ad97cfa065315ff: docs: Automated Changelog Update for v4.3.3 (#3806) (@sealos-release-robot) +* 708baf1bbecf9cbacbfd8d1bb079751767692f3c: docs: Automated Changelog Update for v4.3.4 (#4020) (@sealos-release-robot) +* 6ec63e02620366f726283ffdab2a0d37ca6a59e6: docs: Automated Changelog Update for v4.3.5 (#4054) (@sealos-release-robot) +* 10d9a64be4bc26ecfcf79f6eb0926af482df4785: docs: Automated Changelog Update for v4.3.6 (#4132) (@sealos-release-robot) +* 433e23a84320aee305a83c63b7b9cbdfc6a0fd4f: docs: Automated Changelog Update for v4.3.7-rc1 (#4137) (@sealos-release-robot) +* 6c68bd3cadbe54aa3c0937dd5a49364227536c32: docs: Automated Changelog Update for v4.4.0-alpha1 (#3879) (@sealos-release-robot) +* c59188c32c0925cc648c3d51008021503a802c62: docs: Automated Changelog Update for v4.4.0-alpha3 (#4001) (@sealos-release-robot) +* a7b6260f970758fbd5f01d7d4f55bcad4db0acc0: docs: Automated Changelog Update for v4.4.0-beta1 (#4080) (@sealos-release-robot) +* ccdec922fac94e1a93c5c9226ccf7eac8f34d69c: docs: Automated Changelog Update for v4.4.0-beta2 (#4108) (@sealos-release-robot) +* 82483f269821bb5322d26776a59a9d8103265b52: docs: Update App Launchpad main features and quick start guide (#3532) (@yangchuansheng) +* 9282f909a16950fd9166e35a52a5ad3a50d93f77: docs: add dual-stack docs (#3517) (@ghostloda) +* d3e33501b5b211b6cb5fe820d648761a53b911e7: docs: add install example for fireboom (#3939) (@AnsonCode) +* 05b2f53b449d8f4a9577d50ccc8ed0369b3baa59: docs: add install example for tailchat (#3302) (@moonrailgun) +* 7bbf12268f644d024c0186eddf7e73c6cf13f871: docs: fix sealos gen Clusterfile (#3655) (@iawb-ray) +* 595be04e573563d18a027f09af74f13eb576a58e: docs: modify carousel image (#3971) (@zjy365) +* d51ab2fdeab1a71bf0a69de4a5046edb815aa502: docs: update anki Installation Instructions (#3539) (@yangchuansheng) +* c2a99b609098c82e30be94747071eac2fe01ef5e: docs:sealos recharge activity (#4150) (@zjy365) +* c8dfbd05238af2ee99a502c0fdf084e10175f605: eat(frontend/desktop):change password (#4149) (@xudaotutou) +* 07abd93e74caf17ae9375f3edec56b75032e5410: feat billing with app/type/ns (#3963) (@bxy4543) +* 35a76b4ea96de9fa150f41cc4a167e21c5d0193d: feat float button & add home button (#3617) (@zjy365) +* ebae81831a888b0e0e25b9b8757b863b891053a3: feat template (#4012) (@zjy365) +* cbcec7e3f25ba945351bf85cdcb2dde40c99460d: feat(applaunchpad, dbprovider, template):support multi ns (#3927) (@xudaotutou) +* 3f883f16abd304b6e8a9d3bc1f94cb50af2cc12c: feat(cost center): support multiNs (#3908) (@xudaotutou) +* 96315085aa624770482d7bab8973ef87adb57413: feat(cost center): support multiNs (#3958) (@xudaotutou) +* 7d7819de4c832908c047a3bfa38ac887e2dcd664: feat(desktop/frontend):add backend for multi-namespace (#3804) (@xudaotutou) +* 279973c8cde88d2f4e5c42b9dac238b2beb782f6: feat(desktop/frontend):add curd for multinamespaces (#3559) (@xudaotutou) +* 66837d448e19f76363a0bf6595e94c1500a9306d: feat(frontend/costcenter): add measure gpu && fix makefile && perf i18n (#3602) (@xudaotutou) +* fa7c10c6e6e91ff7a585501fdb27569f5b15580f: feat(frontend/costcenter):configable bonus (#3420) (@xudaotutou) +* 089f2518310a20c849a5d841cde4842772934b68: feat(frontend/costcenter):create invoice (#3533) (@xudaotutou) +* 59bd06800e27b597d374f13625be0b9785e77329: feat(frontend/costcenter):transfer (#3455) (@xudaotutou) +* d673049321afbd149bb2e77c7500c3dc8913c02a: feat(frontend/desktop):auto deploy (#3446) (@xudaotutou) +* fedcf555b52b79f966ab7e260723d827311c579f: feat(frontend/desktop):login by password (#3414) (@xudaotutou) +* 89721304a7e97b23735626839fe244d5b670162e: feat. desktop cluster image (#3436) (@lingdie) +* 52a9d1c63a48fed280022b8256397d8a0236b697: feature(build): add add pr action (#3568) (@cuisongliu) +* 3debc863fb34d6d9e76c35be72a478011a61b46b: feature(build): add force push action (#3563) (@cuisongliu) +* 33870f348bd0ddccc2f7ac39b77b8235ff6d14e5: feature(build): add pr branch (#3591) (@cuisongliu) +* e483a74a6acceac72efe6b5e449b4589ee56ce70: feature(main): add cherry-pick error to new issue notify to @cuisongliu (#3666) (@cuisongliu) +* e51c7627648fd1298fade4a2db8678279e6bb704: feature(main): add commitor for release (#3487) (@cuisongliu) +* e694fb399fa237fee6db7a84b718669ccfa5adf1: feature(main): add initsystem cmd and so on (#3841) (@cuisongliu) +* 3a2e18ced6642307feb1506d210b4d7f2a9a7a14: feature(main): add spec for init-cri and clean-cri (#3840) (@cuisongliu) +* d578fb4b2a4374db8547ae5b39519af93f630569: feature(main): add sreg cache image (#3715) (@cuisongliu) +* 65df34d43c939c448beb374a5a46debbc0aa53ed: feature(main): add test images (#3690) (@cuisongliu) +* cbf61f800b61d4edc075d2e4213a380e426b4813: feature(main): add tf as infra (#4024) (@cuisongliu) +* ada947fb1e3e75ae2b8bbfa3a822603c2fa683ff: feature(main): delete registry feature using sreg code replace (#3714) (@cuisongliu) +* dd906fd08a440b7afa62e6971267d0c4062d2d2c: feature(main): join nodes for k3s (#3944) (@cuisongliu) +* 0e47c47d54fe89ccfce1ea32ecc3a40febdd2626: feature(main): remove docker and containerd ci (#3765) (@cuisongliu) +* a33fc0dec23987f59f48035c2c55ae1ab0d3b1a3: feature(main): reset cluster not delete nodes (#3991) (@cuisongliu) +* 733672922635fd034103267067cb87907ccc8e26: feature(main): sealos run uing defualt ssh port (#3791) (@cuisongliu) +* 9789ee01f3cae66c9d047cc3cde186a186c0f01c: feature(main): support sealctl static_pod add options args (#3946) (@cuisongliu) +* de6e7be348000e93115d2f8d5d66a7673fa8c9cf: feature(main): upgrade registry addr using shanghai (#3772) (@cuisongliu) +* 4695ce15aa6df9c72bdac17bda9553ebb9c48509: feature(main): upgrade robot v2.0.0 (#3769) (@cuisongliu) +* b5db485bd0bc533de3a6a870641e60052f290dbe: feature(main): using 22 default port (#3759) (@cuisongliu) +* 3c8c6395482b8c8e6e031c236705f474576a5e43: feature(main): using sreg 0.1.4 build image (#4023) (@cuisongliu) +* aacb6b6633c5821703f4e7525cf07c35999a1571: feature(v4.3): default support tmp registry build image (#3535) (@cuisongliu) +* f5c221e3d3e8fe8b020a07ec24271474f62a54ab: feature: kubefile CMD support ENV variable format (#3921) (@LZiHaN) +* b226f77fbf1fbc18f6f758dffea217592d6a46c2: feature: recharge gift by configmap (#3579) (@nowinkeyy) +* fc2c8d0cdabcf838946bba385c7aa94a052aac50: feature: sync workdir (#3903) (@ghostloda) +* 2cd464df30e0fcdff8f76c76b476fad7865c614e: fix (#3451) (@lingdie) +* 11563c77f9855afd78e0753c6e9281f6fb0a89a0: fix `ENV must have two arguments` (#4097) (@bxy4543) +* 157090942f20ca9cab226f12444ab385c54c9f92: fix account cause error (#3485) (@bxy4543) +* 97d15f83e345b6d154aada1950f5670949d705dc: fix account user null & language (#4128) (@zjy365) +* 5057497fc2797c089367d14558a20bda83b0372d: fix admin user label. (#4139) (@lingdie) +* 03a3a3f572e68118f330f981e1b1c37166b9a8b9: fix admin user time label. (#4160) (@lingdie) +* 0d129f80f6e80084ca22847cff38c186a3a5be5b: fix admission service selector (#3705) (@lingdie) +* e1a695800c07a2de75117e7c119ec5d0782388f6: fix billing & valuation (#3753) (@xudaotutou) +* 016c05e11d7f97cbb3ece3f5ee7ba3f31ab86e7f: fix billing total pages and count (#3709) (@bxy4543) +* e7a92616ab52338d6338ea4f126a2bbabac5bf4d: fix bugs of licenseissuer deploy (#3895) (@yxxchange) +* 73059dcb890a3907103010cabc0b2f50c80043bd: fix cloud deploy yaml error (#3466) (@bxy4543) +* d73cb2039db01cdca86bb0bb48102c4b05220846: fix cloud deploy yaml error (#3467) (@bxy4543) +* c493112ce5d39e057b7e3c86ce3420470afa8b11: fix cloud kubefile (#3543) (@lingdie) +* 2184acc15654aaf74fc3ece816d2c9f6798f4d6a: fix cluster status (#3572) (@ghostloda) +* 2f24bbe60c03c370a53a3c85f5fb78b8ccfc3b9b: fix deploy of desktop and dbprovider (#3454) (@lingdie) +* e755a86282d32f45fb2a06f26e41181e6875408f: fix desktop deploy.yaml (#3453) (@lingdie) +* e6128527d1a986750ab3f1dde7eb95087564f2f8: fix docs jump link (#4151) (@zjy365) +* ed66c0026ef83b25e053d029fbb756d8d5b3042c: fix e2e apply test issue (#3725) (@bxy4543) +* e8c81d591752dcca9bad2f6d32caeabe165a8070: fix e2e apply test result issue commit (#3823) (@bxy4543) +* e533f2d0dd951da4c55dec812affe5b3ca110161: fix e2e apply test with infra && fix wrong ssh.fetch interface method implementation (#3560) (@bxy4543) +* a4adbb7f5841e31dfbf77033bae4ab57ea60f341: fix golang ci lint version (#4154) (@bxy4543) +* 827e89263345b79c1681d23756fd41e6021a3a9d: fix golang ci lint version (#4156) (@bxy4543) +* 2450d28af306806df34c26b0a4ecaf4a35feafe0: fix golangci-lint (#3935) (@bxy4543) +* b728edcd6debc5ee1be2282d48bf6faa354ca1c1: fix i18n default changed (#4157) (@zjy365) +* 0cc51286251a15050d15a52d04e6a009a2040a77: fix ingress. (#3629) (@lingdie) +* f6c35d9dc9eae4931c511ef586dfe239a4a56c42: fix license base64 decode salt. (#4127) (@lingdie) +* 1cc7563f3f18964f562626fc2f657370f6b09ace: fix link ci (#3582) (@bxy4543) +* 57beea934e56c7a4493271106f385549049f7c3b: fix link error (#4099) (@bxy4543) +* 4da39b827806fe5a7acbcb6e926ed152857db6ed: fix nil point error (#3606) (@bxy4543) +* 03146e2fe7e6f0c44a956505e1a1f3a2b03f16ab: fix port. (#3628) (@lingdie) +* f0ca87183307df8582b927601790271e5fc479f5: fix query length (#3608) (@bxy4543) +* d81f35cf966ece85c6b6c89abf42dfadcfa15733: fix semgrep (#3523) (@lingdie) +* 5b1026847dd1f0dff681502e973e0c17d053b79e: fix semgrep ci (#4103) (@bxy4543) +* 61d9ee94977cdefb29425a947f913a0dfab5fb9a: fix skip not Running pod (#3672) (@bxy4543) +* 176f300f91d7f2e0444257ddc7742de3d2f2b517: fix template delete db (#4014) (@zjy365) +* c1b7682095d05d19d32a5df868f22ac71ea3f1ee: fix termianl port (#3630) (@lingdie) +* 97192ce40d240a4d8ca2a8c2fb6659f615afd358: fix terminal image bugs (#3846) (@yxxchange) +* 2fa92b02f225540610496b66849eb4776ea2179f: fix the current cluster does not exist, but the creationTimestamp of the expected cluster is not zero. (#3549) (@bxy4543) +* db235379c6d4a7e583b4b498211f663ba839554a: fix transfer notification timestamp (#3531) (@bxy4543) +* 0fc3f44fbbcffb656f2da6d0fbcf8116b4b2cdf7: fix transfer self; (#3493) (@bxy4543) +* d198269133b9c961d3722750942640887817af73: fix user controller. (#3894) (@lingdie) +* 500fc6d94bbc786a933dbc760f55b2cf741f131c: fix user sa. (#3727) (@lingdie) +* 5d00f38fb2e46d7f262d817650d4e408e6004d67: fix webhook config name (#3831) (@lingdie) +* deb72cad5ec38ff52c27c323bede32e0ce58e266: fix zh_cn document url (#4094) (@Codingendless) +* 441df0e978e5a2a3a96cafe466ededfc313aa098: fix(frontend/costcenter): storage (#3624) (@xudaotutou) +* 49d21ca1b55cea4b7e9d98e0623df5e9ffbec63f: fix(frontend/desktop&provider/cost center):fix auto update after recharge (#3432) (@xudaotutou) +* f9d086aff6e2d937af3ac282484097f2780b88ce: fix(frontend/desktop): clear query after fastdeploy (#3553) (@xudaotutou) +* 88e5abb9bd6f7adff5bae7b288055bf95ad0fe50: fix(frontend/desktop): fix login different account error (#4168) (@xudaotutou) +* 5876545bb663868f08a566b02a2deb82949072a6: fix(frontend/desktop):payment CRD (#3616) (@xudaotutou) +* 4df0fbb4196103c0ac2fe788f46cd33d8e288324: fixDodError (#3460) (@a497625414) +* b0d7275d220330d3553f86e80c9e99d00c5b2018: fixed Error cherry (#3566) (@cuisongliu) +* 65846957f61f30403b13babaf32fffc36a1f6ad5: gpu node cr name (#3634) (@c121914yu) +* 7031923439530ea62f917cfe25c2f440b5edd828: helm: init cloud and applaunchpad helm charts (#3843) (@lingdie) +* d4ca6cc14b5a5189930a8213dfd767fec6f0d125: i18n and hpa target (#3548) (@c121914yu) +* c8aaeb7450f279680dbf536bd3f086d3ee27585e: i18n for desktop (#3965) (@xudaotutou) +* 27782910998931841bec983d681e11f88313dc2b: launchpad imagePullPolicy (#3775) (@c121914yu) +* 9ea88030869d214f2ed0f035315bc8fe68baa94b: modify GiB to GB (#3736) (@xudaotutou) +* 666bffe23fd2e4c0c9265cc991bc8f55c47d867c: modify the kubefile of licenseissuer (#3680) (@yxxchange) +* b08b39b0bcdb354ec4e5ddd2323307239217353e: modifying license payment (#4009) (@zjy365) +* fadfcba01b20f514754e5a857606510a663de645: opt: add hints for the order of commands in documentations (#3459) (@yangxggo) +* 15a4828e08ff08adbc9395c5607c3a17bd1be522: optimize cloud deploy scripts (#4070) (@bxy4543) +* 099f0f825858c3ad0809991d81340640cae34844: optimize cloud install.sh; (#4153) (@bxy4543) +* 1a0028df18709e3f4a60f6d685eae8a45be21b3f: optimize docs: install latest sealos version (#3638) (@bxy4543) +* 9678c6358876f598a6248fbe808192588d7f5a10: optimize scripts (#4130) (@bxy4543) +* 4945ee2a38ceec68a0665fd1577b1a430ab95abc: optimize transfer controller, add encryption to account and adapt (#3463) (@bxy4543) +* 08b30d8b9b903fec52df14081c6ac95175715551: pending gpu cost (#3750) (@bxy4543) +* 8422e34050c909b0a130874bf843952697ffa467: perf currency symbol (#3723) (@xudaotutou) +* 63ca385cfa3124edb3dc137230e32ef2fc7e307e: perf(frontend/cost center): add feature cost estimation, fix some bugs (#3373) (@xudaotutou) +* a76c0f97244b8a0b5e26baaa5c2ccc1d216fff1b: perf(frontend/desktop): Optimize login experience (#3331) (@xudaotutou) +* 613ce0f0e0024d0cfa673366570b265daf858096: perf(frontend/desktop):add sign in error tips (#3471) (@xudaotutou) +* 186eae5c49137e5070759cceb425a763972c6015: perf: desktop openapp method adds the pathname parameter (#3530) (@zjy365) +* 5258eb438c44877a5606156bc3066d93a06ddba8: perf: env show event analyze (#3631) (@c121914yu) +* 3d75945aaad180b25bb46a1f083962c85749cc65: perf: launchpad request controller (#3497) (@c121914yu) +* 25f1921273c8fd39a854736c1ee167f1ab3df9c4: perf: launchpad request pod and metirces (#3465) (@c121914yu) +* 1f44150bf67b0dec1ec783a2d69317aa760889fa: record the license by mongo (#3848) (@yxxchange) +* ed9ffdd328e536bb1313dada721fbf8b8483aa96: refactor env keys (#4066) (@fengxsong) +* ed40683b9b06557f96f4cb33225a48cde752f5dc: refactor(frontend):Use a Monorepo architecture replace the current architecture of frontend project (#3545) (@xudaotutou) +* 8b4449a9ce3dda5df8e90c1eefbe8667cb330882: refactor(main): add spec for image label v2 (#3816) (@cuisongliu) +* ed82c381a1fe0e24b860739c64ffef87c5831a10: refactor(main): auto cherry pick (#3412) (@cuisongliu) +* 74944fdfb5217dee33f3c52483ef1294d81615b9: refactor(main): delete node for check (#3405) (@cuisongliu) +* b8efed9a1353a2c8046ca10e83b84df9276e605d: refactor(main): fix ci for link error (#3356) (@cuisongliu) +* 345d2d99ebd9691495f49f1b10435bd28ca215e6: refactor(main): fixed golang lint version (#4141) (@cuisongliu) +* 2d1c4ca9d83a516f93063f9f1bc76ca1639f461d: refactor(main): k3s runtime hosts fix (#3865) (@cuisongliu) +* 10676208bda96e1c6e416a6f0ea9561beb63f291: refactor(main): refactor user and update old user data (#3359) (@cuisongliu) +* 9db0a980903d5d7f87d6f7032bb9c93d5ee481eb: refactor(main): remove old code for convert (#3417) (@cuisongliu) +* 94c80092d8b210bda314e23147604e181dc45b59: refactor(main): skip success exec (#4162) (@cuisongliu) +* 432ea4ec6919bf03de5164deac2aab1c26c0510b: refactor(main): ssh interface instance func (#3654) (@cuisongliu) +* 28207d69b49366ed34aad4148fcc294628ae8b01: refactor(main): upgrade bot version (#3366) (@cuisongliu) +* 18283229a2e14863e2dc01a704cc9142f5f3f8e2: refactor(main): using new runtime interface (#3691) (@cuisongliu) +* db75b6ad462c720a6713a60836381f9b9deb6b27: refactor(user): add operator sdk for user (#3503) (@cuisongliu) +* 74beff7eecaf298cd3009b9ab734871548f707ed: refactor(user): delete https://codecov.io check (#3504) (@cuisongliu) +* 9e8e620b0a9c21a16258021cba184ffd93db1331: refactor(user): delete ns controller (#3490) (@cuisongliu) +* e41feefa8add4cd5c160f29839c934ff18c5a133: refactor: cri_server multiple versions of AuthConfig (#3507) (@muicoder) +* 61a2cff7ccf97995f39ef42ae016f9174c4ca71b: refactor: move local action out from pkg/ssh to pkg/exec (#3929) (@fengxsong) +* 5ca58d1eccc209e3c8da1fa7f52eb974198910a9: refactor: remove redundant nil check (#3856) (@Juneezee) +* df0440fdc20850ce1a81d1f2b3b497bfd17cfcbf: refactor: remove versionutils (#3592) (@fengxsong) +* 31749ea895d1da1443a9fec17fd2dc804c7f5a60: refactor: save and load subcommand support multiple images in a single tar… (#3442) (@fengxsong) +* 87b22e9e7f4e406734deb335bd7b45c53eefb99f: refactor: separate runtime into different modules for better directory structure (#3741) (@fengxsong) +* f482831421e3ee31c1c24bec2a3d5013391552f7: refactor: use http mode as default to sync registry contents (#3659) (@fengxsong) +* 0c70a4cbd62be699e80c8cd0026642702a439735: rename secret (#3703) (@lingdie) +* 8aae7e5eb3dc88314c8f87b4147839a1c6feb4b4: restore easter egg feature in diff command (#3388) (@fengxsong) +* e645ff14ec077057dc72249fa3b4de0fd25428a0: revert: rollback mergo version (#3364) (@fengxsong) +* 8e55bc84b180c13b4a8553637414f96990d4f0b8: set the monitor collection retention time (#3737) (@bxy4543) +* 59e7dd10e054091c4ca0368e94224bea71499fbd: simplify the logic of license and remove the ability of scale limit (#3581) (@yxxchange) +* cf77d1ef950af724b697a23f5d0e0f23709e8245: style: modify gpu unit (#3758) (@xudaotutou) +* 1557b7dfb805b715f56572ae08c47a85f6ea43cc: style: remove unused utils (#3830) (@fengxsong) +* d1f0ede85e98485bdcff9b67e325d8c798473994: style: rename some functions and constants related with runtime implementation (#3763) (@fengxsong) +* d6b1f6d61cc32bf4d9eae1f7c3890c14a8bde3aa: styles(chore): add theme package (#4052) (@xudaotutou) +* 69ab72e2e90f31778c2f3b6be5422ddf8247e89a: styles(desktop):fix some styles (#3998) (@xudaotutou) +* 18fef2e156f094f2dbb5785901b02eca579dd819: styles(desktop):modify namespace styles (#3997) (@xudaotutou) +* 24b0292e77df7d1e77730a1888e292e3d7eda1a8: support old version of kubelet-config (#3447) (@yangxggo) +* 7659046d4b8783621ea6509f2512c5fb07246619: test(main): add test for k3s (#3875) (@cuisongliu) +* dd4d67b73dcc30e088feb6705525894bb7bb83b0: test(main): using registry.cn-hongkong.aliyuncs.com (#3697) (@cuisongliu) +* 26aaea076b7c0e9d94fc1f52613ea57d6e409924: unify secret name (#3399) (@lingdie) +* 481f9a7070e3a3e277100bd642b861a41815e06f: update app patch and label key (#3546) (@c121914yu) +* 61a9b279d099fbd84d7a4b17518c3de86d33a31b: update cloud cluster image build process (#3426) (@lingdie) +* 6e3eba5b1aa56f803030d854943bc5034a07dc24: update dockerfile base image to ubuntu22.04 (#3740) (@willzhang) +* 191963bcd543856c2b906b30621c1cdd6dedf081: update en and zhcn version of illa installation instruction (#3377) (@JessicaHu03) +* 47f0f4c75f0a60bba88f631b7f1cadc0bf498f58: update kb 0.6.2 (#4059) (@wallyxjh) +* 288c4f85895cf6e81b0dc3604e7db3548c3da3af: update sealos cloud deploy docs. (#3483) (@lingdie) +* e62bf535da4255e661f11f796ad381610a864fb5: use default `Identity` for traffic stat request (#4104) (@bxy4543) +* be1cc805ec77cc8ab110e73817d557f13d5992cb: user delete request (#3855) (@lingdie) +* f275874096b2c028538bac7f75d00c2d6cf7f755: 🤖 add release changelog using rebot. (#3355) (@) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-alpha1...v5.0.0-alpha2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta1.md b/CHANGELOG/CHANGELOG-5.0.0-beta1.md new file mode 100644 index 000000000000..5192ddba2a92 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta1.md @@ -0,0 +1,42 @@ +Welcome to the v5.0.0-beta1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* c35222490838e9aeb22118ff9f3fd2b0961fdffc: feat: Create a demo plugin Monitor (#4184) (@Wishrem) +* 3bc6c3cb49bfcbb5706fe1afea71eb149dc2399c: feat: imporve sealos cloud install script (#4181) (@lingdie) +* fb52ae20aa9e82295d43a526a612e69df222503e: feat:frontend license app (#4203) (@zjy365) +### Bug fixes +* a3d7b2bd66db557a2e6f8bd4e09457f64e86453b: fix(costcenter): fix unit & invoice amount (#4185) (@xudaotutou) +### Other work +* 52a396ab6a83eaa652c3e8a1c26320a7bb96bbee: Docs: Update the company's information (#4193) (@yangchuansheng) +* 58058204c00f8adbc00cf30932e02ece934375c4: Docs: add ci workflow for building Sealos Docs Site Docker image (#4177) (@yangchuansheng) +* b6be5be640ae406639ccf2e3423923eb03a05069: Docs: add workflow for previwing docs site (#4186) (@yangchuansheng) +* 8f246bf2dca837760dfc741b33b0a8998352be53: Docs: auto copy i18n files to website folder (#4176) (@yangchuansheng) +* f37b27cd5cb6b2f94b2130480dc77d64fc9c331d: Docs: update "What is Sealos?" (#4200) (@yangchuansheng) +* 384bd8dd36c0c1171b575dfd39d4c17db222efb0: Docs: update introduction (#4207) (@yangchuansheng) +* 74b95f1a8fe063ec4a659beccd68a51affa36c9e: Fix typo for "Preview Sealos Docs Site" workflow (#4190) (@yangchuansheng) +* 961d01a364918bf603b5e99fdabee6aa0391a630: Update README (#4197) (@yangchuansheng) +* 2015389e3f5eb2c28d2eea132c0abec5f7cbb442: Update README.md (#4180) (@fanux) +* b868c75d7d12cb89798c6bc5b37e8702f05689c6: Update/pricing (#4182) (@bxy4543) +* 97bd6727a20d678389558d93c55735184a97b9df: add cronjob app to cloud. (#4201) (@lingdie) +* 5cee7fdd982054d140b89fae875430625405fb01: add license app to cloud. (#4196) (@lingdie) +* bd118edbe4bde0fd2f72e4123078048cd87c1b9b: change _ to - (#4183) (@lingdie) +* 2d55ae311c009377f966ff78115a2537a2a4e3d1: docs: Automated Changelog Update for v4.3.7 (#4210) (@sealos-release-robot) +* c726742f53c98aa38296585e3890eeb5816f54d8: feat service license (#4195) (@zjy365) +* 69ee7b309df8caaff6fbde59b7b5ad0440051ce9: feature(main): skip sealos.run rule (#4198) (@cuisongliu) +* 8476d8190de75fe13a46c34b5f4ca403b1a59b6e: feature(main): support crun youki gvisor kata (#4189) (@cuisongliu) +* 5c71c5498d3576cf5c2a64dc3c1e72ce92a32031: fix license db uri (#4211) (@lingdie) +* 1c91ef0b6f676522b60c4441b0c8d1dfecf76884: fix run init job. (#4202) (@lingdie) +* 4fed20a298b95f89782d84ad9349f456efdd0646: refactor(main): unmount override container (#4161) (@cuisongliu) +* 5878953036c4516e833c7016c42caeff25fb3905: update price (#4212) (@bxy4543) +* 8e7207f80872a47f2ea921d63b9737eff80c1118: 🤖 add release changelog using rebot. (#4171) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-alpha2...v5.0.0-beta1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta2.md b/CHANGELOG/CHANGELOG-5.0.0-beta2.md new file mode 100644 index 000000000000..d5b213063ede --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta2.md @@ -0,0 +1,34 @@ +Welcome to the v5.0.0-beta2 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 8835beb079b748276c42b21d1f486909dfbff5f2: feat:docs add private cloud page (#4215) (@zjy365) +* d8d3877bfc25044273f16782402e9fb71529d6ab: feat:docs language-differentiated domain names (#4226) (@zjy365) +* 41a17d872ef6392f1775e10877db5241c2a0ddc9: feat:standard cluster purchase process (#4225) (@zjy365) +### Bug fixes +* 47de26e6e86fad2f898af9d2affd4d99bb1ce357: fix(costcenter): fix unit & invoice amount (#4194) (@xudaotutou) +### Other work +* 26ba68b9fb6406c6d67f3ca988bf1cfd55cb20f6: Docs: update installation section (#4235) (@yangchuansheng) +* ec16d343f33279b134a91a18279be76d8ed98b50: Fix database provider i18n. (#4216) (@zzjin) +* f9232807c3aeb1774730c19e7934741b5ab09949: Update the workflow to automatically update the website image after the image is built (#4209) (@yangchuansheng) +* af1e99fcdef5f33a20028ef2d036c4d5199735db: add acme resolver monitoring (#4223) (@bxy4543) +* 8a9f022a488a5e5a4054cb7aa2eb0cbac90ec6e9: add alt names. (#4214) (@lingdie) +* 183f7c763ddefebd09e99c7d152cf108c238d681: add proxy prefix (#4231) (@bxy4543) +* 77e7e18e44bd37d2580f727f42449085e941349f: docs: `sealos create` support for environment variable based template rendering (#4224) (@LZiHaN) +* 7b6957a113150efb03eed8213e4d4875fc028d2b: feat(frontend/desktop):support modify team limit (#4221) (@xudaotutou) +* 25ac669cd68a9d476a9021b59c5d14c54612c960: feature: `sealos create` support for environment variable based template rendering (#4222) (@LZiHaN) +* 358c57239631bd0aa163960e3902acc812f47bfe: fix oss file name && docs footer (#4220) (@zjy365) +* acd4ae3968410264f638ed933ebbfcd1978b812c: fix result (#4238) (@bxy4543) +* 99d65e6ed784a7e0600aaa4c375139bf703ed274: fix scripts (#4217) (@bxy4543) +* b2fb15a9d623912b8439fc247b2f51252a999c7d: fix user avator && url (#4218) (@zjy365) +* 5a2db108ee5f60fe559f453eb9afe86ba0b65d51: 🤖 add release changelog using rebot. (#4213) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta1...v5.0.0-beta2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta3.md b/CHANGELOG/CHANGELOG-5.0.0-beta3.md new file mode 100644 index 000000000000..cb5f54e21b42 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta3.md @@ -0,0 +1,23 @@ +Welcome to the v5.0.0-beta3 release of Sealos!🎉🎉! + + + +## Changelog +### Other work +* ae3785353748d6dbdf2a8e2baba318c7d7a24788: Docs: add pricing page (#4247) (@yangchuansheng) +* c26318e756a1ed2ff4e931b0823d73280428b746: Update README.md (#4227) (@fanux) +* 74f9426f9a3ffaeacad126d4d3626e73ec1d25f5: Update services.yml (#4254) (@lingdie) +* 05e7e87c3d2fd7e37228725c1715a2918778c6c6: docs: modify doc path (#4241) (@zjy365) +* 4565660de5d038108c686de51cc4917f2590876b: fix cloud version (#4255) (@lingdie) +* 8ef368958ce8acadc200cc185f0cf380abce55bc: fix database service ci (#4253) (@lingdie) +* 90819ea2843a8b92ff507b79406909959a36f18c: fix database service ci. (#4252) (@lingdie) +* e0838da2d9d54fa9dfb7474dcae227483981dd5c: fix svg compatibility problem (#4250) (@zjy365) +* e0b72430e2dffc28b15775fd5bb56440b88eb7df: 🤖 add release changelog using rebot. (#4239) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta2...v5.0.0-beta3 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta4.md b/CHANGELOG/CHANGELOG-5.0.0-beta4.md new file mode 100644 index 000000000000..96d42c087c72 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta4.md @@ -0,0 +1,144 @@ +Welcome to the v5.0.0-beta4 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* dc998b5b2d4d7c8046bfc88fee543e077078aff7: feat(costcenter): pinning table (#4343) (@xudaotutou) +* 7c08451b7461ab4e39f4a42ef4fe846d3acbc56b: feat(desktop): signup api (#4314) (@xudaotutou) +* e84a782111563c803ad07e386245dd47a8373cc9: feat: Replaced mobx with zustand (#4358) (@Wishrem) +* 5330a703060ddfa4211da56b0b56384724cd3f66: feat: accomplished create, update and delete function in Kube Panel (#4300) (@Wishrem) +* 80c948a10bbcc846c42e0f2aa81b9e567762a7bc: feat: add heartbeat for sealos self-host cluster. (#4389) (@lingdie) +* 3eb68232381e705167e7ccdf273eb99db237b9bb: feat: message notification (#4350) (@bxy4543) +* a04ca534543a48f29c9978cc7fb24622472c5de5: feat: release KubePanel v1.0.0 (#4401) (@Wishrem) +* 5c5903031299a2cda106ba800c2c4c68450f872f: feat:app launchpad && template supports api calls (#4322) (@zjy365) +* 33c7084e6d5e6b998700c01969d6c3d01f3a8291: feat:control file migration and template sharing link (#4421) (@zjy365) +* f9f2d61dc114545939457d56ee63859482bc120d: feat:db add database type for migration and external network access (#4317) (@zjy365) +* 552dac8a5ce53df4dd36a4743e4fce9224a1e11a: feat:db add kafka monitor (#4359) (@zjy365) +* d518f580cc6f1a25b8fcbbfad8a057b57aea38dc: feat:db automatic database backup (#4377) (@zjy365) +* b6654c4010a00ddff88a8ed5f8061ecb6405ea56: feat:db delete some databases (#4379) (@zjy365) +* 9bb66ebdfc6fab741cc6eff833df145f37c5eb62: feat:db modify deployment env (#4345) (@zjy365) +* 174f08fb9372c622eaae47870dec1c33134f1599: feat:desktop & applaunchpad novice guide (#4392) (@zjy365) +* bf5379f6d98047eb7b915a6c56c0acc33eb0f3c5: feat:desktop notification (#4371) (@zjy365) +* fc0c3f6f9173614deaa0593f32c5101d0782da65: feat:docs add blog (#4298) (@zjy365) +* 98dbeb49890673e878914c055d3ff1b27945fb17: feat:docs add head meta (#4260) (@zjy365) +* fdbccd6d46c136b1a83cde4b36eddf760ab3c06b: feat:docs add link proxy (#4266) (@zjy365) +* 27b000c83103a2d7d835c8cc5b97ef4a04151a01: feat:docs banner (#4415) (@zjy365) +* ad77db0e9218ec9517334820b853af658edbabb1: feat:docs robots.txt (#4406) (@zjy365) +* 7a8fffa42983334936ff9aa8aa67c13b22da2449: feat:frontend new api call && database file migration (#4332) (@zjy365) +* 94ac016d8bdff38559ed75d4d158791643fcb4c2: feat:implement license id scheme (#4413) (@zjy365) +* 8069caa36bab6b6dcbeced52f73d2895cfc3612c: feat:launchpad jsonpatch & template add filename (#4436) (@zjy365) +* 2255850717bf5b202c490f6d4ccd63cadc0b3520: feat:launchpad support multiple containers (#4380) (@zjy365) +* 675700b3593f667f96b020f9e5f49685b2cb2399: feat:license deploy tutorial & monorepo dependencies (#4274) (@zjy365) +* 7fd40c2480abf8200ca243c44d7da8191efa3a99: feat:license deployment command (#4316) (@zjy365) +### Bug fixes +* 48f9c4b83a05579e9f5e1fadcf4441ff7c165577: bugfix: fix sudo -E '' with an helm cmd has single quotation (#4427) (@daymade) +* f52b1743797c5ce76d97c9efaa2bdfb77ac74903: feat & fix: Event, Ingress and Secret Overview & Errors in using next build (#4374) (@Wishrem) +* 56772da875f21b408d7edcf043daef8a75c97698: fix(objectstorage): regex (#4296) (@xudaotutou) +* 19340f5378b28bf31ed70bd0667a9587abbc7e07: fix(template): laf template (#4351) (@xudaotutou) +* cf3041dc0c1a804bb4b39777910edc19149a6254: fix(template):fix deps, fix styles (#4378) (@xudaotutou) +* fd55bac31490d6f0e87404607a223a51d8da676f: fix: Display of UI and functional integration in Kube Panel (#4365) (@Wishrem) +* a5be5cb7909d0728e18d3d2faf3feaf0c3e7aea4: fix: activator logic. (#4430) (@lingdie) +* 333f05d2aaf2953391fa47a95765065da70e00c8: fix: admission for ingress delete (#4327) (@lingdie) +* 10ec57654023336dc98145e8a5ba07faf114fc80: fix: cloud release ci (#4336) (@lingdie) +* 9c9c9d74f75c371c9a3b2491a9c2c68c4a39261c: fix: desktop rbac for delete request of user. (#4308) (@lingdie) +* cb35d788bdcff0b3558c5ebbc9a8c55b442b85dd: fix: document seo optimization (#4372) (@zjy365) +* c39bd8931dc9414e0221bb14acd8b9a970b55e8a: fix: launchpad rollingupdate (#4285) (@c121914yu) +* 6fc583a87dbc91e54cd23712c4be5383c36001bd: fix: return cors anno as nil if port is 443. (#4356) (@lingdie) +* 2a418813d2fa3bcb644cc209f6315424708feca2: fix: skip vingress when delete ingress. (#4318) (@lingdie) +* 56123b852c7f87b4e7bf54df81f031029d12b594: fix:adapt to desktop open template (#4398) (@zjy365) +* 9466c122041b4c5d6a65bde2a806fcdd53202508: fix:app statefulset & template develop (#4363) (@zjy365) +* d2e6d78b4fcad85a8952237c73249236219f0e96: fix:db service labels (#4344) (@zjy365) +* 18aff5d84a6679b5ecba5a2bbc358e633b9869b2: fix:deploy menuData helpDocs (#4437) (@zjy365) +* 258e565628c27099430df5f4c55424c4b53b7039: fix:desktop usedriver & tempalte yml (#4432) (@zjy365) +* 3c8db86965ca9ee43d504027291b6999190fc52c: fix:docs & delete db role & baidu (#4272) (@zjy365) +* fa3f041cdb7bc92e7ad0fdfcb00f9dc8974ad7ba: fix:docs banner path (#4303) (@zjy365) +* ce5966d7a06d59e21c6d4ef30c75c795e7ec7e8e: fix:docs head meta (#4262) (@zjy365) +* c0492a1e98b8dbb6faa23e3d252892c20ef5d830: fix:docs router path (#4292) (@zjy365) +* 17fe9c2648dd2c9227e3d411434c8a532f7903ca: fix:frontend upload convertdata (#4382) (@zjy365) +* 4c166d3140f00eff8ecc3044768b24088d5c81bc: fix:launchpad issuer and certificates (#4405) (@zjy365) +* b9a7ed74a4b0672139abec44f81f8e39684d84eb: fix:tempalte detail api add cdn (#4428) (@zjy365) +### Other work +* 520c69ded6fb6aacc2423f58fd6bb03be3180942: Add Object Storage (#4333) (@nowinkeyy) +* 72852aa836f3a02003e96f15403e46c97757be65: Add database config docs (#4414) (@wallyxjh) +* 67dd44e0c971df567418db5eeebe7dcaf63898ed: Add post "k8s-multi-tenancy" (#4364) (@yangchuansheng) +* e6a9868cd04b9bb5dd2ab81fe0e6402218a86237: Bump: add sub go works to support bump kubernetes api from v0.25.6 to v0.27.4 or higher. (#4281) (@lingdie) +* e8610d68842c9c9799f9ccac4af519139af4da46: CI: add cloud release ci and use ossutil to upload offline tar package. (#4265) (@lingdie) +* a3efc3a93d1223f0f4083db37c5b201624165d65: Chore: upgrade and fix cloud build logic (#4408) (@lingdie) +* 4efa9d802e426ff282fd2b063860ac9f50ea3d4e: Delete useless code (#4340) (@bxy4543) +* bd68a735b4af26839bc98636a8da166ac8812998: Depre Bytebase and imagehub. (#4273) (@zzjin) +* 30fa9e2e7316946d5b7c96f705f83a5ce5f5b2f7: Depre copilot4prs. (#4418) (@zzjin) +* 60cf98e4049b198e0e73c10e4d09fbf42c828316: Docs: add blog "To run or not to run a database on Kubernetes" (#4385) (@yangchuansheng) +* c3f5bf6bacdbf1987943888f0e09c5823e410002: Docs: add docs for app launchpad (#4411) (@yangchuansheng) +* f1a78a5a2bd48137bb5c5981b4309a234689d3e9: Docs: add environment for building website (#4267) (@yangchuansheng) +* 78543a30dca9bebde3c055946a66f6d4d455c37e: Docs: add post "what is sealos" (#4313) (@yangchuansheng) +* 195165fc7e17f395dfa9e454ad054542dd143229: Docs: add post (#4309) (@yangchuansheng) +* a3412cd822beee24c80b484b11b154f7c4e46fa3: Docs: fix bug for building documentation site (#4310) (@yangchuansheng) +* 999816eea84021cf1167f0f36d5895657ca34d8e: Docs: update Q&A document (#4305) (@yangchuansheng) +* e405276f6ed2a462899e671d9240e0642090cb43: Docs: update README (#4321) (@yangchuansheng) +* 2fed810be6ad4aa9927dcb9ee5677773170f1b56: Docs: update documentation for deploying k8s & sealos (#4367) (@yangchuansheng) +* 88ef18f0124f52add5d86c5393835b087a097239: Docs: update installation (#4261) (@yangchuansheng) +* 4bea4eaf3654e40e68101c4646b3a05d89367592: Feat/cost service (#4391) (@bxy4543) +* 5312580d3f73660accc4121cae93c81330dd5c2a: Feat/gift balance (#4390) (@bxy4543) +* 05e8baf8ab8a40432c7e6c3b75ef3011eb67b81c: Feat/minio cost (#4339) (@bxy4543) +* c9527a7f77ddba8966865838ee5dca000ea6c89c: Fix acme service type. (#4403) (@zzjin) +* 5d8e6706de305db006dbdea2e14ddc6da9a7024a: Fix desktop and use icons from packages instead of public (#4307) (@xudaotutou) +* 2113d17e5ae2f5fec74b2d114495767aefdfcb2e: Fix env for BD_TOKEN (#4268) (@yangchuansheng) +* bfe029c1c22a330835138ef704580954462c58c9: Fix higress annotation. (#4326) (@zzjin) +* 55b4807999e2e03fbc58203a7672635d0dd51666: Fix template deploy ingress temp. (#4234) (@zzjin) +* e82b92f3635edf2584837c8912ead3052f76b61d: Fix vingress (#4319) (@lingdie) +* c4e8187ec073c2c256a2ced74ce03a488bdf5d36: Fix/cloud port (#4271) (@bxy4543) +* 3677c36299d7418fc9c8fff065fc39cfbaf5e1d4: Optimize/remote cmd return error (#4407) (@bxy4543) +* e94d709e718d0651a8c7313a88cfbf784cad9a73: Pref: sealos self-hosting docs. (#4299) (@lingdie) +* edde8600eefc0a23db1a39b9407a13cbe48da797: Update Quick installation of Vue Apps.md (#4434) (@alili) +* 3963a1abf9da541aa5ee2f57137b62cccec3eb22: Update db docs (#4381) (@wallyxjh) +* 9095a2eda2ae8fa87e6e4bd31869f9624230e26f: Update gateway usage (#4264) (@zzjin) +* 881e165d8418d43f57317c0494d56067c961e46b: Update git clone command for template provider. (#4315) (@zzjin) +* 86c2f8fb922eadc7bfedb344015571435b9a817f: Update private cloud link (#4257) (@fanux) +* 40e33e0d956ffed2f4024babbf82bd47061dbe32: add cluster id for license controller. (#4237) (@lingdie) +* ba71f1ded49aab6ff6aaa1f2b0dd8bdaffcffa16: add kafka monitor (#4349) (@wallyxjh) +* b15d62fae53ab392857d661897881872f1a799fc: add suspend os and resume os (#4354) (@nowinkeyy) +* 38ec5da3b7b1b2bbd4f0f4018f1d34f5ab2753e2: change nodeport default price (#4429) (@bxy4543) +* e84c9e39a6a50fb804a0c3d7c510b404f75a0020: chroe: add template cluster image for sealos cloud upgrad. (#4388) (@lingdie) +* 4ef6bb6b934d16f197eea4b795338c9a5140c601: ci: fix sealos cli in cloud release ci. (#4338) (@lingdie) +* bd52c56fe40d045939728faf740523bd7507f059: ci: skip objectstorage ci. (#4329) (@lingdie) +* ac9bd5ec0b2846e0e9ec5c71d1a06a08cf204da8: debt skip license cr. (#4275) (@bxy4543) +* b56174b827cb23b967a492e30380c99aeb0fef68: docs: Automated Changelog Update for v4.4.0-beta3 (#4290) (@sealos-release-robot) +* 94c702ecc3dd7722fe8c0a2f38bda6408d8ead22: docs: add workspace usage doc. (#4368) (@lingdie) +* f21b7c91a46eaee509438fea13662cebf14398eb: docs: upgrade sreg gomod (#4263) (@LZiHaN) +* 0429b0dbdbecb4868952e1cde5b4ab03a5b0396f: docs:support lang env (#4288) (@zjy365) +* 46127eca157b4ae851ce60a500e7a8e6be884c24: feat(frontend/minio): minio provider (#4295) (@xudaotutou) +* a2f3ca09f90f23a9aff5985a208d37a2f8a80bc9: feat. ingress mutate webhook support annotations flags. (#4293) (@lingdie) +* 37709ce8db4ac9db65a241d12d66faee84e54cbe: fix & style: tailwind css conflict and UI problem & clean some codes (#4416) (@Wishrem) +* 6d71ab2e842915ccd0e520f8b75e92270223c724: fix get monitoring result (#4242) (@bxy4543) +* edc5ccb66e8911096a0105874239a500cc413cc0: fix go work (#4289) (@nowinkeyy) +* 7a312773a4a8710abeed3b70a4841cfd1c3873cc: fix license (#4412) (@lingdie) +* 4a67761a440078b762a70e78f888656dd35195d9: fix monitor bug (#4352) (@nowinkeyy) +* e649c51b550a88b81035d06f40b2cf1edc6e81d8: fix monitor object storage (#4362) (@bxy4543) +* 4dedc10ee1f26ca55ff817edc3bc5b398a18335a: fix notification read status (#4369) (@bxy4543) +* 30b47a8bc121a97f358bad18c6c7c1640cf75e4c: fix os endpoint error (#4357) (@nowinkeyy) +* 77fdddcb53abfa1a2c40fa62824a9b3e7c49292f: fix sync error (#4342) (@nowinkeyy) +* 0af9b054f151b6977f71694d8bbc49386a7a4112: fix template readme img src (#4426) (@zjy365) +* d705eb523ca9745db8bd6d8556771299ffce4bfa: fix tmpl cloud port (#4335) (@bxy4543) +* ea37dc9e037ac6b5821ad532ba99fce1b9703819: fix(costcenter: invoice) (#4294) (@xudaotutou) +* 6405d93ee32df974725d969cfefad61301aa1f46: fix(object-storage): switch kc (#4355) (@xudaotutou) +* b6643a466499eaf98c90715876dd1657b98cde60: pref: offline tar support arm64. (#4306) (@lingdie) +* 87a14ea1ab44a21d5fd7a9c2b727d3f31560a1fa: pref: sealos self-hosting qa docs. (#4302) (@lingdie) +* c6c1b58b0f2eef2ad95c5a52a415a99a535130c4: refactor(costcenter): refactor bonus (#4384) (@xudaotutou) +* 5893b55b11a8cf71851e0fcdab9ef150a64d5664: refactor(main): upgrade api v1.27.4 support v1.28+ (#4394) (@cuisongliu) +* f2fef17fce042f3f8c34bd004c5f05c197538ab5: remove switch language annotation (#4393) (@zjy365) +* d82232340fc333ce22a4864834a9aac24fd78c7b: style: rename the plugin from Monitor to Kube Panel and update UI style (#4269) (@Wishrem) +* 160b7a2567a13e333f78789f9aa5aba6b840c9a6: styles(desktop): update desktop worksapce (#4328) (@xudaotutou) +* f72c2954629e395e3fb6277b0066bfedb0addb6a: typo: fix secret name in qa. (#4353) (@lingdie) +* 5718956efc3bc615c7156af9df4ddbe2bf6ef024: update dbprovider docs (#4325) (@wallyxjh) +* 9b2fc94952680adc1f2b8c21406f50d48129485b: update kafka monitor (#4361) (@wallyxjh) +* ce57c323eb675379ea1f7008e187cfb27b6bfe55: update rbac and fix minio user not exist error (#4366) (@nowinkeyy) +* 6d8e1f5128b5e3c20fbda78a0e8f9d5a169571dc: 🤖 add release changelog using rebot. (#4256) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta3...v5.0.0-beta4 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta5.md b/CHANGELOG/CHANGELOG-5.0.0-beta5.md new file mode 100644 index 000000000000..ebcec98457ad --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta5.md @@ -0,0 +1,157 @@ +Welcome to the v5.0.0-beta5 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 8f135d6d708fb6e097f979ff98149b100cc779ef: feat(desktop): add appToken for providers & fix ssr error (#4566) (@xudaotutou) +* b766fb5abeb3c2837140088dfac90778c076af70: feat(desktop): multi region (#4558) (@xudaotutou) +* 559154f5370c41495ea94a9590b5b91e407e3fca: feat(frontend): static host (#4634) (@xudaotutou) +* 0180fad6c713439c96655a275f033cb079436344: feat: @sealos/ui useMessage component (#4523) (@zjy365) +* 3c3563b12a3f34558dccff7cd555694832e49acc: feat: Name modification and deployment count display (#4453) (@zjy365) +* dc60d199e4b2c35b033124a24495c83ab6f97ec2: feat: launchpad api createApp with extra parameters (#4598) (@zjy365) +* 649c535284cd61ad0241411e9381cadf33175cc4: feat: object storage cluster init. (#4510) (@lingdie) +* f3de924d870cef4b81d6248e4a68aba4fcb2a4fc: feat: support multi domains for ingress webhooks. (#4491) (@lingdie) +* 3bdcc735af3d2c5106550443a57058ca82d49e11: feat:Desktop add standard OAuth2 login support (#4671) (@zjy365) +* 2719247f315e212817300dc6f3253fd7f27b0307: feat:Improve App Store rendering speed (#4609) (@zjy365) +* dfb748f9e8aff53109d3ce76d5689b1895d57cc7: feat:app logo colorful & blue bg (#4488) (@zjy365) +* 9125932cd491cdebbb579371bfe9a1439ec55602: feat:db adapt mongo instance count (#4490) (@zjy365) +* aec7020f77a2ecaf677a48a30db9fe13f1ea2575: feat:db provider adapted to 0.8.1 (#4508) (@zjy365) +* 046bb27270afeaa5536141516757f4876e6ef26d: feat:desktop support system configuration (#4538) (@zjy365) +* 84e033d06941d1dac64570fecb6620209e3dbc03: feat:desktop support wechat public account login (#4519) (@zjy365) +* 7155582f1d8d277cad9962b9c3fd52c831e6bfd0: feat:desktop supports invitations (#4613) (@zjy365) +* 63a693af6aa7296f72770d7a926f9429ade10118: feat:docs sale banner (#4564) (@zjy365) +* 08a9f55ddbf6774cbc9571e57f3eec9c57c66510: feat:docs self-hosting entrance (#4521) (@zjy365) +* 929faae785d9e5ceb6be843c313106e4b15f4117: feat:invite app add deployment files (#4614) (@zjy365) +* ae7f63f9b2f848c55bb815ad5b4fb6bf24776230: feat:launchpad monitor api & template ssr (#4482) (@zjy365) +* b6b8108d6e445537d2e9ba68f12a0dab62e2d01f: feat:optimize app store loading (#4607) (@zjy365) +* cd56ca8768452608e6f560c0ff4fcc9bfb73f01d: feat:template carousel image (#4485) (@zjy365) +* 16b92b814a4c9628414e67ddd5ec27ad96e2ef11: feat:template deploy add template-static (#4457) (@zjy365) +* 0495264c9c54d01330a9bd879d66645f57ff2799: feat:template language detection & instance api (#4497) (@zjy365) +* 61e69210ff8b261dc44741faf0b019fbb97b9a73: feat:template obtains service resources (#4502) (@zjy365) +* e0cc9d611982009fee2a5a19ec941c39df2a7c5c: feat:template session store (#4526) (@zjy365) +* f193f89a1129790e53b4eb1d42eb6dcf82a2bad3: feat:template support application marketplace category feature (#4583) (@zjy365) +* b2beb0a00b5b4987cd33a73f1a40ea2201ac0c0b: feat:template support base64 (#4627) (@zjy365) +* 53ecc9e6901a6e4f18b2b1b5b35a662e1d7a3ee5: feat:template support categories & fix launchpad table (#4541) (@zjy365) +* ff26b697c71ee6c663d7597300e624a54e951682: feat:template support parsing option lists (#4533) (@zjy365) +* 5492617d6f56ae6a2f325cbdd424d5dc2ed2ab34: small feat: add database tips one user select 1 replicas (#4507) (@o0vO) +* 26e06adefc8ff7486bf813dcae45b0fa43b8f482: style & feat: new UI components & automatically re-watch (#4433) (@Wishrem) +### Bug fixes +* 8fd13f63288df3bf70fd03b1fe7e0b9bf630fefe: fix(costcenter):fix rerender when select date (#4574) (@xudaotutou) +* 1eafe70326daf678bbf9fa16c32705b72419fea5: fix(desktop):add cloudflare turnstile (#4595) (@xudaotutou) +* 873894f6d2e5f3962295da2f0eb3d1cbaa36dc33: fix(desktop):fix verification code restriction (#4449) (@xudaotutou) +* a748715a85d35ecf6b3ec6419f6db731ff246d2c: fix(objectstorage):fix create bucket error (#4480) (@xudaotutou) +* 375b561a0b4ad574f787be58f8cd17b9e9239aa4: fix(objectstorage):fix predictCard (#4501) (@xudaotutou) +* a6aade62ca56b09f18d671fc2e14ffea45c9d2cb: fix: Add "Deploy" translation key to English locale (#4569) (@tianshanghong) +* 317ee5c2610f2f71738dd367f30696c116bce80a: fix: db role and select default backup repository (#4676) (@zjy365) +* 1e54b69df7f872bac945977f3f64fb3fb8d71911: fix: dbprovider resource already exists problem (#4560) (@zjy365) +* ad83b8433bb8fdb76d1483781997135d5edf7ce8: fix: desktop applaunchpad costcenter styles (#4667) (@zjy365) +* d334b54a3325e70c92035bb46c02c4b8010772ed: fix: issue #4536 (#4540) (@fengxsong) +* b93ca1eca586fc3f445723eda44958db46502c3a: fix: local page auto fall into online site. (#4513) (@o0vO) +* 20a027d9e68a140aea18e8a77a22e40d9906c15c: fix: revised version (#4604) (@rtpacks) +* 6eaec67f7fa481553cf11f17b529648c887fcf32: fix:db auth & template develop (#4445) (@zjy365) +* c35ee00d79226322a566d945fc074779e876129c: fix:db external network connection (#4615) (@zjy365) +* 6727ced5a59d4beb3baf7ba4b266f05994ab410e: fix:dbprovider auto backup (#4515) (@zjy365) +* 73b12c5cf3e6251c13b1623a472d90c4b80a681f: fix:dbprovider maximum storage (#4459) (@zjy365) +* fe50d0986028ac1e78b174c8ac0133ea5874b8a7: fix:dbprovider number of database instances (#4580) (@zjy365) +* 900d3fc8d2701ddb5560446c8442dbc3d241d482: fix:desktop keep last workspace state (#4646) (@zjy365) +* ac3aeefe6221ac6d9c40dc402054e20c340aeed7: fix:desktop modal header style & costcenter breakpoints (#4675) (@zjy365) +* 851cd69542f2bd68dc12ddb78f2f41fc19a0ccc8: fix:desktop password login and static host (#4639) (@zjy365) +* ee6b12979f6d3f65e6b3fc0f3d9a6a4400617fdd: fix:desktop region info (#4631) (@zjy365) +* 2b1ab315ecc4e2266fa0164c10c7216e1fad669a: fix:docs link address (#4666) (@zjy365) +* 8d394239d36fa3d8d6f6b6daa1d7d528dac4c20a: fix:docs reference scripts path problem (#4535) (@zjy365) +* d5c2fecfa757be8cf55a1c06bbfe63c493ef45a3: fix:docs wow.min.js (#4489) (@zjy365) +* 6c596ecf555349027c763a578bdbed12e57bba32: fix:license add CUSTOM_BASE_PATH env (#4654) (@zjy365) +* d73becbf408d9165934f6c41a14105a03c92265c: fix:privoder logo svg (#4446) (@zjy365) +* d7269a9cf9f70e9ee19479368a9104b249462b45: fix:template redirection problem (#4472) (@zjy365) +* 4ff45a89d7426b328d65489048c2a80bc67f7a5f: fix:template service nodeport (#4503) (@zjy365) +* 047428cb2dd2b69e294478a9dcd731859c311cba: fix:terminal already exists (#4651) (@zjy365) +### Other work +* c3fa80d518fd59685eaa2eed59ab2cb4dc35e9c2: :bug: fix net interface sort (#4672) (@cuisongliu) +* 7e97c72bb4460ab31c059d4673451835369bce73: Add Object Storage guide docs (#4579) (@nowinkeyy) +* 6d73bbfc3c036d84cac0ca24b52acd680c643377: Add category to template apps. (#4539) (@zzjin) +* 16c10cf3ace8b3fb37eff3940442c71773baff0a: Build kubepanel (#4551) (@lingdie) +* dabac703d3cea9eac536fa40fdd18ccbd17e6a35: Disable desktop app draggable. (#4571) (@zzjin) +* 883b7df5d9f5c1c67a0822bf7a4cc5eb6c7a76b5: Feat/e2e test (#4486) (@bxy4543) +* 4a0a90cbbe71dbe68cbd744bdb9a0e0147963eb2: Fix adminer namespace method. (#4581) (@zzjin) +* 5c73e629fd35c0b4ea3e2aa47e5ed1d2ae55e30e: Fix controller ingress label override. (#4597) (@zzjin) +* 1c960df32f4f8e41d59ce87a12413e388407cada: Fix controller memory usage, (#4589) (@zzjin) +* 8ca706929e4a078b2f7ac9f9658892c352638942: Fix dead link (#4529) (@fanux) +* 4ab847e9fcbf13813dcf97d38c3bf0103072b65f: Fix init admin account & kbcli addon enable snapshot-controller (#4643) (@bxy4543) +* 3a7a3cd072ddf93c1e7cba20ccd626183fa1884c: Fix service/launchpad typo. (#4655) (@zzjin) +* 2085ee63cf2f394b9342a642c96dbcb65dbf71d3: Fix title typo. (#4628) (@zzjin) +* af438cf5244fecc016579531f11d9728aab418be: Fix waiting for table creation (#4638) (@bxy4543) +* 14e397c38dbef4819b18cafbaeb12bf17b2a14af: Fix/traffic db name (#4548) (@bxy4543) +* 48b18e08854f2462d604e058a88c5e0adcafb2a0: Launchpad (#4645) (@wallyxjh) +* 0755c905e0bab5ebaee3096d5f34243b001c6085: License with accountv2 (#4600) (@bxy4543) +* 2337ab1150cc9867bcb67c0b71bb5036582f9403: Optimize monitor minio (#4679) (@bxy4543) +* 9318fd5a979ae8caa68bb89d4b222ca71b16ca76: Optimize the bucket controller logic of object storage (#4504) (@nowinkeyy) +* b496a20599359d469c750bbfe8822088a006cdb2: Optimize/debt resume speed (#4658) (@bxy4543) +* e819739117a410d7574746d03261b0cb22b10801: Optimize/monitor (#4481) (@bxy4543) +* a35524dfd3c7cacbc43ee92ac68cc7b35f235587: Separate database svc and minio svc (#4447) (@nowinkeyy) +* 0d9d19d4c6f0ceff1696a7c467bdfad7028fad99: The init job waits for the user table to be created before execution. (#4619) (@bxy4543) +* 5e0ea541ca1a4354f315aa9b64f7842f65f98453: Transfer service (#4594) (@bxy4543) +* 0350700b558d738e87a75724e09edc19ee429d3e: Unified account (#4576) (@bxy4543) +* a6e09647921a4e083163a245faee923c233fbd9c: Update README.md (#4492) (@fanux) +* 6b17b9503b4603bf6ef45674d8120f6b57105d3b: Update README.md (#4647) (@fanux) +* 9a9533ba5f2a08695c35e6d064f7b554ddc4043f: Update cockroach (#4605) (@wallyxjh) +* d0b30e6e66e9297f45aa0ed2afea9aaf897426dc: Update doc typo. (#4656) (@zzjin) +* 295e6a543f598b50a3dddeb935a6a70b6ce74a5f: Update doc typos and show `quick-start` tab as default. (#4577) (@zzjin) +* 4488fb61fdd4764e5f6a74213f7add08e4dc52fa: Update gen-apply-cluster.md (#4530) (@lamking) +* e87219f94e2a1135fb11e2ed4ec1aebb197c23b2: adapt ephemeral storage limit range (#4456) (@bxy4543) +* 055fe0bbc8103907e2e13d77311be4a05e0f10a6: adapt install script for cockroachdb. (#4593) (@lingdie) +* e45e3b79bd3805d659d5c4aae0370adc0fb32fb2: add cost service swagger (#4474) (@bxy4543) +* 8d6e23fd7c4810322575caf7c02f179b781f5abe: add init job (#4629) (@bxy4543) +* b12e933026792c720111a99c98c6b327e8a870c1: add limits to monitor service (#4652) (@nowinkeyy) +* 15279bcdae9412c8f57a155d5d65ae3bb3444e62: add minio service module to workflows (#4450) (@nowinkeyy) +* df38d07149d4d6839bfff414083dcf4077b986f1: add object storage key secret (#4623) (@nowinkeyy) +* 145ce7832f275a582c63101fb13c62f0b065d6a1: add pull file func (#4563) (@nowinkeyy) +* 9110edea0aaab5fa5cf8f5a58e9d6fb028e3ad48: change admin to username (#4557) (@nowinkeyy) +* cda0c5cba91f09780faaf57c79b91eb07c3afab7: create region (#4620) (@bxy4543) +* e7b1c54e2dc042328261773faaee020a2ed46bf9: docs: Add new blog (#4500) (@yangchuansheng) +* 972784f0047365de226cdc9ed8fcd879676eafc9: docs: Add templates documentation (#4458) (@yangchuansheng) +* 15fee3b856ca7f56be8a16cea7bb792ab8bbfc2c: docs: Update docs for installing sealos (#4660) (@yangchuansheng) +* 301d44f7dd51f2788508edcaa332053217d0162a: docs: add blog for affine (#4661) (@yangchuansheng) +* fee8721f53a2a6b18d0d3a1add1babb0dc2e4b30: docs: add component Highlight (#4471) (@yangchuansheng) +* b4daa2df1181598e396f0804b0d0a12fd16079f4: docs: add master services agreement (#4452) (@yangchuansheng) +* 43632605a34ad3ea3ee7e29ca234d0493679c327: docs: add static host doc (#4640) (@nowinkeyy) +* 9d606dc1f4c03a29a2fa4062f94d061339e6690b: docs: update lychee config (#4498) (@yangchuansheng) +* 5318703c0b64bc4e8e57548411593a40c5239aa1: docs:offline recharge promotions (#4575) (@zjy365) +* 87269b7997005ea27d0bb35a1eb0dfe234d9ab63: feat(object-storage):add sort feature (#4464) (@xudaotutou) +* 8abdf56a684331ed2fc7c754a9f34290f6c3f9a4: feat(sealos create): Add command preview functionality (#4291) (@LZiHaN) +* c3d58c03b2013ba6bda1b6853e738c55ad8dd424: fix create region (#4632) (@bxy4543) +* 2122fb726a2669b340a67be949e46081480f572d: fix env name error in deploy.yaml.tmpl (#4624) (@nowinkeyy) +* 60e490dbd8dea94881dbcf0f835fb35618342545: fix gen region uid (#4611) (@bxy4543) +* 60c481977f39e1565ff055e482913eaed15b93d2: fix install cli with proxy (#4465) (@bxy4543) +* d72a2b8a1d0b2d79fcaa753b2de07921a7acaa66: fix link 404 (#4664) (@nowinkeyy) +* 2113509f408f47621f62ae9dfe30c0e2e05582e5: fix link 404 err0r (#4669) (@nowinkeyy) +* 5e5e95c759c9d02c38086ac17b974bfaf44bb71b: fix minio init error (#4555) (@nowinkeyy) +* ae7d7edaefd7244224221a78385664b1f6e69cf2: fix object storage workflow error (#4542) (@nowinkeyy) +* e41b40f1b03a8906792ef19a40cc7b3873248eb1: fix scripts (#4612) (@bxy4543) +* 424fcc7275bf4735a4412fe2a084e309d6b591f0: fix user change ns annotations (#4592) (@bxy4543) +* b9418ed6b7ece3977ba078cd37a11926cf0c59b0: fix(object-storage):preview & copyURL (#4444) (@xudaotutou) +* 07fdde5bb10e01befc3283b982975bc5c0b9b7c4: k3s support version doc (#4455) (@bxy4543) +* c6e55b37dc22887d0e4d41003a40d974d73302ca: k3s support version doc (#4461) (@bxy4543) +* 2857df524bb018a104785062b9b5722ff9f0731f: optimize default kube runtime config; (#4487) (@bxy4543) +* e1aa395aaf4c827f8f0b96f7ca852b6c6b8ad3e1: optimize init minio sh (#4537) (@nowinkeyy) +* 5cb7e44a3cc829e04b0aa67595a8b49c433c7105: query traffic at a specified time (#4657) (@nowinkeyy) +* e6bf29b3c54b0bcf863532c57772ff7ce888eff9: refactor(costcenter): service api (#4475) (@xudaotutou) +* 70b1f5c6ecffe8c4301a06eb6ee2099ef0085835: refactor:license optimized deployment command form (#4532) (@zjy365) +* 9487f0be1d68edc99b3cb7e6caef7428ae5cb291: refactor:providers invite app (#4599) (@zjy365) +* 21f2c3a58aa601a55ba9358e699aef2f9e2adb7e: refine cluster image(objectstorage-controller、minio-service) (#4512) (@nowinkeyy) +* a0b3363d9880ecbfe61f47835e2e764c641c7ead: release: update sealos version in install.sh. (#4678) (@lingdie) +* 91142fd90b301c5ceb9a298ad6a5396637f543d7: style(kubepanel): adjust style (#4546) (@mlhiter) +* 2057bca8f047a934ff54e26eff123ee7b2644a8c: style(kubepanel): improve the overall style (#4494) (@mlhiter) +* 1fb9c119613f355077c2d37a59ba75de1c43349f: style: Update UI styles for AppLaunchpad (#4649) (@zjy365) +* 53631c6ef01d95e326eba89902626fd026090b3a: style: switch sealos coin (#4509) (@xudaotutou) +* 307e8682c0f8e7dd8a652bde19bf49c9428c9890: update cpu and memory config requirements for deploying sealos (#4625) (@nowinkeyy) +* 24417328a1ad238d9ace220eee644a2ed5f3923f: user registration switch (#4473) (@bxy4543) +* 967c5bf77d3515857074f34619289316a7d23884: 🤖 add release changelog using rebot. (#4442) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta4...v5.0.0-beta5 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.0.md b/CHANGELOG/CHANGELOG-5.0.0.md new file mode 100644 index 000000000000..81702f81dfa8 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0.md @@ -0,0 +1,130 @@ +Welcome to the v5.0.0 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* e19143794992d526fcd0775f455b1157f0081a4e: feat(applaunchpad): support file browser for pv (#4674) (@0fatal) +* 18464891fca36fd623da4f17faff2a035343dfa6: feat(desktop):add inviting others into workspace by link (#4712) (@xudaotutou) +* b81ec5fe785a3a233490151a20ec7111e6621612: feat(docs): update private pricing on website (#4846) (@zjy365) +* a1442f9d29d12ee22a68fbfbc4f32aacfecf2110: feat: add a function to retrieve the user's backup size. (#4780) (@nowinkeyy) +* 6db3f2b4161e672f3643041085bc2998243f4270: feat: add cluster type license (#4752) (@lingdie) +* 835e22457b23e9be7700a11734d41c694231818a: feat: add providers workorder app (#4718) (@zjy365) +* 708e977d11b50680dda2b481b05cc17748a5ab51: feat: app cost svc (#4843) (@bxy4543) +* d4040594a3cd148bf832651d435cf68d501db689: feat: cloud host supports GPU (#4728) (@zjy365) +* 4b2d3c0e72851ff5573d0c85f32450aa526df7e3: feat: desktop support controlling the size of the evoked window (#4700) (@zjy365) +* a1982f0868bc4262625ca7b5dbe79fcb051f538d: feat: impl cronjob show envs (#4833) (@zijiren233) +* 469e42c20ffcfd032b40a00f865e0e688defb6f9: feat: implement desktop 5.0 UI design (#4783) (@zjy365) +* f3ab3fae684612df0c9015aa5c0cde9d51150c6e: feat: improve animation smoothness for expanding and collapsing sections (#4847) (@zijiren233) +* b109ffe2f10db323938629dce20cd4a4fe0c0488: feat: optimize resource monitoring logic, performance and fix nil pointer issue (#4827) (@zijiren233) +* 4cba736bcba50803d0083be9bac97b54779fb0f2: feat: support configurable favicon (#4864) (@zjy365) +* 34e74b454b851c9698fe35e1169de29bbb543491: feat: support workorder migrate (#4782) (@zjy365) +* 3859de964955dda9a350ddbc5cda7f9f48c2cc6a: feat: upgrade cost center config. (#4756) (@lingdie) +* 02e12d646d47fdfb3304cadbc412b302ba2c6b80: feat: yearly/monthly billing for cloudserver (#4781) (@zjy365) +* f0e17bc07ea8b512cd0895022e3176425b607b3c: feat:add cloudserver app (#4698) (@zjy365) +* e8950db28214fc9f690b91b3fd8a4fc9c9246d96: feat:docs add recharge event (#4784) (@zjy365) +* 4ad5ea1aef290be85260dc64660b961e19933b32: feat:license app adds system node information (#4845) (@zjy365) +### Bug fixes +* add7316542a417b8ab0b1452c76873be60b38b50: fix(costcenter): remove bank account regex (#4713) (@xudaotutou) +* 9c178471c7e72b5beb2a9736a20b45fb4fda0fe4: fix(costcenter): update amount by the query (#4737) (@xudaotutou) +* 950e6cb44eb03f6e097a501a80b160eefe9017fc: fix: Delete unnecessary logs (#4710) (@ghostloda) +* 74e80c5763bfea005969792b820f4b4366d78714: fix: add nodes after upgrading, the version installed on the newly added nodes is incorrect (#4857) (@yangxggo) +* 0ca8f84aadc152a0fa926fbb761a38dbb61e1433: fix: applaunchpad unhandledRejection promise (#4771) (@zjy365) +* 7c509b14bfefb9eaf54f2827190c226686437c1f: fix: db backup label encodeToHex (#4792) (@zjy365) +* c4af244fa180b58a9b3068b94ebf7d9d92c42dbf: fix: db resource quota i18n (#4865) (@zijiren233) +* 2505b97f4102667c1fd65d583b6cd452ac8516ce: fix: default app (#4859) (@zijiren233) +* e119d8faef5c4bd9aa936e1a53ea1e1f6121a62d: fix: desktop notifications forbidden error (#4789) (@zjy365) +* 9ef2a3ed311bb7696c2836d78984c2776350b675: fix: ensure license issuance fields are of number type (#4873) (@zjy365) +* 8382b4b594e7ae157a77f1903c19aae1bf0de5cf: fix: scaledown processor checker error (#4815) (@ghostloda) +* 9d0437a95d5d27a55535ed911edfbde2507bb302: fix: sealos 5 cannot upgrade k8s from v1.27.x to v1.28.y (#4842) (@yangxggo) +* 134a975b96e7ef7d0ec316b27884585b73586c1e: fix: skip renew certificate (#4880) (@ghostloda) +* b5a1ef63b7b16f180d648fa578cf790bdb410416: fix: upgrade from k8s v1.26.x to v1.27.y, kubelet restart failed (#4850) (@yangxggo) +* 18be6fe4d903f3982900f07c80bea40853c9a109: fix:add check containerd (#4748) (@ghostloda) +* 5968bae3f03db7bf520294c497ca20264b0253d2: fix:cloudserver bandwidth price display (#4734) (@zjy365) +* 7b5fdf221c342f7e65611a5cd706dca78fa53bbb: fix:db podAntiAffinity Preferred (#4701) (@zjy365) +* 733beccad1ff3c7da3b35f92467823db9516d166: fix:fix transfer (#4804) (@xudaotutou) +* 02a7510c6d28faed82904c61eeae3588cf2b023f: fix:fix transfer billing (#4837) (@xudaotutou) +* bca847623a2c821c184d42fcf165c6fc5b8059e6: fix:launchpad checkNetworkPorts (#4724) (@zjy365) +* c723346d6ef16d9cc32feec91c46e4043c14eb1f: fix:launchpad config yaml boolean (#4715) (@zjy365) +* 0707cb5074ca096db4ce2954dd8a521e4524f8d1: fix:template frontend leaves zombie git processes (#4799) (@zjy365) +### Documentation updates +* 29fb771de5298272254b6e04e593cc2e6f4e4e2d: doc(Frontend): Rename READMD.md to README.md (#4755) (@luoling8192) +* 1c6f9500aced8c487e1e4d9ce5e81f385cd109ba: doc: add a doc for deploying Object Storage (#4777) (@nowinkeyy) +* c6e037670683209260d5e0ebb966354c7f08cedf: doc: move objectstorage install doc (#4835) (@nowinkeyy) +### Other work +* b4187c53b61f384ca21d36e4b4b3d1de7688b35a: Add clear mongo log (#4757) (@wallyxjh) +* 0041eedb17805bea35ccd3d4cda725194dc4b778: Add default install desktop apps. (#4838) (@zzjin) +* f04556bed1fe47332d9ad8286f308c4e9bb5db63: Change object storage monitor service from prometheus to vm (#4727) (@nowinkeyy) +* aec5facb360ef39d66029f4677a6751a5335e0b8: Database exceptions monitor (#4830) (@wallyxjh) +* 39926854fa70117cd634873ae0313a97611ff62f: Delete clear mongo log (#4806) (@wallyxjh) +* 1ad3c7185c5a88f9ddf3d7fdad96e141ffd24c62: Feat/ add vms/email debt notification (#4763) (@bxy4543) +* 8cb7b734000b94dc59e0b9a9722c1fbc466562e5: Feat: Cloud Virtual Machine Billing (#4699) (@bxy4543) +* 694ddb97645f30166e57aec31d9ff9ce469ab7c4: Feat: impl cronjob envs (#4821) (@zijiren233) +* afb2584ddcebf4d4c1655209704d1647152866e1: Fix desktop config (#4733) (@lingdie) +* 4d02f7661760c2a5fc140697bd95867bf14ea35e: Fix desktop password salt (#4762) (@lingdie) +* 2138f4656ed70f2d9c4a14ccdc8c7ce1a9ab4c0d: Fix doc link 404. (#4743) (@zzjin) +* 49e6422e89b7a11a47631e62c8291a5df870b0d9: Fix kubepanel typo&link. (#4729) (@zzjin) +* 13bd347c7e33097b304a2f159f08e79a6d517d0b: Fix/costcente svc (#4736) (@bxy4543) +* 18402ae8df78325f7fef7797b71f5ec6a8477885: Fix: remove deprecated rlcp options (#4803) (@zijiren233) +* 182fa028da5c04892f89e077ed95f5d650f8ef27: Fix: switch user register (#4813) (@zijiren233) +* 3b044f328db58acdd0381c0aec7809027e2d2025: Launchpad monitor (#4689) (@wallyxjh) +* 701612bdd0c5bbfe64b605513f4f2de1e6df1f16: Replace 404 doc to official link. (#4800) (@zzjin) +* 16c8524f7b05201aea5d3edcbd10bc41be3827ab: Stop clusters (#4644) (@wallyxjh) +* 372632d16ee8f06110399b00c4a7a7e54e31fcce: Update README.md (#4745) (@fanux) +* a7c1d9df0e86ea64fb54860f661b4c52a6ffec16: Update app menu types. (#4735) (@zzjin) +* 17c2b43ac55b1170a236e043d3ed007e0bb8aa14: Update appcr (#4742) (@lingdie) +* 6758e182d7dfafbb9779dffb072ea3056e1d0c79: Update cockroach version (#4844) (@wallyxjh) +* 469e9c9796df59f332ec006db3bd0c4effc5e68c: Update doc fireboom homepage link. (#4778) (@zzjin) +* 2d62a13d0e882a328c4dc0a1a9dde1f597f5f3bd: Update i18n for applaunchpad (#4851) (@yangchuansheng) +* 78cefc7715b6c0952f079cc6bbc1bd32e96adbcb: Update install.sh (#4791) (@lingdie) +* 7d5f5a154251ef5e5a6d0fda321c6c41e5ca7880: Update install.sh (#4812) (@lingdie) +* 28698d2d81ec0f48265c8d163f5a52f7fb47f012: Update sealos mongo role (#4823) (@wallyxjh) +* 85474f252f28e0392944f16c446b827c85f403d3: Update terminal frontend ,support customize keepalived time. (#4810) (@zzjin) +* c0bb1e72dd2564edef48e3b38d7c84719c76d6c4: add a key for a bucket (#4714) (@nowinkeyy) +* d11c5fd0cd464c37c3fe95e6ae5adc6a5065b0f0: add email debt notification (#4820) (@bxy4543) +* 89ada630ae6aa71fd569bebd2863e3f22da40911: add quotaEnabled and defaultQuota env (#4822) (@nowinkeyy) +* 6f5c5905b32ec33b36fed1b67ffc16ac9a9b227c: add set-cert doc. (#4772) (@nowinkeyy) +* 1cec0811ca614c3c1d6eb20205bda1f1896a6883: change cert duration. (#4768) (@lingdie) +* 7362d807e9c1612f97f1b9151a6701090431e338: change mongo 4.0 to 4.4 (#4708) (@lingdie) +* 47ba1b87d3adec8bbd48b1d1f557a36d1d65172b: docs: Update sealos version in self-hosting docs (#4683) (@yangchuansheng) +* ffd72f2c0f2b0dec391bcb13db5ebc2ee755b00d: docs: add README for license-system-frontend (#4868) (@zjy365) +* 123980df36875ae9a5b6b4fe7b8db02d89fcd515: docs: add blog "How To Deploy and Configure Meilisearch using Docker" (#4854) (@yangchuansheng) +* 24697342c516803b99f3c506cc2676ab887eafc5: docs: update header for landing page (#4875) (@yangchuansheng) +* 63ecc33d3c2cf70d9cdcd042837e9efaf95bb133: docs: update landing page (#4858) (@yangchuansheng) +* 85318ad38d687fd09298075b00353238918a0d7d: docs:remove HomeUserBy component (#4879) (@zjy365) +* 467a675d00994ba8dd7222fe3400ad88a4fd532c: feat(frontend/cloudserver): Add a quantity option when creating the v… (#4852) (@HUAHUAI23) +* daff4ac6f63a3f04938ef813c2d387b848b5aef7: feat. change desktop to use config file. (#4709) (@lingdie) +* c56ce7aebd2dd7eab3fdcb45764b513fdbd6ebbb: fix billing record query with app type (#4731) (@bxy4543) +* 786985e3cced44b14d8c1faa1018d208db671883: fix control-plane component status (#4749) (@ghostloda) +* dd1cfc9221c260fa2e4f0f9d344775f9aaeeb7bc: fix cost center frontend manifests (#4761) (@lingdie) +* 1d0ad5c5213e4915f196f7a9348a33c1e9cb65d2: fix object storage monitor (#4691) (@bxy4543) +* f6b1e76230978671d5ff52be4eb6d1cc232ccedc: fix objectstorage frontend deploy.yaml format error (#4725) (@nowinkeyy) +* a4b7f8a6b89dea9ed23a49952523a5a869c480a1: fix pull cert-manager image in install.sh (#4775) (@lingdie) +* 26739d9a6cee1afa95bae74e63c21a4f1b57ec5c: fix set account create region id (#4739) (@bxy4543) +* 9a4f321c1d19d9d0e58dd1569455f1b41316c091: i18n: update i18n for App Launchpad (#4754) (@yangchuansheng) +* 4960ec3d0fe0450c66bd5d464735468eb2d75672: i18n: update i18n for DBProvider (#4787) (@yangchuansheng) +* 4c3fac74d54699dac47d1e029547b34fce601c00: monitor svc multi nodePort (#4726) (@bxy4543) +* 4b80930b9090afc7384ef6d6b875743a186c4d97: none (#4732) (@nowinkeyy) +* 651a27cea9affa9e527e3075318b8af065026134: opt: costcenter get quota default use current namespace (#4828) (@zijiren233) +* 9bb4c44653a379e2e36e21a5e3f9b700d13c974d: optimize query object storage metric (#4686) (@nowinkeyy) +* a05b86025cc091977f2b7ab53853d42b641cdb05: refactor(costcenter): refactor invoice (#4694) (@xudaotutou) +* 2176fbb8b4eb6d24b1f7582aa34703af1f0e4583: refactor: adjusted license billing based on cluster size (#4853) (@zjy365) +* ef4409df911bb1565242b4e9dbc2b705888e6235: refactor: improve the prompt when deleting an app (#4786) (@zjy365) +* 216d68e62195bc3066290c3a1e2fe3b1fad217a8: refactor: rename Signin directory to Login and fix file name case issues (#4870) (@zjy365) +* 02327d53e0b9a13d603db249bd50637217f41e39: release: v5.0.0 release, update sealos version in install.sh. (#4882) (@lingdie) +* 2e7eb9f07c225839dc4b02acadb03e3f6183f54d: revert: remove recharge event (#4788) (@zjy365) +* 2fe621674f37a307d8af515ac7d316b3f6650065: style: launchpad displays monitoring values (#4688) (@zjy365) +* 0e1693c32ac098a0b78d793ab334a98246f03d10: styles: Updated UI design for dbprovider frontend (#4747) (@zjy365) +* a58a5b159dcfe74381ce5e85abff9f95149a8eca: styles: launchpad AppStatusTag & workorder appendixs (#4740) (@zjy365) +* f164e9dd48e8a8f6b4ac229dac96e7036854c29a: temporary hiding suspends kb package import (#4753) (@bxy4543) +* 0992fc04dfb4279c198d6e743719a11334c9b5e5: update cvm interface (#4798) (@bxy4543) +* 7974ba511348f0e1a1acb472e694c460744a9cb4: 为kubepanel network 添加service (#4705) (@bearslyricattack) +* 4535a7fcaceeeb9de1b61018e013abf3674db0a0: 🤖 add release changelog using rebot. (#4681) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta5...v5.0.0 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.1-beta1.md b/CHANGELOG/CHANGELOG-5.0.1-beta1.md new file mode 100644 index 000000000000..1a5773029dbb --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1-beta1.md @@ -0,0 +1,174 @@ +Welcome to the v5.0.1-beta1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 79b0cae57275d2965e7517e11e9a029bef84c250: feat(applaunchpad): Improve domain handling for legacy and new domains (#5011) (@zjy365) +* 68c0244a7369804b08e7bc1b78ceb79479063a5c: feat(costcenter): add gift code (#5026) (@HUAHUAI23) +* 80f468297e83abbf7a94b0d56a663e748d616b82: feat(desktop): Implement smart dock behavior (#4998) (@zjy365) +* 9a7bcba87ada604ba2042e1a65e7f1dacea6b969: feat(desktop): add realname auth (#4931) (@HUAHUAI23) +* f54fc3ab48abb8eb8ab43913335746f0f0e742e0: feat(desktop): add signup user sem info (#4949) (@HUAHUAI23) +* 3d0f9645c823c725e06c035c0e8a86786add1cc1: feat(desktop): adjust the style of real-name authentication (#4979) (@HUAHUAI23) +* 3f6343238e5787dcb6a84a1b366911441b01cb3b: feat(desktop): signup user sem data add keyword data (#4983) (@HUAHUAI23) +* 028fa0c36c3b3fe857674d435ad934be6bd4b09a: feat(devbox): devbox basic frontend ui and logic (#4953) (@mlhiter) +* dd2ac2060769d05f6ed5e48ddba14ac7b98b7eb1: feat(docs): add SEM keywords parameter (#4981) (@zjy365) +* 932cd71be14c1039356ba4dc70566c6f39b56098: feat(objectstorage): web host add history router support (#5076) (@HUAHUAI23) +* 04d823c3ac176c2bd4cd1ea318196da616aefe40: feat(template): add translations for app content (#4961) (@zjy365) +* 3d4b3fe917bb579a21ed71f2c0b83ed3c8d7b6c0: feat: Add .gitignore file and i18n.md documentation (#4911) (@zjy365) +* 2eda19e407cca6cc00dd779451146556b40c430c: feat: Enhance cron schedule, Markdown display (#4888) (@zjy365) +* dbca6f0aff908f9f9d58051895be2666113f9179: feat: acmedns (#5063) (@zijiren233) +* b4ee7b8cba1290d713863cdff42a529c2fdbbcc6: feat: add Kafka and Milvus connection string support (#5061) (@zjy365) +* 9ca435cba8059047bf7d63c4b4c2d0ac6869c1a2: feat: add SEO TDK support and pricebox to template (#5038) (@zjy365) +* 1fe875a91d2464e6d8eb329cb9992fc1b5fc5d21: feat: add desktop tooltip for improved user experience (#4945) (@zjy365) +* 1faf9d65714fb97d1667228c5a4449c34a77aa06: feat: add support for launching creation page in launchpad (#4984) (@zjy365) +* bab49b738cb9a23d86a398d35a45f6ab9b3e32b5: feat: applaunchpad log previous (#5047) (@zijiren233) +* 54c0180ff3e91fbef15a99ce0dad5b5e05ff7aed: feat: cost quota and price (#5014) (@zijiren233) +* 582dd4f4fa80a20515fe29cfec42d277802c2eb7: feat: cronjob implement job (#5093) (@zijiren233) +* 6338cf80af7bfe451614feac8afaede3acc0f672: feat: db log (#5069) (@zijiren233) +* cca4da935f9cdfbc0abb1651cd55720a21d4e668: feat: get os traffic from minio (#4968) (@nowinkeyy) +* fad81885c0f18b5e60e82bb829b60e64c0cd36e3: feat: launchpad implement URL query to form data conversion (#5081) (@zjy365) +* 95c5a7d2005975c52a99d59ee980134985c1cb09: feat: resource quota add object storage size (#4874) (@nowinkeyy) +* 88961c5b64ae7bbc2f63598bd252fe1ef90847bb: feat: strip bin and trimpath (#4860) (@zijiren233) +* 20bf7a538e4d5d4e101437c35cc130bc63197c5e: feat: support tagging launchpad and database application sources (#4975) (@zjy365) +* 022e1a0fcf93574b2a16e637794f8cca0f232982: feat: support user external domain (#5021) (@zijiren233) +* bd3f6d45266be04f3fa10a0a86790d3b8be32b74: feat: templates support conditional rendering (#4937) (@zijiren233) +* 4fd0ebc4742df34b0196b106f67c2271bb95bf30: feat: use dumb-init as the init system for Docker container (#4901) (@zjy365) +* 7091b8665cab2b149c9e2fef629f64289cb17f49: feat: user private ns invite (#5043) (@zijiren233) +* fa8ba1e9667184eb700fff1c1928a06bccf9b1d6: feat:dbprovider support reconfigure (#4926) (@zjy365) +* f4bb39d5a1ea10580c6f0702f8f29091db92b4be: feat:desktop update sem (#4993) (@zjy365) +* 9caf45a82b07513466cbd4c76df610a03c4ca47c: feat:docs add 's' parameter for user source tracking (#4958) (@zjy365) +* d32471d2052ab97f92dc38a377ac3b2102ae058b: feat:launchpad && database add cost tip (#4980) (@zjy365) +* eb838607f089f7bfc78ea5df44132bd2f2c9a824: feat:launchpad update container status reason (#4947) (@zjy365) +### Bug fixes +* 010975259d94df105f25b2ab154a1e3ff8d0302f: fix(desktop): fix realname auth multi notify (#4944) (@HUAHUAI23) +* 2bf0773d44271c94b78d854d87bb14c5437194c1: fix(desktop):fix init database error (#5003) (@xudaotutou) +* b75c85f007631dec814f512c1f61aecc621b663b: fix(docs):header title translation (#4963) (@zjy365) +* 83be88aa639e303de502470a20a6a589afca3c54: fix(launchpad): file upload in Kubernetes pods (#4970) (@zjy365) +* 4e95958ea6335168afb779ee46b3576b23272336: fix(objectstorage): Fix site hosting errors for domains (#5082) (@HUAHUAI23) +* 62a290d42c1df72d012957df725c64f5140e861c: fix: add return in PROMPTS_EN/PROMPTS_CN (#4974) (@cbluebird) +* 4ec113475ba8c61e1f668854b38e31efaa431d20: fix: adjust GPU quota for launchpad (#5041) (@zjy365) +* d1193f70675b6505cbdad60d9d1cb5152ed65b66: fix: bg file ext (#4895) (@zijiren233) +* 6c82972f675fc5cbe6a2f17eed2c1a9ab42bf697: fix: check control plane count of master IPs (#4972) (@cbluebird) +* 4288104427fc497567f7768671e217a76af740d4: fix: concat ldflags str (#4893) (@zijiren233) +* 74f8ba5309b9a8d0978bc4e6018c8ead9857fe67: fix: cronjob kc and no mount sa (#5092) (@zijiren233) +* 94d7e328d8f262a5a306dd8464f91065ed636b3f: fix: cronjob total amount (#5095) (@zijiren233) +* 86823ab2f4d71bd98583c7071b78172bb905110e: fix: db quota check (#4927) (@zijiren233) +* 5d6fab9f3acbdda8f2d19402e9e62f1b39685a21: fix: defaults and inputs maybe empty (#4987) (@zijiren233) +* ce47655f8de9cc74eee553f143c5f571d828e025: fix: desktop right-click menu issue (#4939) (@zjy365) +* c1d447fa7eb229d8b83befcebd5700eb3ccf81e6: fix: download exit and status_code check (#4908) (@zijiren233) +* 76990afc9729e02daf97f0ac28434f037d18708f: fix: failed to delete node by apply (#4936) (@yangxggo) +* febe93b505feb4b333d57d221868c38b473546e7: fix: improve timezone handling in Kubernetes pods (#5087) (@zjy365) +* 738bd0b6b79caa7ea5c93e5f2bac3e56b16186ca: fix: load env before fetching app details in launchpad (#4943) (@zjy365) +* 97939b559c0b7772d0290b744bb069794af807c2: fix: log stream cannot be used with jsonRes after flushHeader (#5064) (@zijiren233) +* ba66b046781a9644ad91806aa42aebf0a00cd127: fix: resolve desktop monitoring issue (#5019) (@zjy365) +* a7a7cacb95df719a6e4eedd8e8729e4ce32a493a: fix: signup with password donot save kc (#5036) (@zijiren233) +* 8eef22eb8879ae0095831133baef990fdf05e1e5: fix: switch pod logs (#5040) (@zijiren233) +* c69a4605ecdc2f9dcccd7417bf6c2f3347d7f456: fix: template undefined value (#4992) (@zijiren233) +* 4a3631716c95ff56f95931a94b66b2ab9fc794a5: fix: update work order status to pending (#4899) (@zjy365) +* 002e7a519f3d09c0e49b7c1c185ac2dac969e94b: fix:launchpad domain for internal app call listener (#5090) (@zjy365) +* d36fa02e8039e62f5f592ba059232f36e3af9850: fix:launchpad resource update in checkPermission API (#5077) (@zjy365) +* e91bce93bb139b05800cc8c155233792c18bb840: fix:permission change check for launchpad (#5072) (@zjy365) +* e210fd876a615266c8732e302c81b0bb095367fe: fix:update copywriting (#4905) (@xudaotutou) +### Build process updates +* ae37edef3f24dff0eb8698f0f04ee61f5e66debd: build: add Husky and lint-staged for code formatting (#4982) (@zjy365) +### Other work +* e3c9b629b409aeff73e61e554078430168f8ec32: add default rbac rules for devbox runtime and runtime class. (#5012) (@lingdie) +* 17601ae99548e62fabff95f2fa069e8e1ed38f6f: 5.0.0 New document (#5032) (@bearslyricattack) +* f3efe285fc019437cf616fddbcbb7a9ec8618478: :bug: fix image sha256 (#5094) (@cuisongliu) +* 0853f2d8b2c8cfc556833841acb59fc07305b412: Accountcenter merge (#4795) (@xudaotutou) +* f4be739dac0e42f09a5debafe9bf0717ed9a2c9e: Add pausing kb cluster database in namespace controller & fix get cost api (#4946) (@bxy4543) +* e10a76e5f8881931037a2878b69eb08401835091: Add terminate ns, skip notifications for abnormal users, set default.NodePort.Limit=10, adapt new user struct. (#5056) (@bxy4543) +* 594f3af2ac9f4bdd6c2eea6fbbb39c4df54ffab0: CostCenter api update (#4932) (@bxy4543) +* e2f66e5fa3852dc29384d2f97c862b35af45dbcb: DB backup monitoring (#4950) (@bxy4543) +* bf2fe19c9d796b0b93893444c248236426741fa1: Devbox runtime support release command and args (#5078) (@lingdie) +* a284f3b318af9796bb0a1ca389016ccc230624e6: Doc: update video (#4915) (@zuoFeng59556) +* 2882addbbc621a61843d9c76e37634058e7adb90: Feat/invoice (#4988) (@bxy4543) +* 1decf9a8487d976cb9f1a367d9e66fe9d3b3682a: Fix devbox podPhase logic and license in deploy.yaml.tmpl (#5067) (@lingdie) +* 7268c174d0cca59347c3f6a12426e087be5a27bb: Fix doc platform-components relative link. (#4896) (@zzjin) +* c23bad822748ed1cf0d9709c93b6be2081553578: Fix/get invoice payment (#5088) (@bxy4543) +* f488b6fbb74837a278ecb82faf485ef65b3bbf3f: Fix: Resolve bug introduced by launchpad JSON patching (#4935) (@zjy365) +* 9468f7fa69b5b02cab5d7153f223191de25655f4: Forward compatibility terminal label. (#4921) (@zzjin) +* 7e6cb8bccb9e65fe9405dd8dcf432e7cd604e689: Monitor api adapt token (#5049) (@bxy4543) +* e91f227c171defe16cebe34e78125af03aac4137: Optimize/payment (#5000) (@bxy4543) +* 9e2faf929afde56b5a3745fb8ae412b793f8ad07: Patch kb addon (#4969) (@wallyxjh) +* 456aaaf65a599cd686b342a66699a0d13e46c855: Region amount api (#5070) (@bxy4543) +* ee997b24a5a1520e6a06745c1279f2ff13fc5438: Remove registry with no more needed. (#4910) (@zzjin) +* 3127c815bd290b2229af05bbe72c286c5ba2943c: Replace gomail/gomail with wneessen/go-mail in email mail utility (#4867) (@wneessen) +* 31471d7a572346df2ad693dcf46e40758770f752: Update costcenter (#4990) (@xudaotutou) +* 52a1e82e43bd078677bece7759871e096bdbb2d8: Update costcenter (#5085) (@xudaotutou) +* 0549a9a8eec347ce2a69ff7dbb0d387180de4adc: Update db backup (#4976) (@wallyxjh) +* 525b4dd81c3c5c47ca7f89acde5555a95456ee02: Update feishu notification (#4920) (@wallyxjh) +* 7ef586d2e873ed3ffe289c648e0e58661ab760e5: WIP: Replace `ingress-nginx` with `higress`. (#4831) (@zzjin) +* bcb8ab419c97be1890a21904523c7973ded8641a: add LastTerminatedState and improve squash logic for devbox. (#5080) (@lingdie) +* 3d625499da793e60124dd9dece02deef441de8c5: add additionalPrinterColumns for devbox. (#5020) (@lingdie) +* 132eb38634dbeb70afe171a27f7edfbf49d815b5: add backuprepo (#4986) (@wallyxjh) +* a42f12a9ef59bad8033f0e875a2403c6ef791955: add backuprepo (#4989) (@wallyxjh) +* 7e77b7a8795b27473f3ba1ffdfa490ad90f2f850: add devbox controller rbac, enable in ci to build docker images (#5009) (@lingdie) +* 306bbda51914f2e7ab0fa39cdf667cacb3029a39: add devbox controller rbac. (#5028) (@lingdie) +* a3c9c55ae71f258df704b0c9fdfe00a3d9eded77: add devbox controller. (#4999) (@lingdie) +* 980e796e93a14b77b5d01caeb735e6b37a646ec5: add devbox default runtime ref namespace to devbox-system. (#5034) (@lingdie) +* 6507e5379e737404136340d32ab8d1862d92ff80: add devbox monitor (#5086) (@bxy4543) +* 4a17349ac2f65dcbd955bf339ba58d0204b67639: add devbox node and containerId (#5045) (@bearslyricattack) +* 0ed063560f8ae1898e24298a795dab6934023786: add devbox phase and controller (#5042) (@bearslyricattack) +* e52f5d3c51ac314af58c35fa4f14bed31fcfef1b: add devbox pod hostname set to devbox name (#5015) (@lingdie) +* 086681e078871b356340d41880b2cc04ead87426: add devbox proposal (#4900) (@fanux) +* d58375ebd643599dbee95cd3669dbb3260244b74: add devbox restart pod (#5010) (@bearslyricattack) +* 103c3ac27c5c49cd0e2f12dc49ec3282f305b33f: add devbox runtime app port. (#5099) (@lingdie) +* 7f8ff72737d2defaaf8921496066b59eff8a36ff: add dify installation QA (#4991) (@wallyxjh) +* 569bcfd27035965e4375d7b8e541ee0470c5c019: add extraEnv toleration affinity,ephemeral-storage limit (#5023) (@bearslyricattack) +* 5877ddb053faa770d923db313a3f7fa129692354: add generate public and private key (#5004) (@bearslyricattack) +* 7d882d0504748d52e40c811270bb1523232742cd: add logs for CheckPodConsistency. (#5035) (@lingdie) +* e4e57023de116f259deddcba4083fbaa98b074fc: add random (#5050) (@bearslyricattack) +* d47170c6897924140ac2379b067bb698e7b7a53e: add release old tag (#5053) (@bearslyricattack) +* 60c4065bb9e4dcab4a4842c192fda7b0c62f8726: add service port (#5048) (@bearslyricattack) +* e3347059be70e04f548ffa3fce32a4b950f79d2b: bump format go version (#5062) (@bxy4543) +* 35c1c569d8eb7b68a00bb9ba31d88ac2822037b0: change label location (#5022) (@bearslyricattack) +* 5d1d9ba83ecc96a4f2408e5cb7765b476df469dc: change runtime and runtime class to namespace scope, improve devbox c… (#5024) (@lingdie) +* d59fd4a0c29ee6d7e4977fe10ca6001d992dac62: change svc version to 1.22 (#4916) (@bxy4543) +* 2163655376aaa2f9a2a7b9ceff27ee0711d4055e: chrome(desktop):update token (#5089) (@xudaotutou) +* e9087cf8e69b34dcbb6d45c384715538548dc15a: dep: upgrade controllers client-go and controller-runtime (#4898) (@lingdie) +* 93c5a9a7087ae70f7bc16a6948eeead692a6bb88: docs:update scripts (#4925) (@zjy365) +* 8a6844f602b8912716b39874834f236faa550bf6: feat(account-service): add use giftcode (#5013) (@HUAHUAI23) +* 47001c9ba85927dc846e13b6078ec4b9fe5f38ee: fix release bug (#5051) (@bearslyricattack) +* 4e13ab5fd865062811b321f03b557252fbbe8356: fix devbox get runtime. (#5030) (@lingdie) +* 0f6d5cdd7f010440b65d168942e736b92db548c2: fix env render with app images (#5079) (@bxy4543) +* 331229e388e3b327f2ab1e99ee006fd151cf2726: fix flow error (#4964) (@nowinkeyy) +* 73c3919fbc0da65f735569e8f989c6111977e6e8: fix read notice cause panic (#4930) (@bxy4543) +* 95ce6b8177f91fcf32275e497fa022e58b71257e: fix ssh volume bug (#5039) (@bearslyricattack) +* 6647429d6c8e66181d6e617552afaeb6191a125c: fix webhook (#5068) (@bxy4543) +* 8c21c3705bcbcdcd5935f5fc3c886619b2976bf9: fix(desktop) add phone number bind logic to realname auth (#4940) (@HUAHUAI23) +* a541722e59437b822d059d475117dc4467acb51d: i18n: update i18n for template provider (#4917) (@yangchuansheng) +* 26604f99fa5eca579a6123a8cdae6d4cf3c00dbb: opt: desktop default config (#4876) (@zijiren233) +* 3c418c33641627e33938f4d7df5566fb29527992: optimize get app cost with index(owner+order_id) (#5002) (@bxy4543) +* 3515cdb3d1a960c6c33a2b9b1faa0ed7f49bb2d7: perf(desktop): reduce network traffic overhead (#4948) (@zjy365) +* 22e4ff25251249ee2212d70d3b22f43c2feb6c25: refactor(desktop): refactor delete (#5055) (@xudaotutou) +* d2ea1ed9215ad7387edbeb057a9b976c25d43417: refactor: enhance i18n type annotations for better developer experience (#4878) (@zjy365) +* 80c052f101c1b678e55a96b0e9058f9038471182: refactor: improve i18n in dbprovider for better developer experience (#4919) (@zjy365) +* 11221466e78621b4cb734bc43acb124104c4c522: refactor:Fetch Launchpad pricing from service API (#5025) (@zjy365) +* b03274cf3ff0ebf27cd09f3dedef7df0fbc82106: remove pod finalizer (#5060) (@bearslyricattack) +* 1d82b3fe69a13b9dc331a3a202333cca7531e69b: rollback email package (#4929) (@bxy4543) +* fd6733eaaba0177d39b9112cd5ffb692c6d5aa58: set AutomountServiceAccountToken to false (#5001) (@lingdie) +* e8d627101ef54033fe1402224e487d913301008c: styles(objectstorage): fix styles (#4959) (@xudaotutou) +* 570c1a0495f89efe1861f06f70b2cf83863ae25a: update (#5075) (@nowinkeyy) +* 06caa81dc975e64d7a96a070ad5a8e756f6e3679: update cpu/mem usage (#5006) (@wallyxjh) +* 9e3327a7e6009d550c22ba91998b95264ab5e711: update cpu/mem usage (#5008) (@wallyxjh) +* d2eb60c857fea42ccc9ad65b930c756f74f73737: update devbox to add delete resource. (#5017) (@lingdie) +* b2f04454666dc5ef162c452b16008f5bc7cbf119: update launchpad cpu/mem usage (#5016) (@wallyxjh) +* 4fabfc989886d3cf3473125874b41691ea6424f7: update launchpad single (#5052) (@wallyxjh) +* 8ad51bcb2e96cb3bec3a695a1f91fe5a64b42d97: update launchpad test (#5044) (@wallyxjh) +* c5445068d090167d775ac1e7237427d54c880911: update self-deployment (#4938) (@bearslyricattack) +* 1b92dcde5bc48b91eb0b48e531e557b57bf6c182: update: Set default k8sVersion in license-system (#4889) (@zjy365) +* e292b8417fbd41910c53736103a7d42cb9348866: update:dbprovider Pause Hint message (#4902) (@zjy365) +* d2cfd468fabd8741caad2f980a6fb588d4998dd6: update:desktop add scripts & workorder closedBy (#4928) (@zjy365) +* 2baac3760cc06b451ca9afb79289325dd385e76a: update:docs announcementBar (#4906) (@zjy365) +* 7a19c64dba46599366b38988af5e36d4c2b92065: upgrade devbox runtime crd, rewrite devbox controller. (#5066) (@lingdie) +* 62e61ac41fc3a08c750e3eb4e1a5f31779c9ecd5: 🤖 add release changelog using rebot. (#4887) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0...v5.0.1-beta1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.1-beta2.md b/CHANGELOG/CHANGELOG-5.0.1-beta2.md new file mode 100644 index 000000000000..1864a514d6d0 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1-beta2.md @@ -0,0 +1,32 @@ +Welcome to the v5.0.1-beta2 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* ebe7f51afe023810fc4e3538f4f2e46007d43002: feat: launchpad support secret userDomains (#5119) (@zjy365) +### Bug fixes +* 2fd2f07892660ce4fa0ad0e167082fbeb372d579: fix: cronjob arm64 image (#5113) (@zijiren233) +* d3ba17bc4925707a32ae999e88019aaf3af23800: fix: node tls reject unauthorized (#5116) (@zijiren233) +* bc0b5f72006a173f9bcac0e287fd9893efe728af: fix: some ui adjust and bug fix (#5097) (@mlhiter) +### Other work +* 8c0c34592369874ce178c956af713dba3c17a9ea: devobx ignore extra ports. (#5112) (@lingdie) +* 1243fd31e8f2f8828e35f1c47e6e6cd19155ae3e: fix desktop and costcenter configs (#5114) (@xudaotutou) +* 22d9a138f9a3f4cd04a4eb927639fd008972adff: fix devbox phase generate. (#5120) (@lingdie) +* b6053c1c2601fafa0752cdaac4501606bd020a20: fix get default property (#5108) (@bxy4543) +* 7b025ae80d215cd1fe860ed08bc69dc132185be3: fix init user (#5104) (@bxy4543) +* d21832075623248f831df5f04b7a8962c3c83fb6: fix scripts: init account jwt secret (#5117) (@bxy4543) +* 3081c5dc1abf65d751de29b52b1febeb3dbbad9a: fix(account-service): fix concurrency issue and add real name info api (#5103) (@HUAHUAI23) +* 7b0be9352fa422f1abca8f232cd8c864db548f7e: release: sealos v5.0.1 beta2 (#5110) (@lingdie) +* 7a4b7b91fe63b3b4747fc99d2753c1b7a409fd39: sealos v5.0.1-beta2 (#5071) (@lingdie) +* 54a3bc28684df2f8427c2ff1cc3969a0dd79c534: style(costcenter): recharge discount (#5111) (@xudaotutou) +* b78691a56704884d1da0544a2779b8f42b45d3f1: sync resourcequota objectstorage/size status used (#5102) (@nowinkeyy) +* c80c265fd6838bbf071cefcfa077a4e4d0274fa4: 🤖 add release changelog using rebot. (#5100) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.1-beta1...v5.0.1-beta2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.1.md b/CHANGELOG/CHANGELOG-5.0.1.md new file mode 100644 index 000000000000..d4d59318e2cb --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1.md @@ -0,0 +1,23 @@ +Welcome to the v5.0.1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 2b74a1281cdd72ec5f02a0cc9edf042639a1e054: feat: Implement app guide module (#5115) (@zjy365) +### Bug fixes +* 6489ce6a6d7aaf2aa24ee3514bacab924b0f31a9: fix(desktop):fix merge user (#5101) (@xudaotutou) +### Other work +* 351e7616134950859fe559c57499b6fdff67fa0d: Feat/active task (#5121) (@bxy4543) +* 9817f6fb81b163f42c7d0b31f7e6350101b0f569: devbox cache improve. (#5122) (@lingdie) +* d9b34cd60044590b6a5807d1725398017a735e19: fix build offline scripts and ci. (#5125) (@lingdie) +* a9154f858db2b50ce72e1e0a6fc8b15a954d37a0: fix invaild (#5118) (@xudaotutou) +* 56ffcb829d61b2d5531fe4bb544e17bbe12032ba: 🤖 add release changelog using rebot. (#5123) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.1-beta3...v5.0.1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta1.md b/CHANGELOG/CHANGELOG-5.1.0-beta1.md new file mode 100644 index 000000000000..0e932f6161d2 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta1.md @@ -0,0 +1,262 @@ + +# Welcome to the v5.1.0-beta1 release of Sealos!🎉🎉! + + +## [v5.1.0-beta1](https://github.com/labring/sealos/compare/v5.0.1...v5.1.0-beta1) (2025-08-27) + +### New Features + +* **ci:** add automated workflow for updating tagpr configuration ([#5807](https://github.com/labring/sealos/issues/5807)) +* **ci:** add support for labeled and unlabeled events in PR title linting workflow ([#5869](https://github.com/labring/sealos/issues/5869)) +* **ci:** update workflow to validate pull request titles against semantic rules ([#5860](https://github.com/labring/sealos/issues/5860)) +* **ci:** add workflow for linting pull request titles ([#5790](https://github.com/labring/sealos/issues/5790)) +* **tagpr:** add configuration files and scripts for automated changelog generation ([#5809](https://github.com/labring/sealos/issues/5809)) +* **review:** add CODEOWNERS file to define repository ownership and review responsibilities ([#5849](https://github.com/labring/sealos/issues/5849)) +* **lifecycle** validate kubeadm config files before pulling images ([#5715](https://github.com/labring/sealos/issues/5715)) +* **lifecycle** upgrade Go version to 1.23 and update kubeadm API versions to v1beta4 ([#5718](https://github.com/labring/sealos/issues/5718)) +* **lifecycle** specify the host IP as the k8s internal IP ([#5196](https://github.com/labring/sealos/issues/5196)) +* **database:** add GTM v2 tracking for database module navigation and operations ([#5730](https://github.com/labring/sealos/issues/5730)) +* **database:** database api v1 create/modify endpoint ([#5723](https://github.com/labring/sealos/issues/5723)) +* **invite:** new invite referral doc link ([#5738](https://github.com/labring/sealos/issues/5738)) +* **dbprovider:** Added search funtion in dblist ([#5873](https://github.com/labring/sealos/issues/5873)) +* **dbprovider:** Chat2DB adapt ([#5826](https://github.com/labring/sealos/issues/5826)) +* **frontend:** change ObjectStorage and AppLaunchpad CSS Devbox page ([#5839](https://github.com/labring/sealos/issues/5839)) +* **frontend:** shadcn ui package ([#5830](https://github.com/labring/sealos/issues/5830)) +* **frontend:** gtmv2 guide events ([#5815](https://github.com/labring/sealos/issues/5815)) +* **frontend:** template api v1 ([#5712](https://github.com/labring/sealos/issues/5712)) +* **frontend/costcenter:** add recharge limit for non real name user ([#5174](https://github.com/labring/sealos/issues/5174)) +* **launchpad:** new custom domain form ([#5701](https://github.com/labring/sealos/issues/5701)) +* **launchpad:** use deterministic hash for service names ([#5828](https://github.com/labring/sealos/issues/5828)) +* **launchpad:** create random service name to avoid conflicts ([#5782](https://github.com/labring/sealos/issues/5782)) +* **desktop:** basic layout for mobile version of the guide modal ([#5757](https://github.com/labring/sealos/issues/5757)) +* **desktop:** add umami ([#5346](https://github.com/labring/sealos/issues/5346)) +* **desktop:** show alert before closing the desktop page ([#5825](https://github.com/labring/sealos/issues/5825)) +* **desktop:** Add real-name restrictions ([#5368](https://github.com/labring/sealos/issues/5368)) +* **desktop:** prompt on github email conflicts with another user ([#5819](https://github.com/labring/sealos/issues/5819)) +* **desktop:** desktop design catchup ([#5703](https://github.com/labring/sealos/issues/5703)) +* **desktop:** new signin email design ([#5706](https://github.com/labring/sealos/issues/5706)) +* **desktop:** make desktop usable on mobile screens ([#5685](https://github.com/labring/sealos/issues/5685)) +* **desktop:** better ux when chaging phone/email binding ([#5690](https://github.com/labring/sealos/issues/5690)) +* **desktop:** handle sem params on signin page ([#5673](https://github.com/labring/sealos/issues/5673)) +* **desktop:** change real name reward ([#5555](https://github.com/labring/sealos/issues/5555)) +* **desktop:** hidden enterprise auth ([#5149](https://github.com/labring/sealos/issues/5149)) +* **desktop:** add face auth and enterprise auth ([#5124](https://github.com/labring/sealos/issues/5124)) +* **desktop:** Introduce new console and user onboarding guide ([#5643](https://github.com/labring/sealos/issues/5643)) +* **desktop:** add email get and optimize user real name info ([#5499](https://github.com/labring/sealos/issues/5499)) +* **desktop:** Add Real Name Authentication User Rewards ([#5206](https://github.com/labring/sealos/issues/5206)) +* **devbox:** v2 ui and code refactor ([#5686](https://github.com/labring/sealos/issues/5686)) +* **devbox:** search,remark,sort ([#5801](https://github.com/labring/sealos/issues/5801)) +* **devbox:** cold shutdown ([#5507](https://github.com/labring/sealos/issues/5507)) +* **devbox:** devbox gtm support ([#5735](https://github.com/labring/sealos/issues/5735)) +* **devbox:** zod to openapi ([#5572](https://github.com/labring/sealos/issues/5572)) +* **devbox:** support Jetbrains Toolbox ([#5528](https://github.com/labring/sealos/issues/5528)) +* **devbox:** little address tootip and release info ([#5464](https://github.com/labring/sealos/issues/5464)) +* **devbox:** create mcp support ([#5509](https://github.com/labring/sealos/issues/5509)) +* **devbox:** support trae and windsurf ([#5356](https://github.com/labring/sealos/issues/5356)) +* **vscode/devbox:** plugin transform init ([#5188](https://github.com/labring/sealos/issues/5188)) + +### Bug Fixes + +* **ci:** fix cloud release version ([#5874](https://github.com/labring/sealos/issues/5874)) +* **ci:** update permissions to allow write access for contents in new_version_dispatch.yml ([#5816](https://github.com/labring/sealos/issues/5816)) +* **ci:** update paths to use PROJECT_PATH variable in ci-patch-image.yml ([#5742](https://github.com/labring/sealos/issues/5742)) +* **ci:** update token and committer information in new_version_dispatch.yml ([#5823](https://github.com/labring/sealos/issues/5823)) +* **ci:** enable fetching tags in GitHub Actions workflow for changelog generation ([#5837](https://github.com/labring/sealos/issues/5837)) +* **ci:** update GitHub Actions token and committer information in tagpr.yml ([#5832](https://github.com/labring/sealos/issues/5832)) +* **ci:** update permissions and script path in ci-patch-image.yml ([#5746](https://github.com/labring/sealos/issues/5746)) +* **ci:** streamline image pulling and saving process with associative array ([#5872](https://github.com/labring/sealos/issues/5872)) +* **ci:** include LICENSE and workflow files in sync paths ([#5749](https://github.com/labring/sealos/issues/5749)) +* **ci:** update source paths and sync action configuration ([#5740](https://github.com/labring/sealos/issues/5740)) +* **ci:** update LICENSE file references to LICENSE.md in sync_code.yml ([#5802](https://github.com/labring/sealos/issues/5802)) +* **ci:** frontend ci ([#5541](https://github.com/labring/sealos/issues/5541)) +* **tagpr:** remove unused release variable from output in tagpr.yml ([#5843](https://github.com/labring/sealos/issues/5843)) +* **security:** add non-root user to Dockerfile and update TLS configuration in req.go ([#5818](https://github.com/labring/sealos/issues/5818)) +* **docs:** remove/archive outdated docs ([#5812](https://github.com/labring/sealos/issues/5812)) +* **docs:** update LICENSE file references ([#5811](https://github.com/labring/sealos/issues/5811)) +* **docs** update Kubernetes and Sealos version references to v5.0.1 in documentation and scripts ([#5806](https://github.com/labring/sealos/issues/5806)) +* **docs:** simplify announcement bar configuration ([#5314](https://github.com/labring/sealos/issues/5314)) + +* **changelog:** update release notes path in CI configuration and copy latest changelog ([#5883](https://github.com/labring/sealos/issues/5883)) +* **changelog:** update sorting criteria and improve title mapping in config ([#5876](https://github.com/labring/sealos/issues/5876)) +* **changelog:** automate git-chglog installation and update CI triggers for changelog generation ([#5856](https://github.com/labring/sealos/issues/5856)) +* **changelog:** automate git-chglog installation and update CI triggers for changelog generation ([#5855](https://github.com/labring/sealos/issues/5855)) +* **lychee:** add configuration argument for lychee-action ([#5867](https://github.com/labring/sealos/issues/5867)) +* **lychee:** refine workflow triggers and enhance comment handling for pull requests ([#5743](https://github.com/labring/sealos/issues/5743)) + +* **lifecycle** sealos reset panic ([#5147](https://github.com/labring/sealos/issues/5147)) + +* **dbprovider:** resolve migration failure error display issue ([#5750](https://github.com/labring/sealos/issues/5750)) +* **launchpad:** extract AppCR URL update logic to prevent API response blocking ([#5835](https://github.com/labring/sealos/issues/5835)) +* **applaunchpad:** persistent volume size cauculation ([#5744](https://github.com/labring/sealos/issues/5744)) +* **frontend:** custom scripts in env vars are not injected into the page ([#5792](https://github.com/labring/sealos/issues/5792)) +* **launchpad:** app error state not correctly shown ([#5754](https://github.com/labring/sealos/issues/5754)) +* **frontend:** some i18n entries are missing ([#5751](https://github.com/labring/sealos/issues/5751)) +* **desktop:** do not try closing the page in app running prompt ([#5870](https://github.com/labring/sealos/issues/5870)) +* **desktop:** workspace name incorrectly encoded on signup init ([#5737](https://github.com/labring/sealos/issues/5737)) +* **desktop:** remove recharge entry restriction ([#5732](https://github.com/labring/sealos/issues/5732)) +* **desktop:** email/phone binding/unbinding ux improvments ([#5717](https://github.com/labring/sealos/issues/5717)) +* **desktop:** incorrect titlecase in mail subject ([#5714](https://github.com/labring/sealos/issues/5714)) +* **desktop:** reduce flickers when calculating desktop grids ([#5813](https://github.com/labring/sealos/issues/5813)) +* **desktop:** send button in change phone/email form not disabled when on timer ([#5702](https://github.com/labring/sealos/issues/5702)) +* **desktop:** turnstile captcha and email check ux ([#5698](https://github.com/labring/sealos/issues/5698)) +* **desktop:** hide logo on mobile screens ([#5700](https://github.com/labring/sealos/issues/5700)) +* **desktop:** gtm signup event not fired in phone check process ([#5696](https://github.com/labring/sealos/issues/5696)) +* **desktop:** signin SMS resend timer fired when CAPTCHA is not completed ([#5692](https://github.com/labring/sealos/issues/5692)) +* **desktop:** add missing forced language logic ([#5693](https://github.com/labring/sealos/issues/5693)) +* **desktop:** ux issue on workspace creation popover in /workspace ([#5691](https://github.com/labring/sealos/issues/5691)) +* **desktop:** sem&ad click data not correctly stored sometimes ([#5680](https://github.com/labring/sealos/issues/5680)) +* **desktop:** update-namespace-error ([#5495](https://github.com/labring/sealos/issues/5495)) +* **desktop:** fix real name error when account is not exists ([#5504](https://github.com/labring/sealos/issues/5504)) +* **desktop:** sms not automically sends when captcha is disabled ([#5753](https://github.com/labring/sealos/issues/5753)) +* **desktop:** Fix Tencent blocking real-name callback issue. ([#5617](https://github.com/labring/sealos/issues/5617)) +* **desktop:** twice captcha ([#5399](https://github.com/labring/sealos/issues/5399)) +* **desktop:** fix operationRequest ([#5161](https://github.com/labring/sealos/issues/5161)) +* **devbox:** ssh connnect info adjust ([#5494](https://github.com/labring/sealos/issues/5494)) +* **devbox:** devbox v2 some little bug ([#5739](https://github.com/labring/sealos/issues/5739)) +* **devbox:** template list show bug ([#5821](https://github.com/labring/sealos/issues/5821)) +* **devbox:** template show bug ([#5798](https://github.com/labring/sealos/issues/5798)) +* **devbox:** devbox launch launchpad twice not working ([#5745](https://github.com/labring/sealos/issues/5745)) +* **devbox:** openapi show bug ([#5831](https://github.com/labring/sealos/issues/5831)) +* **devbox:** jb bug download circle ([#5354](https://github.com/labring/sealos/issues/5354)) +* **devbox:** fix invaild devbox cr ([#5339](https://github.com/labring/sealos/issues/5339)) +* **devbox/extension:** cursor 1.0 extension compatibility bug ([#5655](https://github.com/labring/sealos/issues/5655)) +* **devbox/plugin:** windsurf can not open ([#5380](https://github.com/labring/sealos/issues/5380)) + +### Other Workers + +* add database billing in template system ([#5582](https://github.com/labring/sealos/issues/5582)) +* service nodeport modification issue ([#5579](https://github.com/labring/sealos/issues/5579)) +* devbox pod create logic should not retry on conflict, just return. ([#5543](https://github.com/labring/sealos/issues/5543)) +* devbox release target image tag ([#5527](https://github.com/labring/sealos/issues/5527)) +* frontend packages permission ([#5529](https://github.com/labring/sealos/issues/5529)) +* devbox release target image tag ([#5525](https://github.com/labring/sealos/issues/5525)) +* fix run-shell-injection in workflows ([#5502](https://github.com/labring/sealos/issues/5502)) +* update objectstorage cloud image and fix workflow error ([#5497](https://github.com/labring/sealos/issues/5497)) +* devbox controller with event filter. ([#5460](https://github.com/labring/sealos/issues/5460)) +* user controller with event filter. ([#5433](https://github.com/labring/sealos/issues/5433)) +* template yaml separator regex ([#5440](https://github.com/labring/sealos/issues/5440)) +* relay retry delay ([#5429](https://github.com/labring/sealos/issues/5429)) +* empty subnet ([#5426](https://github.com/labring/sealos/issues/5426)) +* admin deploy and sed command error ([#5423](https://github.com/labring/sealos/issues/5423)) +* higress deploy config ([#5422](https://github.com/labring/sealos/issues/5422)) +* sealos cloud deploy script ([#5417](https://github.com/labring/sealos/issues/5417)) +* empty key support ([#5412](https://github.com/labring/sealos/issues/5412)) +* save empty key ([#5411](https://github.com/labring/sealos/issues/5411)) +* docs select style & cronjob bug, desktop guide bug ([#5386](https://github.com/labring/sealos/issues/5386)) +* database deletion policy ([#5382](https://github.com/labring/sealos/issues/5382)) +* devbox release tag use last predicated success commit. ([#5379](https://github.com/labring/sealos/issues/5379)) +* devboxList monitor show bug ([#5374](https://github.com/labring/sealos/issues/5374)) +* ensure that the controller and gateway can be deployed to the master and the tainted node ([#5359](https://github.com/labring/sealos/issues/5359)) +* devbox list big counts bug ([#5341](https://github.com/labring/sealos/issues/5341)) +* launchpad ingress ([#5320](https://github.com/labring/sealos/issues/5320)) +* db miss dbtype & update operation log ([#5315](https://github.com/labring/sealos/issues/5315)) +* devbox deploy bug version2 ([#5312](https://github.com/labring/sealos/issues/5312)) +* devbox plugin gzg http bug ([#5301](https://github.com/labring/sealos/issues/5301)) +* missing dbtype in database ([#5302](https://github.com/labring/sealos/issues/5302)) +* version refresh bug ([#5284](https://github.com/labring/sealos/issues/5284)) +* reward for real name authentication ([#5282](https://github.com/labring/sealos/issues/5282)) +* install remote-ssh manually to unify windsurf and cursor ([#5264](https://github.com/labring/sealos/issues/5264)) +* devbox detail icon show bug ([#5273](https://github.com/labring/sealos/issues/5273)) +* devbox plugin refresh performance bug ([#5276](https://github.com/labring/sealos/issues/5276)) +* document typo error ([#5270](https://github.com/labring/sealos/issues/5270)) +* devboxList not found “devbox_config” cause plugin activating error ([#5259](https://github.com/labring/sealos/issues/5259)) +* actions upload artifactv4 name ([#5258](https://github.com/labring/sealos/issues/5258)) +* remote-ssh remove config code ([#5255](https://github.com/labring/sealos/issues/5255)) +* use container status to set commit status. ([#5220](https://github.com/labring/sealos/issues/5220)) +* group api param id ([#5224](https://github.com/labring/sealos/issues/5224)) +* desktop update translation ([#5216](https://github.com/labring/sealos/issues/5216)) +* change ssh key mode, mount key by subpath. ([#5203](https://github.com/labring/sealos/issues/5203)) +* controller tolerations ([#5162](https://github.com/labring/sealos/issues/5162)) +* add dbType to getDBSecret query parameters ([#5158](https://github.com/labring/sealos/issues/5158)) +* upgrade higress to 2.0.1 to fix higress-ca-root-cert appearing in other namespaces ([#5133](https://github.com/labring/sealos/issues/5133)) +* template deploy env bool conv to str ([#5138](https://github.com/labring/sealos/issues/5138)) +* launchpad configmap logic ([#5650](https://github.com/labring/sealos/issues/5650)) +* simplify invitation app ([#5576](https://github.com/labring/sealos/issues/5576)) +* optimize i18n English translations ([#5191](https://github.com/labring/sealos/issues/5191)) +* update sdk readme and adjust db wording ([#5710](https://github.com/labring/sealos/issues/5710)) +* Add UserAccountType and GiftCodeCreation database tables ([#5822](https://github.com/labring/sealos/issues/5822)) +* add gtm sdk ([#5728](https://github.com/labring/sealos/issues/5728)) +* launchpad support remark ([#5656](https://github.com/labring/sealos/issues/5656)) +* track gtm v2 events ([#5733](https://github.com/labring/sealos/issues/5733)) +* improve app error handling ([#5620](https://github.com/labring/sealos/issues/5620)) +* add devbox frontend init container. ([#5621](https://github.com/labring/sealos/issues/5621)) +* desktop add banner ([#5557](https://github.com/labring/sealos/issues/5557)) +* Add SDK whitelist and optimize database API ([#5551](https://github.com/labring/sealos/issues/5551)) +* Incremental updates of database applications ([#5536](https://github.com/labring/sealos/issues/5536)) +* support custom scripts for aiproxy / applaunchpad / dbprovider / devbox ([#5533](https://github.com/labring/sealos/issues/5533)) +* add region switch proxy ([#5532](https://github.com/labring/sealos/issues/5532)) +* devbox cold shutdown. ([#5492](https://github.com/labring/sealos/issues/5492)) +* update launchpad nodeport ([#5414](https://github.com/labring/sealos/issues/5414)) +* add Open Graph meta tag for better social media sharing ([#5501](https://github.com/labring/sealos/issues/5501)) +* applaunchpad network tooltip ([#5465](https://github.com/labring/sealos/issues/5465)) +* trae cn support web&plugin ([#5466](https://github.com/labring/sealos/issues/5466)) +* Allow coupons on Stripe checkout ([#5459](https://github.com/labring/sealos/issues/5459)) +* update devbox network status ([#5452](https://github.com/labring/sealos/issues/5452)) +* update launchpad network status ([#5444](https://github.com/labring/sealos/issues/5444)) +* doc2x parse pdf support ([#5441](https://github.com/labring/sealos/issues/5441)) +* sonic replaces json ([#5438](https://github.com/labring/sealos/issues/5438)) +* relay retry ignore forbidden channel ([#5435](https://github.com/labring/sealos/issues/5435)) +* async clean log ([#5434](https://github.com/labring/sealos/issues/5434)) +* impl disable billing ([#5431](https://github.com/labring/sealos/issues/5431)) +* global log index ([#5430](https://github.com/labring/sealos/issues/5430)) +* model cost rank and ali tts ([#5428](https://github.com/labring/sealos/issues/5428)) +* dbprovider api ([#5820](https://github.com/labring/sealos/issues/5820)) +* token and group model update ([#5419](https://github.com/labring/sealos/issues/5419)) +* make admin user id fixed ([#5424](https://github.com/labring/sealos/issues/5424)) +* launchpad add zod openapi ([#5578](https://github.com/labring/sealos/issues/5578)) +* add a config when deploy sealos cloud in sealos-system ([#5421](https://github.com/labring/sealos/issues/5421)) +* load status code from error code filed, and create global index ([#5416](https://github.com/labring/sealos/issues/5416)) +* claude think support ([#5413](https://github.com/labring/sealos/issues/5413)) +* support desktop invitation code input ([#5403](https://github.com/labring/sealos/issues/5403)) +* baidu keyhelp and doubao mega ([#5409](https://github.com/labring/sealos/issues/5409)) +* detail control ([#5406](https://github.com/labring/sealos/issues/5406)) +* reduce the number of stream memory copies ([#5402](https://github.com/labring/sealos/issues/5402)) +* devbox support gpu ([#5281](https://github.com/labring/sealos/issues/5281)) +* support ui 2.0 add logs view ([#5357](https://github.com/labring/sealos/issues/5357)) +* devbox controller concurrent and config qps flag. ([#5365](https://github.com/labring/sealos/issues/5365)) +* update invite page and adjust cashback ([#5364](https://github.com/labring/sealos/issues/5364)) +* frontend arm runner ([#5351](https://github.com/labring/sealos/issues/5351)) +* jetbrain modal ([#5253](https://github.com/labring/sealos/issues/5253)) +* support multiple volumes ([#5337](https://github.com/labring/sealos/issues/5337)) +* support node selector and runtime class name for devbox. support custom resource name. ([#5330](https://github.com/labring/sealos/issues/5330)) +* cronjob sts && template readme ([#5324](https://github.com/labring/sealos/issues/5324)) +* start and pause will modify ingress ([#5321](https://github.com/labring/sealos/issues/5321)) +* add driver for devbox ([#5322](https://github.com/labring/sealos/issues/5322)) +* add devbox service ([#5275](https://github.com/labring/sealos/issues/5275)) +* database UI 2.0 ([#5293](https://github.com/labring/sealos/issues/5293)) +* doubao tts ([#5285](https://github.com/labring/sealos/issues/5285)) +* cronjob history limit from env ([#5249](https://github.com/labring/sealos/issues/5249)) +* desktop add two beginner tasks ([#5279](https://github.com/labring/sealos/issues/5279)) +* add GPU horizontal pod autoscaling for launchpad ([#5277](https://github.com/labring/sealos/issues/5277)) +* support backup selection during database creation ([#5252](https://github.com/labring/sealos/issues/5252)) +* simple backup ([#5246](https://github.com/labring/sealos/issues/5246)) +* update deploy modal ([#5245](https://github.com/labring/sealos/issues/5245)) +* optimize database pause and backup handling ([#5242](https://github.com/labring/sealos/issues/5242)) +* get channel default models and modelmapping ([#5232](https://github.com/labring/sealos/issues/5232)) +* qwen vl image ([#5226](https://github.com/labring/sealos/issues/5226)) +* support configurable currency symbol ([#5221](https://github.com/labring/sealos/issues/5221)) +* support ali qwen-vl ([#5222](https://github.com/labring/sealos/issues/5222)) +* optimize user task and support customer service config ([#5214](https://github.com/labring/sealos/issues/5214)) +* delete password, add authorized_key. ([#5194](https://github.com/labring/sealos/issues/5194)) +* add two types in workorder ([#5197](https://github.com/labring/sealos/issues/5197)) +* add database log analysis ([#5189](https://github.com/labring/sealos/issues/5189)) +* support devbox token to fetch details API ([#5154](https://github.com/labring/sealos/issues/5154)) +* add 1024 activity banner ([#5171](https://github.com/labring/sealos/issues/5171)) +* add runtime version and state. ([#5169](https://github.com/labring/sealos/issues/5169)) +* add jwt secret in devbox secret. ([#5166](https://github.com/labring/sealos/issues/5166)) + +### Performance Improvements + +* **devbox** some little perf ([#5547](https://github.com/labring/sealos/issues/5547)) +* **devbox** devbox ide button style and perf ([#5268](https://github.com/labring/sealos/issues/5268)) + +### Pull Requests + +* Merge pull request [#5471](https://github.com/labring/sealos/issues/5471) from labring/ShortCipher5-patch-1 +* Merge pull request [#5439](https://github.com/labring/sealos/issues/5439) from ShortCipher5/main + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta2.md b/CHANGELOG/CHANGELOG-5.1.0-beta2.md new file mode 100644 index 000000000000..db28cc6974c9 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta2.md @@ -0,0 +1,24 @@ + +# Welcome to the v5.1.0-beta2 release of Sealos!🎉🎉! + + +## [v5.1.0-beta2](https://github.com/labring/sealos/compare/v5.1.0-beta1...v5.1.0-beta2) (2025-08-30) + +### Bug Fixes + +* **ci:** add missing packages:write permission for cloud release workflows ([#5907](https://github.com/labring/sealos/issues/5907)) +* **dbprovider:** solved bugs in redirecting to chat2db ([#5897](https://github.com/labring/sealos/issues/5897)) +* **workflow:** pin GitHub translate action to version 1.1.2 ([#5900](https://github.com/labring/sealos/issues/5900)) +* **workflow:** update issues-translator to use lizheming/github-translate-action with discussion support and ubuntu-24.04 ([#5892](https://github.com/labring/sealos/issues/5892)) +* **desktop:** fix workspace not quitting properly ([#5882](https://github.com/labring/sealos/issues/5882)) + +### New Features + +* **dbprovider:** added alert info when database is abnormal ([#5881](https://github.com/labring/sealos/issues/5881)) +* **kubeadm:** increase event burst and QPS limits, adjust image GC thresholds ([#5889](https://github.com/labring/sealos/issues/5889)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta3.md b/CHANGELOG/CHANGELOG-5.1.0-beta3.md new file mode 100644 index 000000000000..e69e66edd557 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta3.md @@ -0,0 +1,29 @@ + +# Welcome to the v5.1.0-beta3 release of Sealos!🎉🎉! + + +## [v5.1.0-beta3](https://github.com/labring/sealos/compare/v5.1.0-beta2...v5.1.0-beta3) (2025-09-02) + +### Bug Fixes + +* **deploy:** add wait loop for desktop-frontend pods to ensure readiness ([#5920](https://github.com/labring/sealos/issues/5920)) +* **devbox:** fix build issue for devbox, adjust Dockerfile ([#5919](https://github.com/labring/sealos/issues/5919)) +* **account:** fix remove account service smtp config verification ([#5917](https://github.com/labring/sealos/issues/5917)) +* **desktop:** change bank input field to dropdown ([#5913](https://github.com/labring/sealos/issues/5913)) +* **dbprovider:** the database deployed from the laf backup cannot be deleted ([#5915](https://github.com/labring/sealos/issues/5915)) +* **ci:** cleanup unused scripts files ([#5911](https://github.com/labring/sealos/issues/5911)) + +### New Features + +* **images:** add sealos cloud deps images ([#5912](https://github.com/labring/sealos/issues/5912)) +* **desktop:** add username/password signin v2 ([#5918](https://github.com/labring/sealos/issues/5918)) +* **launchpad:** update ports api & schema ([#5914](https://github.com/labring/sealos/issues/5914)) +* **kubeadm:** refactor kubeadm config merging and improve node handling ([#5908](https://github.com/labring/sealos/issues/5908)) +* **dbprovider:** added prompt database update process when backup is running ([#5905](https://github.com/labring/sealos/issues/5905)) +* **apply:** conditionally append components based on distribution type ([#5909](https://github.com/labring/sealos/issues/5909)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta4.md b/CHANGELOG/CHANGELOG-5.1.0-beta4.md new file mode 100644 index 000000000000..dfe407e60397 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta4.md @@ -0,0 +1,52 @@ + +# Welcome to the v5.1.0-beta4 release of Sealos!🎉🎉! + + +## [v5.1.0-beta4](https://github.com/labring/sealos/compare/v5.1.0-beta3...v5.1.0-beta4) (2025-09-15) + +### Bug Fixes + +* **release:** update CI runner to Ubuntu 22.04 ([#5985](https://github.com/labring/sealos/issues/5985)) +* **template:** app card title line incorrectly wraps ([#5984](https://github.com/labring/sealos/issues/5984)) +* **kubepanel:** fix Monaco Editor SSR and CDN issues ([#5982](https://github.com/labring/sealos/issues/5982)) +* **checker:** update time synchronization check and add CockroachDB max offset configuration ([#5981](https://github.com/labring/sealos/issues/5981)) +* **config:** use local variable for registry domain normalization ([#5980](https://github.com/labring/sealos/issues/5980)) +* **build:** add platform validation for amd64 and arm64 builds in Makefile ([#5976](https://github.com/labring/sealos/issues/5976)) +* **changelog:** update installation script reference for release version handling ([#5971](https://github.com/labring/sealos/issues/5971)) +* **helm:** update payment secrets handling and add missing certificate files ([#5969](https://github.com/labring/sealos/issues/5969)) +* **devbox:** adjust privacy document url en ([#5941](https://github.com/labring/sealos/issues/5941)) +* **deploy:** set default value for NODE_TLS_REJECT_UNAUTHORIZED to "1" ([#5966](https://github.com/labring/sealos/issues/5966)) +* **cloud:** fix mirror images for base images and cloud script v2 ([#5950](https://github.com/labring/sealos/issues/5950)) +* **deploy:** update configMap reference for devbox environment ([#5949](https://github.com/labring/sealos/issues/5949)) +* **env:** add loggerfile variable to environment template ([#5947](https://github.com/labring/sealos/issues/5947)) +* **images:** improve logging and streamline command execution ([#5946](https://github.com/labring/sealos/issues/5946)) +* **init.sh:** wait for desktop-frontend pods to be in Running state ([#5943](https://github.com/labring/sealos/issues/5943)) +* **desktop:** guide order incorrect if some apps are not installed ([#5930](https://github.com/labring/sealos/issues/5930)) +* **devbox:** remove init container and add migration job for database deployment ([#5934](https://github.com/labring/sealos/issues/5934)) +* **release:** update runner version to Ubuntu 20.04 ([#5929](https://github.com/labring/sealos/issues/5929)) +* **ci:** exclude deploy/base directory from link checking to prevent third-party dependency issues ([#5928](https://github.com/labring/sealos/issues/5928)) + +### New Features + +* **dbprovider:** applied parameter configuration & added mysql-5.7.42 & added addon api ([#5862](https://github.com/labring/sealos/issues/5862)) +* **devbox:** add some ide support ([#5940](https://github.com/labring/sealos/issues/5940)) +* **service:** add Hubble service to enhance observability ([#5729](https://github.com/labring/sealos/issues/5729)) +* **config:** add allowed origins for cloud domain in configmap ([#5974](https://github.com/labring/sealos/issues/5974)) +* **proxy:** add proxy support for image pulling in build-offline-tar.sh ([#5968](https://github.com/labring/sealos/issues/5968)) +* **desktop:** join discord prompt ([#5967](https://github.com/labring/sealos/issues/5967)) +* **images:** refactor installation process and clean up unused functions for cloud ([#5954](https://github.com/labring/sealos/issues/5954)) +* **images:** add cloud v2 image ([#5965](https://github.com/labring/sealos/issues/5965)) +* **launchpad:** update log api ([#5964](https://github.com/labring/sealos/issues/5964)) +* **devbox:** remove job devbox to init db ([#5963](https://github.com/labring/sealos/issues/5963)) +* **deploy:** add NODE_TLS_REJECT_UNAUTHORIZED environment variable to frontend containers ([#5958](https://github.com/labring/sealos/issues/5958)) +* **docs:** add README files for Higress, Kubernetes, and OpenEBS with usage instructions ([#5937](https://github.com/labring/sealos/issues/5937)) +* **rbac:** add permissions for backup repositories in cluster role ([#5939](https://github.com/labring/sealos/issues/5939)) +* **chart:** add cloud base image and remove unused scripts ([#5936](https://github.com/labring/sealos/issues/5936)) +* **app:** fix miss config for yaml ([#5932](https://github.com/labring/sealos/issues/5932)) +* **applaunchpad:** add configurable log feature toggle ([#5933](https://github.com/labring/sealos/issues/5933)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta5.md b/CHANGELOG/CHANGELOG-5.1.0-beta5.md new file mode 100644 index 000000000000..81b5492b2842 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta5.md @@ -0,0 +1,31 @@ + +# Welcome to the v5.1.0-beta5 release of Sealos!🎉🎉! + + +## [v5.1.0-beta5](https://github.com/labring/sealos/compare/v5.1.0-beta4...v5.1.0-beta5) (2025-09-19) + +### Bug Fixes + +* **dbprovider:** resolve createdb storage issue ([#6001](https://github.com/labring/sealos/issues/6001)) +* **webhooks:** add permissions for contents and packages in GitHub Actions workflow ([#5994](https://github.com/labring/sealos/issues/5994)) +* **devbox:** cn show bug ([#5991](https://github.com/labring/sealos/issues/5991)) +* **dbprovider:** database cannot be deleted & disabled mysql 5.7.42 parameter configuration ([#5988](https://github.com/labring/sealos/issues/5988)) +* **devbox:** optimize API and fix page bugs ([#5951](https://github.com/labring/sealos/issues/5951)) + +### New Features + +* **desktop:** show captcha on signin page ([#5995](https://github.com/labring/sealos/issues/5995)) +* **service:** change hubble redis deploy logic. ([#5999](https://github.com/labring/sealos/issues/5999)) +* **dbprovider:** optimize database api ([#5993](https://github.com/labring/sealos/issues/5993)) +* **vlogs service:** fix stderr handling and improve number logic validation. ([#5859](https://github.com/labring/sealos/issues/5859)) +* **dbprovider:** used session storage for storing db details ([#5990](https://github.com/labring/sealos/issues/5990)) + +### Performance Improvements + +* **applaunchpad:** optimize API performance and remove ports endpoints ([#5989](https://github.com/labring/sealos/issues/5989)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta6.md b/CHANGELOG/CHANGELOG-5.1.0-beta6.md new file mode 100644 index 000000000000..5ec657d3810c --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta6.md @@ -0,0 +1,15 @@ + +# Welcome to the v5.1.0-beta6 release of Sealos!🎉🎉! + + +## [v5.1.0-beta6](https://github.com/labring/sealos/compare/v5.1.0-beta5...v5.1.0-beta6) (2025-09-19) + +### Bug Fixes + +* **template:** fix kubectl image in template deployment ([#6004](https://github.com/labring/sealos/issues/6004)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-beta7.md b/CHANGELOG/CHANGELOG-5.1.0-beta7.md new file mode 100644 index 000000000000..6435bfcafeea --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-beta7.md @@ -0,0 +1,33 @@ + +# Welcome to the v5.1.0-beta7 release of Sealos!🎉🎉! + + +## [v5.1.0-beta7](https://github.com/labring/sealos/compare/v5.1.0-beta5...v5.1.0-beta7) (2025-09-22) + +### Bug Fixes + +* **dbprovider:** adjusted database api ([#6012](https://github.com/labring/sealos/issues/6012)) +* **devbox:** template hover show bug ([#6019](https://github.com/labring/sealos/issues/6019)) +* **deploy:** add namespace to cluster role binding in deploy manifest ([#6020](https://github.com/labring/sealos/issues/6020)) +* **costcenter:** internationalize count unit and set default recharge amount to 16 ([#6022](https://github.com/labring/sealos/issues/6022)) +* **db:** update db default config ([#6017](https://github.com/labring/sealos/issues/6017)) +* **init.sh:** conditionally apply configmap and remove redundant kubectl commands ([#6013](https://github.com/labring/sealos/issues/6013)) +* **desktop:** update desktop configmap.yaml.tmpl ([#6011](https://github.com/labring/sealos/issues/6011)) +* **template:** fix kubectl image in template deployment ([#6004](https://github.com/labring/sealos/issues/6004)) + +### Code Refactoring + +* **deploy:** migrate Hubble deployment to Helm charts ([#6024](https://github.com/labring/sealos/issues/6024)) +* **workflows:** remove objectstorage workflow and update controllers workflow ([#6016](https://github.com/labring/sealos/issues/6016)) +* **workflows:** release and append webhooks steps in CI workflow ([#6015](https://github.com/labring/sealos/issues/6015)) + +### New Features + +* **db:** add CRD config reading and parameter updates ([#6008](https://github.com/labring/sealos/issues/6008)) +* **account:** add corporate type ([#6006](https://github.com/labring/sealos/issues/6006)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-rc1.md b/CHANGELOG/CHANGELOG-5.1.0-rc1.md new file mode 100644 index 000000000000..c0ff20950e21 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-rc1.md @@ -0,0 +1,32 @@ + +# Welcome to the v5.1.0-rc1 release of Sealos!🎉🎉! + + +## [v5.1.0-rc1](https://github.com/labring/sealos/compare/v5.1.0-beta7...v5.1.0-rc1) (2025-09-27) + +### Bug Fixes + +* **lifecycle:** Avoid map data races for env cache when getHostEnvInCache ([#6049](https://github.com/labring/sealos/issues/6049)) +* **template:** unnecessary screenWidth listener causes rerender ([#6045](https://github.com/labring/sealos/issues/6045)) +* **deploy:** add permissions for addons in rbac.yaml ([#6044](https://github.com/labring/sealos/issues/6044)) +* **devbox:** add devbox controller flag ([#6043](https://github.com/labring/sealos/issues/6043)) +* **dbprovider:** supportParameterConfig does not update correctly ([#6041](https://github.com/labring/sealos/issues/6041)) +* **applaunchpad:** app config not always loaded before calling apis ([#6038](https://github.com/labring/sealos/issues/6038)) +* **deploy:** update image to ghcr.io/labring/sealos-admission-webhook:latest ([#6037](https://github.com/labring/sealos/issues/6037)) +* **cronjob:** include curl-kubectl image in shim imagelist ([#6032](https://github.com/labring/sealos/issues/6032)) +* **deploy:** add optional payment secret to account manager deployment ([#6031](https://github.com/labring/sealos/issues/6031)) +* **webhooks:** add Aliyun registry and repository prefix to webhooks configuration ([#6027](https://github.com/labring/sealos/issues/6027)) + +### New Features + +* **template:** update git repo in instrumentation hook ([#6042](https://github.com/labring/sealos/issues/6042)) +* **launchpad:** add domain challenge authentication endpoints ([#6040](https://github.com/labring/sealos/issues/6040)) +* **template:** append uid to template share links ([#6034](https://github.com/labring/sealos/issues/6034)) +* **vlogs:** change vlogs query logic ([#6029](https://github.com/labring/sealos/issues/6029)) +* **dbprovider:** adjusted database api and icon ([#6028](https://github.com/labring/sealos/issues/6028)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-rc2.md b/CHANGELOG/CHANGELOG-5.1.0-rc2.md new file mode 100644 index 000000000000..0b681f23303e --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-rc2.md @@ -0,0 +1,20 @@ + +# Welcome to the v5.1.0-rc2 release of Sealos!🎉🎉! + + +## [v5.1.0-rc2](https://github.com/labring/sealos/compare/v5.1.0-rc1...v5.1.0-rc2) (2025-09-29) + +### Bug Fixes + +* **template:** app yaml parsing fails if not rendered ([#6058](https://github.com/labring/sealos/issues/6058)) +* **deploy:** add ADMIN_PASSWORD environment variable to deploy manifest ([#6054](https://github.com/labring/sealos/issues/6054)) + +### New Features + +* **template:** include resource requirements in getTemplateSource api ([#6053](https://github.com/labring/sealos/issues/6053)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0-rc3.md b/CHANGELOG/CHANGELOG-5.1.0-rc3.md new file mode 100644 index 000000000000..5009c8e1506c --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0-rc3.md @@ -0,0 +1,41 @@ + +# Welcome to the v5.1.0-rc3 release of Sealos!🎉🎉! + + +## [v5.1.0-rc3](https://github.com/labring/sealos/compare/v5.1.0-rc2...v5.1.0-rc3) (2025-10-16) + +### Bug Fixes + +* **devbox:** some 403 error text adjust ([#6099](https://github.com/labring/sealos/issues/6099)) +* **alerts:** update ns alert ([#6098](https://github.com/labring/sealos/issues/6098)) +* **applaunchpad:** add null check for appName in domain verification handler ([#6094](https://github.com/labring/sealos/issues/6094)) +* **aiproxy:** correct internal backend URL in values.yaml ([#6081](https://github.com/labring/sealos/issues/6081)) +* **ci:** enhance tagpr workflow with version input validation and retry logic for image pulling ([#6086](https://github.com/labring/sealos/issues/6086)) +* **desktop:** prevent pod crash on SMS/email failures ([#6072](https://github.com/labring/sealos/issues/6072)) +* **costcenter:** billing detail display ([#6070](https://github.com/labring/sealos/issues/6070)) +* **devbox:** claude-code template shortcut bug ([#6069](https://github.com/labring/sealos/issues/6069)) +* **proxy:** update image registry to use dockerproxy.net ([#6065](https://github.com/labring/sealos/issues/6065)) + +### Code Refactoring + +* **image-cri-shim:** remove registry.d support ([#6089](https://github.com/labring/sealos/issues/6089)) +* **kubepanel:** try remove kubepanel's metadata.managedFields. ([#6076](https://github.com/labring/sealos/issues/6076)) + +### New Features + +* **image-cri-shim:** implement synchronization of image-cri-shim config from ConfigMap ([#6091](https://github.com/labring/sealos/issues/6091)) +* **release:** update release workflow to trigger on tag pushes ([#6093](https://github.com/labring/sealos/issues/6093)) +* **aiproxy-froent:** add swagger doc ([#6084](https://github.com/labring/sealos/issues/6084)) +* **check:** refactor kernel version check into separate script ([#6079](https://github.com/labring/sealos/issues/6079)) +* **image-cri-shim:** sync inline registries to registry.d ([#6075](https://github.com/labring/sealos/issues/6075)) +* **aiproxy:** add aiproxy chart build ([#6073](https://github.com/labring/sealos/issues/6073)) +* **applaunchpad:** optimize domain binding interaction ([#6071](https://github.com/labring/sealos/issues/6071)) +* **image-cri-shim:** Dynamic Configuration Reloading ([#6067](https://github.com/labring/sealos/issues/6067)) +* **launchpad:** add PVC monitor support and fix unified time axis rendering issue ([#6068](https://github.com/labring/sealos/issues/6068)) +* **service:** Add launchpad PVC query function and bug fix ([#6063](https://github.com/labring/sealos/issues/6063)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.1.0.md b/CHANGELOG/CHANGELOG-5.1.0.md new file mode 100644 index 000000000000..14e1477b6749 --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.1.0.md @@ -0,0 +1,38 @@ + +# Welcome to the v5.1.0 release of Sealos!🎉🎉! + + +## [v5.1.0](https://github.com/labring/sealos/compare/v5.1.0-rc3...v5.1.0) (2025-11-03) + +### Bug Fixes + +* **service:** fix launchpad metrics query logic. ([#6133](https://github.com/labring/sealos/issues/6133)) +* **costcenter:** add send note ([#6110](https://github.com/labring/sealos/issues/6110)) +* **template:** dev yaml render error ([#6059](https://github.com/labring/sealos/issues/6059)) +* **devbox:** add tooltip for truncated template title and description ([#6087](https://github.com/labring/sealos/issues/6087)) +* **account:** optimize resume with payment ([#6082](https://github.com/labring/sealos/issues/6082)) + +### Code Refactoring + +* **desktop:** optimize workspace state stores ([#6102](https://github.com/labring/sealos/issues/6102)) + +### New Features + +* **dbprovider:** add disk full alert display for databases ([#6115](https://github.com/labring/sealos/issues/6115)) +* **desktop:** add banner for desktop notifications ([#6136](https://github.com/labring/sealos/issues/6136)) +* **dbprovider:** update logs ([#6127](https://github.com/labring/sealos/issues/6127)) +* **kubepanel:** init support edit ingress rules. ([#6126](https://github.com/labring/sealos/issues/6126)) +* **desktop:** can set alias for users in workspaces ([#6111](https://github.com/labring/sealos/issues/6111)) +* **account:** Add workspace subscription plan ([#6103](https://github.com/labring/sealos/issues/6103)) +* **job/init:** add default values for admin configuration in init-job deployment template ([#6117](https://github.com/labring/sealos/issues/6117)) +* **docs:** update usage guide for image-cri-shim binary service with English translations ([#6118](https://github.com/labring/sealos/issues/6118)) +* **desktop:** do not show modals if there's any auto opened app ([#6113](https://github.com/labring/sealos/issues/6113)) +* **service:** add zombie detoctor service ([#6104](https://github.com/labring/sealos/issues/6104)) +* **service:** add iamge monitor to watch pull image error and slow pull ([#6100](https://github.com/labring/sealos/issues/6100)) +* **desktop:** update appdock styling with solid background and enhanced shadows ([#6055](https://github.com/labring/sealos/issues/6055)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-latest.md b/CHANGELOG/CHANGELOG-latest.md new file mode 100644 index 000000000000..14e1477b6749 --- /dev/null +++ b/CHANGELOG/CHANGELOG-latest.md @@ -0,0 +1,38 @@ + +# Welcome to the v5.1.0 release of Sealos!🎉🎉! + + +## [v5.1.0](https://github.com/labring/sealos/compare/v5.1.0-rc3...v5.1.0) (2025-11-03) + +### Bug Fixes + +* **service:** fix launchpad metrics query logic. ([#6133](https://github.com/labring/sealos/issues/6133)) +* **costcenter:** add send note ([#6110](https://github.com/labring/sealos/issues/6110)) +* **template:** dev yaml render error ([#6059](https://github.com/labring/sealos/issues/6059)) +* **devbox:** add tooltip for truncated template title and description ([#6087](https://github.com/labring/sealos/issues/6087)) +* **account:** optimize resume with payment ([#6082](https://github.com/labring/sealos/issues/6082)) + +### Code Refactoring + +* **desktop:** optimize workspace state stores ([#6102](https://github.com/labring/sealos/issues/6102)) + +### New Features + +* **dbprovider:** add disk full alert display for databases ([#6115](https://github.com/labring/sealos/issues/6115)) +* **desktop:** add banner for desktop notifications ([#6136](https://github.com/labring/sealos/issues/6136)) +* **dbprovider:** update logs ([#6127](https://github.com/labring/sealos/issues/6127)) +* **kubepanel:** init support edit ingress rules. ([#6126](https://github.com/labring/sealos/issues/6126)) +* **desktop:** can set alias for users in workspaces ([#6111](https://github.com/labring/sealos/issues/6111)) +* **account:** Add workspace subscription plan ([#6103](https://github.com/labring/sealos/issues/6103)) +* **job/init:** add default values for admin configuration in init-job deployment template ([#6117](https://github.com/labring/sealos/issues/6117)) +* **docs:** update usage guide for image-cri-shim binary service with English translations ([#6118](https://github.com/labring/sealos/issues/6118)) +* **desktop:** do not show modals if there's any auto opened app ([#6113](https://github.com/labring/sealos/issues/6113)) +* **service:** add zombie detoctor service ([#6104](https://github.com/labring/sealos/issues/6104)) +* **service:** add iamge monitor to watch pull image error and slow pull ([#6100](https://github.com/labring/sealos/issues/6100)) +* **desktop:** update appdock styling with solid background and enhanced shadows ([#6055](https://github.com/labring/sealos/issues/6055)) + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG.md b/CHANGELOG/CHANGELOG.md new file mode 100644 index 000000000000..0486debdf293 --- /dev/null +++ b/CHANGELOG/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +- [CHANGELOG-5.1.0-rc3.md](./CHANGELOG-5.1.0-rc3.md) +- [CHANGELOG-5.1.0-rc2.md](./CHANGELOG-5.1.0-rc2.md) +- [CHANGELOG-5.1.0-rc1.md](./CHANGELOG-5.1.0-rc1.md) +- [CHANGELOG-5.1.0-beta7.md](./CHANGELOG-5.1.0-beta7.md) +- [CHANGELOG-5.1.0-beta6.md](./CHANGELOG-5.1.0-beta6.md) +- [CHANGELOG-5.1.0-beta5.md](./CHANGELOG-5.1.0-beta5.md) +- [CHANGELOG-5.1.0-beta4.md](./CHANGELOG-5.1.0-beta4.md) +- [CHANGELOG-5.1.0-beta3.md](./CHANGELOG-5.1.0-beta3.md) +- [CHANGELOG-5.1.0-beta2.md](./CHANGELOG-5.1.0-beta2.md) +- [CHANGELOG-5.1.0-beta1.md](./CHANGELOG-5.1.0-beta1.md) +- [CHANGELOG-5.1.0.md](./CHANGELOG-5.1.0.md) +- [CHANGELOG-5.0.1-beta2.md](./CHANGELOG-5.0.1-beta2.md) +- [CHANGELOG-5.0.1-beta1.md](./CHANGELOG-5.0.1-beta1.md) +- [CHANGELOG-5.0.1.md](./CHANGELOG-5.0.1.md) +- [CHANGELOG-5.0.0-beta5.md](./CHANGELOG-5.0.0-beta5.md) +- [CHANGELOG-5.0.0-beta4.md](./CHANGELOG-5.0.0-beta4.md) +- [CHANGELOG-5.0.0-beta3.md](./CHANGELOG-5.0.0-beta3.md) +- [CHANGELOG-5.0.0-beta2.md](./CHANGELOG-5.0.0-beta2.md) +- [CHANGELOG-5.0.0-beta1.md](./CHANGELOG-5.0.0-beta1.md) +- [CHANGELOG-5.0.0-alpha2.md](./CHANGELOG-5.0.0-alpha2.md) +- [CHANGELOG-5.0.0-alpha1.md](./CHANGELOG-5.0.0-alpha1.md) +- [CHANGELOG-5.0.0.md](./CHANGELOG-5.0.0.md) +- [CHANGELOG-4.4.0-beta3.md](./CHANGELOG-4.4.0-beta3.md) +- [CHANGELOG-4.4.0-beta2.md](./CHANGELOG-4.4.0-beta2.md) +- [CHANGELOG-4.4.0-beta1.md](./CHANGELOG-4.4.0-beta1.md) +- [CHANGELOG-4.4.0-alpha3.md](./CHANGELOG-4.4.0-alpha3.md) +- [CHANGELOG-4.4.0-alpha1.md](./CHANGELOG-4.4.0-alpha1.md) +- [CHANGELOG-4.3.7-rc1.md](./CHANGELOG-4.3.7-rc1.md) +- [CHANGELOG-4.3.7.md](./CHANGELOG-4.3.7.md) +- [CHANGELOG-4.3.6.md](./CHANGELOG-4.3.6.md) +- [CHANGELOG-4.3.5.md](./CHANGELOG-4.3.5.md) +- [CHANGELOG-4.3.4.md](./CHANGELOG-4.3.4.md) +- [CHANGELOG-4.3.3.md](./CHANGELOG-4.3.3.md) +- [CHANGELOG-4.3.2.md](./CHANGELOG-4.3.2.md) +- [CHANGELOG-4.3.1-rc2.md](./CHANGELOG-4.3.1-rc2.md) +- [CHANGELOG-4.3.1-rc1.md](./CHANGELOG-4.3.1-rc1.md) +- [CHANGELOG-4.3.1.md](./CHANGELOG-4.3.1.md) +- [CHANGELOG-4.3.0-rc1.md](./CHANGELOG-4.3.0-rc1.md) +- [CHANGELOG-4.3.0.md](./CHANGELOG-4.3.0.md) +- [CHANGELOG-4.2.3.md](./CHANGELOG-4.2.3.md) +- [CHANGELOG-4.2.2.md](./CHANGELOG-4.2.2.md) +- [CHANGELOG-4.2.1-rc6.md](./CHANGELOG-4.2.1-rc6.md) +- [CHANGELOG-4.2.1-rc5.md](./CHANGELOG-4.2.1-rc5.md) +- [CHANGELOG-4.2.1-rc4.md](./CHANGELOG-4.2.1-rc4.md) +- [CHANGELOG-4.2.1-rc3.md](./CHANGELOG-4.2.1-rc3.md) +- [CHANGELOG-4.2.1-rc1.md](./CHANGELOG-4.2.1-rc1.md) +- [CHANGELOG-4.2.1.md](./CHANGELOG-4.2.1.md) +- [CHANGELOG-4.2.0-alpha3.md](./CHANGELOG-4.2.0-alpha3.md) +- [CHANGELOG-4.2.0-alpha2.md](./CHANGELOG-4.2.0-alpha2.md) +- [CHANGELOG-4.2.0-alpha1.md](./CHANGELOG-4.2.0-alpha1.md) +- [CHANGELOG-4.2.0.md](./CHANGELOG-4.2.0.md) +- [CHANGELOG-4.1.7.md](./CHANGELOG-4.1.7.md) +- [CHANGELOG-4.1.5-rc3.md](./CHANGELOG-4.1.5-rc3.md) +- [CHANGELOG-4.1.5-rc2.md](./CHANGELOG-4.1.5-rc2.md) +- [CHANGELOG-4.1.5-rc1.md](./CHANGELOG-4.1.5-rc1.md) +- [CHANGELOG-4.1.5-alpha2.md](./CHANGELOG-4.1.5-alpha2.md) +- [CHANGELOG-4.1.5-alpha1.md](./CHANGELOG-4.1.5-alpha1.md) +- [CHANGELOG-4.1.5.md](./CHANGELOG-4.1.5.md) +- [CHANGELOG-4.1.4-rc4.md](./CHANGELOG-4.1.4-rc4.md) +- [CHANGELOG-4.1.4-rc3.md](./CHANGELOG-4.1.4-rc3.md) +- [CHANGELOG-4.1.4-rc2.md](./CHANGELOG-4.1.4-rc2.md) +- [CHANGELOG-4.1.4-rc1.md](./CHANGELOG-4.1.4-rc1.md) +- [CHANGELOG-4.1.4.md](./CHANGELOG-4.1.4.md) +- [CHANGELOG-4.1.3-rc1.md](./CHANGELOG-4.1.3-rc1.md) +- [CHANGELOG-4.1.3.md](./CHANGELOG-4.1.3.md) +- [CHANGELOG-4.1.2-rc1.md](./CHANGELOG-4.1.2-rc1.md) +- [CHANGELOG-4.1.2.md](./CHANGELOG-4.1.2.md) +- [CHANGELOG-4.1.1.md](./CHANGELOG-4.1.1.md) +- [CHANGELOG-4.1.0-rc3.md](./CHANGELOG-4.1.0-rc3.md) +- [CHANGELOG-4.1.0-rc2.md](./CHANGELOG-4.1.0-rc2.md) +- [CHANGELOG-4.1.0-rc1.md](./CHANGELOG-4.1.0-rc1.md) +- [CHANGELOG-4.1.0.md](./CHANGELOG-4.1.0.md) +- [CHANGELOG-4.0.0.md](./CHANGELOG-4.0.0.md) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000000..d572ebd3e915 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,21 @@ +# Sealos CODEOWNERS + +* @labring/sealos-general-reviewer + +/extensions/ @labring/sealos-frontend-reviewer +/frontend/ @labring/sealos-frontend-reviewer + +/controllers/ @labring/sealos-backend-reviewer +/service/ @labring/sealos-backend-reviewer +/webhooks/ @labring/sealos-backend-reviewer + +/lifecycle/ @labring/sealos-lifecycle-reviewer + +/.chglog/ @labring/sealos-ci-reviewer +/.github/ @labring/sealos-ci-reviewer +.tagpr.json @labring/sealos-ci-reviewer +lychee.toml @labring/sealos-ci-reviewer +/scripts/ @labring/sealos-ci-reviewer + +CONTRIBUTOR_LICENSE_AGREEMENT.md @labring/sealos-policy-reviewer +LICENSE.md @labring/sealos-policy-reviewer diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..ade312a65f00 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,63 @@ +# Code of Conduct + +We are committed to providing a welcoming and harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. This Code of Conduct applies to all project spaces, including GitHub repositories, issue trackers, Discord servers, mailing lists, and in-person events. + +## Our Pledge + +As members and maintainers, we pledge to make participation in our community a harassment-free experience for everyone. We will act and interact in ways that contribute to an open, friendly, diverse, inclusive, and healthy community. + +## Expected Behavior + +We encourage the following behaviors: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +## Unacceptable Behavior + +The following behaviors are considered harassment and are unacceptable: + +- The use of sexualized language or imagery, and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Scope + +This Code of Conduct applies within all project spaces and also applies when an individual is officially representing the project in public spaces. Examples of representing the project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an event. + +## Enforcement Responsibilities & Guidelines + +Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior they deem inappropriate. + +Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct. + +## Reporting and Enforcement Procedures + +### Reporting + +Community members should contact the project team via contact@sealos.io to report incidents. All reports will be reviewed and investigated promptly and fairly. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. + +### Enforcement + +The following enforcement process will be followed: + +#### Correction +A private, written warning from the maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +#### Warning +A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. + +#### Temporary Ban +A temporary ban from any sort of public interaction or communication with the community. + +#### Permanent Ban +A permanent ban from the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c92e5ff7089f..76c637b22b2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,20 @@ +If you don't understand the github open source project contribution process at all, it is strongly recommended to read: [first contributions](https://github.com/firstcontributions/first-contributions) + # Contributing to sealos It is warmly welcomed if you have interest to hack on sealos. First, we encourage this kind of willing very much. And here is a list of contributing guide for you. ## Topics -* [Reporting security issues](#reporting-security-issues) -* [Reporting general issues](#reporting-general-issues) -* [Code and doc contribution](#code-and-doc-contribution) -* [Engage to help anything](#engage-to-help-anything) +- [Reporting security issues](#reporting-security-issues) +- [Reporting general issues](#reporting-general-issues) +- [Code and doc contribution](#code-and-doc-contribution) +- [License and Contributor Agreement](#license-and-contributor-agreement) +- [Engage to help anything](#engage-to-help-anything) ## Reporting security issues -Security issues are always treated seriously. As our usual principle, we discourage anyone to spread security issues. If you find a security issue of sealos, please do not discuss it in public and even do not open a public issue. Instead we encourage you to send us a private email to [admin@sealyun.com](mailto:admin@sealyun.com) to report this. +Security issues are always treated seriously. As our usual principle, we discourage anyone to spread security issues. If you find a security issue of sealos, please do not discuss it in public and even do not open a public issue. Instead we encourage you to send us a private email to [security@sealos.io](mailto:security@sealos.io) to report this. ## Reporting general issues @@ -19,37 +22,37 @@ To be honest, we regard every user of sealos as a very kind contributor. After e Since we collaborate project sealos in a distributed way, we appreciate **WELL-WRITTEN**, **DETAILED**, **EXPLICIT** issue reports. To make the communication more efficient, we wish everyone could search if your issue is an existing one in the searching list. If you find it existing, please add your details in comments under the existing issue instead of opening a brand new one. -To make the issue details as standard as possible, we setup an [ISSUE TEMPLATE](./.github/ISSUE_TEMPLATE) for issue reporters. You can find three kinds of issue templates there: question, bug report and feature request. Please **BE SURE** to follow the instructions to fill fields in template. +To make the issue details as standard as possible, we setup an [ISSUE TEMPLATE](https://github.com/labring/sealos/tree/main/.github/ISSUE_TEMPLATE) for issue reporters. You can find three kinds of issue templates there: question, bug report and feature request. Please **BE SURE** to follow the instructions to fill fields in template. -There are lot of cases when you could open an issue: +There are a lot of cases when you could open an issue: -* bug report -* feature request -* performance issues -* feature proposal -* feature design -* help wanted -* doc incomplete -* test improvement -* any questions on project -* and so on +- bug report +- feature request +- performance issues +- feature proposal +- feature design +- help wanted +- doc incomplete +- test improvement +- any questions on project +- and so on -Also we must remind that when filing a new issue, please remember to remove the sensitive data from your post. Sensitive data could be password, secret key, network locations, private business data and so on. +Also, we must remind that when filing a new issue, please remember to remove the sensitive data from your post. Sensitive data could be password, secret key, network locations, private business data and so on. ## Code and doc contribution Every action to make project sealos better is encouraged. On GitHub, every improvement for sealos could be via a PR (short for pull request). -* If you find a typo, try to fix it! -* If you find a bug, try to fix it! -* If you find some redundant codes, try to remove them! -* If you find some test cases missing, try to add them! -* If you could enhance a feature, please **DO NOT** hesitate! -* If you find code implicit, try to add comments to make it clear! -* If you find code ugly, try to refactor that! -* If you can help to improve documents, it could not be better! -* If you find document incorrect, just do it and fix that! -* ... +- If you find a typo, try to fix it! +- If you find a bug, try to fix it! +- If you find some redundant codes, try to remove them! +- If you find some test cases missing, try to add them! +- If you could enhance a feature, please **DO NOT** hesitate! +- If you find code implicit, try to add comments to make it clear! +- If you find code ugly, try to refactor that! +- If you can help to improve documents, it could not be better! +- If you find document incorrect, just do it and fix that! +- ... Actually it is impossible to list them completely. Just remember one princinple: @@ -57,70 +60,98 @@ Actually it is impossible to list them completely. Just remember one princinple: Since you are ready to improve sealos with a PR, we suggest you could take a look at the PR rules here. -* [Workspace Preparation](#workspace-preparation) -* [Branch Definition](#branch-definition) -* [Commit Rules](#commit-rules) -* [PR Description](#pr-description) -* [Developing Environment](#developing-environment) -* [Golang Dependency Management](#golang-dependency-management) +- [Workspace Preparation](#workspace-preparation) +- [Branch Definition](#branch-definition) +- [Commit Rules](#commit-rules) +- [PR Description](#pr-description) +- [Developing Environment](#developing-environment) +- [Docs Contribution](#docs-contribution) ### Workspace Preparation To put forward a PR, we assume you have registered a GitHub ID. Then you could finish the preparation in the following steps: -1. **FORK** sealos to your repository. To make this work, you just need to click the button Fork in right-left of [fanux/sealos](https://github.com/labring/sealos) main page. Then you will end up with your repository in `https://github.com//sealos`, in which `your-username` is your GitHub username. +1. **FORK** sealos to your repository. To make this work, you just need to click the button Fork in right-left of [labring/sealos](https://github.com/labring/sealos) main page. Then you will end up with your repository in `https://github.com//sealos`, in which `your-username` is your GitHub username. 1. **CLONE** your own repository to master locally. Use `git clone https://github.com//sealos.git` to clone repository to your local machine. Then you can create new branches to finish the change you wish to make. 1. **Set Remote** upstream to be `https://github.com/labring/sealos.git` using the following two commands: - ``` - git remote add upstream https://github.com/labring/sealos.git - git remote set-url --push upstream no-pushing - ``` + ```shell + git remote add upstream https://github.com/labring/sealos.git + git remote set-url --push upstream no-pushing + ``` - With this remote setting, you can check your git remote configuration like this: + With this remote setting, you can check your git remote configuration like this: - ``` - $ git remote -v - origin https://github.com//sealos.git (fetch) - origin https://github.com//sealos.git (push) - upstream https://github.com/labring/sealos.git (fetch) - upstream no-pushing (push) - ``` + ```shell + $ git remote -v + origin https://github.com//sealos.git (fetch) + origin https://github.com//sealos.git (push) + upstream https://github.com/labring/sealos.git (fetch) + upstream no-pushing (push) + ``` - Adding this, we can easily synchronize local branches with upstream branches. + Adding this, we can easily synchronize local branches with upstream branches. 1. **Create a branch** to add a new feature or fix issues - Update local working directory and remote forked repository: + Update local working directory and remote forked repository: - ``` + ```shell cd sealos git fetch upstream git checkout main - git rebase upstream/main - git push // default origin, update your forked repository ``` Create a new branch: - ``` + ```shell git checkout -b ``` Make any change on the `new-branch` then build and test your codes. +1. **Commit your changes** to your local branch, lint before committing and commit with sign-off + ```shell + git rebase upstream/main + golangci-lint run -c .golangci.yml # lint + git add -A # add changes to staging + git commit -s -m "message for your changes" # -s adds a Signed-off-by trailer + ``` -1. **Push your branch** to your forked repository, try not to generate multiple commit message within a pr. +1. **Push your branch** to your forked repository, it is recommended to have only one commit for a PR. + ```shell + # sync up with upstream + git fetch upstream main + git rebase upstream/main + + git rebase -i # rebase with interactive mode to squash your commits into a single one + git push # push to the remote repository, if it's a first time push, run git push --set-upstream origin ``` - golangci-lint run -c .golangci.yml // lint - git commit -a -m "message for your changes" // -a is git add . - git rebase -i // do this if your pr has multiple commits - git push // push to your forked repository after rebase done + + You can also use `git commit -s --amend && git push -f` to update modifications on the previous commit. + + If you have developed multiple features in the same branch, you should create PR separately by rebasing to the main branch between each push: + + ```shell + # create new branch, for example git checkout -b feature/infra + git checkout -b + # update some code, feature1 + git add -A + git commit -m -s "feature one" + git push # if it's first time push, run git push --set-upstream origin + # then create pull request, and merge + # update some new feature, feature2, rebase main branch first. + git rebase upstream/main # rebase the current branch to upstream/main branch + git add -A + git commit -m -s "feature two" + # then create pull request, and merge ``` -1. **File a pull request** to fanux/sealos:master +1. **File a pull request** to labring/sealos:master + + It is recommended to review your changes before filing a pull request. Check if your code doesn't conflict with the main branch and no redundant code is included. ### Branch Definition @@ -138,61 +169,121 @@ When backporting some fixes to existing released version, we will checkout backp Actually in sealos, we take two rules serious when committing: -* [Commit Message](#commit-message) -* [Commit Content](#commit-content) +- [Commit Message](#commit-message) +- [Commit Content](#commit-content) #### Commit Message Commit message could help reviewers better understand what the purpose of submitted PR is. It could help accelerate the code review procedure as well. We encourage contributors to use **EXPLICIT** commit message rather than ambiguous message. In general, we advocate the following commit message type: -* docs: xxxx. For example, "docs: add docs about storage installation". -* feature: xxxx.For example, "feature: make result show in sorted order". -* bugfix: xxxx. For example, "bugfix: fix panic when input nil parameter". -* style: xxxx. For example, "style: format the code style of Constants.java". -* refactor: xxxx. For example, "refactor: simplify to make codes more readable". -* test: xxx. For example, "test: add unit test case for func InsertIntoArray". -* chore: xxx. For example, "chore: integrate travis-ci". It's the type of mantainance change. -* other readable and explicit expression ways. +- docs: xxxx. For example, "docs: add docs about storage installation". +- feature: xxxx.For example, "feature: make result show in sorted order". +- bugfix: xxxx. For example, "bugfix: fix panic when input nil parameter". +- style: xxxx. For example, "style: format the code style of Constants.java". +- refactor: xxxx. For example, "refactor: simplify to make codes more readable". +- test: xxx. For example, "test: add unit test case for func InsertIntoArray". +- chore: xxx. For example, "chore: integrate travis-ci". It's the type of mantainance change. +- other readable and explicit expression ways. On the other side, we discourage contributors from committing message like the following ways: -* ~~fix bug~~ -* ~~update~~ -* ~~add doc~~ +- ~~fix bug~~ +- ~~update~~ +- ~~add doc~~ #### Commit Content Commit content represents all content changes included in one commit. We had better include things in one single commit which could support reviewer's complete review without any other commits' help. In another word, contents in one single commit can pass the CI to avoid code mess. In brief, there are two minor rules for us to keep in mind: -* avoid very large change in a commit; -* complete and reviewable for each commit. +- avoid very large change in a commit; +- complete and reviewable for each commit. No matter what the commit message, or commit content is, we do take more emphasis on code review. ### PR Description -PR is the only way to make change to sealos project files. To help reviewers better get your purpose, PR description could not be too detailed. We encourage contributors to follow the [PR template](./.github/PULL_REQUEST_TEMPLATE.md) to finish the pull request. +PR is the only way to make change to sealos project files. To help reviewers better get your purpose, PR description could not be too detailed. We encourage contributors to follow the [PR template](https://github.com/labring/sealos/tree/main/.github/PULL_REQUEST_TEMPLATE.md) to finish the pull request. ### Developing Environment As a contributor, if you want to make any contribution to sealos project, we should reach an agreement on the version of tools used in the development environment. Here are some dependents with specific version: -* golang : v1.16+ -* golangci-lint: 1.39.0 +- golang : v1.16+ +- golangci-lint: 1.46.2 When you develop the sealos project at the local environment, you should use subcommands of Makefile to help yourself to check and build the latest version of sealos. For the convenience of developers, we use the docker to build sealos. It can reduce problems of the developing environment. +### Docs Contribution + +#### Structure and Repo + +The documentation for sealos includes: + +- [README.md](https://github.com/labring/sealos/blob/main/README.md) +- [CONTRIBUTING.md](https://github.com/labring/sealos/blob/main/CONTRIBUTING.md) +- [lifecycle/DEVELOPGUIDE.md](https://github.com/labring/sealos/blob/main/lifecycle/DEVELOPGUIDE.md) +- [Sealos Website & Documentation](https://github.com/labring/sealos.io) + +#### Formatting + +Please obey the following rules to better format the docs, which would greatly improve the reading experience. + +1. Please do not use Chinese punctuations in English docs, and vice versa. +1. Please use upper case letters where applicable, like the first letter of sentences / headings, etc. +1. Please specify a language for each Markdown code blocks, unless there's no associated languages. +1. Please insert a whitespace between Chinese and English words. +1. Please use the correct case for technical terms, such as using HTTP instead of http, MySQL rather than mysql, Kubernetes instead of kubernetes, etc. +1. Please check if there's any typos in the docs before submitting PRs. + +You can also check out the [Docusaurus docs](https://docusaurus.io/docs/markdown-features) to write docs with richer feature. + +## License and Contributor Agreement + +### Sealos Sustainable Use License + +Sealos is licensed under the **Sealos Sustainable Use License**, a custom license based on the Sustainable Use License. This is **not** a standard open source license. Key points: + +- ✅ **Permitted**: Internal business use and personal non-commercial use +- ❌ **Prohibited**: Providing cloud services to third parties (public, private, or managed services) +- ❌ **Prohibited**: Commercial distribution without permission + +📄 **Full license terms**: [LICENSE.md](./LICENSE.md) + +### Contributor License Agreement (CLA) + +**By contributing to Sealos, you agree to our Contributor License Agreement (CLA).** + +The CLA grants labring (the company behind Sealos) and the community certain rights to your contributions, including: +- Perpetual, worldwide, royalty-free license to use, modify, and distribute your contributions +- Patent license for your contributions +- Right to change the license of your contributions as part of the project + +📋 **Important**: When you submit any contribution (code, documentation, etc.), you automatically accept the terms of our CLA. + +📄 **Full CLA terms**: [CONTRIBUTOR_LICENSE_AGREEMENT.md](./CONTRIBUTOR_LICENSE_AGREEMENT.md) + +### License Change Declaration + +**⚠️ Notice to Contributors**: By contributing to this project, you acknowledge and agree that: + +1. Your contributions will be licensed under the Sealos Sustainable Use License +2. labring reserves the right to change the license of the project and your contributions +3. You grant labring the necessary rights to make such license changes +4. You have read and understood both the current license terms and the CLA + +If you do not agree to these terms, please do not contribute to this project. + ## Engage to help anything We choose GitHub as the primary place for sealos to collaborate. So the latest updates of sealos are always here. Although contributions via PR is an explicit way to help, we still call for any other ways. -* reply to other's issues if you could; -* help solve other user's problems; -* help review other's PR design; -* help review other's codes in PR; -* discuss about sealos to make things clearer; -* advocate sealos technology beyond GitHub; -* write blogs on sealos and so on. +- reply to other's issues if you could; +- help solve other user's problems; +- help review other's PR design; +- help review other's codes in PR; +- discuss about sealos to make things clearer; +- advocate sealos technology beyond GitHub; +- write blogs on sealos and so on. In a word, **ANY HELP IS CONTRIBUTION.** diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 97e9a0833d1a..000000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,49 +0,0 @@ -中弈 -中弈 -aiyijing -Cluas -cuisongliu -currycan -dujiacheng -fanux -fzkun <412657308@qq.com> -HongbinZheng <13405999037@139.com> -huizhi.szh -ideal -ImgBotApp -jimmy -jinnzy <33449123+jinnzy@users.noreply.github.com> -jzy <839444083@qq.com> -liuyuchao -louis <38313565+oldthreefeng@users.noreply.github.com> -louisehong -Lucien -LucienShui -oceanwen -oldhtreefeng -oldthreefeng <38313565+oldthreefeng@users.noreply.github.com> -oldthreefeng -panda-lab <64345457+panda-lab@users.noreply.github.com> -PatHoo -rick -Ryan -scott lewis <33612882+dk-lockdown@users.noreply.github.com> -sealyun robot <60811237+sealrobot@users.noreply.github.com> -SorryMaker <41097970+CodeSorryMaker@users.noreply.github.com> -steven -svolence -svolence -vimerr <474785153@qq.com> -wenshihong -wisheen -wuxingzhong <330332812@qq.com> -yangchuansheng -ysicing -zhangguanzhang -zhangguanzhang -zhangguanzhang -zhangzhitao -疯狂的石头 <39077997+fengkuangdestone@users.noreply.github.com> -郑黉宾 -付亮 <48649837+hellolittlewei@users.noreply.github.com> -gitccl \ No newline at end of file diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md new file mode 100644 index 000000000000..74ed2f3bfafe --- /dev/null +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -0,0 +1,51 @@ +# SEALOS CONTRIBUTOR LICENSE AGREEMENT + +**Version 1.0** + +Thank you for your interest in contributing to Sealos (the "Project"), owned and operated by labring (the "Company"). This Contributor License Agreement ("Agreement") documents the rights granted by contributors to the Company. + +## 1. DEFINITIONS + +**"You"** (or "Your") means the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with the Company. + +**"Contribution"** means any work of authorship, including the original version of the work and any modifications or additions to that work or derivative works thereof, that is intentionally submitted by You to the Company for inclusion in, or documentation of, any of the products owned or managed by the Company. + +**"Submit"** means any form of electronic, verbal, or written communication sent to the Company or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems. + +## 2. GRANT OF COPYRIGHT LICENSE + +Subject to the terms and conditions of this Agreement, You hereby grant to the Company and to recipients of software distributed by the Company a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to use, copy, modify, distribute, publicly perform, publicly display, sublicense, and prepare derivative works of Your Contributions. + +## 3. GRANT OF PATENT LICENSE + +Subject to the terms and conditions of this Agreement, You hereby grant to the Company and to recipients of software distributed by the Company a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contributions. + +## 4. REPRESENTATIONS + +You represent that: + +a) You are legally entitled to grant the above licenses; + +b) If Your employer has rights to intellectual property that You create that includes Your Contributions, You have received permission to make Contributions on behalf of that employer, or Your employer has waived such rights for Your Contributions to the Company; + +c) Each of Your Contributions is Your original creation and does not violate any third party's rights; + +d) Your Contribution submissions include complete details of any third-party license or other restriction associated with any part of Your Contributions. + +## 5. SUPPORT AND WARRANTY DISCLAIMER + +You provide Your Contributions on an "AS IS" basis, without warranties or conditions of any kind, either express or implied, including without limitation any warranties or conditions of title, non-infringement, merchantability, or fitness for a particular purpose. + +## 6. LIABILITY LIMITATION + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law, shall You be liable to anyone for any damages arising out of or related to this Agreement or use of Your Contributions. + +## 7. MISCELLANEOUS + +You agree to notify the Company of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect. + +This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction where the Company is incorporated, without regard to its conflict of laws provisions. + +--- + +**By submitting a Contribution, You accept and agree to the terms and conditions in this Agreement for Your present and future Contributions to the Company.** \ No newline at end of file diff --git a/DEVELOPGUIDE.md b/DEVELOPGUIDE.md deleted file mode 100644 index 68a4084162f5..000000000000 --- a/DEVELOPGUIDE.md +++ /dev/null @@ -1,31 +0,0 @@ -# Environment settings - -sealos only support linux now, you need a linux server to test it. - -Some tools can be very handy to help you start a virtual machine such as [multipass](https://multipass.run/) - -# Build the project - -```shell script -git clone https://github.com/labrirng/sealos -cd sealos -make build -``` - -You can scp the bin file to your linux host. - -If you use multipaas, you can mount the bin dir to the vm: - -```shell script -multipass mount /your-bin-dir [:] -``` - -Then test it locally. - -## Notes about cross-platform building - -If not explicitly specified, `CGO_ENABLED` will be `0` (disabled), which allows cross-platform building but sacrificing support for subcommands like `images` that relies on CGO. That is, when running `sealos images`, since overlay driver is not supported without CGO, a "driver not supported" error will occur. - -Therefore, if you are developing or debugging images storage related functions, you have to build sealos under Linux. - -In addition, the released version of sealos is built under Ubuntu (thanks to Github Actions) and has `CGO_ENABLED=1` to support overlay driver. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e80872fa555a..000000000000 --- a/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright © 2022 sealos. -# -# 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. - -FROM --platform=$BUILDPLATFORM golang:1.17-buster as builder -ARG GITHUB_TOKEN=$GITHUB_TOKEN -ARG TARGETARCH -ARG ACTION=build-pack - -WORKDIR /work -COPY . /work -# in china using this -# RUN mv /work/.github/sources.list /etc/apt -RUN dpkg --add-architecture arm64 && \ - apt update && \ - apt install -y gcc-aarch64-linux-gnu && \ - apt install -y libbtrfs-dev btrfs-tools && \ - apt install -y libgpgme-dev libdevmapper-dev && \ - apt install -y libbtrfs-dev:arm64 btrfs-tools:arm64 && \ - apt install -y libgpgme-dev:arm64 libdevmapper-dev:arm64 && \ - make ${ACTION} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d64569567334..000000000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000000..6c0b8113acf2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,94 @@ +# SEALOS SUSTAINABLE USE LICENSE + +**Version 1.0** + +--- + +## PREAMBLE + +Sealos is licensed under the Sealos Sustainable Use License, a modified version of Sustainable Use License (https://faircode.io/#licenses). + +Portions of this software are licensed as follows: + +- All external components incorporated into Sealos are licensed under the original license provided by the owner of the applicable component. +- Content outside of the above mentioned files or restrictions is available under the "Sealos Sustainable Use License" as defined below. + +--- + +## 1. ACCEPTANCE + +By using the software, you agree to all of the terms and conditions below. + +## 2. COPYRIGHT LICENSE + +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below. + +## 3. LIMITATIONS + +You may use or modify this software only for: +- Internal business purposes, or +- Personal, non-commercial use + +**You may not:** + +- Use it to provide any form of cloud services—including public, private, or managed services—to third parties, whether free or paid +- Distribute the software to others, except for free distribution for non-commercial purposes +- Alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software + +Any use of the licensor's trademarks is subject to applicable law. + +## 4. PATENTS + +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. + +This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. + +**Patent Claim Termination:** If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## 5. NOTICES + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software. + +## 6. NO OTHER RIGHTS + +These terms do not imply any licenses other than those expressly granted in these terms. + +## 7. TERMINATION + +If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. + +**Grace Period:** If the licensor provides you with a notice of your violation, and you cease all violations of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. + +**Permanent Termination:** However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently. + +## 8. NO LIABILITY + +**AS FAR AS THE LAW ALLOWS, THE SOFTWARE COMES AS IS, WITHOUT ANY WARRANTY OR CONDITION, AND THE LICENSOR WILL NOT BE LIABLE TO YOU FOR ANY DAMAGES ARISING OUT OF THESE TERMS OR THE USE OR NATURE OF THE SOFTWARE, UNDER ANY KIND OF LEGAL CLAIM.** + +## 9. DEFINITIONS + +**"Distribute/Distribution"** means to make available, provide, deliver, or transfer copies of the software or any portion thereof to any third party in a form that allows the recipient to install, run, or execute the software on their own systems. This includes traditional software distribution methods such as downloads, physical media, package repositories, or embedded distributions, but does not include providing access to the software's functionality through a software-as-a-service (SaaS) model where end users interact with the software remotely without receiving a copy. + +**"Licensor"** is the entity offering these terms. + +**"Software"** is the software the licensor makes available under these terms, including any portion of it. + +**"Third party"** means any individual, entity, or organization other than you, your company, or the licensor. + +**"You"** refers to the individual or entity agreeing to these terms. + +**"Your company"** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**"Your license"** is the license granted to you for the software under these terms. + +**"Use"** means anything you do with the software requiring your license. + +**"Trademark"** means trademarks, service marks, and similar rights. + +--- + +*End of License Terms* + +For license inquiries, please contact: license@sealos.io \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index d7ee92971cd7..0035dd85fa7d 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,19 +2,21 @@ maintainers: - fanux - cuisongliu -- ysicing -- oldthreefeng -- zhangguanzhang -- berlinsaint +- fengxsong +- bxy4543 +- LeezQ +- yangchuansheng +- zzjin reviewers: - fanux - cuisongliu -- berlinsaint +- zzjin approvers: - fanux - cuisongliu -- berlinsaint \ No newline at end of file +- zzjin + diff --git a/Makefile b/Makefile deleted file mode 100644 index abe87cee14d8..000000000000 --- a/Makefile +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright © 2022 sealos. -# -# 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. - -# ============================================================================== -# Build options - -ROOT_PACKAGE=github.com/labring/sealos -VERSION_PACKAGE=github.com/labring/sealos/pkg/version - -# ============================================================================== -# Includes - -include scripts/make-rules/common.mk # must be the first to include -include scripts/make-rules/golang.mk -include scripts/make-rules/gen.mk -include scripts/make-rules/license.mk -include scripts/make-rules/oss.mk -include scripts/make-rules/release.mk -include scripts/make-rules/tools.mk - -# ============================================================================== -# Usage - -define USAGE_OPTIONS - -Options: - DEBUG Whether or not to generate debug symbols. Default is 0. - BINS Binaries to build. Default is all binaries under cmd. - This option is available when using: make build/compress - Example: make build BINS="sealos sealctl" - PACKAGES Packages to build. Default is rpm and deb. - This option is available when using: make package - Example: make package PACKAGES="rpm deb" - PLATFORM Alternate platform to build for. Default is the host platform. - This option is available when using: make build/compress/package - Example: make build PLATFORM="linux_arm64" - V Set to 1 enable verbose build. Default is 0. -endef -export USAGE_OPTIONS - -# ============================================================================== -# Targets - -.DEFAULT_GOAL = build - -## build: Build source code for host platform. -.PHONY: build -build: - @$(MAKE) go.build - -## lint: Check syntax and styling of go sources. -.PHONY: lint -lint: - @$(MAKE) go.lint - -## format: Gofmt (reformat) package sources. -.PHONY: format -format: - @$(MAKE) go.format - -## coverage: Run unit tests and output test coverage. -.PHONY: coverage -coverage: - @$(MAKE) go.coverage - -## verify-license: Verify the license headers for all files. -.PHONY: verify-license -verify-license: - @$(MAKE) license.verify - -## add-license: Ensure source code files have license headers. -.PHONY: add-license -add-license: - @$(MAKE) license.add - -## gen: Generate all necessary files. -.PHONY: gen -gen: - @$(MAKE) gen.run - -## tools: Install dependent tools. -.PHONY: tools -tools: - @$(MAKE) tools.install - -## clean: Remove all files that are created by building. -.PHONY: clean -clean: - @echo "===========> Cleaning all build output" - @-rm -vrf $(OUTPUT_DIR) $(BIN_DIR) - -## release-build: Build release binaries for multiple platforms. -.PHONY: release-build -release-build: - @$(MAKE) release.build - -## release: Create a release with custom release notes. -.PHONY: release -release: - @$(MAKE) release.release - -## compress: Compress the binaries using upx for host platform. -.PHONY: compress -compress: - @$(MAKE) release.upx - -## package: Build rpm/deb packages for host platform. -.PHONY: package -package: - @$(MAKE) release.package - -## update-contrib: Update list of contributors. -.PHONY: update-contrib -update-contrib: - @git log --format='%aN <%aE>' | sort -uf > CONTRIBUTORS - -## help: Show this help info. -.PHONY: help -help: Makefile - @echo -e "\nUsage: make ...\n\nTargets:" - @sed -n 's/^##//p' $< | awk -F':' '{printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' | sed -e 's/^/ /' - @echo "$$USAGE_OPTIONS" diff --git a/README.md b/README.md index f803844ec6b7..cd80f2ee4cf4 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,156 @@ - - trackgit-views - +
+ + + + + + + + + +# 🚀 Develop, deploy, and scale in one seamless cloud platform ☁️ + +Sealos['siːləs] is an AI-native Cloud Operating System built on Kubernetes that unifies the entire application lifecycle, from development in cloud IDEs to production deployment and management. It is perfect for building and scaling modern AI applications, SaaS platforms, managed databases (MySQL, PostgreSQL, Redis, MongoDB) and complex microservice architectures. + +
-

- Popularize cloud native technologies with ease -

-

-[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/labring/sealos) +[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/labring/sealos) [![Build Status](https://github.com/labring/sealos/actions/workflows/release.yml/badge.svg)](https://github.com/labring/sealos/actions) -[![codecov](https://codecov.io/gh/labring/sealos/branch/main/graph/badge.svg?token=e41ZDcj06N)](https://codecov.io/gh/labring/sealos) -[![Website](https://img.shields.io/website?url=https%3A%2F%2Fpostwoman.io&logo=Postwoman)](https://sealyun.com) -[![OSCS Status](https://www.oscs1024.com/platform/badge/labring/sealos.svg?size=small)](https://www.oscs1024.com/project/labring/sealos?ref=badge_small) -[![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-blueviolet?logo=Telegram)](https://t.me/cloudnativer) +[![](https://img.shields.io/docker/pulls/labring/kubernetes)](https://hub.docker.com/r/labring/kubernetes) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flabring%2Fsealos.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Flabring%2Fsealos?ref=badge_shield) -

+[![codecov](https://codecov.io/gh/labring/sealos/branch/main/graph/badge.svg?token=e41ZDcj06N)](https://codecov.io/gh/labring/sealos) +[![Website](https://img.shields.io/website?url=https%3A%2F%2Fpostwoman.io&logo=Postwoman)](https://sealos.io) +[![OSCS Status](https://www.oscs1024.com/platform/badge/labring/sealos.svg?size=small)](https://www.oscs1024.com/repo/labring/sealos?ref=badge_small) + + + ROSS Index - Fastest Growing Open-Source Startups | Runa Capital + +
---- +
-[Docs](https://sealos.io) | [简体中文](https://www.sealos.io/zh-Hans/) +
+ Website • + Docs • + Discord • + Twitter • + 简体中文 +
+ +
+ +
+ +[![Deploy on Sealos][deploy-badge]][deploy-link] + +https://github.com/user-attachments/assets/cc8599da-6c3e-4503-bb53-55e13bf61ef6 + +image + +
-## What is sealos +## Get started -**sealos is a cloud operating system distribution with Kubernetes as its kernel.** +### Create your Development Environment in Sealos with just one click -In the early stages, operatings systems have adopted a layered architecture, which later evolved into kernel architecture like Linux and Windows. With the emergence of container technologies, cloud OS will migrate to a "cloud kernel" architecture with strong cohesion in the future. +1. Open Sealos Devbox. -![](https://user-images.githubusercontent.com/8912557/173866494-379ba0dd-05af-4095-b63d-08f594581c52.png) + image -- From now on, think of all your machines as an abstract supercomputer whose operating system is sealos, where Kubernetes serves as the OS kernel. -- Instead of IaaS, PaaS and SaaS, there will only be cloud OS drivers(CSI, CNI and CRI implementations), cloud OS kernel(Kubernetes) and distributed applications. +2. Create a development environment, choosing from a range of languages and frameworks. -> Core Capabilities + image -- Cluster image - - [x] The entire cluster will be able to build, ship and run. - - [x] Semantics of docker will be extended to clusters so that any distributed applications can be defined and run smoothly. -- hub.sealos - - [x] A cluster image repository where you can retrieve pre-built distributed applications such as basic Kubernetes cluster image, high availability pgsql cluster image, high availability minio cluster image, etc. -- desktop.sealos - - [ ] A desktop for cloud OS, not to be confused with traditional cloud desktop. It is similar to macOS, but manages cluster and distributed applications instead of personal computers. -- Distributed application matrix - - [x] Anything you need including storage / network / HA database / message queue / monitoring can be retrieved with a click of the mouse, or simply `sealos run`. - - [x] sealos now support 20+ HA applications. +3. Access your environment from a selection of IDEs, such as VSCode and Cursor. -## Core features + image -- Manage clusters lifecycle - - [x] Quickly install HA Kubernetes clusters - - [x] Add / remove nodes - - [x] Clean the cluster, backup and auto recovering, etc. -- Download and use OCI-compatible distributed applications - - [x] Openebs, minio, ingress, pgsql, mysql, redis, etc. -- Customize you own distributed applications - - [x] Using Dockerfile to build distributed applications images, save all dependencies. - - [x] Push distributed applications images to docker hub. - - [x] Combine many applications to build your own cloud platform. + image -## Quickstart +### Create your Database on Sealos -> Installing an HA kubernetes cluster with calico as CNI +1. Open Sealos database. -Here `kubernetes:v1.24.0` and `calico:v3.22.1` are the cluster images in the registry which are fully compatible with OCI standard. Wonder if we can use flannel instead? Of course! + image -```shell script -# Download and install sealos. sealos is a golang binary so you can just download and copy to bin. You may also download it from release page. -$ wget -c https://sealyun-home.oss-cn-beijing.aliyuncs.com/sealos-4.0/latest/sealos-amd64 -O sealos && \ - chmod +x sealos && mv sealos /usr/bin -# Create a cluster -$ sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] -``` +2. Create your database. -> Building a custom cluster image + image -See [Building an ingress cluster image](https://github.com/labring/sealos/blob/main/docs/4.0/build-example-ingress-helm.md). +3. View your database and access details. -> Storage, message queue, database, etc. + image -Don't be shocked by the following: +### Deploy your Docker Image on Sealos -```shell script -$ sealos run labring/helm:v3.8.2 # install helm -$ sealos run labring/openebs:v1.9.0 # install openebs -$ sealos run labring/minio-operator:v4.4.16 labring/ingress-nginx:4.1.0 \ - labring/mysql-operator:8.0.23-14.1 labring/redis-operator:3.1.4 # oneliner -``` +1. Open Sealos App Launchpad. -And now everything is ready. + image + +2. Deploy the Docker image using a Kubernetes Deployment and expose it with an Ingress. + + image + +3. View your app details and access your service. + + image + +## 💡 Core features + +- Integrated Cloud IDEs: Zero-setup, collaborative development in the cloud. Eliminate local environment inconsistencies with DevBox. +- Managed Databases & Storage: Production-ready PostgreSQL, MySQL, MongoDB, Redis, and built-in S3-compatible Object Storage. +- Extensive App Store: Deploy complex applications with a single click. No YAML configuration, no container orchestration complexity - just point, click, and deploy. +- Full Kubernetes Power: Access the full power of Kubernetes without the complexity. K8s-native from day one. +- Enterprise Multi-Tenancy: Workspace-based isolation with granular RBAC and per-workspace resource quotas for secure collaboration. +- AI-Native Infrastructure: Build and scale anything with AI simply by describing them. + +## 🏘️ Community & support + +- 🌐 Visit the [Sealos Website](https://sealos.io/) for full documentation and useful links. +- 💬 Join our [Discord Server](https://discord.gg/wdUn538zVP) to chat with the Sealos team and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. +- 🐦 Tweet at @Sealos_io on [X/Twitter](https://twitter.com/Sealos_io) and follow us. +- 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. + +## 🚧 Roadmap + +Sealos maintains a [public roadmap](https://github.com/orgs/labring/projects/4/views/9). It gives a high-level view of the main priorities for the project, the maturity of different features and projects, and how to influence the project direction. + +## 👩‍💻 Contributing & Development + +Have a look through [existing issues](https://github.com/labring/sealos/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and [Pull Requests](https://github.com/labring/sealos/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc) that you could help with. If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/labring/sealos/issues/new/choose) using one of the templates provided. + +📖 [See contribution guide →](./CONTRIBUTING.md) + +🔧 [See development guide →](./DEVELOPGUIDE.md) ## Links -- [Contribution Guidelines](./CONTRIBUTING.md) -- [Development Guide](./DEVELOPGUIDE.md) -- [sealos 3.0(older version)](https://github.com/labring/sealos/tree/release-v3.3.9#readme) For older version users. Note that sealos 4.0 includes significant improvements, so please upgrade ASAP. -- [buildah](https://github.com/containers/buildah) Capabilities of buildah is widely used in sealos 4.0 to make cluster images compatible with container images and docker registry. -- [sealer](https://github.com/sealerio/sealer) Capabilities of sealer is widely used in sealos 4.0 to make Clusterfile compatible with sealer, some module forked sealer source code. +- [FastGPT](https://github.com/labring/FastGPT) is a free, open-source, and powerful AI knowledge base platform, offers out-of-the-box data processing, model invocation, RAG retrieval, and visual AI workflows. Easily build complex LLM applications. +- [Buildah](https://github.com/containers/buildah) The functionalities of Buildah are extensively utilized in Sealos 4.0 to ensure that cluster images are compatible with OCI standard. + +[deploy-badge]: https://sealos.io/Deploy-on-Sealos.svg +[deploy-link]: https://os.sealos.io + +## 📄 License + +Sealos is licensed under the [Sealos Sustainable Use License](./LICENSE.md), a custom license that allows: + +- ✅ Internal business use and personal non-commercial use +- ❌ Providing cloud services to third parties + +**Not a standard open source license** - please review the [full license terms](./LICENSE.md) before use. -**Join us: [Telegram](https://t.me/cloudnativer), QQ Group(98488045), Wechat:fangnux** +**Contributing**: By contributing, you agree to our [Contributor License Agreement](./CONTRIBUTOR_LICENSE_AGREEMENT.md) and license change terms. + -## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flabring%2Fsealos.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Flabring%2Fsealos?ref=badge_large) + diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 000000000000..753089308311 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,125 @@ +

+ + + + + + + +一款以 Kubernetes 为内核的云操作系统发行版 + +

+ +
+ +[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/labring/sealos) +[![Build Status](https://github.com/labring/sealos/actions/workflows/release.yml/badge.svg)](https://github.com/labring/sealos/actions) +[![](https://img.shields.io/docker/pulls/labring/kubernetes)](https://hub.docker.com/r/labring/kubernetes) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flabring%2Fsealos.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Flabring%2Fsealos?ref=badge_shield) +[![codecov](https://codecov.io/gh/labring/sealos/branch/main/graph/badge.svg?token=e41ZDcj06N)](https://codecov.io/gh/labring/sealos) +[![Website](https://img.shields.io/website?url=https%3A%2F%2Fpostwoman.io&logo=Postwoman)](https://sealos.run) +[![OSCS Status](https://www.oscs1024.com/platform/badge/labring/sealos.svg?size=small)](https://www.oscs1024.com/repo/labring/sealos?ref=badge_small) + +
+ +[![discord](https://theme.zdassets.com/theme_assets/678183/cc59daa07820943e943c2fc283b9079d7003ff76.svg)](https://discord.gg/qzBmGGZGk7) + +
+ + + +https://github.com/user-attachments/assets/8418437c-51ff-42b0-9195-38d1017c8522 + + + +

+ 文档 | + English | + 发展规划 +

+ +Sealos 是一款以 Kubernetes 为内核的**云操作系统发行版**。它以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,使企业能够**像使用个人电脑一样**简单地使用云。 + + +## 🚀 在 Sealos 上快速部署分布式应用 + +[在线使用](https://cloud.sealos.run) + +* [在 Sealos 上 30 秒内轻松部署 Nginx](https://sealos.run/docs/quick-start/use-app-launchpad) +* [在 Sealos 上 30 秒启动 mysql/pgsql/mongo 高可用数据库](https://sealos.run/docs/quick-start/use-database) +* [在 Sealos 上 运行 WordPress](https://sealos.run/docs/examples/blog-platform/install-wordpress) +* [在 Sealos 上 运行 Uptime Kuma 拨测系统](https://sealos.run/docs/examples/dial-testing-system/install-uptime-kuma) +* [在 Sealos 上 运行低代码平台](https://sealos.run/docs/category/low-code-platform) +* [在 Sealos 上 运行搭建聊天应用](https://sealos.run/docs/examples/social-communication/install-tailchat) + +![](/docs/img/sealos-desktop-zh.webp) + +🔍 您可以通过以下的屏幕截图进一步了解 Sealos,关于 Sealos 更为详细的介绍与说明,请参阅 [什么是 Sealos](https://sealos.run/docs/Intro)。 + +
+ +| 模板市场 | 应用管理 | +| :---: | :---: | +| ![](/docs/img/templates-zh.jpg) | ![](/docs/img/app-launchpad-1-zh.jpg) | +| 数据库管理 | 函数计算 | +| ![](/docs/img/database-zh.jpg) | ![](/docs/img/laf-zh.jpg) | + +
+ +## 💡 核心功能 + +- 🚀 **应用管理**:在模板市场中轻松管理并快速发布可公网访问的分布式应用。 +- 🗄️ **数据库管理**:秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。 +- 🌥️ **公私一致**:即是公有云也是私有云,支持传统应用无缝迁移到云环境。 + +## 🌟 优势 + +- 💰 **高效 & 经济**:仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。 +- 🌐 **通用性强,无心智负担**:专注于业务本身,无需担心复杂性,几乎没有学习成本。 +- 🛡️ **灵活 & 安全**:多租户共享机制在确保安全的同时,实现资源隔离与高效协作。 + + +## 🏘️ 社区与支持 + +- 🌐 访问 [Sealos官网](https://sealos.run/) 获取完整的文档和实用链接。 + +- 📱 扫码加入社区微信交流群👇 + + + +- 💬 加入我们的 [Discord服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。这是了解 Sealos 和 Kubernetes 以及提问和分享经验的理想之地。 + +- 🐦 在 [Twitter](https://twitter.com/Sealos_io) 上关注我们。 + +- 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 + +## 🚧 发展规划 + +Sealos 维护了一个[公开的发展路线图](https://github.com/orgs/labring/projects/4/views/9),为项目的主要优先事项、不同功能和项目的成熟度,以及如何影响项目方向提供了高级视图。 + +## 👩‍💻 贡献与开发 + +翻阅[现有的 Issue](https://github.com/labring/sealos/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 和 [Pull Requests](https://github.com/labring/sealos/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc),看看您是否能提供帮助。如果您想要新增需求或报告 Bug,请使用我们提供的模板[创建一个 GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 + +📖 [查看贡献指南 →](./CONTRIBUTING.md) + +🔧 [查看开发指南 →](./DEVELOPGUIDE.md) + +## 🔗 链接 + +- [Laf](https://github.com/labring/laf) 是 Sealos 上的一款函数计算应用,让写代码像写博客一样简单,随时随地发布上线。 +- [Buildah](https://github.com/containers/buildah) 在 Sealos 4.0 中,我们广泛地利用了 Buildah 的能力,以确保集群镜像与 OCI 标准兼容。 + +## 📄 许可证 + +Sealos 采用 [Sealos 可持续使用许可证](./LICENSE.md),这是一个定制许可证,允许: +- ✅ 内部商业用途和个人非商业用途 +- ❌ 向第三方提供云服务 + +**这不是标准的开源许可证** - 使用前请查看 [完整许可证条款](./LICENSE.md)。 + +**贡献代码**: 通过贡献代码,您同意我们的 [贡献者许可协议](./CONTRIBUTOR_LICENSE_AGREEMENT.md) 和许可证变更条款。 + + + + diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 6c15c1c52c6e..000000000000 --- a/ROADMAP.md +++ /dev/null @@ -1,16 +0,0 @@ -# ROADMAP - -## sealos 4.0 - -- [x] support oci cluster image -- [ ] using buildah SDK instead commandline -- [ ] support using registry to distribute cluster image [docs](./docs/4.0/design/registry-image.md) -- [ ] other commands support - -## sealos desktop - -- [ ] cluster image marketing place -- [ ] desktop to manage all the applications -- [ ] build in applications, like terminal, marketing place - -## sealos cloud \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..c8d7ff6a741f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,88 @@ +# Security Policy + +We sincerely thank security researchers and users for helping to keep the Sealos project and its community safe. Your contributions to identifying and responsibly disclosing security vulnerabilities are invaluable to maintaining the security and integrity of our platform. + +## Supported Versions + +The following table shows which versions of Sealos are currently supported with security updates: + +| Sealos Version | Version Range | Supported | +| -------------- | ------------- | --------- | +| 5.x | >= 5.0.0 | ✅ Yes | +| 4.x | >= 4.0.0, < 5.0.0 | ⚠️ Best-effort | +| 3.x and older | < 4.0.0 | ❌ No | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously and appreciate your help in responsibly disclosing them. Please follow these steps to report a security vulnerability: + +### Preferred Method + +**Please do NOT create a public GitHub Issue for security vulnerabilities.** Instead, please report security vulnerabilities privately using one of the following methods: + +1. **Email**: Send a detailed report to [security@sealos.io](mailto:security@sealos.io) +2. **GitHub Security Advisory**: Use GitHub's "Privately report a security vulnerability" feature on the repository's "Security" tab + +### What to Include + +When reporting a vulnerability, please include the following information: + +- **Clear description**: A detailed description of the vulnerability +- **Steps to reproduce**: Step-by-step instructions to reproduce the issue +- **Affected versions**: Which versions of Sealos are affected +- **Potential impact**: Assessment of the potential security impact +- **Environment details**: Operating system, Kubernetes version, and other relevant environment information +- **Proof of concept**: If applicable, include a proof of concept (but please be responsible) +- **Suggested fix**: If you have ideas for a fix, please share them (optional but appreciated) + +## What to Expect After Reporting + +### Response Time + +- **Initial acknowledgment**: We will acknowledge receipt of your report within **48 hours** +- **Status updates**: We will provide regular updates on the progress of our investigation + +### Process Outline + +1. **Triage**: Our security team will review and triage your report +2. **Investigation**: We will investigate the vulnerability and assess its impact +3. **Fix development**: We will work on developing and testing a fix +4. **Timeline**: We aim to address critical vulnerabilities within **14 days** of confirmation +5. **Coordination**: We will keep you informed throughout the process and coordinate on disclosure timing + +### Post-Fix Disclosure + +After a fix has been developed, tested, and released: + +- **Credit**: We will give you appropriate credit for the discovery (unless you prefer to remain anonymous) +- **Public disclosure**: We will issue a public security advisory through GitHub Security Advisories (GHSA) +- **User notification**: Users will be notified through our standard communication channels +- **Disclosure timing**: We follow responsible disclosure practices, typically allowing 90 days for users to update before full public disclosure + +## Vulnerability Management Philosophy + +We treat security vulnerabilities as our highest priority. Our approach to security includes: + +- **Proactive security**: We continuously work to improve the security of Sealos +- **Responsible disclosure**: We believe in coordinated disclosure that protects users while giving credit to researchers +- **Community collaboration**: We work closely with the security research community +- **Transparency**: We strive to be transparent about security issues while protecting users during the remediation process + +## Security Updates + +Users will be notified of security updates through the following channels: + +- **GitHub Releases**: Security fixes will be clearly marked in release notes with a `[SECURITY]` prefix +- **GitHub Security Advisories (GHSA)**: Critical security vulnerabilities will be published as GitHub Security Advisories +- **Project Documentation**: Security updates will be documented in our official documentation +- **Community Channels**: Important security updates may be announced through our official community channels + +## Additional Resources + +- [Contributing Guidelines](CONTRIBUTING.md#reporting-security-issues) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Sealos Documentation](https://docs.sealos.io) + +--- + +For any questions about this security policy, please contact us at [security@sealos.io](mailto:security@sealos.io). \ No newline at end of file diff --git a/applications/calico/Clusterfile b/applications/calico/Clusterfile deleted file mode 100644 index 68bf6b1837c2..000000000000 --- a/applications/calico/Clusterfile +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: kubeadm.k8s.io/v1beta2 -kind: ClusterConfiguration -networking: - podSubnet: 10.160.0.0/12 ---- -apiVersion: apps.sealos.io/v1beta1 -kind: Config -metadata: - name: calico -spec: - path: manifests/calico.yaml - data: | - apiVersion: operator.tigera.io/v1 - kind: Installation - metadata: - name: default - spec: - # Configures Calico networking. - calicoNetwork: - # Note: The ipPools section cannot be modified post-install. - ipPools: - - blockSize: 26 - # Note: Must be the same as podCIDR - cidr: 10.160.0.0/12 - encapsulation: IPIP - natOutgoing: Enabled - nodeSelector: all() - nodeAddressAutodetectionV4: - interface: "eth.*|en.*" diff --git a/applications/calico/Kubefile b/applications/calico/Kubefile deleted file mode 100644 index d44180771843..000000000000 --- a/applications/calico/Kubefile +++ /dev/null @@ -1,3 +0,0 @@ -FROM labring/kubernetes:v1.24.0 -COPY manifests ./cni -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] diff --git a/applications/calico/manifests/calico.yaml b/applications/calico/manifests/calico.yaml deleted file mode 100644 index 9f2bb5a25333..000000000000 --- a/applications/calico/manifests/calico.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright © 2022 cuisongliu@qq.com. -# -# 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. -# Source: https://docs.projectcalico.org/archive/v3.19/manifests/custom-resources.yaml ---- -# This section includes base Calico installation configuration. -# For more information, see: https://docs.projectcalico.org/v3.19/reference/installation/api#operator.tigera.io/v1.Installation -apiVersion: operator.tigera.io/v1 -kind: Installation -metadata: - name: default -spec: - # Configures Calico networking. - calicoNetwork: - # Note: The ipPools section cannot be modified post-install. - ipPools: - - blockSize: 26 - cidr: 100.64.0.0/10 - encapsulation: IPIP - natOutgoing: Enabled - nodeSelector: all() - nodeAddressAutodetectionV4: - # Interface enables IP auto-detection based on interfaces that match the given regex. - interface: "eth.*|en.*" diff --git a/applications/calico/manifests/tigera-operator.yaml b/applications/calico/manifests/tigera-operator.yaml deleted file mode 100644 index 0a6c28da6808..000000000000 --- a/applications/calico/manifests/tigera-operator.yaml +++ /dev/null @@ -1,5884 +0,0 @@ ---- -# Source: crds/calico/kdd/crd.projectcalico.org_bgpconfigurations.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: bgpconfigurations.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: BGPConfiguration - listKind: BGPConfigurationList - plural: bgpconfigurations - singular: bgpconfiguration - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: BGPConfiguration contains the configuration for any BGP routing. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: BGPConfigurationSpec contains the values of the BGP configuration. - properties: - asNumber: - description: 'ASNumber is the default AS number used by a node. [Default: - 64512]' - format: int32 - type: integer - communities: - description: Communities is a list of BGP community values and their - arbitrary names for tagging routes. - items: - description: Community contains standard or large community value - and its name. - properties: - name: - description: Name given to community value. - type: string - value: - description: Value must be of format `aa:nn` or `aa:nn:mm`. - For standard community use `aa:nn` format, where `aa` and - `nn` are 16 bit number. For large community use `aa:nn:mm` - format, where `aa`, `nn` and `mm` are 32 bit number. Where, - `aa` is an AS Number, `nn` and `mm` are per-AS identifier. - pattern: ^(\d+):(\d+)$|^(\d+):(\d+):(\d+)$ - type: string - type: object - type: array - listenPort: - description: ListenPort is the port where BGP protocol should listen. - Defaults to 179 - maximum: 65535 - minimum: 1 - type: integer - logSeverityScreen: - description: 'LogSeverityScreen is the log severity above which logs - are sent to the stdout. [Default: INFO]' - type: string - nodeToNodeMeshEnabled: - description: 'NodeToNodeMeshEnabled sets whether full node to node - BGP mesh is enabled. [Default: true]' - type: boolean - prefixAdvertisements: - description: PrefixAdvertisements contains per-prefix advertisement - configuration. - items: - description: PrefixAdvertisement configures advertisement properties - for the specified CIDR. - properties: - cidr: - description: CIDR for which properties should be advertised. - type: string - communities: - description: Communities can be list of either community names - already defined in `Specs.Communities` or community value - of format `aa:nn` or `aa:nn:mm`. For standard community use - `aa:nn` format, where `aa` and `nn` are 16 bit number. For - large community use `aa:nn:mm` format, where `aa`, `nn` and - `mm` are 32 bit number. Where,`aa` is an AS Number, `nn` and - `mm` are per-AS identifier. - items: - type: string - type: array - type: object - type: array - serviceClusterIPs: - description: ServiceClusterIPs are the CIDR blocks from which service - cluster IPs are allocated. If specified, Calico will advertise these - blocks, as well as any cluster IPs within them. - items: - description: ServiceClusterIPBlock represents a single allowed ClusterIP - CIDR block. - properties: - cidr: - type: string - type: object - type: array - serviceExternalIPs: - description: ServiceExternalIPs are the CIDR blocks for Kubernetes - Service External IPs. Kubernetes Service ExternalIPs will only be - advertised if they are within one of these blocks. - items: - description: ServiceExternalIPBlock represents a single allowed - External IP CIDR block. - properties: - cidr: - type: string - type: object - type: array - serviceLoadBalancerIPs: - description: ServiceLoadBalancerIPs are the CIDR blocks for Kubernetes - Service LoadBalancer IPs. Kubernetes Service status.LoadBalancer.Ingress - IPs will only be advertised if they are within one of these blocks. - items: - description: ServiceLoadBalancerIPBlock represents a single allowed - LoadBalancer IP CIDR block. - properties: - cidr: - type: string - type: object - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_bgppeers.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: bgppeers.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: BGPPeer - listKind: BGPPeerList - plural: bgppeers - singular: bgppeer - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: BGPPeerSpec contains the specification for a BGPPeer resource. - properties: - asNumber: - description: The AS Number of the peer. - format: int32 - type: integer - keepOriginalNextHop: - description: Option to keep the original nexthop field when routes - are sent to a BGP Peer. Setting "true" configures the selected BGP - Peers node to use the "next hop keep;" instead of "next hop self;"(default) - in the specific branch of the Node on "bird.cfg". - type: boolean - maxRestartTime: - description: Time to allow for software restart. When specified, - this is configured as the graceful restart timeout. When not specified, - the BIRD default of 120s is used. - type: string - node: - description: The node name identifying the Calico node instance that - is targeted by this peer. If this is not set, and no nodeSelector - is specified, then this BGP peer selects all nodes in the cluster. - type: string - nodeSelector: - description: Selector for the nodes that should have this peering. When - this is set, the Node field must be empty. - type: string - password: - description: Optional BGP password for the peerings generated by this - BGPPeer resource. - properties: - secretKeyRef: - description: Selects a key of a secret in the node pod's namespace. - properties: - key: - description: The key of the secret to select from. Must be - a valid secret key. - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - optional: - description: Specify whether the Secret or its key must be - defined - type: boolean - required: - - key - type: object - type: object - peerIP: - description: The IP address of the peer followed by an optional port - number to peer with. If port number is given, format should be `[]:port` - or `:` for IPv4. If optional port number is not set, - and this peer IP and ASNumber belongs to a calico/node with ListenPort - set in BGPConfiguration, then we use that port to peer. - type: string - peerSelector: - description: Selector for the remote nodes to peer with. When this - is set, the PeerIP and ASNumber fields must be empty. For each - peering between the local node and selected remote nodes, we configure - an IPv4 peering if both ends have NodeBGPSpec.IPv4Address specified, - and an IPv6 peering if both ends have NodeBGPSpec.IPv6Address specified. The - remote AS number comes from the remote node's NodeBGPSpec.ASNumber, - or the global default if that is not set. - type: string - sourceAddress: - description: Specifies whether and how to configure a source address - for the peerings generated by this BGPPeer resource. Default value - "UseNodeIP" means to configure the node IP as the source address. "None" - means not to configure a source address. - type: string - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_blockaffinities.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: blockaffinities.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: BlockAffinity - listKind: BlockAffinityList - plural: blockaffinities - singular: blockaffinity - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: BlockAffinitySpec contains the specification for a BlockAffinity - resource. - properties: - cidr: - type: string - deleted: - description: Deleted indicates that this block affinity is being deleted. - This field is a string for compatibility with older releases that - mistakenly treat this field as a string. - type: string - node: - type: string - state: - type: string - required: - - cidr - - deleted - - node - - state - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_caliconodestatuses.yaml - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: (devel) - creationTimestamp: null - name: caliconodestatuses.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: CalicoNodeStatus - listKind: CalicoNodeStatusList - plural: caliconodestatuses - singular: caliconodestatus - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: CalicoNodeStatusSpec contains the specification for a CalicoNodeStatus - resource. - properties: - classes: - description: Classes declares the types of information to monitor - for this calico/node, and allows for selective status reporting - about certain subsets of information. - items: - type: string - type: array - node: - description: The node name identifies the Calico node instance for - node status. - type: string - updatePeriodSeconds: - description: UpdatePeriodSeconds is the period at which CalicoNodeStatus - should be updated. Set to 0 to disable CalicoNodeStatus refresh. - Maximum update period is one day. - format: int32 - type: integer - type: object - status: - description: CalicoNodeStatusStatus defines the observed state of CalicoNodeStatus. - No validation needed for status since it is updated by Calico. - properties: - agent: - description: Agent holds agent status on the node. - properties: - birdV4: - description: BIRDV4 represents the latest observed status of bird4. - properties: - lastBootTime: - description: LastBootTime holds the value of lastBootTime - from bird.ctl output. - type: string - lastReconfigurationTime: - description: LastReconfigurationTime holds the value of lastReconfigTime - from bird.ctl output. - type: string - routerID: - description: Router ID used by bird. - type: string - state: - description: The state of the BGP Daemon. - type: string - version: - description: Version of the BGP daemon - type: string - type: object - birdV6: - description: BIRDV6 represents the latest observed status of bird6. - properties: - lastBootTime: - description: LastBootTime holds the value of lastBootTime - from bird.ctl output. - type: string - lastReconfigurationTime: - description: LastReconfigurationTime holds the value of lastReconfigTime - from bird.ctl output. - type: string - routerID: - description: Router ID used by bird. - type: string - state: - description: The state of the BGP Daemon. - type: string - version: - description: Version of the BGP daemon - type: string - type: object - type: object - bgp: - description: BGP holds node BGP status. - properties: - numberEstablishedV4: - description: The total number of IPv4 established bgp sessions. - type: integer - numberEstablishedV6: - description: The total number of IPv6 established bgp sessions. - type: integer - numberNotEstablishedV4: - description: The total number of IPv4 non-established bgp sessions. - type: integer - numberNotEstablishedV6: - description: The total number of IPv6 non-established bgp sessions. - type: integer - peersV4: - description: PeersV4 represents IPv4 BGP peers status on the node. - items: - description: CalicoNodePeer contains the status of BGP peers - on the node. - properties: - peerIP: - description: IP address of the peer whose condition we are - reporting. - type: string - since: - description: Since the state or reason last changed. - type: string - state: - description: State is the BGP session state. - type: string - type: - description: Type indicates whether this peer is configured - via the node-to-node mesh, or via en explicit global or - per-node BGPPeer object. - type: string - type: object - type: array - peersV6: - description: PeersV6 represents IPv6 BGP peers status on the node. - items: - description: CalicoNodePeer contains the status of BGP peers - on the node. - properties: - peerIP: - description: IP address of the peer whose condition we are - reporting. - type: string - since: - description: Since the state or reason last changed. - type: string - state: - description: State is the BGP session state. - type: string - type: - description: Type indicates whether this peer is configured - via the node-to-node mesh, or via en explicit global or - per-node BGPPeer object. - type: string - type: object - type: array - required: - - numberEstablishedV4 - - numberEstablishedV6 - - numberNotEstablishedV4 - - numberNotEstablishedV6 - type: object - lastUpdated: - description: LastUpdated is a timestamp representing the server time - when CalicoNodeStatus object last updated. It is represented in - RFC3339 form and is in UTC. - format: date-time - nullable: true - type: string - routes: - description: Routes reports routes known to the Calico BGP daemon - on the node. - properties: - routesV4: - description: RoutesV4 represents IPv4 routes on the node. - items: - description: CalicoNodeRoute contains the status of BGP routes - on the node. - properties: - destination: - description: Destination of the route. - type: string - gateway: - description: Gateway for the destination. - type: string - interface: - description: Interface for the destination - type: string - learnedFrom: - description: LearnedFrom contains information regarding - where this route originated. - properties: - peerIP: - description: If sourceType is NodeMesh or BGPPeer, IP - address of the router that sent us this route. - type: string - sourceType: - description: Type of the source where a route is learned - from. - type: string - type: object - type: - description: Type indicates if the route is being used for - forwarding or not. - type: string - type: object - type: array - routesV6: - description: RoutesV6 represents IPv6 routes on the node. - items: - description: CalicoNodeRoute contains the status of BGP routes - on the node. - properties: - destination: - description: Destination of the route. - type: string - gateway: - description: Gateway for the destination. - type: string - interface: - description: Interface for the destination - type: string - learnedFrom: - description: LearnedFrom contains information regarding - where this route originated. - properties: - peerIP: - description: If sourceType is NodeMesh or BGPPeer, IP - address of the router that sent us this route. - type: string - sourceType: - description: Type of the source where a route is learned - from. - type: string - type: object - type: - description: Type indicates if the route is being used for - forwarding or not. - type: string - type: object - type: array - type: object - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_clusterinformations.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: clusterinformations.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: ClusterInformation - listKind: ClusterInformationList - plural: clusterinformations - singular: clusterinformation - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: ClusterInformation contains the cluster specific information. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: ClusterInformationSpec contains the values of describing - the cluster. - properties: - calicoVersion: - description: CalicoVersion is the version of Calico that the cluster - is running - type: string - clusterGUID: - description: ClusterGUID is the GUID of the cluster - type: string - clusterType: - description: ClusterType describes the type of the cluster - type: string - datastoreReady: - description: DatastoreReady is used during significant datastore migrations - to signal to components such as Felix that it should wait before - accessing the datastore. - type: boolean - variant: - description: Variant declares which variant of Calico should be active. - type: string - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_felixconfigurations.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: felixconfigurations.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: FelixConfiguration - listKind: FelixConfigurationList - plural: felixconfigurations - singular: felixconfiguration - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Felix Configuration contains the configuration for Felix. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: FelixConfigurationSpec contains the values of the Felix configuration. - properties: - allowIPIPPacketsFromWorkloads: - description: 'AllowIPIPPacketsFromWorkloads controls whether Felix - will add a rule to drop IPIP encapsulated traffic from workloads - [Default: false]' - type: boolean - allowVXLANPacketsFromWorkloads: - description: 'AllowVXLANPacketsFromWorkloads controls whether Felix - will add a rule to drop VXLAN encapsulated traffic from workloads - [Default: false]' - type: boolean - awsSrcDstCheck: - description: 'Set source-destination-check on AWS EC2 instances. Accepted - value must be one of "DoNothing", "Enable" or "Disable". [Default: - DoNothing]' - enum: - - DoNothing - - Enable - - Disable - type: string - bpfConnectTimeLoadBalancingEnabled: - description: 'BPFConnectTimeLoadBalancingEnabled when in BPF mode, - controls whether Felix installs the connection-time load balancer. The - connect-time load balancer is required for the host to be able to - reach Kubernetes services and it improves the performance of pod-to-service - connections. The only reason to disable it is for debugging purposes. [Default: - true]' - type: boolean - bpfDataIfacePattern: - description: BPFDataIfacePattern is a regular expression that controls - which interfaces Felix should attach BPF programs to in order to - catch traffic to/from the network. This needs to match the interfaces - that Calico workload traffic flows over as well as any interfaces - that handle incoming traffic to nodeports and services from outside - the cluster. It should not match the workload interfaces (usually - named cali...). - type: string - bpfDisableUnprivileged: - description: 'BPFDisableUnprivileged, if enabled, Felix sets the kernel.unprivileged_bpf_disabled - sysctl to disable unprivileged use of BPF. This ensures that unprivileged - users cannot access Calico''s BPF maps and cannot insert their own - BPF programs to interfere with Calico''s. [Default: true]' - type: boolean - bpfEnabled: - description: 'BPFEnabled, if enabled Felix will use the BPF dataplane. - [Default: false]' - type: boolean - bpfExtToServiceConnmark: - description: 'BPFExtToServiceConnmark in BPF mode, control a 32bit - mark that is set on connections from an external client to a local - service. This mark allows us to control how packets of that connection - are routed within the host and how is routing intepreted by RPF - check. [Default: 0]' - type: integer - bpfExternalServiceMode: - description: 'BPFExternalServiceMode in BPF mode, controls how connections - from outside the cluster to services (node ports and cluster IPs) - are forwarded to remote workloads. If set to "Tunnel" then both - request and response traffic is tunneled to the remote node. If - set to "DSR", the request traffic is tunneled but the response traffic - is sent directly from the remote node. In "DSR" mode, the remote - node appears to use the IP of the ingress node; this requires a - permissive L2 network. [Default: Tunnel]' - type: string - bpfKubeProxyEndpointSlicesEnabled: - description: BPFKubeProxyEndpointSlicesEnabled in BPF mode, controls - whether Felix's embedded kube-proxy accepts EndpointSlices or not. - type: boolean - bpfKubeProxyIptablesCleanupEnabled: - description: 'BPFKubeProxyIptablesCleanupEnabled, if enabled in BPF - mode, Felix will proactively clean up the upstream Kubernetes kube-proxy''s - iptables chains. Should only be enabled if kube-proxy is not running. [Default: - true]' - type: boolean - bpfKubeProxyMinSyncPeriod: - description: 'BPFKubeProxyMinSyncPeriod, in BPF mode, controls the - minimum time between updates to the dataplane for Felix''s embedded - kube-proxy. Lower values give reduced set-up latency. Higher values - reduce Felix CPU usage by batching up more work. [Default: 1s]' - type: string - bpfLogLevel: - description: 'BPFLogLevel controls the log level of the BPF programs - when in BPF dataplane mode. One of "Off", "Info", or "Debug". The - logs are emitted to the BPF trace pipe, accessible with the command - `tc exec bpf debug`. [Default: Off].' - type: string - chainInsertMode: - description: 'ChainInsertMode controls whether Felix hooks the kernel''s - top-level iptables chains by inserting a rule at the top of the - chain or by appending a rule at the bottom. insert is the safe default - since it prevents Calico''s rules from being bypassed. If you switch - to append mode, be sure that the other rules in the chains signal - acceptance by falling through to the Calico rules, otherwise the - Calico policy will be bypassed. [Default: insert]' - type: string - dataplaneDriver: - type: string - debugDisableLogDropping: - type: boolean - debugMemoryProfilePath: - type: string - debugSimulateCalcGraphHangAfter: - type: string - debugSimulateDataplaneHangAfter: - type: string - defaultEndpointToHostAction: - description: 'DefaultEndpointToHostAction controls what happens to - traffic that goes from a workload endpoint to the host itself (after - the traffic hits the endpoint egress policy). By default Calico - blocks traffic from workload endpoints to the host itself with an - iptables "DROP" action. If you want to allow some or all traffic - from endpoint to host, set this parameter to RETURN or ACCEPT. Use - RETURN if you have your own rules in the iptables "INPUT" chain; - Calico will insert its rules at the top of that chain, then "RETURN" - packets to the "INPUT" chain once it has completed processing workload - endpoint egress policy. Use ACCEPT to unconditionally accept packets - from workloads after processing workload endpoint egress policy. - [Default: Drop]' - type: string - deviceRouteProtocol: - description: This defines the route protocol added to programmed device - routes, by default this will be RTPROT_BOOT when left blank. - type: integer - deviceRouteSourceAddress: - description: This is the source address to use on programmed device - routes. By default the source address is left blank, leaving the - kernel to choose the source address used. - type: string - disableConntrackInvalidCheck: - type: boolean - endpointReportingDelay: - type: string - endpointReportingEnabled: - type: boolean - externalNodesList: - description: ExternalNodesCIDRList is a list of CIDR's of external-non-calico-nodes - which may source tunnel traffic and have the tunneled traffic be - accepted at calico nodes. - items: - type: string - type: array - failsafeInboundHostPorts: - description: 'FailsafeInboundHostPorts is a list of UDP/TCP ports - and CIDRs that Felix will allow incoming traffic to host endpoints - on irrespective of the security policy. This is useful to avoid - accidentally cutting off a host with incorrect configuration. For - back-compatibility, if the protocol is not specified, it defaults - to "tcp". If a CIDR is not specified, it will allow traffic from - all addresses. To disable all inbound host ports, use the value - none. The default value allows ssh access and DHCP. [Default: tcp:22, - udp:68, tcp:179, tcp:2379, tcp:2380, tcp:6443, tcp:6666, tcp:6667]' - items: - description: ProtoPort is combination of protocol, port, and CIDR. - Protocol and port must be specified. - properties: - net: - type: string - port: - type: integer - protocol: - type: string - required: - - port - - protocol - type: object - type: array - failsafeOutboundHostPorts: - description: 'FailsafeOutboundHostPorts is a list of UDP/TCP ports - and CIDRs that Felix will allow outgoing traffic from host endpoints - to irrespective of the security policy. This is useful to avoid - accidentally cutting off a host with incorrect configuration. For - back-compatibility, if the protocol is not specified, it defaults - to "tcp". If a CIDR is not specified, it will allow traffic from - all addresses. To disable all outbound host ports, use the value - none. The default value opens etcd''s standard ports to ensure that - Felix does not get cut off from etcd as well as allowing DHCP and - DNS. [Default: tcp:179, tcp:2379, tcp:2380, tcp:6443, tcp:6666, - tcp:6667, udp:53, udp:67]' - items: - description: ProtoPort is combination of protocol, port, and CIDR. - Protocol and port must be specified. - properties: - net: - type: string - port: - type: integer - protocol: - type: string - required: - - port - - protocol - type: object - type: array - featureDetectOverride: - description: FeatureDetectOverride is used to override the feature - detection. Values are specified in a comma separated list with no - spaces, example; "SNATFullyRandom=true,MASQFullyRandom=false,RestoreSupportsLock=". - "true" or "false" will force the feature, empty or omitted values - are auto-detected. - type: string - genericXDPEnabled: - description: 'GenericXDPEnabled enables Generic XDP so network cards - that don''t support XDP offload or driver modes can use XDP. This - is not recommended since it doesn''t provide better performance - than iptables. [Default: false]' - type: boolean - healthEnabled: - type: boolean - healthHost: - type: string - healthPort: - type: integer - interfaceExclude: - description: 'InterfaceExclude is a comma-separated list of interfaces - that Felix should exclude when monitoring for host endpoints. The - default value ensures that Felix ignores Kubernetes'' IPVS dummy - interface, which is used internally by kube-proxy. If you want to - exclude multiple interface names using a single value, the list - supports regular expressions. For regular expressions you must wrap - the value with ''/''. For example having values ''/^kube/,veth1'' - will exclude all interfaces that begin with ''kube'' and also the - interface ''veth1''. [Default: kube-ipvs0]' - type: string - interfacePrefix: - description: 'InterfacePrefix is the interface name prefix that identifies - workload endpoints and so distinguishes them from host endpoint - interfaces. Note: in environments other than bare metal, the orchestrators - configure this appropriately. For example our Kubernetes and Docker - integrations set the ''cali'' value, and our OpenStack integration - sets the ''tap'' value. [Default: cali]' - type: string - interfaceRefreshInterval: - description: InterfaceRefreshInterval is the period at which Felix - rescans local interfaces to verify their state. The rescan can be - disabled by setting the interval to 0. - type: string - ipipEnabled: - type: boolean - ipipMTU: - description: 'IPIPMTU is the MTU to set on the tunnel device. See - Configuring MTU [Default: 1440]' - type: integer - ipsetsRefreshInterval: - description: 'IpsetsRefreshInterval is the period at which Felix re-checks - all iptables state to ensure that no other process has accidentally - broken Calico''s rules. Set to 0 to disable iptables refresh. [Default: - 90s]' - type: string - iptablesBackend: - description: IptablesBackend specifies which backend of iptables will - be used. The default is legacy. - type: string - iptablesFilterAllowAction: - type: string - iptablesLockFilePath: - description: 'IptablesLockFilePath is the location of the iptables - lock file. You may need to change this if the lock file is not in - its standard location (for example if you have mapped it into Felix''s - container at a different path). [Default: /run/xtables.lock]' - type: string - iptablesLockProbeInterval: - description: 'IptablesLockProbeInterval is the time that Felix will - wait between attempts to acquire the iptables lock if it is not - available. Lower values make Felix more responsive when the lock - is contended, but use more CPU. [Default: 50ms]' - type: string - iptablesLockTimeout: - description: 'IptablesLockTimeout is the time that Felix will wait - for the iptables lock, or 0, to disable. To use this feature, Felix - must share the iptables lock file with all other processes that - also take the lock. When running Felix inside a container, this - requires the /run directory of the host to be mounted into the calico/node - or calico/felix container. [Default: 0s disabled]' - type: string - iptablesMangleAllowAction: - type: string - iptablesMarkMask: - description: 'IptablesMarkMask is the mask that Felix selects its - IPTables Mark bits from. Should be a 32 bit hexadecimal number with - at least 8 bits set, none of which clash with any other mark bits - in use on the system. [Default: 0xff000000]' - format: int32 - type: integer - iptablesNATOutgoingInterfaceFilter: - type: string - iptablesPostWriteCheckInterval: - description: 'IptablesPostWriteCheckInterval is the period after Felix - has done a write to the dataplane that it schedules an extra read - back in order to check the write was not clobbered by another process. - This should only occur if another application on the system doesn''t - respect the iptables lock. [Default: 1s]' - type: string - iptablesRefreshInterval: - description: 'IptablesRefreshInterval is the period at which Felix - re-checks the IP sets in the dataplane to ensure that no other process - has accidentally broken Calico''s rules. Set to 0 to disable IP - sets refresh. Note: the default for this value is lower than the - other refresh intervals as a workaround for a Linux kernel bug that - was fixed in kernel version 4.11. If you are using v4.11 or greater - you may want to set this to, a higher value to reduce Felix CPU - usage. [Default: 10s]' - type: string - ipv6Support: - type: boolean - kubeNodePortRanges: - description: 'KubeNodePortRanges holds list of port ranges used for - service node ports. Only used if felix detects kube-proxy running - in ipvs mode. Felix uses these ranges to separate host and workload - traffic. [Default: 30000:32767].' - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - logFilePath: - description: 'LogFilePath is the full path to the Felix log. Set to - none to disable file logging. [Default: /var/log/calico/felix.log]' - type: string - logPrefix: - description: 'LogPrefix is the log prefix that Felix uses when rendering - LOG rules. [Default: calico-packet]' - type: string - logSeverityFile: - description: 'LogSeverityFile is the log severity above which logs - are sent to the log file. [Default: Info]' - type: string - logSeverityScreen: - description: 'LogSeverityScreen is the log severity above which logs - are sent to the stdout. [Default: Info]' - type: string - logSeveritySys: - description: 'LogSeveritySys is the log severity above which logs - are sent to the syslog. Set to None for no logging to syslog. [Default: - Info]' - type: string - maxIpsetSize: - type: integer - metadataAddr: - description: 'MetadataAddr is the IP address or domain name of the - server that can answer VM queries for cloud-init metadata. In OpenStack, - this corresponds to the machine running nova-api (or in Ubuntu, - nova-api-metadata). A value of none (case insensitive) means that - Felix should not set up any NAT rule for the metadata path. [Default: - 127.0.0.1]' - type: string - metadataPort: - description: 'MetadataPort is the port of the metadata server. This, - combined with global.MetadataAddr (if not ''None''), is used to - set up a NAT rule, from 169.254.169.254:80 to MetadataAddr:MetadataPort. - In most cases this should not need to be changed [Default: 8775].' - type: integer - mtuIfacePattern: - description: MTUIfacePattern is a regular expression that controls - which interfaces Felix should scan in order to calculate the host's - MTU. This should not match workload interfaces (usually named cali...). - type: string - natOutgoingAddress: - description: NATOutgoingAddress specifies an address to use when performing - source NAT for traffic in a natOutgoing pool that is leaving the - network. By default the address used is an address on the interface - the traffic is leaving on (ie it uses the iptables MASQUERADE target) - type: string - natPortRange: - anyOf: - - type: integer - - type: string - description: NATPortRange specifies the range of ports that is used - for port mapping when doing outgoing NAT. When unset the default - behavior of the network stack is used. - pattern: ^.* - x-kubernetes-int-or-string: true - netlinkTimeout: - type: string - openstackRegion: - description: 'OpenstackRegion is the name of the region that a particular - Felix belongs to. In a multi-region Calico/OpenStack deployment, - this must be configured somehow for each Felix (here in the datamodel, - or in felix.cfg or the environment on each compute node), and must - match the [calico] openstack_region value configured in neutron.conf - on each node. [Default: Empty]' - type: string - policySyncPathPrefix: - description: 'PolicySyncPathPrefix is used to by Felix to communicate - policy changes to external services, like Application layer policy. - [Default: Empty]' - type: string - prometheusGoMetricsEnabled: - description: 'PrometheusGoMetricsEnabled disables Go runtime metrics - collection, which the Prometheus client does by default, when set - to false. This reduces the number of metrics reported, reducing - Prometheus load. [Default: true]' - type: boolean - prometheusMetricsEnabled: - description: 'PrometheusMetricsEnabled enables the Prometheus metrics - server in Felix if set to true. [Default: false]' - type: boolean - prometheusMetricsHost: - description: 'PrometheusMetricsHost is the host that the Prometheus - metrics server should bind to. [Default: empty]' - type: string - prometheusMetricsPort: - description: 'PrometheusMetricsPort is the TCP port that the Prometheus - metrics server should bind to. [Default: 9091]' - type: integer - prometheusProcessMetricsEnabled: - description: 'PrometheusProcessMetricsEnabled disables process metrics - collection, which the Prometheus client does by default, when set - to false. This reduces the number of metrics reported, reducing - Prometheus load. [Default: true]' - type: boolean - prometheusWireGuardMetricsEnabled: - description: 'PrometheusWireGuardMetricsEnabled disables wireguard - metrics collection, which the Prometheus client does by default, - when set to false. This reduces the number of metrics reported, - reducing Prometheus load. [Default: true]' - type: boolean - removeExternalRoutes: - description: Whether or not to remove device routes that have not - been programmed by Felix. Disabling this will allow external applications - to also add device routes. This is enabled by default which means - we will remove externally added routes. - type: boolean - reportingInterval: - description: 'ReportingInterval is the interval at which Felix reports - its status into the datastore or 0 to disable. Must be non-zero - in OpenStack deployments. [Default: 30s]' - type: string - reportingTTL: - description: 'ReportingTTL is the time-to-live setting for process-wide - status reports. [Default: 90s]' - type: string - routeRefreshInterval: - description: 'RouteRefreshInterval is the period at which Felix re-checks - the routes in the dataplane to ensure that no other process has - accidentally broken Calico''s rules. Set to 0 to disable route refresh. - [Default: 90s]' - type: string - routeSource: - description: 'RouteSource configures where Felix gets its routing - information. - WorkloadIPs: use workload endpoints to construct - routes. - CalicoIPAM: the default - use IPAM data to construct routes.' - type: string - routeTableRange: - description: Calico programs additional Linux route tables for various - purposes. RouteTableRange specifies the indices of the route tables - that Calico should use. - properties: - max: - type: integer - min: - type: integer - required: - - max - - min - type: object - serviceLoopPrevention: - description: 'When service IP advertisement is enabled, prevent routing - loops to service IPs that are not in use, by dropping or rejecting - packets that do not get DNAT''d by kube-proxy. Unless set to "Disabled", - in which case such routing loops continue to be allowed. [Default: - Drop]' - type: string - sidecarAccelerationEnabled: - description: 'SidecarAccelerationEnabled enables experimental sidecar - acceleration [Default: false]' - type: boolean - usageReportingEnabled: - description: 'UsageReportingEnabled reports anonymous Calico version - number and cluster size to projectcalico.org. Logs warnings returned - by the usage server. For example, if a significant security vulnerability - has been discovered in the version of Calico being used. [Default: - true]' - type: boolean - usageReportingInitialDelay: - description: 'UsageReportingInitialDelay controls the minimum delay - before Felix makes a report. [Default: 300s]' - type: string - usageReportingInterval: - description: 'UsageReportingInterval controls the interval at which - Felix makes reports. [Default: 86400s]' - type: string - useInternalDataplaneDriver: - type: boolean - vxlanEnabled: - type: boolean - vxlanMTU: - description: 'VXLANMTU is the MTU to set on the tunnel device. See - Configuring MTU [Default: 1440]' - type: integer - vxlanPort: - type: integer - vxlanVNI: - type: integer - wireguardEnabled: - description: 'WireguardEnabled controls whether Wireguard is enabled. - [Default: false]' - type: boolean - wireguardHostEncryptionEnabled: - description: 'WireguardHostEncryptionEnabled controls whether Wireguard - host-to-host encryption is enabled. [Default: false]' - type: boolean - wireguardInterfaceName: - description: 'WireguardInterfaceName specifies the name to use for - the Wireguard interface. [Default: wg.calico]' - type: string - wireguardListeningPort: - description: 'WireguardListeningPort controls the listening port used - by Wireguard. [Default: 51820]' - type: integer - wireguardMTU: - description: 'WireguardMTU controls the MTU on the Wireguard interface. - See Configuring MTU [Default: 1420]' - type: integer - wireguardRoutingRulePriority: - description: 'WireguardRoutingRulePriority controls the priority value - to use for the Wireguard routing rule. [Default: 99]' - type: integer - xdpEnabled: - description: 'XDPEnabled enables XDP acceleration for suitable untracked - incoming deny rules. [Default: true]' - type: boolean - xdpRefreshInterval: - description: 'XDPRefreshInterval is the period at which Felix re-checks - all XDP state to ensure that no other process has accidentally broken - Calico''s BPF maps or attached programs. Set to 0 to disable XDP - refresh. [Default: 90s]' - type: string - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_globalnetworkpolicies.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: globalnetworkpolicies.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: GlobalNetworkPolicy - listKind: GlobalNetworkPolicyList - plural: globalnetworkpolicies - singular: globalnetworkpolicy - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - properties: - applyOnForward: - description: ApplyOnForward indicates to apply the rules in this policy - on forward traffic. - type: boolean - doNotTrack: - description: DoNotTrack indicates whether packets matched by the rules - in this policy should go through the data plane's connection tracking, - such as Linux conntrack. If True, the rules in this policy are - applied before any data plane connection tracking, and packets allowed - by this policy are marked as not to be tracked. - type: boolean - egress: - description: The ordered set of egress rules. Each rule contains - a set of packet match criteria and a corresponding action to apply. - items: - description: "A Rule encapsulates a set of match criteria and an - action. Both selector-based security Policy and security Profiles - reference rules - separated out as a list of rules for both ingress - and egress packet matching. \n Each positive match criteria has - a negated version, prefixed with \"Not\". All the match criteria - within a rule must be satisfied for a packet to match. A single - rule can contain the positive and negative version of a match - and both must be satisfied for the rule to match." - properties: - action: - type: string - destination: - description: Destination contains the match criteria that apply - to destination entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - http: - description: HTTP contains match criteria that apply to HTTP - requests. - properties: - methods: - description: Methods is an optional field that restricts - the rule to apply only to HTTP requests that use one of - the listed HTTP Methods (e.g. GET, PUT, etc.) Multiple - methods are OR'd together. - items: - type: string - type: array - paths: - description: 'Paths is an optional field that restricts - the rule to apply to HTTP requests that use one of the - listed HTTP Paths. Multiple paths are OR''d together. - e.g: - exact: /foo - prefix: /bar NOTE: Each entry may - ONLY specify either a `exact` or a `prefix` match. The - validator will check for it.' - items: - description: 'HTTPPath specifies an HTTP path to match. - It may be either of the form: exact: : which matches - the path exactly or prefix: : which matches - the path prefix' - properties: - exact: - type: string - prefix: - type: string - type: object - type: array - type: object - icmp: - description: ICMP is an optional field that restricts the rule - to apply to a specific type and code of ICMP traffic. This - should only be specified if the Protocol field is set to "ICMP" - or "ICMPv6". - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - ipVersion: - description: IPVersion is an optional field that restricts the - rule to only match a specific IP version. - type: integer - metadata: - description: Metadata contains additional information for this - rule - properties: - annotations: - additionalProperties: - type: string - description: Annotations is a set of key value pairs that - give extra information about the rule - type: object - type: object - notICMP: - description: NotICMP is the negated version of the ICMP field. - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - notProtocol: - anyOf: - - type: integer - - type: string - description: NotProtocol is the negated version of the Protocol - field. - pattern: ^.* - x-kubernetes-int-or-string: true - protocol: - anyOf: - - type: integer - - type: string - description: "Protocol is an optional field that restricts the - rule to only apply to traffic of a specific IP protocol. Required - if any of the EntityRules contain Ports (because ports only - apply to certain protocols). \n Must be one of these string - values: \"TCP\", \"UDP\", \"ICMP\", \"ICMPv6\", \"SCTP\", - \"UDPLite\" or an integer in the range 1-255." - pattern: ^.* - x-kubernetes-int-or-string: true - source: - description: Source contains the match criteria that apply to - source entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - required: - - action - type: object - type: array - ingress: - description: The ordered set of ingress rules. Each rule contains - a set of packet match criteria and a corresponding action to apply. - items: - description: "A Rule encapsulates a set of match criteria and an - action. Both selector-based security Policy and security Profiles - reference rules - separated out as a list of rules for both ingress - and egress packet matching. \n Each positive match criteria has - a negated version, prefixed with \"Not\". All the match criteria - within a rule must be satisfied for a packet to match. A single - rule can contain the positive and negative version of a match - and both must be satisfied for the rule to match." - properties: - action: - type: string - destination: - description: Destination contains the match criteria that apply - to destination entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - http: - description: HTTP contains match criteria that apply to HTTP - requests. - properties: - methods: - description: Methods is an optional field that restricts - the rule to apply only to HTTP requests that use one of - the listed HTTP Methods (e.g. GET, PUT, etc.) Multiple - methods are OR'd together. - items: - type: string - type: array - paths: - description: 'Paths is an optional field that restricts - the rule to apply to HTTP requests that use one of the - listed HTTP Paths. Multiple paths are OR''d together. - e.g: - exact: /foo - prefix: /bar NOTE: Each entry may - ONLY specify either a `exact` or a `prefix` match. The - validator will check for it.' - items: - description: 'HTTPPath specifies an HTTP path to match. - It may be either of the form: exact: : which matches - the path exactly or prefix: : which matches - the path prefix' - properties: - exact: - type: string - prefix: - type: string - type: object - type: array - type: object - icmp: - description: ICMP is an optional field that restricts the rule - to apply to a specific type and code of ICMP traffic. This - should only be specified if the Protocol field is set to "ICMP" - or "ICMPv6". - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - ipVersion: - description: IPVersion is an optional field that restricts the - rule to only match a specific IP version. - type: integer - metadata: - description: Metadata contains additional information for this - rule - properties: - annotations: - additionalProperties: - type: string - description: Annotations is a set of key value pairs that - give extra information about the rule - type: object - type: object - notICMP: - description: NotICMP is the negated version of the ICMP field. - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - notProtocol: - anyOf: - - type: integer - - type: string - description: NotProtocol is the negated version of the Protocol - field. - pattern: ^.* - x-kubernetes-int-or-string: true - protocol: - anyOf: - - type: integer - - type: string - description: "Protocol is an optional field that restricts the - rule to only apply to traffic of a specific IP protocol. Required - if any of the EntityRules contain Ports (because ports only - apply to certain protocols). \n Must be one of these string - values: \"TCP\", \"UDP\", \"ICMP\", \"ICMPv6\", \"SCTP\", - \"UDPLite\" or an integer in the range 1-255." - pattern: ^.* - x-kubernetes-int-or-string: true - source: - description: Source contains the match criteria that apply to - source entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - required: - - action - type: object - type: array - namespaceSelector: - description: NamespaceSelector is an optional field for an expression - used to select a pod based on namespaces. - type: string - order: - description: Order is an optional field that specifies the order in - which the policy is applied. Policies with higher "order" are applied - after those with lower order. If the order is omitted, it may be - considered to be "infinite" - i.e. the policy will be applied last. Policies - with identical order will be applied in alphanumerical order based - on the Policy "Name". - type: number - preDNAT: - description: PreDNAT indicates to apply the rules in this policy before - any DNAT. - type: boolean - selector: - description: "The selector is an expression used to pick pick out - the endpoints that the policy should be applied to. \n Selector - expressions follow this syntax: \n \tlabel == \"string_literal\" - \ -> comparison, e.g. my_label == \"foo bar\" \tlabel != \"string_literal\" - \ -> not equal; also matches if label is not present \tlabel in - { \"a\", \"b\", \"c\", ... } -> true if the value of label X is - one of \"a\", \"b\", \"c\" \tlabel not in { \"a\", \"b\", \"c\", - ... } -> true if the value of label X is not one of \"a\", \"b\", - \"c\" \thas(label_name) -> True if that label is present \t! expr - -> negation of expr \texpr && expr -> Short-circuit and \texpr - || expr -> Short-circuit or \t( expr ) -> parens for grouping \tall() - or the empty selector -> matches all endpoints. \n Label names are - allowed to contain alphanumerics, -, _ and /. String literals are - more permissive but they do not support escape characters. \n Examples - (with made-up labels): \n \ttype == \"webserver\" && deployment - == \"prod\" \ttype in {\"frontend\", \"backend\"} \tdeployment != - \"dev\" \t! has(label_name)" - type: string - serviceAccountSelector: - description: ServiceAccountSelector is an optional field for an expression - used to select a pod based on service accounts. - type: string - types: - description: "Types indicates whether this policy applies to ingress, - or to egress, or to both. When not explicitly specified (and so - the value on creation is empty or nil), Calico defaults Types according - to what Ingress and Egress rules are present in the policy. The - default is: \n - [ PolicyTypeIngress ], if there are no Egress rules - (including the case where there are also no Ingress rules) \n - - [ PolicyTypeEgress ], if there are Egress rules but no Ingress - rules \n - [ PolicyTypeIngress, PolicyTypeEgress ], if there are - both Ingress and Egress rules. \n When the policy is read back again, - Types will always be one of these values, never empty or nil." - items: - description: PolicyType enumerates the possible values of the PolicySpec - Types field. - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_globalnetworksets.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: globalnetworksets.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: GlobalNetworkSet - listKind: GlobalNetworkSetList - plural: globalnetworksets - singular: globalnetworkset - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: GlobalNetworkSet contains a set of arbitrary IP sub-networks/CIDRs - that share labels to allow rules to refer to them via selectors. The labels - of GlobalNetworkSet are not namespaced. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: GlobalNetworkSetSpec contains the specification for a NetworkSet - resource. - properties: - nets: - description: The list of IP networks that belong to this set. - items: - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_hostendpoints.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: hostendpoints.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: HostEndpoint - listKind: HostEndpointList - plural: hostendpoints - singular: hostendpoint - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: HostEndpointSpec contains the specification for a HostEndpoint - resource. - properties: - expectedIPs: - description: "The expected IP addresses (IPv4 and IPv6) of the endpoint. - If \"InterfaceName\" is not present, Calico will look for an interface - matching any of the IPs in the list and apply policy to that. Note: - \tWhen using the selector match criteria in an ingress or egress - security Policy \tor Profile, Calico converts the selector into - a set of IP addresses. For host \tendpoints, the ExpectedIPs field - is used for that purpose. (If only the interface \tname is specified, - Calico does not learn the IPs of the interface for use in match - \tcriteria.)" - items: - type: string - type: array - interfaceName: - description: "Either \"*\", or the name of a specific Linux interface - to apply policy to; or empty. \"*\" indicates that this HostEndpoint - governs all traffic to, from or through the default network namespace - of the host named by the \"Node\" field; entering and leaving that - namespace via any interface, including those from/to non-host-networked - local workloads. \n If InterfaceName is not \"*\", this HostEndpoint - only governs traffic that enters or leaves the host through the - specific interface named by InterfaceName, or - when InterfaceName - is empty - through the specific interface that has one of the IPs - in ExpectedIPs. Therefore, when InterfaceName is empty, at least - one expected IP must be specified. Only external interfaces (such - as \"eth0\") are supported here; it isn't possible for a HostEndpoint - to protect traffic through a specific local workload interface. - \n Note: Only some kinds of policy are implemented for \"*\" HostEndpoints; - initially just pre-DNAT policy. Please check Calico documentation - for the latest position." - type: string - node: - description: The node name identifying the Calico node instance. - type: string - ports: - description: Ports contains the endpoint's named ports, which may - be referenced in security policy rules. - items: - properties: - name: - type: string - port: - type: integer - protocol: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - required: - - name - - port - - protocol - type: object - type: array - profiles: - description: A list of identifiers of security Profile objects that - apply to this endpoint. Each profile is applied in the order that - they appear in this list. Profile rules are applied after the selector-based - security policy. - items: - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_ipamblocks.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: ipamblocks.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: IPAMBlock - listKind: IPAMBlockList - plural: ipamblocks - singular: ipamblock - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPAMBlockSpec contains the specification for an IPAMBlock - resource. - properties: - affinity: - type: string - allocations: - items: - type: integer - # TODO: This nullable is manually added in. We should update controller-gen - # to handle []*int properly itself. - nullable: true - type: array - attributes: - items: - properties: - handle_id: - type: string - secondary: - additionalProperties: - type: string - type: object - type: object - type: array - cidr: - type: string - deleted: - type: boolean - strictAffinity: - type: boolean - unallocated: - items: - type: integer - type: array - required: - - allocations - - attributes - - cidr - - strictAffinity - - unallocated - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_ipamconfigs.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: ipamconfigs.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: IPAMConfig - listKind: IPAMConfigList - plural: ipamconfigs - singular: ipamconfig - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPAMConfigSpec contains the specification for an IPAMConfig - resource. - properties: - autoAllocateBlocks: - type: boolean - maxBlocksPerHost: - description: MaxBlocksPerHost, if non-zero, is the max number of blocks - that can be affine to each host. - type: integer - strictAffinity: - type: boolean - required: - - autoAllocateBlocks - - strictAffinity - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_ipamhandles.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: ipamhandles.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: IPAMHandle - listKind: IPAMHandleList - plural: ipamhandles - singular: ipamhandle - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPAMHandleSpec contains the specification for an IPAMHandle - resource. - properties: - block: - additionalProperties: - type: integer - type: object - deleted: - type: boolean - handleID: - type: string - required: - - block - - handleID - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_ippools.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: ippools.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: IPPool - listKind: IPPoolList - plural: ippools - singular: ippool - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPPoolSpec contains the specification for an IPPool resource. - properties: - allowedUses: - description: AllowedUse controls what the IP pool will be used for. If - not specified or empty, defaults to ["Tunnel", "Workload"] for back-compatibility - items: - type: string - type: array - blockSize: - description: The block size to use for IP address assignments from - this pool. Defaults to 26 for IPv4 and 112 for IPv6. - type: integer - cidr: - description: The pool CIDR. - type: string - disabled: - description: When disabled is true, Calico IPAM will not assign addresses - from this pool. - type: boolean - disableBGPExport: - description: "Disable exporting routes from this IP Pool's CIDR over - BGP. [Default: false]" - type: boolean - ipip: - description: 'Deprecated: this field is only used for APIv1 backwards - compatibility. Setting this field is not allowed, this field is - for internal use only.' - properties: - enabled: - description: When enabled is true, ipip tunneling will be used - to deliver packets to destinations within this pool. - type: boolean - mode: - description: The IPIP mode. This can be one of "always" or "cross-subnet". A - mode of "always" will also use IPIP tunneling for routing to - destination IP addresses within this pool. A mode of "cross-subnet" - will only use IPIP tunneling when the destination node is on - a different subnet to the originating node. The default value - (if not specified) is "always". - type: string - type: object - ipipMode: - description: Contains configuration for IPIP tunneling for this pool. - If not specified, then this is defaulted to "Never" (i.e. IPIP tunneling - is disabled). - type: string - nat-outgoing: - description: 'Deprecated: this field is only used for APIv1 backwards - compatibility. Setting this field is not allowed, this field is - for internal use only.' - type: boolean - natOutgoing: - description: When nat-outgoing is true, packets sent from Calico networked - containers in this pool to destinations outside of this pool will - be masqueraded. - type: boolean - nodeSelector: - description: Allows IPPool to allocate for a specific node by label - selector. - type: string - vxlanMode: - description: Contains configuration for VXLAN tunneling for this pool. - If not specified, then this is defaulted to "Never" (i.e. VXLAN - tunneling is disabled). - type: string - required: - - cidr - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_ipreservations.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: ipreservations.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: IPReservation - listKind: IPReservationList - plural: ipreservations - singular: ipreservation - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IPReservationSpec contains the specification for an IPReservation - resource. - properties: - reservedCIDRs: - description: ReservedCIDRs is a list of CIDRs and/or IP addresses - that Calico IPAM will exclude from new allocations. - items: - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_kubecontrollersconfigurations.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: kubecontrollersconfigurations.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: KubeControllersConfiguration - listKind: KubeControllersConfigurationList - plural: kubecontrollersconfigurations - singular: kubecontrollersconfiguration - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: KubeControllersConfigurationSpec contains the values of the - Kubernetes controllers configuration. - properties: - controllers: - description: Controllers enables and configures individual Kubernetes - controllers - properties: - namespace: - description: Namespace enables and configures the namespace controller. - Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform reconciliation - with the Calico datastore. [Default: 5m]' - type: string - type: object - node: - description: Node enables and configures the node controller. - Enabled by default, set to nil to disable. - properties: - hostEndpoint: - description: HostEndpoint controls syncing nodes to host endpoints. - Disabled by default, set to nil to disable. - properties: - autoCreate: - description: 'AutoCreate enables automatic creation of - host endpoints for every node. [Default: Disabled]' - type: string - type: object - leakGracePeriod: - description: 'LeakGracePeriod is the period used by the controller - to determine if an IP address has been leaked. Set to 0 - to disable IP garbage collection. [Default: 15m]' - type: string - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform reconciliation - with the Calico datastore. [Default: 5m]' - type: string - syncLabels: - description: 'SyncLabels controls whether to copy Kubernetes - node labels to Calico nodes. [Default: Enabled]' - type: string - type: object - policy: - description: Policy enables and configures the policy controller. - Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform reconciliation - with the Calico datastore. [Default: 5m]' - type: string - type: object - serviceAccount: - description: ServiceAccount enables and configures the service - account controller. Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform reconciliation - with the Calico datastore. [Default: 5m]' - type: string - type: object - workloadEndpoint: - description: WorkloadEndpoint enables and configures the workload - endpoint controller. Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform reconciliation - with the Calico datastore. [Default: 5m]' - type: string - type: object - type: object - etcdV3CompactionPeriod: - description: 'EtcdV3CompactionPeriod is the period between etcdv3 - compaction requests. Set to 0 to disable. [Default: 10m]' - type: string - healthChecks: - description: 'HealthChecks enables or disables support for health - checks [Default: Enabled]' - type: string - logSeverityScreen: - description: 'LogSeverityScreen is the log severity above which logs - are sent to the stdout. [Default: Info]' - type: string - prometheusMetricsPort: - description: 'PrometheusMetricsPort is the TCP port that the Prometheus - metrics server should bind to. Set to 0 to disable. [Default: 9094]' - type: integer - required: - - controllers - type: object - status: - description: KubeControllersConfigurationStatus represents the status - of the configuration. It's useful for admins to be able to see the actual - config that was applied, which can be modified by environment variables - on the kube-controllers process. - properties: - environmentVars: - additionalProperties: - type: string - description: EnvironmentVars contains the environment variables on - the kube-controllers that influenced the RunningConfig. - type: object - runningConfig: - description: RunningConfig contains the effective config that is running - in the kube-controllers pod, after merging the API resource with - any environment variables. - properties: - controllers: - description: Controllers enables and configures individual Kubernetes - controllers - properties: - namespace: - description: Namespace enables and configures the namespace - controller. Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform - reconciliation with the Calico datastore. [Default: - 5m]' - type: string - type: object - node: - description: Node enables and configures the node controller. - Enabled by default, set to nil to disable. - properties: - hostEndpoint: - description: HostEndpoint controls syncing nodes to host - endpoints. Disabled by default, set to nil to disable. - properties: - autoCreate: - description: 'AutoCreate enables automatic creation - of host endpoints for every node. [Default: Disabled]' - type: string - type: object - leakGracePeriod: - description: 'LeakGracePeriod is the period used by the - controller to determine if an IP address has been leaked. - Set to 0 to disable IP garbage collection. [Default: - 15m]' - type: string - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform - reconciliation with the Calico datastore. [Default: - 5m]' - type: string - syncLabels: - description: 'SyncLabels controls whether to copy Kubernetes - node labels to Calico nodes. [Default: Enabled]' - type: string - type: object - policy: - description: Policy enables and configures the policy controller. - Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform - reconciliation with the Calico datastore. [Default: - 5m]' - type: string - type: object - serviceAccount: - description: ServiceAccount enables and configures the service - account controller. Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform - reconciliation with the Calico datastore. [Default: - 5m]' - type: string - type: object - workloadEndpoint: - description: WorkloadEndpoint enables and configures the workload - endpoint controller. Enabled by default, set to nil to disable. - properties: - reconcilerPeriod: - description: 'ReconcilerPeriod is the period to perform - reconciliation with the Calico datastore. [Default: - 5m]' - type: string - type: object - type: object - etcdV3CompactionPeriod: - description: 'EtcdV3CompactionPeriod is the period between etcdv3 - compaction requests. Set to 0 to disable. [Default: 10m]' - type: string - healthChecks: - description: 'HealthChecks enables or disables support for health - checks [Default: Enabled]' - type: string - logSeverityScreen: - description: 'LogSeverityScreen is the log severity above which - logs are sent to the stdout. [Default: Info]' - type: string - prometheusMetricsPort: - description: 'PrometheusMetricsPort is the TCP port that the Prometheus - metrics server should bind to. Set to 0 to disable. [Default: - 9094]' - type: integer - required: - - controllers - type: object - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_networkpolicies.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: networkpolicies.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: NetworkPolicy - listKind: NetworkPolicyList - plural: networkpolicies - singular: networkpolicy - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - properties: - egress: - description: The ordered set of egress rules. Each rule contains - a set of packet match criteria and a corresponding action to apply. - items: - description: "A Rule encapsulates a set of match criteria and an - action. Both selector-based security Policy and security Profiles - reference rules - separated out as a list of rules for both ingress - and egress packet matching. \n Each positive match criteria has - a negated version, prefixed with \"Not\". All the match criteria - within a rule must be satisfied for a packet to match. A single - rule can contain the positive and negative version of a match - and both must be satisfied for the rule to match." - properties: - action: - type: string - destination: - description: Destination contains the match criteria that apply - to destination entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - http: - description: HTTP contains match criteria that apply to HTTP - requests. - properties: - methods: - description: Methods is an optional field that restricts - the rule to apply only to HTTP requests that use one of - the listed HTTP Methods (e.g. GET, PUT, etc.) Multiple - methods are OR'd together. - items: - type: string - type: array - paths: - description: 'Paths is an optional field that restricts - the rule to apply to HTTP requests that use one of the - listed HTTP Paths. Multiple paths are OR''d together. - e.g: - exact: /foo - prefix: /bar NOTE: Each entry may - ONLY specify either a `exact` or a `prefix` match. The - validator will check for it.' - items: - description: 'HTTPPath specifies an HTTP path to match. - It may be either of the form: exact: : which matches - the path exactly or prefix: : which matches - the path prefix' - properties: - exact: - type: string - prefix: - type: string - type: object - type: array - type: object - icmp: - description: ICMP is an optional field that restricts the rule - to apply to a specific type and code of ICMP traffic. This - should only be specified if the Protocol field is set to "ICMP" - or "ICMPv6". - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - ipVersion: - description: IPVersion is an optional field that restricts the - rule to only match a specific IP version. - type: integer - metadata: - description: Metadata contains additional information for this - rule - properties: - annotations: - additionalProperties: - type: string - description: Annotations is a set of key value pairs that - give extra information about the rule - type: object - type: object - notICMP: - description: NotICMP is the negated version of the ICMP field. - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - notProtocol: - anyOf: - - type: integer - - type: string - description: NotProtocol is the negated version of the Protocol - field. - pattern: ^.* - x-kubernetes-int-or-string: true - protocol: - anyOf: - - type: integer - - type: string - description: "Protocol is an optional field that restricts the - rule to only apply to traffic of a specific IP protocol. Required - if any of the EntityRules contain Ports (because ports only - apply to certain protocols). \n Must be one of these string - values: \"TCP\", \"UDP\", \"ICMP\", \"ICMPv6\", \"SCTP\", - \"UDPLite\" or an integer in the range 1-255." - pattern: ^.* - x-kubernetes-int-or-string: true - source: - description: Source contains the match criteria that apply to - source entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - required: - - action - type: object - type: array - ingress: - description: The ordered set of ingress rules. Each rule contains - a set of packet match criteria and a corresponding action to apply. - items: - description: "A Rule encapsulates a set of match criteria and an - action. Both selector-based security Policy and security Profiles - reference rules - separated out as a list of rules for both ingress - and egress packet matching. \n Each positive match criteria has - a negated version, prefixed with \"Not\". All the match criteria - within a rule must be satisfied for a packet to match. A single - rule can contain the positive and negative version of a match - and both must be satisfied for the rule to match." - properties: - action: - type: string - destination: - description: Destination contains the match criteria that apply - to destination entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - http: - description: HTTP contains match criteria that apply to HTTP - requests. - properties: - methods: - description: Methods is an optional field that restricts - the rule to apply only to HTTP requests that use one of - the listed HTTP Methods (e.g. GET, PUT, etc.) Multiple - methods are OR'd together. - items: - type: string - type: array - paths: - description: 'Paths is an optional field that restricts - the rule to apply to HTTP requests that use one of the - listed HTTP Paths. Multiple paths are OR''d together. - e.g: - exact: /foo - prefix: /bar NOTE: Each entry may - ONLY specify either a `exact` or a `prefix` match. The - validator will check for it.' - items: - description: 'HTTPPath specifies an HTTP path to match. - It may be either of the form: exact: : which matches - the path exactly or prefix: : which matches - the path prefix' - properties: - exact: - type: string - prefix: - type: string - type: object - type: array - type: object - icmp: - description: ICMP is an optional field that restricts the rule - to apply to a specific type and code of ICMP traffic. This - should only be specified if the Protocol field is set to "ICMP" - or "ICMPv6". - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - ipVersion: - description: IPVersion is an optional field that restricts the - rule to only match a specific IP version. - type: integer - metadata: - description: Metadata contains additional information for this - rule - properties: - annotations: - additionalProperties: - type: string - description: Annotations is a set of key value pairs that - give extra information about the rule - type: object - type: object - notICMP: - description: NotICMP is the negated version of the ICMP field. - properties: - code: - description: Match on a specific ICMP code. If specified, - the Type value must also be specified. This is a technical - limitation imposed by the kernel's iptables firewall, - which Calico uses to enforce the rule. - type: integer - type: - description: Match on a specific ICMP type. For example - a value of 8 refers to ICMP Echo Request (i.e. pings). - type: integer - type: object - notProtocol: - anyOf: - - type: integer - - type: string - description: NotProtocol is the negated version of the Protocol - field. - pattern: ^.* - x-kubernetes-int-or-string: true - protocol: - anyOf: - - type: integer - - type: string - description: "Protocol is an optional field that restricts the - rule to only apply to traffic of a specific IP protocol. Required - if any of the EntityRules contain Ports (because ports only - apply to certain protocols). \n Must be one of these string - values: \"TCP\", \"UDP\", \"ICMP\", \"ICMPv6\", \"SCTP\", - \"UDPLite\" or an integer in the range 1-255." - pattern: ^.* - x-kubernetes-int-or-string: true - source: - description: Source contains the match criteria that apply to - source entity. - properties: - namespaceSelector: - description: "NamespaceSelector is an optional field that - contains a selector expression. Only traffic that originates - from (or terminates at) endpoints within the selected - namespaces will be matched. When both NamespaceSelector - and another selector are defined on the same rule, then - only workload endpoints that are matched by both selectors - will be selected by the rule. \n For NetworkPolicy, an - empty NamespaceSelector implies that the Selector is limited - to selecting only workload endpoints in the same namespace - as the NetworkPolicy. \n For NetworkPolicy, `global()` - NamespaceSelector implies that the Selector is limited - to selecting only GlobalNetworkSet or HostEndpoint. \n - For GlobalNetworkPolicy, an empty NamespaceSelector implies - the Selector applies to workload endpoints across all - namespaces." - type: string - nets: - description: Nets is an optional field that restricts the - rule to only apply to traffic that originates from (or - terminates at) IP addresses in any of the given subnets. - items: - type: string - type: array - notNets: - description: NotNets is the negated version of the Nets - field. - items: - type: string - type: array - notPorts: - description: NotPorts is the negated version of the Ports - field. Since only some protocols have ports, if any ports - are specified it requires the Protocol match in the Rule - to be set to "TCP" or "UDP". - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - notSelector: - description: NotSelector is the negated version of the Selector - field. See Selector field for subtleties with negated - selectors. - type: string - ports: - description: "Ports is an optional field that restricts - the rule to only apply to traffic that has a source (destination) - port that matches one of these ranges/values. This value - is a list of integers or strings that represent ranges - of ports. \n Since only some protocols have ports, if - any ports are specified it requires the Protocol match - in the Rule to be set to \"TCP\" or \"UDP\"." - items: - anyOf: - - type: integer - - type: string - pattern: ^.* - x-kubernetes-int-or-string: true - type: array - selector: - description: "Selector is an optional field that contains - a selector expression (see Policy for sample syntax). - \ Only traffic that originates from (terminates at) endpoints - matching the selector will be matched. \n Note that: in - addition to the negated version of the Selector (see NotSelector - below), the selector expression syntax itself supports - negation. The two types of negation are subtly different. - One negates the set of matched endpoints, the other negates - the whole match: \n \tSelector = \"!has(my_label)\" matches - packets that are from other Calico-controlled \tendpoints - that do not have the label \"my_label\". \n \tNotSelector - = \"has(my_label)\" matches packets that are not from - Calico-controlled \tendpoints that do have the label \"my_label\". - \n The effect is that the latter will accept packets from - non-Calico sources whereas the former is limited to packets - from Calico-controlled endpoints." - type: string - serviceAccounts: - description: ServiceAccounts is an optional field that restricts - the rule to only apply to traffic that originates from - (or terminates at) a pod running as a matching service - account. - properties: - names: - description: Names is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account whose name is in the list. - items: - type: string - type: array - selector: - description: Selector is an optional field that restricts - the rule to only apply to traffic that originates - from (or terminates at) a pod running as a service - account that matches the given label selector. If - both Names and Selector are specified then they are - AND'ed. - type: string - type: object - services: - description: "Services is an optional field that contains - options for matching Kubernetes Services. If specified, - only traffic that originates from or terminates at endpoints - within the selected service(s) will be matched, and only - to/from each endpoint's port. \n Services cannot be specified - on the same rule as Selector, NotSelector, NamespaceSelector, - Nets, NotNets or ServiceAccounts. \n Ports and NotPorts - can only be specified with Services on ingress rules." - properties: - name: - description: Name specifies the name of a Kubernetes - Service to match. - type: string - namespace: - description: Namespace specifies the namespace of the - given Service. If left empty, the rule will match - within this policy's namespace. - type: string - type: object - type: object - required: - - action - type: object - type: array - order: - description: Order is an optional field that specifies the order in - which the policy is applied. Policies with higher "order" are applied - after those with lower order. If the order is omitted, it may be - considered to be "infinite" - i.e. the policy will be applied last. Policies - with identical order will be applied in alphanumerical order based - on the Policy "Name". - type: number - selector: - description: "The selector is an expression used to pick pick out - the endpoints that the policy should be applied to. \n Selector - expressions follow this syntax: \n \tlabel == \"string_literal\" - \ -> comparison, e.g. my_label == \"foo bar\" \tlabel != \"string_literal\" - \ -> not equal; also matches if label is not present \tlabel in - { \"a\", \"b\", \"c\", ... } -> true if the value of label X is - one of \"a\", \"b\", \"c\" \tlabel not in { \"a\", \"b\", \"c\", - ... } -> true if the value of label X is not one of \"a\", \"b\", - \"c\" \thas(label_name) -> True if that label is present \t! expr - -> negation of expr \texpr && expr -> Short-circuit and \texpr - || expr -> Short-circuit or \t( expr ) -> parens for grouping \tall() - or the empty selector -> matches all endpoints. \n Label names are - allowed to contain alphanumerics, -, _ and /. String literals are - more permissive but they do not support escape characters. \n Examples - (with made-up labels): \n \ttype == \"webserver\" && deployment - == \"prod\" \ttype in {\"frontend\", \"backend\"} \tdeployment != - \"dev\" \t! has(label_name)" - type: string - serviceAccountSelector: - description: ServiceAccountSelector is an optional field for an expression - used to select a pod based on service accounts. - type: string - types: - description: "Types indicates whether this policy applies to ingress, - or to egress, or to both. When not explicitly specified (and so - the value on creation is empty or nil), Calico defaults Types according - to what Ingress and Egress are present in the policy. The default - is: \n - [ PolicyTypeIngress ], if there are no Egress rules (including - the case where there are also no Ingress rules) \n - [ PolicyTypeEgress - ], if there are Egress rules but no Ingress rules \n - [ PolicyTypeIngress, - PolicyTypeEgress ], if there are both Ingress and Egress rules. - \n When the policy is read back again, Types will always be one - of these values, never empty or nil." - items: - description: PolicyType enumerates the possible values of the PolicySpec - Types field. - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/calico/kdd/crd.projectcalico.org_networksets.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: networksets.crd.projectcalico.org -spec: - group: crd.projectcalico.org - names: - kind: NetworkSet - listKind: NetworkSetList - plural: networksets - singular: networkset - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: NetworkSet is the Namespaced-equivalent of the GlobalNetworkSet. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: NetworkSetSpec contains the specification for a NetworkSet - resource. - properties: - nets: - description: The list of IP networks that belong to this set. - items: - type: string - type: array - type: object - type: object - served: true - storage: true -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/operator.tigera.io_apiservers_crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.3.0 - name: apiservers.operator.tigera.io -spec: - group: operator.tigera.io - names: - kind: APIServer - listKind: APIServerList - plural: apiservers - singular: apiserver - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: APIServer installs the Tigera API server and related resources. - At most one instance of this resource is supported. It must be named "tigera-secure". - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Specification of the desired state for the Tigera API server. - type: object - status: - description: Most recently observed status for the Tigera API server. - properties: - state: - description: State provides user-readable status. - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} - ---- -# Source: crds/operator.tigera.io_imagesets_crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.3.0 - name: imagesets.operator.tigera.io -spec: - group: operator.tigera.io - names: - kind: ImageSet - listKind: ImageSetList - plural: imagesets - singular: imageset - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: ImageSet is used to specify image digests for the images that - the operator deploys. The name of the ImageSet is expected to be in the - format `-`. The `variant` used is `enterprise` if the - InstallationSpec Variant is `TigeraSecureEnterprise` otherwise it is `calico`. - The `release` must match the version of the variant that the operator is - built to deploy, this version can be obtained by passing the `--version` - flag to the operator binary. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: ImageSetSpec defines the desired state of ImageSet. - properties: - images: - description: Images is the list of images to use digests. All images - that the operator will deploy must be specified. - items: - properties: - digest: - description: Digest is the image identifier that will be used - for the Image. The field should not include a leading `@` - and must be prefixed with `sha256:`. - type: string - image: - description: Image is an image that the operator deploys and - instead of using the built in tag the operator will use the - Digest for the image identifier. The value should be the image - name without registry or tag or digest. For the image `docker.io/calico/node:v3.17.1` - it should be represented as `calico/node` - type: string - required: - - digest - - image - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/operator.tigera.io_installations_crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.3.0 - name: installations.operator.tigera.io -spec: - group: operator.tigera.io - names: - kind: Installation - listKind: InstallationList - plural: installations - singular: installation - scope: Cluster - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Installation configures an installation of Calico or Calico Enterprise. - At most one instance of this resource is supported. It must be named "default". - The Installation API installs core networking and network policy components, - and provides general install-time configuration. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Specification of the desired state for the Calico or Calico - Enterprise installation. - properties: - calicoNetwork: - description: CalicoNetwork specifies networking configuration options - for Calico. - properties: - bgp: - description: BGP configures whether or not to enable Calico's - BGP capabilities. - enum: - - Enabled - - Disabled - type: string - containerIPForwarding: - description: 'ContainerIPForwarding configures whether ip forwarding - will be enabled for containers in the CNI configuration. Default: - Disabled' - enum: - - Enabled - - Disabled - type: string - hostPorts: - description: 'HostPorts configures whether or not Calico will - support Kubernetes HostPorts. Valid only when using the Calico - CNI plugin. Default: Enabled' - enum: - - Enabled - - Disabled - type: string - ipPools: - description: IPPools contains a list of IP pools to create if - none exist. At most one IP pool of each address family may be - specified. If omitted, a single pool will be configured if needed. - items: - properties: - blockSize: - description: 'BlockSize specifies the CIDR prefex length - to use when allocating per-node IP blocks from the main - IP pool CIDR. Default: 26 (IPv4), 122 (IPv6)' - format: int32 - type: integer - cidr: - description: CIDR contains the address range for the IP - Pool in classless inter-domain routing format. - type: string - encapsulation: - description: 'Encapsulation specifies the encapsulation - type that will be used with the IP Pool. Default: IPIP' - enum: - - IPIPCrossSubnet - - IPIP - - VXLAN - - VXLANCrossSubnet - - None - type: string - natOutgoing: - description: 'NATOutgoing specifies if NAT will be enabled - or disabled for outgoing traffic. Default: Enabled' - enum: - - Enabled - - Disabled - type: string - nodeSelector: - description: 'NodeSelector specifies the node selector that - will be set for the IP Pool. Default: ''all()''' - type: string - required: - - cidr - type: object - type: array - linuxDataplane: - description: 'LinuxDataplane is used to select the dataplane used - for Linux nodes. In particular, it causes the operator to add - required mounts and environment variables for the particular - dataplane. If not specified, iptables mode is used. Default: - Iptables' - enum: - - Iptables - - BPF - - VPP - type: string - mtu: - description: MTU specifies the maximum transmission unit to use - on the pod network. If not specified, Calico will perform MTU - auto-detection based on the cluster network. - format: int32 - type: integer - multiInterfaceMode: - description: 'MultiInterfaceMode configures what will configure - multiple interface per pod. Only valid for Calico Enterprise - installations using the Calico CNI plugin. Default: None' - enum: - - None - - Multus - type: string - nodeAddressAutodetectionV4: - description: NodeAddressAutodetectionV4 specifies an approach - to automatically detect node IPv4 addresses. If not specified, - will use default auto-detection settings to acquire an IPv4 - address for each node. - properties: - canReach: - description: CanReach enables IP auto-detection based on which - source address on the node is used to reach the specified - IP or domain. - type: string - cidrs: - description: CIDRS enables IP auto-detection based on which - addresses on the nodes are within one of the provided CIDRs. - items: - type: string - type: array - firstFound: - description: FirstFound uses default interface matching parameters - to select an interface, performing best-effort filtering - based on well-known interface names. - type: boolean - interface: - description: Interface enables IP auto-detection based on - interfaces that match the given regex. - type: string - kubernetes: - description: Kubernetes configures Calico to detect node addresses - based on the Kubernetes API. - enum: - - NodeInternalIP - type: string - skipInterface: - description: SkipInterface enables IP auto-detection based - on interfaces that do not match the given regex. - type: string - type: object - nodeAddressAutodetectionV6: - description: NodeAddressAutodetectionV6 specifies an approach - to automatically detect node IPv6 addresses. If not specified, - IPv6 addresses will not be auto-detected. - properties: - canReach: - description: CanReach enables IP auto-detection based on which - source address on the node is used to reach the specified - IP or domain. - type: string - cidrs: - description: CIDRS enables IP auto-detection based on which - addresses on the nodes are within one of the provided CIDRs. - items: - type: string - type: array - firstFound: - description: FirstFound uses default interface matching parameters - to select an interface, performing best-effort filtering - based on well-known interface names. - type: boolean - interface: - description: Interface enables IP auto-detection based on - interfaces that match the given regex. - type: string - kubernetes: - description: Kubernetes configures Calico to detect node addresses - based on the Kubernetes API. - enum: - - NodeInternalIP - type: string - skipInterface: - description: SkipInterface enables IP auto-detection based - on interfaces that do not match the given regex. - type: string - type: object - type: object - certificateManagement: - description: CertificateManagement configures pods to submit a CertificateSigningRequest - to the certificates.k8s.io/v1beta1 API in order to obtain TLS certificates. - This feature requires that you bring your own CSR signing and approval - process, otherwise pods will be stuck during initialization. - properties: - caCert: - description: Certificate of the authority that signs the CertificateSigningRequests - in PEM format. - format: byte - type: string - keyAlgorithm: - description: 'Specify the algorithm used by pods to generate a - key pair that is associated with the X.509 certificate request. - Default: RSAWithSize2048' - enum: - - "" - - RSAWithSize2048 - - RSAWithSize4096 - - RSAWithSize8192 - - ECDSAWithCurve256 - - ECDSAWithCurve384 - - ECDSAWithCurve521 - type: string - signatureAlgorithm: - description: 'Specify the algorithm used for the signature of - the X.509 certificate request. Default: SHA256WithRSA' - enum: - - "" - - SHA256WithRSA - - SHA384WithRSA - - SHA512WithRSA - - ECDSAWithSHA256 - - ECDSAWithSHA384 - - ECDSAWithSHA512 - type: string - signerName: - description: 'When a CSR is issued to the certificates.k8s.io - API, the signerName is added to the request in order to accommodate - for clusters with multiple signers. Must be formatted as: `/`.' - type: string - required: - - caCert - - signerName - type: object - cni: - description: CNI specifies the CNI that will be used by this installation. - properties: - ipam: - description: IPAM specifies the pod IP address management that - will be used in the Calico or Calico Enterprise installation. - properties: - type: - description: "Specifies the IPAM plugin that will be used - in the Calico or Calico Enterprise installation. * For CNI - Plugin Calico, this field defaults to Calico. * For CNI - Plugin GKE, this field defaults to HostLocal. * For CNI - Plugin AzureVNET, this field defaults to AzureVNET. * For - CNI Plugin AmazonVPC, this field defaults to AmazonVPC. - \n The IPAM plugin is installed and configured only if the - CNI plugin is set to Calico, for all other values of the - CNI plugin the plugin binaries and CNI config is a dependency - that is expected to be installed separately. \n Default: - Calico" - enum: - - Calico - - HostLocal - - AmazonVPC - - AzureVNET - type: string - required: - - type - type: object - type: - description: "Specifies the CNI plugin that will be used in the - Calico or Calico Enterprise installation. * For KubernetesProvider - GKE, this field defaults to GKE. * For KubernetesProvider AKS, - this field defaults to AzureVNET. * For KubernetesProvider EKS, - this field defaults to AmazonVPC. * If aws-node daemonset exists - in kube-system when the Installation resource is created, this - field defaults to AmazonVPC. * For all other cases this field - defaults to Calico. \n For the value Calico, the CNI plugin - binaries and CNI config will be installed as part of deployment, - for all other values the CNI plugin binaries and CNI config - is a dependency that is expected to be installed separately. - \n Default: Calico" - enum: - - Calico - - GKE - - AmazonVPC - - AzureVNET - type: string - required: - - type - type: object - componentResources: - description: ComponentResources can be used to customize the resource - requirements for each component. Node, Typha, and KubeControllers - are supported for installations. - items: - description: The ComponentResource struct associates a ResourceRequirements - with a component by name - properties: - componentName: - description: ComponentName is an enum which identifies the component - enum: - - Node - - Typha - - KubeControllers - type: string - resourceRequirements: - description: ResourceRequirements allows customization of limits - and requests for compute resources such as cpu and memory. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - required: - - componentName - - resourceRequirements - type: object - type: array - controlPlaneNodeSelector: - additionalProperties: - type: string - description: ControlPlaneNodeSelector is used to select control plane - nodes on which to run Calico components. This is globally applied - to all resources created by the operator excluding daemonsets. - type: object - controlPlaneReplicas: - description: ControlPlaneReplicas defines how many replicas of the - control plane core components will be deployed. This field applies - to all control plane components that support High Availability. - Defaults to 2. - format: int32 - type: integer - controlPlaneTolerations: - description: ControlPlaneTolerations specify tolerations which are - then globally applied to all resources created by the operator. - items: - description: The pod this Toleration is attached to tolerates any - taint that matches the triple using the matching - operator . - properties: - effect: - description: Effect indicates the taint effect to match. Empty - means match all taint effects. When specified, allowed values - are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match all - values and all keys. - type: string - operator: - description: Operator represents a key's relationship to the - value. Valid operators are Exists and Equal. Defaults to Equal. - Exists is equivalent to wildcard for value, so that a pod - can tolerate all taints of a particular category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of time - the toleration (which must be of effect NoExecute, otherwise - this field is ignored) tolerates the taint. By default, it - is not set, which means tolerate the taint forever (do not - evict). Zero and negative values will be treated as 0 (evict - immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - flexVolumePath: - description: FlexVolumePath optionally specifies a custom path for - FlexVolume. If not specified, FlexVolume will be enabled by default. - If set to 'None', FlexVolume will be disabled. The default is based - on the kubernetesProvider. - type: string - imagePath: - description: "ImagePath allows for the path part of an image to be - specified. If specified then the specified value will be used as - the image path for each image. If not specified or empty, the default - for each image will be used. A special case value, UseDefault, is - supported to explicitly specify the default image path will be used - for each image. \n Image format: `/:` - \n This option allows configuring the `` portion of the - above format." - type: string - imagePrefix: - description: "ImagePrefix allows for the prefix part of an image to - be specified. If specified then the given value will be used as - a prefix on each image. If not specified or empty, no prefix will - be used. A special case value, UseDefault, is supported to explicitly - specify the default image prefix will be used for each image. \n - Image format: `/:` - \n This option allows configuring the `` portion of - the above format." - type: string - imagePullSecrets: - description: ImagePullSecrets is an array of references to container - registry pull secrets to use. These are applied to all images to - be pulled. - items: - description: LocalObjectReference contains enough information to - let you locate the referenced object inside the same namespace. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - type: array - kubernetesProvider: - description: KubernetesProvider specifies a particular provider of - the Kubernetes platform and enables provider-specific configuration. - If the specified value is empty, the Operator will attempt to automatically - determine the current provider. If the specified value is not empty, - the Operator will still attempt auto-detection, but will additionally - compare the auto-detected value to the specified value to confirm - they match. - enum: - - "" - - EKS - - GKE - - AKS - - OpenShift - - DockerEnterprise - type: string - nodeMetricsPort: - description: NodeMetricsPort specifies which port calico/node serves - prometheus metrics on. By default, metrics are not enabled. If specified, - this overrides any FelixConfiguration resources which may exist. - If omitted, then prometheus metrics may still be configured through - FelixConfiguration. - format: int32 - type: integer - nodeUpdateStrategy: - description: NodeUpdateStrategy can be used to customize the desired - update strategy, such as the MaxUnavailable field. - properties: - rollingUpdate: - description: 'Rolling update config params. Present only if type - = "RollingUpdate". --- TODO: Update this to follow our convention - for oneOf, whatever we decide it to be. Same as Deployment `strategy.rollingUpdate`. - See https://github.com/kubernetes/kubernetes/issues/35345' - properties: - maxSurge: - anyOf: - - type: integer - - type: string - description: 'The maximum number of nodes with an existing - available DaemonSet pod that can have an updated DaemonSet - pod during during an update. Value can be an absolute number - (ex: 5) or a percentage of desired pods (ex: 10%). This - can not be 0 if MaxUnavailable is 0. Absolute number is - calculated from percentage by rounding up to a minimum of - 1. Default value is 0. Example: when this is set to 30%, - at most 30% of the total number of nodes that should be - running the daemon pod (i.e. status.desiredNumberScheduled) - can have their a new pod created before the old pod is marked - as deleted. The update starts by launching new pods on 30% - of nodes. Once an updated pod is available (Ready for at - least minReadySeconds) the old DaemonSet pod on that node - is marked deleted. If the old pod becomes unavailable for - any reason (Ready transitions to false, is evicted, or is - drained) an updated pod is immediatedly created on that - node without considering surge limits. Allowing surge implies - the possibility that the resources consumed by the daemonset - on any given node can double if the readiness check fails, - and so resource intensive daemonsets should take into account - that they may cause evictions during disruption. This is - an alpha field and requires enabling DaemonSetUpdateSurge - feature gate.' - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: 'The maximum number of DaemonSet pods that can - be unavailable during the update. Value can be an absolute - number (ex: 5) or a percentage of total number of DaemonSet - pods at the start of the update (ex: 10%). Absolute number - is calculated from percentage by rounding down to a minimum - of one. This cannot be 0 if MaxSurge is 0 Default value - is 1. Example: when this is set to 30%, at most 30% of the - total number of nodes that should be running the daemon - pod (i.e. status.desiredNumberScheduled) can have their - pods stopped for an update at any given time. The update - starts by stopping at most 30% of those DaemonSet pods and - then brings up new DaemonSet pods in their place. Once the - new pods are available, it then proceeds onto other DaemonSet - pods, thus ensuring that at least 70% of original number - of DaemonSet pods are available at all times during the - update.' - x-kubernetes-int-or-string: true - type: object - type: - description: Type of daemon set update. Can be "RollingUpdate" - or "OnDelete". Default is RollingUpdate. - type: string - type: object - nonPrivileged: - description: NonPrivileged configures Calico to be run in non-privileged - containers as non-root users where possible. - type: string - registry: - description: "Registry is the default Docker registry used for component - Docker images. If specified then the given value must end with a - slash character (`/`) and all images will be pulled from this registry. - If not specified then the default registries will be used. A special - case value, UseDefault, is supported to explicitly specify the default - registries will be used. \n Image format: `/:` - \n This option allows configuring the `` portion of the - above format." - type: string - typhaAffinity: - description: TyphaAffinity allows configuration of node affinity characteristics - for Typha pods. - properties: - nodeAffinity: - description: NodeAffinity describes node affinity scheduling rules - for typha. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods to - nodes that satisfy the affinity expressions specified by - this field, but it may choose a node that violates one or - more of the expressions. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects (i.e. - is also a no-op). - properties: - preference: - description: A node selector term, associated with the - corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - weight: - description: Weight associated with matching the corresponding - nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: 'WARNING: Please note that if the affinity requirements - specified by this field are not met at scheduling time, - the pod will NOT be scheduled onto the node. There is no - fallback to another affinity rules with this setting. This - may cause networking disruption or even catastrophic failure! - PreferredDuringSchedulingIgnoredDuringExecution should be - used for affinity unless there is a specific well understood - reason to use RequiredDuringSchedulingIgnoredDuringExecution - and you can guarantee that the RequiredDuringSchedulingIgnoredDuringExecution - will always have sufficient nodes to satisfy the requirement. - NOTE: RequiredDuringSchedulingIgnoredDuringExecution is - set by default for AKS nodes, to avoid scheduling Typhas - on virtual-nodes. If the affinity requirements specified - by this field cease to be met at some point during pod execution - (e.g. due to an update), the system may or may not try to - eventually evict the pod from its node.' - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term matches - no objects. The requirements of them are ANDed. The - TopologySelectorTerm type implements a subset of the - NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists, DoesNotExist. Gt, and - Lt. - type: string - values: - description: An array of string values. If - the operator is In or NotIn, the values - array must be non-empty. If the operator - is Exists or DoesNotExist, the values array - must be empty. If the operator is Gt or - Lt, the values array must have a single - element, which will be interpreted as an - integer. This array is replaced during a - strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - type: array - required: - - nodeSelectorTerms - type: object - type: object - type: object - typhaMetricsPort: - description: TyphaMetricsPort specifies which port calico/typha serves - prometheus metrics on. By default, metrics are not enabled. - format: int32 - type: integer - variant: - description: 'Variant is the product to install - one of Calico or - TigeraSecureEnterprise Default: Calico' - enum: - - Calico - - TigeraSecureEnterprise - type: string - type: object - status: - description: Most recently observed state for the Calico or Calico Enterprise - installation. - properties: - computed: - description: Computed is the final installation including overlaid - resources. - properties: - calicoNetwork: - description: CalicoNetwork specifies networking configuration - options for Calico. - properties: - bgp: - description: BGP configures whether or not to enable Calico's - BGP capabilities. - enum: - - Enabled - - Disabled - type: string - containerIPForwarding: - description: 'ContainerIPForwarding configures whether ip - forwarding will be enabled for containers in the CNI configuration. - Default: Disabled' - enum: - - Enabled - - Disabled - type: string - hostPorts: - description: 'HostPorts configures whether or not Calico will - support Kubernetes HostPorts. Valid only when using the - Calico CNI plugin. Default: Enabled' - enum: - - Enabled - - Disabled - type: string - ipPools: - description: IPPools contains a list of IP pools to create - if none exist. At most one IP pool of each address family - may be specified. If omitted, a single pool will be configured - if needed. - items: - properties: - blockSize: - description: 'BlockSize specifies the CIDR prefex length - to use when allocating per-node IP blocks from the - main IP pool CIDR. Default: 26 (IPv4), 122 (IPv6)' - format: int32 - type: integer - cidr: - description: CIDR contains the address range for the - IP Pool in classless inter-domain routing format. - type: string - encapsulation: - description: 'Encapsulation specifies the encapsulation - type that will be used with the IP Pool. Default: - IPIP' - enum: - - IPIPCrossSubnet - - IPIP - - VXLAN - - VXLANCrossSubnet - - None - type: string - natOutgoing: - description: 'NATOutgoing specifies if NAT will be enabled - or disabled for outgoing traffic. Default: Enabled' - enum: - - Enabled - - Disabled - type: string - nodeSelector: - description: 'NodeSelector specifies the node selector - that will be set for the IP Pool. Default: ''all()''' - type: string - required: - - cidr - type: object - type: array - linuxDataplane: - description: 'LinuxDataplane is used to select the dataplane - used for Linux nodes. In particular, it causes the operator - to add required mounts and environment variables for the - particular dataplane. If not specified, iptables mode is - used. Default: Iptables' - enum: - - Iptables - - BPF - - VPP - type: string - mtu: - description: MTU specifies the maximum transmission unit to - use on the pod network. If not specified, Calico will perform - MTU auto-detection based on the cluster network. - format: int32 - type: integer - multiInterfaceMode: - description: 'MultiInterfaceMode configures what will configure - multiple interface per pod. Only valid for Calico Enterprise - installations using the Calico CNI plugin. Default: None' - enum: - - None - - Multus - type: string - nodeAddressAutodetectionV4: - description: NodeAddressAutodetectionV4 specifies an approach - to automatically detect node IPv4 addresses. If not specified, - will use default auto-detection settings to acquire an IPv4 - address for each node. - properties: - canReach: - description: CanReach enables IP auto-detection based - on which source address on the node is used to reach - the specified IP or domain. - type: string - cidrs: - description: CIDRS enables IP auto-detection based on - which addresses on the nodes are within one of the provided - CIDRs. - items: - type: string - type: array - firstFound: - description: FirstFound uses default interface matching - parameters to select an interface, performing best-effort - filtering based on well-known interface names. - type: boolean - interface: - description: Interface enables IP auto-detection based - on interfaces that match the given regex. - type: string - kubernetes: - description: Kubernetes configures Calico to detect node - addresses based on the Kubernetes API. - enum: - - NodeInternalIP - type: string - skipInterface: - description: SkipInterface enables IP auto-detection based - on interfaces that do not match the given regex. - type: string - type: object - nodeAddressAutodetectionV6: - description: NodeAddressAutodetectionV6 specifies an approach - to automatically detect node IPv6 addresses. If not specified, - IPv6 addresses will not be auto-detected. - properties: - canReach: - description: CanReach enables IP auto-detection based - on which source address on the node is used to reach - the specified IP or domain. - type: string - cidrs: - description: CIDRS enables IP auto-detection based on - which addresses on the nodes are within one of the provided - CIDRs. - items: - type: string - type: array - firstFound: - description: FirstFound uses default interface matching - parameters to select an interface, performing best-effort - filtering based on well-known interface names. - type: boolean - interface: - description: Interface enables IP auto-detection based - on interfaces that match the given regex. - type: string - kubernetes: - description: Kubernetes configures Calico to detect node - addresses based on the Kubernetes API. - enum: - - NodeInternalIP - type: string - skipInterface: - description: SkipInterface enables IP auto-detection based - on interfaces that do not match the given regex. - type: string - type: object - type: object - certificateManagement: - description: CertificateManagement configures pods to submit a - CertificateSigningRequest to the certificates.k8s.io/v1beta1 - API in order to obtain TLS certificates. This feature requires - that you bring your own CSR signing and approval process, otherwise - pods will be stuck during initialization. - properties: - caCert: - description: Certificate of the authority that signs the CertificateSigningRequests - in PEM format. - format: byte - type: string - keyAlgorithm: - description: 'Specify the algorithm used by pods to generate - a key pair that is associated with the X.509 certificate - request. Default: RSAWithSize2048' - enum: - - "" - - RSAWithSize2048 - - RSAWithSize4096 - - RSAWithSize8192 - - ECDSAWithCurve256 - - ECDSAWithCurve384 - - ECDSAWithCurve521 - type: string - signatureAlgorithm: - description: 'Specify the algorithm used for the signature - of the X.509 certificate request. Default: SHA256WithRSA' - enum: - - "" - - SHA256WithRSA - - SHA384WithRSA - - SHA512WithRSA - - ECDSAWithSHA256 - - ECDSAWithSHA384 - - ECDSAWithSHA512 - type: string - signerName: - description: 'When a CSR is issued to the certificates.k8s.io - API, the signerName is added to the request in order to - accommodate for clusters with multiple signers. Must be - formatted as: `/`.' - type: string - required: - - caCert - - signerName - type: object - cni: - description: CNI specifies the CNI that will be used by this installation. - properties: - ipam: - description: IPAM specifies the pod IP address management - that will be used in the Calico or Calico Enterprise installation. - properties: - type: - description: "Specifies the IPAM plugin that will be used - in the Calico or Calico Enterprise installation. * For - CNI Plugin Calico, this field defaults to Calico. * - For CNI Plugin GKE, this field defaults to HostLocal. - * For CNI Plugin AzureVNET, this field defaults to AzureVNET. - * For CNI Plugin AmazonVPC, this field defaults to AmazonVPC. - \n The IPAM plugin is installed and configured only - if the CNI plugin is set to Calico, for all other values - of the CNI plugin the plugin binaries and CNI config - is a dependency that is expected to be installed separately. - \n Default: Calico" - enum: - - Calico - - HostLocal - - AmazonVPC - - AzureVNET - type: string - required: - - type - type: object - type: - description: "Specifies the CNI plugin that will be used in - the Calico or Calico Enterprise installation. * For KubernetesProvider - GKE, this field defaults to GKE. * For KubernetesProvider - AKS, this field defaults to AzureVNET. * For KubernetesProvider - EKS, this field defaults to AmazonVPC. * If aws-node daemonset - exists in kube-system when the Installation resource is - created, this field defaults to AmazonVPC. * For all other - cases this field defaults to Calico. \n For the value Calico, - the CNI plugin binaries and CNI config will be installed - as part of deployment, for all other values the CNI plugin - binaries and CNI config is a dependency that is expected - to be installed separately. \n Default: Calico" - enum: - - Calico - - GKE - - AmazonVPC - - AzureVNET - type: string - required: - - type - type: object - componentResources: - description: ComponentResources can be used to customize the resource - requirements for each component. Node, Typha, and KubeControllers - are supported for installations. - items: - description: The ComponentResource struct associates a ResourceRequirements - with a component by name - properties: - componentName: - description: ComponentName is an enum which identifies the - component - enum: - - Node - - Typha - - KubeControllers - type: string - resourceRequirements: - description: ResourceRequirements allows customization of - limits and requests for compute resources such as cpu - and memory. - properties: - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of - compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is omitted - for a container, it defaults to Limits if that is - explicitly specified, otherwise to an implementation-defined - value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - required: - - componentName - - resourceRequirements - type: object - type: array - controlPlaneNodeSelector: - additionalProperties: - type: string - description: ControlPlaneNodeSelector is used to select control - plane nodes on which to run Calico components. This is globally - applied to all resources created by the operator excluding daemonsets. - type: object - controlPlaneReplicas: - description: ControlPlaneReplicas defines how many replicas of - the control plane core components will be deployed. This field - applies to all control plane components that support High Availability. - Defaults to 2. - format: int32 - type: integer - controlPlaneTolerations: - description: ControlPlaneTolerations specify tolerations which - are then globally applied to all resources created by the operator. - items: - description: The pod this Toleration is attached to tolerates - any taint that matches the triple using - the matching operator . - properties: - effect: - description: Effect indicates the taint effect to match. - Empty means match all taint effects. When specified, allowed - values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match - all values and all keys. - type: string - operator: - description: Operator represents a key's relationship to - the value. Valid operators are Exists and Equal. Defaults - to Equal. Exists is equivalent to wildcard for value, - so that a pod can tolerate all taints of a particular - category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of - time the toleration (which must be of effect NoExecute, - otherwise this field is ignored) tolerates the taint. - By default, it is not set, which means tolerate the taint - forever (do not evict). Zero and negative values will - be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - flexVolumePath: - description: FlexVolumePath optionally specifies a custom path - for FlexVolume. If not specified, FlexVolume will be enabled - by default. If set to 'None', FlexVolume will be disabled. The - default is based on the kubernetesProvider. - type: string - imagePath: - description: "ImagePath allows for the path part of an image to - be specified. If specified then the specified value will be - used as the image path for each image. If not specified or empty, - the default for each image will be used. A special case value, - UseDefault, is supported to explicitly specify the default image - path will be used for each image. \n Image format: `/:` - \n This option allows configuring the `` portion - of the above format." - type: string - imagePrefix: - description: "ImagePrefix allows for the prefix part of an image - to be specified. If specified then the given value will be used - as a prefix on each image. If not specified or empty, no prefix - will be used. A special case value, UseDefault, is supported - to explicitly specify the default image prefix will be used - for each image. \n Image format: `/:` - \n This option allows configuring the `` portion - of the above format." - type: string - imagePullSecrets: - description: ImagePullSecrets is an array of references to container - registry pull secrets to use. These are applied to all images - to be pulled. - items: - description: LocalObjectReference contains enough information - to let you locate the referenced object inside the same namespace. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - type: array - kubernetesProvider: - description: KubernetesProvider specifies a particular provider - of the Kubernetes platform and enables provider-specific configuration. - If the specified value is empty, the Operator will attempt to - automatically determine the current provider. If the specified - value is not empty, the Operator will still attempt auto-detection, - but will additionally compare the auto-detected value to the - specified value to confirm they match. - enum: - - "" - - EKS - - GKE - - AKS - - OpenShift - - DockerEnterprise - type: string - nodeMetricsPort: - description: NodeMetricsPort specifies which port calico/node - serves prometheus metrics on. By default, metrics are not enabled. - If specified, this overrides any FelixConfiguration resources - which may exist. If omitted, then prometheus metrics may still - be configured through FelixConfiguration. - format: int32 - type: integer - nodeUpdateStrategy: - description: NodeUpdateStrategy can be used to customize the desired - update strategy, such as the MaxUnavailable field. - properties: - rollingUpdate: - description: 'Rolling update config params. Present only if - type = "RollingUpdate". --- TODO: Update this to follow - our convention for oneOf, whatever we decide it to be. Same - as Deployment `strategy.rollingUpdate`. See https://github.com/kubernetes/kubernetes/issues/35345' - properties: - maxSurge: - anyOf: - - type: integer - - type: string - description: 'The maximum number of nodes with an existing - available DaemonSet pod that can have an updated DaemonSet - pod during during an update. Value can be an absolute - number (ex: 5) or a percentage of desired pods (ex: - 10%). This can not be 0 if MaxUnavailable is 0. Absolute - number is calculated from percentage by rounding up - to a minimum of 1. Default value is 0. Example: when - this is set to 30%, at most 30% of the total number - of nodes that should be running the daemon pod (i.e. - status.desiredNumberScheduled) can have their a new - pod created before the old pod is marked as deleted. - The update starts by launching new pods on 30% of nodes. - Once an updated pod is available (Ready for at least - minReadySeconds) the old DaemonSet pod on that node - is marked deleted. If the old pod becomes unavailable - for any reason (Ready transitions to false, is evicted, - or is drained) an updated pod is immediatedly created - on that node without considering surge limits. Allowing - surge implies the possibility that the resources consumed - by the daemonset on any given node can double if the - readiness check fails, and so resource intensive daemonsets - should take into account that they may cause evictions - during disruption. This is an alpha field and requires - enabling DaemonSetUpdateSurge feature gate.' - x-kubernetes-int-or-string: true - maxUnavailable: - anyOf: - - type: integer - - type: string - description: 'The maximum number of DaemonSet pods that - can be unavailable during the update. Value can be an - absolute number (ex: 5) or a percentage of total number - of DaemonSet pods at the start of the update (ex: 10%). - Absolute number is calculated from percentage by rounding - down to a minimum of one. This cannot be 0 if MaxSurge - is 0 Default value is 1. Example: when this is set to - 30%, at most 30% of the total number of nodes that should - be running the daemon pod (i.e. status.desiredNumberScheduled) - can have their pods stopped for an update at any given - time. The update starts by stopping at most 30% of those - DaemonSet pods and then brings up new DaemonSet pods - in their place. Once the new pods are available, it - then proceeds onto other DaemonSet pods, thus ensuring - that at least 70% of original number of DaemonSet pods - are available at all times during the update.' - x-kubernetes-int-or-string: true - type: object - type: - description: Type of daemon set update. Can be "RollingUpdate" - or "OnDelete". Default is RollingUpdate. - type: string - type: object - nonPrivileged: - description: NonPrivileged configures Calico to be run in non-privileged - containers as non-root users where possible. - type: string - registry: - description: "Registry is the default Docker registry used for - component Docker images. If specified then the given value must - end with a slash character (`/`) and all images will be pulled - from this registry. If not specified then the default registries - will be used. A special case value, UseDefault, is supported - to explicitly specify the default registries will be used. \n - Image format: `/:` - \n This option allows configuring the `` portion of - the above format." - type: string - typhaAffinity: - description: TyphaAffinity allows configuration of node affinity - characteristics for Typha pods. - properties: - nodeAffinity: - description: NodeAffinity describes node affinity scheduling - rules for typha. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects - (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated with - the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - weight: - description: Weight associated with matching the - corresponding nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: 'WARNING: Please note that if the affinity - requirements specified by this field are not met at - scheduling time, the pod will NOT be scheduled onto - the node. There is no fallback to another affinity rules - with this setting. This may cause networking disruption - or even catastrophic failure! PreferredDuringSchedulingIgnoredDuringExecution - should be used for affinity unless there is a specific - well understood reason to use RequiredDuringSchedulingIgnoredDuringExecution - and you can guarantee that the RequiredDuringSchedulingIgnoredDuringExecution - will always have sufficient nodes to satisfy the requirement. - NOTE: RequiredDuringSchedulingIgnoredDuringExecution - is set by default for AKS nodes, to avoid scheduling - Typhas on virtual-nodes. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from - its node.' - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term - matches no objects. The requirements of them are - ANDed. The TopologySelectorTerm type implements - a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - type: array - required: - - nodeSelectorTerms - type: object - type: object - type: object - typhaMetricsPort: - description: TyphaMetricsPort specifies which port calico/typha - serves prometheus metrics on. By default, metrics are not enabled. - format: int32 - type: integer - variant: - description: 'Variant is the product to install - one of Calico - or TigeraSecureEnterprise Default: Calico' - enum: - - Calico - - TigeraSecureEnterprise - type: string - type: object - imageSet: - description: ImageSet is the name of the ImageSet being used, if there - is an ImageSet that is being used. If an ImageSet is not being used - then this will not be set. - type: string - mtu: - description: MTU is the most recently observed value for pod network - MTU. This may be an explicitly configured value, or based on Calico's - native auto-detetion. - format: int32 - type: integer - variant: - description: Variant is the most recently observed installed variant - - one of Calico or TigeraSecureEnterprise - enum: - - Calico - - TigeraSecureEnterprise - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: crds/operator.tigera.io_tigerastatuses_crd.yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.3.0 - name: tigerastatuses.operator.tigera.io -spec: - group: operator.tigera.io - names: - kind: TigeraStatus - listKind: TigeraStatusList - plural: tigerastatuses - singular: tigerastatus - scope: Cluster - versions: - - additionalPrinterColumns: - - description: Whether the component running and stable. - jsonPath: .status.conditions[?(@.type=='Available')].status - name: Available - type: string - - description: Whether the component is processing changes. - jsonPath: .status.conditions[?(@.type=='Progressing')].status - name: Progressing - type: string - - description: Whether the component is degraded. - jsonPath: .status.conditions[?(@.type=='Degraded')].status - name: Degraded - type: string - - description: The time the component's Available status last changed. - jsonPath: .status.conditions[?(@.type=='Available')].lastTransitionTime - name: Since - type: date - name: v1 - schema: - openAPIV3Schema: - description: TigeraStatus represents the most recently observed status for - Calico or a Calico Enterprise functional area. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TigeraStatusSpec defines the desired state of TigeraStatus - type: object - status: - description: TigeraStatusStatus defines the observed state of TigeraStatus - properties: - conditions: - description: Conditions represents the latest observed set of conditions - for this component. A component may be one or more of Available, - Progressing, or Degraded. - items: - description: TigeraStatusCondition represents a condition attached - to a particular component. - properties: - lastTransitionTime: - description: The timestamp representing the start time for the - current status. - format: date-time - type: string - message: - description: Optionally, a detailed message providing additional - context. - type: string - reason: - description: A brief reason explaining the condition. - type: string - status: - description: The status of the condition. May be True, False, - or Unknown. - type: string - type: - description: The type of condition. May be Available, Progressing, - or Degraded. - type: string - required: - - lastTransitionTime - - status - - type - type: object - type: array - required: - - conditions - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -# Source: tigera-operator/templates/tigera-operator/00-namespace-tigera-operator.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: tigera-operator - annotations: - labels: - name: tigera-operator ---- -# Source: tigera-operator/templates/tigera-operator/02-podsecuritypolicy-tigera-operator.yaml -# This should not be rendered for an OpenShift install. -# OpenShift uses SecurityContextConstraints instead. -apiVersion: policy/v1beta1 -kind: PodSecurityPolicy -metadata: - name: tigera-operator - annotations: - seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' -spec: - privileged: false - allowPrivilegeEscalation: false - requiredDropCapabilities: - - ALL - volumes: - - 'hostPath' - - 'configMap' - - 'emptyDir' - - 'projected' - - 'secret' - - 'downwardAPI' - # Assume that persistentVolumes set up by the cluster admin are safe to use. - - 'persistentVolumeClaim' - hostNetwork: true - hostPorts: - - min: 0 - max: 65535 - hostIPC: false - hostPID: false - runAsUser: - rule: 'MustRunAsNonRoot' - seLinux: - rule: 'RunAsAny' - supplementalGroups: - rule: 'MustRunAs' - ranges: - # Forbid adding the root group. - - min: 1 - max: 65535 - fsGroup: - rule: 'MustRunAs' - ranges: - # Forbid adding the root group. - - min: 1 - max: 65535 - readOnlyRootFilesystem: false ---- -# Source: tigera-operator/templates/tigera-operator/02-serviceaccount-tigera-operator.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: tigera-operator - namespace: tigera-operator ---- -# Source: tigera-operator/templates/tigera-operator/02-role-tigera-operator.yaml -# Permissions required when running the operator for a Calico cluster. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: tigera-operator -rules: - - apiGroups: - - "" - resources: - - namespaces - - pods - - podtemplates - - services - - endpoints - - events - - configmaps - - secrets - - serviceaccounts - verbs: - - create - - get - - list - - update - - delete - - watch - - apiGroups: - - "" - resources: - - resourcequotas - verbs: - - list - - get - - watch - - apiGroups: - - "" - resources: - - resourcequotas - verbs: - - create - - get - - list - - update - - delete - - watch - resourceNames: - - calico-critical-pods - - tigera-critical-pods - - apiGroups: - - "" - resources: - - nodes - verbs: - # Need to update node labels when migrating nodes. - - get - - patch - - list - # We need this for Typha autoscaling - - watch - - apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterroles - - clusterrolebindings - - rolebindings - - roles - verbs: - - create - - get - - list - - update - - delete - - watch - - bind - - escalate - - apiGroups: - - apps - resources: - - deployments - - daemonsets - - statefulsets - verbs: - - create - - get - - list - - patch - - update - - delete - - watch - - apiGroups: - - apps - resourceNames: - - tigera-operator - resources: - - deployments/finalizers - verbs: - - update - - apiGroups: - - operator.tigera.io - resources: - - '*' - verbs: - - create - - get - - list - - update - - patch - - delete - - watch - - apiGroups: - - networking.k8s.io - resources: - - networkpolicies - verbs: - - create - - update - - delete - - get - - list - - watch - - apiGroups: - - crd.projectcalico.org - resources: - - felixconfigurations - verbs: - - create - - patch - - list - - get - - watch - - apiGroups: - - crd.projectcalico.org - resources: - - ippools - - kubecontrollersconfigurations - verbs: - - get - - list - - watch - - apiGroups: - - scheduling.k8s.io - resources: - - priorityclasses - verbs: - - create - - get - - list - - update - - delete - - watch - - apiGroups: - - policy - resources: - - poddisruptionbudgets - verbs: - - create - - get - - list - - update - - delete - - watch - - apiGroups: - - apiregistration.k8s.io - resources: - - apiservices - verbs: - - list - - watch - - create - - update - # Needed for operator lock - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - create - - get - - list - - update - - delete - - watch - # Add the appropriate pod security policy permissions - - apiGroups: - - policy - resources: - - podsecuritypolicies - resourceNames: - - tigera-operator - verbs: - - use - - apiGroups: - - policy - resources: - - podsecuritypolicies - verbs: - - get - - list - - watch - - create - - update - - delete - # Add the permissions to monitor the status of certificatesigningrequests when certificate management is enabled. - - apiGroups: - - certificates.k8s.io - resources: - - certificatesigningrequests - verbs: - - list ---- -# Source: tigera-operator/templates/tigera-operator/02-rolebinding-tigera-operator.yaml -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: tigera-operator -subjects: - - kind: ServiceAccount - name: tigera-operator - namespace: tigera-operator -roleRef: - kind: ClusterRole - name: tigera-operator - apiGroup: rbac.authorization.k8s.io ---- -# Source: tigera-operator/templates/tigera-operator/02-tigera-operator.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tigera-operator - namespace: tigera-operator - labels: - k8s-app: tigera-operator -spec: - replicas: 1 - selector: - matchLabels: - name: tigera-operator - template: - metadata: - labels: - name: tigera-operator - k8s-app: tigera-operator - spec: - nodeSelector: - kubernetes.io/os: linux - tolerations: - - effect: NoExecute - operator: Exists - - effect: NoSchedule - operator: Exists - serviceAccountName: tigera-operator - hostNetwork: true - # This must be set when hostNetwork is true or else the cluster services won't resolve - dnsPolicy: ClusterFirstWithHostNet - containers: - - name: tigera-operator - image: quay.io/tigera/operator:v1.25.3 - imagePullPolicy: IfNotPresent - command: - - operator - volumeMounts: - - name: var-lib-calico - readOnly: true - mountPath: /var/lib/calico - env: - - name: WATCH_NAMESPACE - value: "" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: OPERATOR_NAME - value: "tigera-operator" - - name: TIGERA_OPERATOR_INIT_IMAGE_VERSION - value: v1.25.3 - envFrom: - - configMapRef: - name: kubernetes-services-endpoint - optional: true - volumes: - - name: var-lib-calico - hostPath: - path: /var/lib/calico diff --git a/applications/openebs/Kubefile b/applications/openebs/Kubefile deleted file mode 100644 index 650c2accb34d..000000000000 --- a/applications/openebs/Kubefile +++ /dev/null @@ -1,4 +0,0 @@ -FROM registry.cn-hongkong.aliyuncs.com/sealyun/oci-kubernetes-calico:1.22.8-amd64 -COPY manifests ./manifests -COPY registry ./registry -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml","kubectl apply -f manifests/openebs-operator.yaml"] diff --git a/applications/openebs/manifests/openebs-operator.yaml b/applications/openebs/manifests/openebs-operator.yaml deleted file mode 100644 index 446d06bcefb6..000000000000 --- a/applications/openebs/manifests/openebs-operator.yaml +++ /dev/null @@ -1,1004 +0,0 @@ -# This manifest deploys the OpenEBS control plane components, -# with associated CRs & RBAC rules -# NOTE: On GKE, deploy the openebs-operator.yaml in admin context -# -# NOTE: The Jiva and cStor components previously included in the Operator File -# are now removed and it is recommended for users to use cStor and Jiva CSI operators. -# To upgrade your Jiva and cStor volumes to CSI, please checkout the documentation at: -# https://github.com/openebs/upgrade -# -# To deploy the legacy Jiva and cStor: -# kubectl apply -f https://openebs.github.io/charts/legacy-openebs-operator.yaml -# -# To deploy cStor CSI: -# kubectl apply -f https://openebs.github.io/charts/cstor-operator.yaml -# -# To deploy Jiva CSI: -# kubectl apply -f https://openebs.github.io/charts/jiva-operator.yaml -# - -# Create the OpenEBS namespace -apiVersion: v1 -kind: Namespace -metadata: - name: openebs ---- -# Create Maya Service Account -apiVersion: v1 -kind: ServiceAccount -metadata: - name: openebs-maya-operator - namespace: openebs ---- -# Define Role that allows operations on K8s pods/deployments -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: openebs-maya-operator -rules: - - apiGroups: ["*"] - resources: ["nodes", "nodes/proxy"] - verbs: ["*"] - - apiGroups: ["*"] - resources: ["namespaces", "services", "pods", "pods/exec", "deployments", "deployments/finalizers", "replicationcontrollers", "replicasets", "events", "endpoints", "configmaps", "secrets", "jobs", "cronjobs"] - verbs: ["*"] - - apiGroups: ["*"] - resources: ["statefulsets", "daemonsets"] - verbs: ["*"] - - apiGroups: ["*"] - resources: ["resourcequotas", "limitranges"] - verbs: ["list", "watch"] - - apiGroups: ["*"] - resources: ["ingresses", "horizontalpodautoscalers", "verticalpodautoscalers", "certificatesigningrequests"] - verbs: ["list", "watch"] - - apiGroups: ["*"] - resources: ["storageclasses", "persistentvolumeclaims", "persistentvolumes"] - verbs: ["*"] - - apiGroups: ["volumesnapshot.external-storage.k8s.io"] - resources: ["volumesnapshots", "volumesnapshotdatas"] - verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: [ "get", "list", "create", "update", "delete", "patch"] - - apiGroups: ["openebs.io"] - resources: [ "*"] - verbs: ["*" ] - - apiGroups: ["cstor.openebs.io"] - resources: [ "*"] - verbs: ["*" ] - - apiGroups: ["coordination.k8s.io"] - resources: ["leases"] - verbs: ["get", "watch", "list", "delete", "update", "create"] - - apiGroups: ["admissionregistration.k8s.io"] - resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"] - verbs: ["get", "create", "list", "delete", "update", "patch"] - - nonResourceURLs: ["/metrics"] - verbs: ["get"] - - apiGroups: ["*"] - resources: ["poddisruptionbudgets"] - verbs: ["get", "list", "create", "delete", "watch"] ---- -# Bind the Service Account with the Role Privileges. -# TODO: Check if default account also needs to be there -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: openebs-maya-operator -subjects: - - kind: ServiceAccount - name: openebs-maya-operator - namespace: openebs -roleRef: - kind: ClusterRole - name: openebs-maya-operator - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.5.0 - creationTimestamp: null - name: blockdevices.openebs.io -spec: - group: openebs.io - names: - kind: BlockDevice - listKind: BlockDeviceList - plural: blockdevices - shortNames: - - bd - singular: blockdevice - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.nodeAttributes.nodeName - name: NodeName - type: string - - jsonPath: .spec.path - name: Path - priority: 1 - type: string - - jsonPath: .spec.filesystem.fsType - name: FSType - priority: 1 - type: string - - jsonPath: .spec.capacity.storage - name: Size - type: string - - jsonPath: .status.claimState - name: ClaimState - type: string - - jsonPath: .status.state - name: Status - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: BlockDevice is the Schema for the blockdevices API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: DeviceSpec defines the properties and runtime status of a BlockDevice - properties: - aggregateDevice: - description: AggregateDevice was intended to store the hierarchical information in cases of LVM. However this is currently not implemented and may need to be re-looked into for better design. To be deprecated - type: string - capacity: - description: Capacity - properties: - logicalSectorSize: - description: LogicalSectorSize is blockdevice logical-sector size in bytes - format: int32 - type: integer - physicalSectorSize: - description: PhysicalSectorSize is blockdevice physical-Sector size in bytes - format: int32 - type: integer - storage: - description: Storage is the blockdevice capacity in bytes - format: int64 - type: integer - required: - - storage - type: object - claimRef: - description: ClaimRef is the reference to the BDC which has claimed this BD - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.' - type: string - kind: - description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - namespace: - description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' - type: string - resourceVersion: - description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' - type: string - uid: - description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' - type: string - type: object - details: - description: Details contain static attributes of BD like model,serial, and so forth - properties: - compliance: - description: Compliance is standards/specifications version implemented by device firmware such as SPC-1, SPC-2, etc - type: string - deviceType: - description: DeviceType represents the type of device like sparse, disk, partition, lvm, crypt - enum: - - disk - - partition - - sparse - - loop - - lvm - - crypt - - dm - - mpath - type: string - driveType: - description: DriveType is the type of backing drive, HDD/SSD - enum: - - HDD - - SSD - - Unknown - - "" - type: string - firmwareRevision: - description: FirmwareRevision is the disk firmware revision - type: string - hardwareSectorSize: - description: HardwareSectorSize is the hardware sector size in bytes - format: int32 - type: integer - logicalBlockSize: - description: LogicalBlockSize is the logical block size in bytes reported by /sys/class/block/sda/queue/logical_block_size - format: int32 - type: integer - model: - description: Model is model of disk - type: string - physicalBlockSize: - description: PhysicalBlockSize is the physical block size in bytes reported by /sys/class/block/sda/queue/physical_block_size - format: int32 - type: integer - serial: - description: Serial is serial number of disk - type: string - vendor: - description: Vendor is vendor of disk - type: string - type: object - devlinks: - description: DevLinks contains soft links of a block device like /dev/by-id/... /dev/by-uuid/... - items: - description: DeviceDevLink holds the mapping between type and links like by-id type or by-path type link - properties: - kind: - description: Kind is the type of link like by-id or by-path. - enum: - - by-id - - by-path - type: string - links: - description: Links are the soft links - items: - type: string - type: array - type: object - type: array - filesystem: - description: FileSystem contains mountpoint and filesystem type - properties: - fsType: - description: Type represents the FileSystem type of the block device - type: string - mountPoint: - description: MountPoint represents the mountpoint of the block device. - type: string - type: object - nodeAttributes: - description: NodeAttributes has the details of the node on which BD is attached - properties: - nodeName: - description: NodeName is the name of the Kubernetes node resource on which the device is attached - type: string - type: object - parentDevice: - description: "ParentDevice was intended to store the UUID of the parent Block Device as is the case for partitioned block devices. \n For example: /dev/sda is the parent for /dev/sda1 To be deprecated" - type: string - partitioned: - description: Partitioned represents if BlockDevice has partitions or not (Yes/No) Currently always default to No. To be deprecated - enum: - - "Yes" - - "No" - type: string - path: - description: Path contain devpath (e.g. /dev/sdb) - type: string - required: - - capacity - - devlinks - - nodeAttributes - - path - type: object - status: - description: DeviceStatus defines the observed state of BlockDevice - properties: - claimState: - description: ClaimState represents the claim state of the block device - enum: - - Claimed - - Unclaimed - - Released - type: string - state: - description: State is the current state of the blockdevice (Active/Inactive/Unknown) - enum: - - Active - - Inactive - - Unknown - type: string - required: - - claimState - - state - type: object - type: object - served: true - storage: true - subresources: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.5.0 - creationTimestamp: null - name: blockdeviceclaims.openebs.io -spec: - group: openebs.io - names: - kind: BlockDeviceClaim - listKind: BlockDeviceClaimList - plural: blockdeviceclaims - shortNames: - - bdc - singular: blockdeviceclaim - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.blockDeviceName - name: BlockDeviceName - type: string - - jsonPath: .status.phase - name: Phase - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: BlockDeviceClaim is the Schema for the blockdeviceclaims API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: DeviceClaimSpec defines the request details for a BlockDevice - properties: - blockDeviceName: - description: BlockDeviceName is the reference to the block-device backing this claim - type: string - blockDeviceNodeAttributes: - description: BlockDeviceNodeAttributes is the attributes on the node from which a BD should be selected for this claim. It can include nodename, failure domain etc. - properties: - hostName: - description: HostName represents the hostname of the Kubernetes node resource where the BD should be present - type: string - nodeName: - description: NodeName represents the name of the Kubernetes node resource where the BD should be present - type: string - type: object - deviceClaimDetails: - description: Details of the device to be claimed - properties: - allowPartition: - description: AllowPartition represents whether to claim a full block device or a device that is a partition - type: boolean - blockVolumeMode: - description: 'BlockVolumeMode represents whether to claim a device in Block mode or Filesystem mode. These are use cases of BlockVolumeMode: 1) Not specified: VolumeMode check will not be effective 2) VolumeModeBlock: BD should not have any filesystem or mountpoint 3) VolumeModeFileSystem: BD should have a filesystem and mountpoint. If DeviceFormat is specified then the format should match with the FSType in BD' - type: string - formatType: - description: Format of the device required, eg:ext4, xfs - type: string - type: object - deviceType: - description: DeviceType represents the type of drive like SSD, HDD etc., - nullable: true - type: string - hostName: - description: Node name from where blockdevice has to be claimed. To be deprecated. Use NodeAttributes.HostName instead - type: string - resources: - description: Resources will help with placing claims on Capacity, IOPS - properties: - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum resources required. eg: if storage resource of 10G is requested minimum capacity of 10G should be available TODO for validating' - type: object - required: - - requests - type: object - selector: - description: Selector is used to find block devices to be considered for claiming - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - type: object - status: - description: DeviceClaimStatus defines the observed state of BlockDeviceClaim - properties: - phase: - description: Phase represents the current phase of the claim - type: string - required: - - phase - type: object - type: object - served: true - storage: true - subresources: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] ---- -# This is the node-disk-manager related config. -# It can be used to customize the disks probes and filters -apiVersion: v1 -kind: ConfigMap -metadata: - name: openebs-ndm-config - namespace: openebs - labels: - openebs.io/component-name: ndm-config -data: - # udev-probe is default or primary probe it should be enabled to run ndm - # filterconfigs contains configs of filters. To provide a group of include - # and exclude values add it as , separated string - node-disk-manager.config: | - probeconfigs: - - key: udev-probe - name: udev probe - state: true - - key: seachest-probe - name: seachest probe - state: false - - key: smart-probe - name: smart probe - state: true - filterconfigs: - - key: os-disk-exclude-filter - name: os disk exclude filter - state: true - exclude: "/,/etc/hosts,/boot" - - key: vendor-filter - name: vendor filter - state: true - include: "" - exclude: "CLOUDBYT,OpenEBS" - - key: path-filter - name: path filter - state: true - include: "" - exclude: "/dev/loop,/dev/fd0,/dev/sr0,/dev/ram,/dev/md,/dev/dm-,/dev/rbd,/dev/zd" - # metconfig can be used to decorate the block device with different types of labels - # that are available on the node or come in a device properties. - # node labels - the node where bd is discovered. A whitlisted label prefixes - # attribute labels - a property of the BD can be added as a ndm label as ndm.io/= - metaconfigs: - - key: node-labels - name: node labels - pattern: "" - - key: device-labels - name: device labels - type: "" ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: openebs-ndm - namespace: openebs - labels: - name: openebs-ndm - openebs.io/component-name: ndm - openebs.io/version: 3.1.0 -spec: - selector: - matchLabels: - name: openebs-ndm - openebs.io/component-name: ndm - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - name: openebs-ndm - openebs.io/component-name: ndm - openebs.io/version: 3.1.0 - spec: - # By default the node-disk-manager will be run on all kubernetes nodes - # If you would like to limit this to only some nodes, say the nodes - # that have storage attached, you could label those node and use - # nodeSelector. - # - # e.g. label the storage nodes with - "openebs.io/nodegroup"="storage-node" - # kubectl label node "openebs.io/nodegroup"="storage-node" - #nodeSelector: - # "openebs.io/nodegroup": "storage-node" - serviceAccountName: openebs-maya-operator - hostNetwork: true - # host PID is used to check status of iSCSI Service when the NDM - # API service is enabled - #hostPID: true - containers: - - name: node-disk-manager - image: openebs/node-disk-manager:1.8.0 - args: - - -v=4 - # The feature-gate is used to enable the new UUID algorithm. - - --feature-gates="GPTBasedUUID" - # Use partition table UUID instead of create single partition to get - # partition UUID. Require `GPTBasedUUID` to be enabled with. - # - --feature-gates="PartitionTableUUID" - # Detect changes to device size, filesystem and mount-points without restart. - # - --feature-gates="ChangeDetection" - # The feature gate is used to start the gRPC API service. The gRPC server - # starts at 9115 port by default. This feature is currently in Alpha state - # - --feature-gates="APIService" - # The feature gate is used to enable NDM, to create blockdevice resources - # for unused partitions on the OS disk - # - --feature-gates="UseOSDisk" - imagePullPolicy: IfNotPresent - securityContext: - privileged: true - volumeMounts: - - name: config - mountPath: /host/node-disk-manager.config - subPath: node-disk-manager.config - readOnly: true - # make udev database available inside container - - name: udev - mountPath: /run/udev - - name: procmount - mountPath: /host/proc - readOnly: true - - name: devmount - mountPath: /dev - - name: basepath - mountPath: /var/openebs/ndm - - name: sparsepath - mountPath: /var/openebs/sparse - env: - # namespace in which NDM is installed will be passed to NDM Daemonset - # as environment variable - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - # pass hostname as env variable using downward API to the NDM container - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - # specify the directory where the sparse files need to be created. - # if not specified, then sparse files will not be created. - - name: SPARSE_FILE_DIR - value: "/var/openebs/sparse" - # Size(bytes) of the sparse file to be created. - - name: SPARSE_FILE_SIZE - value: "10737418240" - # Specify the number of sparse files to be created - - name: SPARSE_FILE_COUNT - value: "0" - livenessProbe: - exec: - command: - - pgrep - - "ndm" - initialDelaySeconds: 30 - periodSeconds: 60 - volumes: - - name: config - configMap: - name: openebs-ndm-config - - name: udev - hostPath: - path: /run/udev - type: Directory - # mount /proc (to access mount file of process 1 of host) inside container - # to read mount-point of disks and partitions - - name: procmount - hostPath: - path: /proc - type: Directory - - name: devmount - # the /dev directory is mounted so that we have access to the devices that - # are connected at runtime of the pod. - hostPath: - path: /dev - type: Directory - - name: basepath - hostPath: - path: /var/openebs/ndm - type: DirectoryOrCreate - - name: sparsepath - hostPath: - path: /var/openebs/sparse ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: openebs-ndm-operator - namespace: openebs - labels: - name: openebs-ndm-operator - openebs.io/component-name: ndm-operator - openebs.io/version: 3.1.0 -spec: - selector: - matchLabels: - name: openebs-ndm-operator - openebs.io/component-name: ndm-operator - replicas: 1 - strategy: - type: Recreate - template: - metadata: - labels: - name: openebs-ndm-operator - openebs.io/component-name: ndm-operator - openebs.io/version: 3.1.0 - spec: - serviceAccountName: openebs-maya-operator - containers: - - name: node-disk-operator - image: openebs/node-disk-operator:1.8.0 - imagePullPolicy: IfNotPresent - env: - - name: WATCH_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - # the service account of the ndm-operator pod - - name: SERVICE_ACCOUNT - valueFrom: - fieldRef: - fieldPath: spec.serviceAccountName - - name: OPERATOR_NAME - value: "node-disk-operator" - - name: CLEANUP_JOB_IMAGE - value: "openebs/linux-utils:3.1.0" - # OPENEBS_IO_IMAGE_PULL_SECRETS environment variable is used to pass the image pull secrets - # to the cleanup pod launched by NDM operator - #- name: OPENEBS_IO_IMAGE_PULL_SECRETS - # value: "" - livenessProbe: - httpGet: - path: /healthz - port: 8585 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8585 - initialDelaySeconds: 5 - periodSeconds: 10 ---- -# Create NDM cluster exporter deployment. -# This is an optional component and is not required for the basic -# functioning of NDM -apiVersion: apps/v1 -kind: Deployment -metadata: - name: openebs-ndm-cluster-exporter - namespace: openebs - labels: - name: openebs-ndm-cluster-exporter - openebs.io/component-name: ndm-cluster-exporter - openebs.io/version: 3.1.0 -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - name: openebs-ndm-cluster-exporter - openebs.io/component-name: ndm-cluster-exporter - template: - metadata: - labels: - name: openebs-ndm-cluster-exporter - openebs.io/component-name: ndm-cluster-exporter - openebs.io/version: 3.1.0 - spec: - serviceAccountName: openebs-maya-operator - containers: - - name: ndm-cluster-exporter - image: openebs/node-disk-exporter:1.8.0 - command: - - /usr/local/bin/exporter - args: - - "start" - - "--mode=cluster" - - "--port=$(METRICS_LISTEN_PORT)" - - "--metrics=/metrics" - ports: - - containerPort: 9100 - protocol: TCP - name: metrics - imagePullPolicy: IfNotPresent - env: - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: METRICS_LISTEN_PORT - value: :9100 ---- -# Create NDM cluster exporter service -# This is optional and required only when -# ndm-cluster-exporter deployment is used -apiVersion: v1 -kind: Service -metadata: - name: openebs-ndm-cluster-exporter-service - namespace: openebs - labels: - name: openebs-ndm-cluster-exporter-service - openebs.io/component-name: ndm-cluster-exporter - app: openebs-ndm-exporter -spec: - clusterIP: None - ports: - - name: metrics - port: 9100 - targetPort: 9100 - selector: - name: openebs-ndm-cluster-exporter ---- -# Create NDM node exporter daemonset. -# This is an optional component used for getting disk level -# metrics from each of the storage nodes -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: openebs-ndm-node-exporter - namespace: openebs - labels: - name: openebs-ndm-node-exporter - openebs.io/component-name: ndm-node-exporter - openebs.io/version: 3.1.0 -spec: - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - name: openebs-ndm-node-exporter - openebs.io/component-name: ndm-node-exporter - template: - metadata: - labels: - name: openebs-ndm-node-exporter - openebs.io/component-name: ndm-node-exporter - openebs.io/version: 3.1.0 - spec: - serviceAccountName: openebs-maya-operator - containers: - - name: node-disk-exporter - image: openebs/node-disk-exporter:1.8.0 - command: - - /usr/local/bin/exporter - args: - - "start" - - "--mode=node" - - "--port=$(METRICS_LISTEN_PORT)" - - "--metrics=/metrics" - ports: - - containerPort: 9101 - protocol: TCP - name: metrics - imagePullPolicy: IfNotPresent - securityContext: - privileged: true - env: - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: METRICS_LISTEN_PORT - value: :9101 ---- -# Create NDM node exporter service -# This is optional and required only when -# ndm-node-exporter daemonset is used -apiVersion: v1 -kind: Service -metadata: - name: openebs-ndm-node-exporter-service - namespace: openebs - labels: - name: openebs-ndm-node-exporter - openebs.io/component: openebs-ndm-node-exporter - app: openebs-ndm-exporter -spec: - clusterIP: None - ports: - - name: metrics - port: 9101 - targetPort: 9101 - selector: - name: openebs-ndm-node-exporter ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: openebs-localpv-provisioner - namespace: openebs - labels: - name: openebs-localpv-provisioner - openebs.io/component-name: openebs-localpv-provisioner - openebs.io/version: 3.1.0 -spec: - selector: - matchLabels: - name: openebs-localpv-provisioner - openebs.io/component-name: openebs-localpv-provisioner - replicas: 1 - strategy: - type: Recreate - template: - metadata: - labels: - name: openebs-localpv-provisioner - openebs.io/component-name: openebs-localpv-provisioner - openebs.io/version: 3.1.0 - spec: - serviceAccountName: openebs-maya-operator - containers: - - name: openebs-provisioner-hostpath - imagePullPolicy: IfNotPresent - image: openebs/provisioner-localpv:3.1.0 - args: - - "--bd-time-out=$(BDC_BD_BIND_RETRIES)" - env: - # OPENEBS_IO_K8S_MASTER enables openebs provisioner to connect to K8s - # based on this address. This is ignored if empty. - # This is supported for openebs provisioner version 0.5.2 onwards - #- name: OPENEBS_IO_K8S_MASTER - # value: "http://10.128.0.12:8080" - # OPENEBS_IO_KUBE_CONFIG enables openebs provisioner to connect to K8s - # based on this config. This is ignored if empty. - # This is supported for openebs provisioner version 0.5.2 onwards - #- name: OPENEBS_IO_KUBE_CONFIG - # value: "/home/ubuntu/.kube/config" - # This sets the number of times the provisioner should try - # with a polling interval of 5 seconds, to get the Blockdevice - # Name from a BlockDeviceClaim, before the BlockDeviceClaim - # is deleted. E.g. 12 * 5 seconds = 60 seconds timeout - - name: BDC_BD_BIND_RETRIES - value: "12" - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: OPENEBS_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - # OPENEBS_SERVICE_ACCOUNT provides the service account of this pod as - # environment variable - - name: OPENEBS_SERVICE_ACCOUNT - valueFrom: - fieldRef: - fieldPath: spec.serviceAccountName - - name: OPENEBS_IO_ENABLE_ANALYTICS - value: "true" - - name: OPENEBS_IO_INSTALLER_TYPE - value: "openebs-operator" - - name: OPENEBS_IO_HELPER_IMAGE - value: "openebs/linux-utils:3.1.0" - - name: OPENEBS_IO_BASE_PATH - value: "/var/openebs/local" - # LEADER_ELECTION_ENABLED is used to enable/disable leader election. By default - # leader election is enabled. - #- name: LEADER_ELECTION_ENABLED - # value: "true" - # OPENEBS_IO_IMAGE_PULL_SECRETS environment variable is used to pass the image pull secrets - # to the helper pod launched by local-pv hostpath provisioner - #- name: OPENEBS_IO_IMAGE_PULL_SECRETS - # value: "" - # Process name used for matching is limited to the 15 characters - # present in the pgrep output. - # So fullname can't be used here with pgrep (>15 chars).A regular expression - # that matches the entire command name has to specified. - # Anchor `^` : matches any string that starts with `provisioner-loc` - # `.*`: matches any string that has `provisioner-loc` followed by zero or more char - livenessProbe: - exec: - command: - - sh - - -c - - test `pgrep -c "^provisioner-loc.*"` = 1 - initialDelaySeconds: 30 - periodSeconds: 60 ---- -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: openebs-hostpath - annotations: - openebs.io/cas-type: local - cas.openebs.io/config: | - #hostpath type will create a PV by - # creating a sub-directory under the - # BASEPATH provided below. - - name: StorageType - value: "hostpath" - #Specify the location (directory) where - # where PV(volume) data will be saved. - # A sub-directory with pv-name will be - # created. When the volume is deleted, - # the PV sub-directory will be deleted. - #Default value is /var/openebs/local - - name: BasePath - value: "/var/openebs/local/" -provisioner: openebs.io/local -volumeBindingMode: WaitForFirstConsumer -reclaimPolicy: Delete ---- -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: openebs-device - annotations: - openebs.io/cas-type: local - cas.openebs.io/config: | - #device type will create a PV by - # issuing a BDC and will extract the path - # values from the associated BD. - - name: StorageType - value: "device" -provisioner: openebs.io/local -volumeBindingMode: WaitForFirstConsumer -reclaimPolicy: Delete ---- diff --git a/cmd/image-cri-shim/cmd/root.go b/cmd/image-cri-shim/cmd/root.go deleted file mode 100644 index 178262232935..000000000000 --- a/cmd/image-cri-shim/cmd/root.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright © 2022 cuisongliu@qq.com - -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 cmd - -import ( - "encoding/base64" - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/labring/image-cri-shim/pkg/cri" - "github.com/labring/image-cri-shim/pkg/glog" - "github.com/labring/image-cri-shim/pkg/server" - "github.com/labring/image-cri-shim/pkg/shim" - "github.com/labring/image-cri-shim/pkg/utils" - "github.com/labring/sealos/pkg/utils/version" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var shimSocket, criSocket string -var force bool - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "image-cri-shim", - Short: "cri for kubelet endpoint-image-service", - // Uncomment the following line if your bare application - // has an action associated with it: - Version: version.Get().String(), - Run: func(cmd *cobra.Command, args []string) { - - run(shimSocket, criSocket) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - data, err := utils.Unmarshal(server.ConfigFile) - if err != nil { - return errors.Wrap(err, "image shim config load error") - } - shimSocket, _, _ = unstructured.NestedString(data, "shim") - glog.Infof("shim-socket: %s", shimSocket) - criSocket, _, _ = unstructured.NestedString(data, "cri") - glog.Infof("cri-socket: %s", criSocket) - server.SealosHub, _, _ = unstructured.NestedString(data, "address") - glog.Infof("hub-address: %s", server.SealosHub) - force, _, _ = unstructured.NestedBool(data, "force") - glog.Infof("force: %v", force) - server.Debug, _, _ = unstructured.NestedBool(data, "debug") - glog.Infof("debug: %v", server.Debug) - imageDir, _, _ := unstructured.NestedString(data, "image") - glog.Infof("image-dir: %v", imageDir) - server.Auth, _, _ = unstructured.NestedString(data, "auth") - if server.Auth != "" { - glog.Infof("auth: %v", server.Auth) - server.Base64Auth = base64.StdEncoding.EncodeToString([]byte(server.Auth)) - glog.Infof("base64 auth: %v", server.Base64Auth) - } - - if imageDir != "" { - server.RunLoad() - } - if shimSocket == "" { - shimSocket = server.SealosShimSock - } - - if server.SealosHub == "" { - glog.Warning("registry addr is empty") - } - if criSocket == "" { - socket, err := cri.DetectCRISocket() - if err != nil { - return err - } - criSocket = socket - } - if !force { - if !isExist(criSocket) { - return errors.New("cri is running?") - } - } - return nil - }, -} - -func isExist(fileName string) bool { - if _, err := os.Stat(fileName); err != nil { - return os.IsExist(err) - } - return true -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - rootCmd.Flags().StringVarP(&server.ConfigFile, "file", "f", "", "config file top image shim") -} - -func run(socket string, criSocket string) { - options := shim.Options{ - ShimSocket: socket, - ImageSocket: criSocket, - } - glog.Infof("socket info shim: %v ,image: %v, registry: %v", socket, criSocket, server.SealosHub) - _shim, err := shim.NewShim(options) - if err != nil { - glog.Fatalf("failed to new _shim, %s", err) - } - - err = _shim.Setup() - if err != nil { - glog.Fatalf("failed to setup image _shim, %s", err) - } - - err = _shim.Start() - if err != nil { - glog.Fatalf(fmt.Sprintf("failed to start image _shim, %s", err)) - } - - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) - - stopCh := make(chan struct{}, 1) - select { - case <-signalCh: - close(stopCh) - case <-stopCh: - } - _ = os.Remove(socket) - glog.Infof("Shutting down the image _shim") -} diff --git a/cmd/lvscare/cmd/care.go b/cmd/lvscare/cmd/care.go deleted file mode 100644 index 658b4945f3d0..000000000000 --- a/cmd/lvscare/cmd/care.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright © 2022 sealos. -// -// 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 cmd - -import ( - "github.com/labring/lvscare/care" - "github.com/labring/lvscare/pkg/glog" - "github.com/spf13/cobra" -) - -// careCmd represents the care command -var careCmd = &cobra.Command{ - Use: "care", - Short: "A lightweight LVS baby care, support ipvs health check.", - Run: func(cmd *cobra.Command, args []string) { - care.LVS.VsAndRsCare() - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if err := care.SetTargetIP(); err != nil { - return err - } - // if target ip is empty, skip sync router - if care.LVS.TargetIP == nil { - return nil - } - return care.LVS.SyncRouter() - }, -} - -func init() { - cobra.OnInitialize(func() { - switch care.LVS.Logger { - case "INFO": - glog.InitVerbosity(0) - case "DEBG": - glog.InitVerbosity(9) - default: - glog.InitVerbosity(0) - } - }) - rootCmd.AddCommand(careCmd) - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // careCmd.PersistentFlags().String("foo", "", "A help for foo") - careCmd.Flags().IPVar(&care.LVS.TargetIP, "ip", nil, "target ip") - careCmd.Flags().BoolVar(&care.LVS.RunOnce, "run-once", false, "is run once mode") - careCmd.Flags().StringVar(&care.LVS.VirtualServer, "vs", "", "virtual server like 10.54.0.2:6443") - careCmd.Flags().StringSliceVar(&care.LVS.RealServer, "rs", []string{}, "real server like 192.168.0.2:6443") - careCmd.Flags().StringVar(&care.LVS.Logger, "logger", "INFO", "logger level: DEBG/INFO") - careCmd.Flags().BoolVar(&care.LVS.Clean, "clean", false, "before run clean ipvs rules") - - careCmd.Flags().StringVar(&care.LVS.HealthPath, "health-path", "/healthz", "health check path") - careCmd.Flags().StringVar(&care.LVS.HealthSchem, "health-schem", "https", "health check schem") - careCmd.Flags().Int32Var(&care.LVS.Interval, "interval", 5, "health check interval, unit is sec.") - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // careCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/lvscare/cmd/version.go b/cmd/lvscare/cmd/version.go deleted file mode 100644 index cf177b57e49a..000000000000 --- a/cmd/lvscare/cmd/version.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "encoding/json" - "fmt" - - "github.com/labring/sealos/pkg/utils/version" - - "github.com/spf13/cobra" -) - -var shortPrint bool - -func newVersionCmd() *cobra.Command { - var versionCmd = &cobra.Command{ - Use: "version", - Short: "version", - Args: cobra.NoArgs, - Example: `lvscare version`, - RunE: func(cmd *cobra.Command, args []string) error { - marshalled, err := json.Marshal(version.Get()) - if err != nil { - return err - } - if shortPrint { - fmt.Println(version.Get().String()) - } else { - fmt.Println(string(marshalled)) - } - return nil - }, - } - versionCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just the version number.") - return versionCmd -} - -func init() { - rootCmd.AddCommand(newVersionCmd()) -} diff --git a/cmd/sealctl/cmd/cert.go b/cmd/sealctl/cmd/cert.go deleted file mode 100755 index 6db06eaba9b6..000000000000 --- a/cmd/sealctl/cmd/cert.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "os" - - "github.com/labring/sealos/pkg/utils/flags" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/cert" -) - -func newCertCmd() *cobra.Command { - type certFlag struct { - AltNames []string - NodeName string - ServiceCIDR string - NodeIP string - DNSDomain string - CertPath string - CertEtcdPath string - } - var flag certFlag - - certCmd := &cobra.Command{ - Use: "cert", - Short: "generate certs", - Long: `you can specify expire time`, - Run: func(cmd *cobra.Command, args []string) { - flags.PrintFlags(cmd.Flags()) - err := cert.GenerateCert(flag.CertPath, flag.CertEtcdPath, flag.AltNames, flag.NodeIP, flag.NodeName, flag.ServiceCIDR, flag.DNSDomain) - if err != nil { - logger.Error(err) - os.Exit(1) - } - }, - } - certCmd.Flags().StringSliceVar(&flag.AltNames, "alt-names", []string{}, "like sealos.io or 10.103.97.2") - certCmd.Flags().StringVar(&flag.NodeName, "node-name", "", "like master0") - certCmd.Flags().StringVar(&flag.ServiceCIDR, "service-cidr", "", "like 10.103.97.2/24") - certCmd.Flags().StringVar(&flag.NodeIP, "node-ip", "", "like 10.103.97.2") - certCmd.Flags().StringVar(&flag.DNSDomain, "dns-domain", "cluster.local", "cluster dns domain") - certCmd.Flags().StringVar(&flag.CertPath, "cert-path", "/etc/kubernetes/pki", "kubernetes cert file path") - certCmd.Flags().StringVar(&flag.CertEtcdPath, "cert-etcd-path", "/etc/kubernetes/pki/etcd", "kubernetes etcd cert file path") - - return certCmd -} - -func init() { - rootCmd.AddCommand(newCertCmd()) -} diff --git a/cmd/sealctl/cmd/cri.go b/cmd/sealctl/cmd/cri.go deleted file mode 100644 index 8264e60cc747..000000000000 --- a/cmd/sealctl/cmd/cri.go +++ /dev/null @@ -1,274 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "os" - "strconv" - "strings" - - "github.com/labring/image-cri-shim/pkg/cri" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - utilsexec "k8s.io/utils/exec" -) - -var ( - criSocketPath string - criConfigPath string -) - -func newCRICmd() *cobra.Command { - var criCmd = &cobra.Command{ - Use: "cri", - Short: "cri manager", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - criCmd.AddCommand(newIsDockerCmd()) - criCmd.AddCommand(newIsRunningCmd()) - criCmd.AddCommand(newListKubeContainersCmd()) - criCmd.AddCommand(newRemoveContainersCmd()) - criCmd.AddCommand(newPullImageCmd()) - criCmd.AddCommand(newImageExistsCmd()) - criCmd.AddCommand(newCGroupDriverCmd()) - criCmd.AddCommand(newCRISocketCmd()) - criCmd.PersistentFlags().StringVar(&criSocketPath, "socket-path", "", "cri socket path") - criCmd.PersistentFlags().StringVar(&criConfigPath, "config", "", "cri config file") - - return criCmd -} - -func newCRISocketCmd() *cobra.Command { - var criSocketCmd = &cobra.Command{ - Use: "socket", - Short: "cri manager socket", - Run: func(cmd *cobra.Command, args []string) { - criSocket, err := cri.DetectCRISocket() - if err != nil { - logger.Error(err) - return - } - println(criSocket) - }, - } - return criSocketCmd -} - -func newIsDockerCmd() *cobra.Command { - var isDockerCmd = &cobra.Command{ - Use: "is-docker", - Short: "cri manager is-docker", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - isDocker := runtime.IsDocker() - println(strconv.FormatBool(isDocker)) - }, - } - return isDockerCmd -} - -func newIsRunningCmd() *cobra.Command { - var shortPrint bool - var isRunningCmd = &cobra.Command{ - Use: "is-running", - Short: "cri manager is-running", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - err := runtime.IsRunning() - if shortPrint { - println(strconv.FormatBool(err == nil)) - return - } - if err != nil { - logger.Error(err) - return - } - logger.Info("container runtime is running") - }, - } - isRunningCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just result.") - return isRunningCmd -} - -func newListKubeContainersCmd() *cobra.Command { - var sPrint bool - var listKubeContainersCmd = &cobra.Command{ - Use: "list-containers", - Short: "cri manager list-containers", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - containers, err := runtime.ListKubeContainers() - if err != nil { - logger.Error(err) - os.Exit(1) - } - if sPrint { - println(strings.Join(containers, ",")) - return - } - logger.Info("container runtime containers is %+v", containers) - }, - } - listKubeContainersCmd.Flags().BoolVar(&sPrint, "short", false, "if true, print just result.") - return listKubeContainersCmd -} - -func newRemoveContainersCmd() *cobra.Command { - var containers []string - var removeContainersCmd = &cobra.Command{ - Use: "remove-containers", - Short: "cri manager remove-containers", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - if len(containers) == 0 { - logger.Error("container runtime containers is empty.") - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - err := runtime.RemoveContainers(containers) - if err != nil { - logger.Error(err) - return - } - logger.Info("container runtime remove containers %+v success.", containers) - }, - } - removeContainersCmd.Flags().StringSliceVar(&containers, "containers", []string{}, "containers name list") - return removeContainersCmd -} - -func newPullImageCmd() *cobra.Command { - var imageName string - var pullImageCmd = &cobra.Command{ - Use: "pull-image", - Short: "cri manager pull-image", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - if imageName == "" { - logger.Error("container runtime pull image name is empty.", imageName) - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - err := runtime.PullImage(imageName) - if err != nil { - logger.Error(err) - return - } - logger.Info("container runtime pull image %s success.", imageName) - }, - } - pullImageCmd.Flags().StringVar(&imageName, "image", "", "image name") - return pullImageCmd -} - -func newImageExistsCmd() *cobra.Command { - var shortPrint bool - var imageName string - var imageExistsCmd = &cobra.Command{ - Use: "image-exists", - Short: "cri manager image-exists", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - if imageName == "" { - logger.Error("container runtime image exists name is empty.", imageName) - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - b := runtime.ImageExists(imageName) - if shortPrint { - println(strconv.FormatBool(b)) - return - } - if !b { - logger.Warn("container runtime image name %s is not exists", imageName) - return - } - logger.Info("container runtime image name %s is exists", imageName) - }, - } - imageExistsCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just result.") - return imageExistsCmd -} - -func newCGroupDriverCmd() *cobra.Command { - var shortPrint bool - var cGroupDriverCmd = &cobra.Command{ - Use: "cgroup-driver", - Short: "cri manager cgroup-driver", - PreRun: func(cmd *cobra.Command, args []string) { - criCheck() - }, - Run: func(cmd *cobra.Command, args []string) { - runtime := criRuntime() - driver, err := runtime.CGroupDriver() - if err != nil { - logger.Error(err) - os.Exit(1) - } - if shortPrint { - println(driver) - return - } - logger.Info("container runtime cgroup-driver is %s", driver) - }, - } - cGroupDriverCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just result.") - return cGroupDriverCmd -} - -func criCheck() { - var err error - if criSocketPath == "" { - criSocketPath, err = cri.DetectCRISocket() - } - if err != nil { - logger.Error(err) - os.Exit(1) - } -} - -func criRuntime() cri.ContainerRuntime { - rt, err := cri.NewContainerRuntime(utilsexec.New(), criSocketPath, criConfigPath) - if err != nil { - logger.Error(err) - os.Exit(1) - } - return rt -} - -func init() { - rootCmd.AddCommand(newCRICmd()) -} diff --git a/cmd/sealctl/cmd/hosts.go b/cmd/sealctl/cmd/hosts.go deleted file mode 100644 index 4a51ac3a9ecb..000000000000 --- a/cmd/sealctl/cmd/hosts.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "os" - - "github.com/labring/sealos/pkg/constants" - "github.com/labring/sealos/pkg/utils/hosts" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" -) - -var hostsPath string - -func newHostsCmd() *cobra.Command { - var hostsCmd = &cobra.Command{ - Use: "hosts", - Short: "hosts manager", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - // check route for host - hostsCmd.AddCommand(newHostsListCmd()) - hostsCmd.AddCommand(newHostsAddCmd()) - hostsCmd.AddCommand(newHostsDeleteCmd()) - hostsCmd.PersistentFlags().StringVar(&hostsPath, "path", constants.DefaultHostsPath, "default hosts path") - return hostsCmd -} - -func newHostsListCmd() *cobra.Command { - var hostsListCmd = &cobra.Command{ - Use: "list", - Short: "hosts manager list", - Run: func(cmd *cobra.Command, args []string) { - hf := &hosts.HostFile{Path: hostsPath} - hf.ListCurrentHosts() - }, - } - return hostsListCmd -} - -func newHostsAddCmd() *cobra.Command { - var ip, domain string - var hostsAddCmd = &cobra.Command{ - Use: "add", - Short: "hosts manager add", - PreRun: func(cmd *cobra.Command, args []string) { - if ip == "" { - logger.Error("ip not empty") - os.Exit(1) - } - if domain == "" { - logger.Error("domain not empty") - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - hf := &hosts.HostFile{Path: hostsPath} - if _, ok := hf.HasDomain(domain); ok { - hf.DeleteDomain(domain) - logger.Info("domain %s delete success", domain) - } - hf.AppendHost(domain, ip) - logger.Info("domain %s:%s append success", domain, ip) - }, - } - hostsAddCmd.Flags().StringVar(&ip, "ip", "", "ip address") - hostsAddCmd.Flags().StringVar(&domain, "domain", "", "domain address") - - return hostsAddCmd -} - -func newHostsDeleteCmd() *cobra.Command { - var domain string - var hostsDeleteCmd = &cobra.Command{ - Use: "delete", - Short: "hosts manager delete", - PreRun: func(cmd *cobra.Command, args []string) { - if domain == "" { - logger.Error("domain not empty") - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - hf := &hosts.HostFile{Path: hostsPath} - hf.DeleteDomain(domain) - logger.Info("domain %s delete success", domain) - }, - } - hostsDeleteCmd.Flags().StringVar(&domain, "domain", "", "domain address") - - return hostsDeleteCmd -} - -func init() { - rootCmd.AddCommand(newHostsCmd()) -} diff --git a/cmd/sealctl/cmd/ipvs.go b/cmd/sealctl/cmd/ipvs.go deleted file mode 100644 index 1a608c86b307..000000000000 --- a/cmd/sealctl/cmd/ipvs.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "net" - - "github.com/labring/lvscare/pkg/route" - - "github.com/labring/lvscare/care" - "github.com/labring/sealos/pkg/constants" - "github.com/labring/sealos/pkg/utils/flags" - "github.com/labring/sealos/pkg/utils/hosts" - "github.com/labring/sealos/pkg/utils/iputils" - "github.com/labring/sealos/pkg/utils/logger" - "github.com/spf13/cobra" -) - -func newIPVSCmd() *cobra.Command { - var clean bool - var vip string - var ipvsCmd = &cobra.Command{ - Use: "ipvs", - Short: "sealos create or care local ipvs lb", - RunE: func(cmd *cobra.Command, args []string) error { - if clean { - lvs := care.BuildLvscare() - if err := lvs.DeleteVirtualServer(care.LVS.VirtualServer, false); err != nil { - return err - } - logger.Info("lvscare delete vip: %s success", care.LVS.VirtualServer) - routeOperator := route.NewRoute(vip, care.LVS.TargetIP.String()) - if err := routeOperator.DelRoute(); err != nil { - return err - } - logger.Info("lvscare delete route: %s success", care.LVS.VirtualServer) - return nil - } - care.LVS.VsAndRsCare() - return nil - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - flags.PrintFlags(cmd.Flags()) - if care.LVS.TargetIP == nil { - hf := &hosts.HostFile{Path: constants.DefaultHostsPath} - if ip, ok := hf.HasDomain(constants.DefaultLvscareDomain); ok { - care.LVS.TargetIP = net.ParseIP(ip) - } - logger.Debug("found target route ip is %s", care.LVS.TargetIP.String()) - if !clean { - if err := care.LVS.SyncRouter(); err != nil { - return err - } - } - } - care.LVS.Clean = true - vip = iputils.GetHostIP(care.LVS.VirtualServer) - return nil - }, - } - ipvsCmd.Flags().BoolVarP(&clean, "clean", "C", false, "clean ipvs and route") - ipvsCmd.Flags().BoolVar(&care.LVS.RunOnce, "run-once", false, "is run once mode") - ipvsCmd.Flags().StringVar(&care.LVS.VirtualServer, "vs", "", "virturl server like 10.54.0.2:6443") - ipvsCmd.Flags().StringSliceVar(&care.LVS.RealServer, "rs", []string{}, "real server like 192.168.0.2:6443") - ipvsCmd.Flags().IPVar(&care.LVS.TargetIP, "ip", nil, "target ip") - - ipvsCmd.Flags().StringVar(&care.LVS.HealthPath, "health-path", "/healthz", "health check path") - ipvsCmd.Flags().StringVar(&care.LVS.HealthSchem, "health-schem", "https", "health check schem") - ipvsCmd.Flags().Int32Var(&care.LVS.Interval, "interval", 5, "health check interval, unit is sec.") - return ipvsCmd -} - -func init() { - rootCmd.AddCommand(newIPVSCmd()) -} diff --git a/cmd/sealctl/cmd/kube.go b/cmd/sealctl/cmd/kube.go deleted file mode 100755 index 973b6356137b..000000000000 --- a/cmd/sealctl/cmd/kube.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "fmt" - "os" - - "github.com/labring/sealos/pkg/utils/exec" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" -) - -func newKubeCmd() *cobra.Command { - var fieldSelector, selector, kubeConfig, sources, namespace string - var allNamespace bool - var kubeCmd = &cobra.Command{ - Use: "kube", - Short: "kube resource list json", - Run: func(cmd *cobra.Command, args []string) { - template := "kubectl get %s %s %s %s %s -o json" - ns := "" - c := "" - fs := "" - ls := "" - if allNamespace { - ns = "--all-namespaces" - } else { - if namespace != "" { - ns = "-n " + namespace - } - } - - if kubeConfig != "" { - c = "--kubeconfig=" + kubeConfig - } - if fieldSelector != "" { - c = "--field-selector=" + fieldSelector - } - if selector != "" { - c = "--selector=" + selector - } - data, err := exec.RunBashCmd(fmt.Sprintf(template, sources, ns, c, fs, ls)) - if err != nil { - logger.Error(err) - os.Exit(1) - } - println(data) - }, - } - kubeCmd.Flags().BoolVar(&allNamespace, "--all-namespaces", false, " If present, list the requested object(s) across all namespaces. Namespace in current\ncontext is ignored even if specified with --namespace.") - kubeCmd.Flags().StringVar(&namespace, "namespace", "", "kubernetes namespace") - kubeCmd.Flags().StringVar(&sources, "sources", "nodes", "e.g. pods,deployments") - kubeCmd.Flags().StringVar(&fieldSelector, "field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector\nkey1=value1,key2=value2). The server only supports a limited number of field queries per type.") - kubeCmd.Flags().StringVar(&selector, "selector", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") - kubeCmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") - - return kubeCmd -} - -func init() { - rootCmd.AddCommand(newKubeCmd()) -} diff --git a/cmd/sealctl/cmd/password.go b/cmd/sealctl/cmd/password.go deleted file mode 100755 index f04226c3f8b8..000000000000 --- a/cmd/sealctl/cmd/password.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "io/ioutil" - "os" - - "github.com/labring/sealos/pkg/utils/file" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/passwd" -) - -func newPasswordCmd() *cobra.Command { - var passwordCmd = &cobra.Command{ - Use: "password", - Short: "generator password", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - // check route for host - passwordCmd.AddCommand(newRegistryCmd()) - passwordCmd.AddCommand(newContainerdCmd()) - return passwordCmd -} - -func newRegistryCmd() *cobra.Command { - var pwdPath, username, password string - var printBool bool - var registryCmd = &cobra.Command{ - Use: "registry", - Short: "generator registry password and file", - Run: func(cmd *cobra.Command, args []string) { - pwd := passwd.Htpasswd(username, password) - if printBool { - println(pwd) - return - } - logger.Debug("password registry is %s", pwd) - if err := file.MkDirs(pwdPath); err != nil { - logger.Error("init dir is error: %v", err) - os.Exit(1) - } - err := ioutil.WriteFile(pwdPath, []byte(pwd), 0755) - if err != nil { - logger.Error(err) - os.Exit(1) - } - logger.Info("generator registry password is success") - }, - } - // manually to set host via gateway - registryCmd.Flags().StringVar(&pwdPath, "path", "/etc/registry/registry_htpasswd", "default password file") - registryCmd.Flags().StringVar(&username, "username", "admin", "username") - registryCmd.Flags().StringVar(&password, "password", "admin", "password") - - registryCmd.Flags().BoolVar(&printBool, "print", false, "is print") - - return registryCmd -} - -func newContainerdCmd() *cobra.Command { - var username, password string - var containerdCmd = &cobra.Command{ - Use: "containerd", - Short: "generator containerd password", - Run: func(cmd *cobra.Command, args []string) { - pwd := passwd.LoginAuth(username, password) - println(pwd) - }, - } - // manually to set host via gateway - containerdCmd.Flags().StringVar(&username, "username", "admin", "username") - containerdCmd.Flags().StringVar(&password, "password", "admin", "password") - - return containerdCmd -} - -func init() { - rootCmd.AddCommand(newPasswordCmd()) -} diff --git a/cmd/sealctl/cmd/registry.go b/cmd/sealctl/cmd/registry.go deleted file mode 100644 index 670771922734..000000000000 --- a/cmd/sealctl/cmd/registry.go +++ /dev/null @@ -1,199 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "context" - "os" - "strings" - - "github.com/labring/sealos/pkg/utils/file" - "github.com/labring/sealos/pkg/utils/flags" - "github.com/labring/sealos/pkg/utils/logger" - "github.com/labring/sealos/pkg/utils/maps" - - "github.com/docker/docker/api/types" - v1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/buildimage" - "github.com/labring/sealos/pkg/passwd" - "github.com/labring/sealos/pkg/registry" -) - -var ( - registryPullRegistryDir string - registryPullAuths []string - registryPullArch string - registryPullMaxPullProcs int - registryPullAuthBasic bool -) - -func newRegistryImageCmd() *cobra.Command { - var registryImageCmd = &cobra.Command{ - Use: "registry", - Short: "registry images manager", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - registryImageCmd.AddCommand(newRegistryImagePullCmd()) - return registryImageCmd -} - -func newRegistryImagePullCmd() *cobra.Command { - var registryImagePullCmd = &cobra.Command{ - Use: "pull", - Short: "registry images manager pull to local dir", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - registryImagePullCmd.PersistentFlags().StringVar(®istryPullArch, "arch", "amd64", "pull images arch") - registryImagePullCmd.PersistentFlags().StringVar(®istryPullRegistryDir, "data-dir", "/var/lib/registry", "registry data dir path") - registryImagePullCmd.PersistentFlags().StringSliceVar(®istryPullAuths, "auths", []string{}, "auths data for login mirror registry, format example is \"address=docker.io&&auth=YWRtaW46YWRtaW4=\".") - registryImagePullCmd.PersistentFlags().IntVar(®istryPullMaxPullProcs, "max-pull-procs", 5, "maximum number of goroutines for pulling") - registryImagePullCmd.PersistentFlags().BoolVar(®istryPullAuthBasic, "basic-auth", false, "pull image auth policy,default is token auth") - registryImagePullCmd.AddCommand(newRegistryImagePullRawCmd()) - registryImagePullCmd.AddCommand(newRegistryImagePullYamlCmd()) - registryImagePullCmd.AddCommand(newRegistryImagePullDefaultCmd()) - return registryImagePullCmd -} - -func newRegistryImagePullRawCmd() *cobra.Command { - var imageFile string - var auth map[string]types.AuthConfig - var registryImagePullRaw = &cobra.Command{ - Use: "raw", - Short: "registry images manager pull to local dir by raw type", - Run: func(cmd *cobra.Command, args []string) { - flags.PrintFlags(cmd.Flags()) - images, err := file.ReadLines(imageFile) - if err != nil { - logger.Error("ImageFile convert images is error: %s", err.Error()) - os.Exit(1) - } - is := registry.NewImageSaver(context.Background(), registryPullMaxPullProcs, auth, registryPullAuthBasic) - _, err = is.SaveImages(images, registryPullRegistryDir, v1.Platform{OS: "linux", Architecture: registryPullArch}) - if err != nil { - logger.Error("pull registry images is error: %s", err.Error()) - os.Exit(1) - } - }, - PreRun: func(cmd *cobra.Command, args []string) { - auth = validateRegistryImagePull() - if !file.IsExist(imageFile) { - logger.Error("ImageFile path is not exist") - os.Exit(1) - } - }, - } - - registryImagePullRaw.PersistentFlags().StringVarP(&imageFile, "image-file", "f", "ImageFile", "ImageFile path") - return registryImagePullRaw -} - -func newRegistryImagePullYamlCmd() *cobra.Command { - var yamlPath string - var auth map[string]types.AuthConfig - var registryImagePullYaml = &cobra.Command{ - Use: "yaml", - Short: "registry images manager pull to local dir by yaml type", - Run: func(cmd *cobra.Command, args []string) { - flags.PrintFlags(cmd.Flags()) - images, err := buildimage.ParseYamlImages(yamlPath) - if err != nil { - logger.Error("yaml path convert images is error: %s", err.Error()) - os.Exit(1) - } - is := registry.NewImageSaver(context.Background(), registryPullMaxPullProcs, auth, registryPullAuthBasic) - _, err = is.SaveImages(images, registryPullRegistryDir, v1.Platform{OS: "linux", Architecture: registryPullArch}) - if err != nil { - logger.Error("pull registry images is error: %s", err.Error()) - os.Exit(1) - } - }, - PreRun: func(cmd *cobra.Command, args []string) { - auth = validateRegistryImagePull() - if !file.IsExist(yamlPath) { - logger.Error("yaml data dir path is not exist") - os.Exit(1) - } - }, - } - - registryImagePullYaml.PersistentFlags().StringVarP(&yamlPath, "yaml-path", "p", "", "yaml data dir path") - return registryImagePullYaml -} - -func newRegistryImagePullDefaultCmd() *cobra.Command { - var images []string - var auth map[string]types.AuthConfig - var registryImagePullDefault = &cobra.Command{ - Use: "default", - Short: "registry images manager pull to local dir by default type", - Run: func(cmd *cobra.Command, args []string) { - flags.PrintFlags(cmd.Flags()) - is := registry.NewImageSaver(context.Background(), registryPullMaxPullProcs, auth, registryPullAuthBasic) - _, err := is.SaveImages(images, registryPullRegistryDir, v1.Platform{OS: "linux", Architecture: registryPullArch}) - if err != nil { - logger.Error("pull registry images is error: %s", err.Error()) - os.Exit(1) - } - }, - PreRun: func(cmd *cobra.Command, args []string) { - auth = validateRegistryImagePull() - }, - } - - registryImagePullDefault.PersistentFlags().StringSliceVar(&images, "images", []string{}, "images list") - return registryImagePullDefault -} - -func validateRegistryImagePull() map[string]types.AuthConfig { - if !file.IsExist(registryPullRegistryDir) { - logger.Error("registry data dir is not exist") - os.Exit(1) - } - data := make(map[string]types.AuthConfig) - for _, a := range registryPullAuths { - auth := maps.StringToMap(a, "&&") - var ok bool - logger.Debug("range auth: %v", auth) - if _, ok = auth["address"]; !ok { - logger.Error("auths format is error, format is \"address=docker.io&&auth=YWRtaW46YWRtaW4=\".") - os.Exit(1) - } else { - userAndPwd, _ := passwd.LoginAuthDecode(auth["auth"]) - authConfig := types.AuthConfig{ - ServerAddress: auth["address"], - } - if userAndPwd != "" { - if userAndPwdArr := strings.Split(userAndPwd, ":"); len(userAndPwdArr) == 2 { - authConfig.Username = userAndPwdArr[0] - authConfig.Password = userAndPwdArr[1] - } - } - data[authConfig.ServerAddress] = authConfig - } - } - return data -} - -func init() { - rootCmd.AddCommand(newRegistryImageCmd()) -} diff --git a/cmd/sealctl/cmd/root.go b/cmd/sealctl/cmd/root.go deleted file mode 100644 index 49bde14f4b8f..000000000000 --- a/cmd/sealctl/cmd/root.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "fmt" - "os" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" -) - -var ( - debug bool - showPath bool -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "sealctl", - Short: "tools for sealos.", - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(func() { - logger.Cfg(debug, showPath) - }) - - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logger") - rootCmd.PersistentFlags().BoolVar(&showPath, "show-path", false, "enable show code path") -} diff --git a/cmd/sealctl/cmd/static_pod.go b/cmd/sealctl/cmd/static_pod.go deleted file mode 100755 index 578ea9540c4a..000000000000 --- a/cmd/sealctl/cmd/static_pod.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path" - - "github.com/labring/sealos/pkg/constants" - "github.com/labring/sealos/pkg/utils/file" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/ipvs" -) - -var staticPodPath string - -func newStaticPodCmd() *cobra.Command { - var staticPodCmd = &cobra.Command{ - Use: "static-pod", - Short: "generator static pod", - //Run: func(cmd *cobra.Command, args []string) { - // - //}, - } - // check route for host - staticPodCmd.AddCommand(newLvscareCmd()) - staticPodCmd.PersistentFlags().StringVar(&staticPodPath, "path", constants.KubernetesEtcStaticPod, "default kubernetes static pod path") - return staticPodCmd -} - -func newLvscareCmd() *cobra.Command { - var vip, image, name string - var masters []string - var printBool bool - var lvscareCmd = &cobra.Command{ - Use: "lvscare", - Short: "generator lvscare static pod file", - PreRun: func(cmd *cobra.Command, args []string) { - if len(masters) == 0 { - logger.Error("master not allow empty") - os.Exit(1) - } - }, - Run: func(cmd *cobra.Command, args []string) { - fileName := fmt.Sprintf("%s.%s", name, constants.YamlFileSuffix) - yaml, err := ipvs.LvsStaticPodYaml(vip, masters, image, name) - if err != nil { - logger.Error(err) - os.Exit(1) - } - if printBool { - println(yaml) - return - } - logger.Debug("lvscare static pod yaml is %s", yaml) - if err = file.MkDirs(staticPodPath); err != nil { - logger.Error("init dir is error: %v", err) - os.Exit(1) - } - err = ioutil.WriteFile(path.Join(staticPodPath, fileName), []byte(yaml), 0755) - if err != nil { - logger.Error(err) - os.Exit(1) - } - logger.Info("generator lvscare static pod is success") - }, - } - // manually to set host via gateway - lvscareCmd.Flags().StringVar(&vip, "vip", "10.103.97.2:6443", "default vip IP") - lvscareCmd.Flags().StringVar(&name, "name", constants.LvsCareStaticPodName, "generator lvscare static pod name") - lvscareCmd.Flags().StringVar(&image, "image", constants.DefaultLvsCareImage, "generator lvscare static pod image") - lvscareCmd.Flags().StringSliceVar(&masters, "masters", []string{}, "generator masters addrs") - lvscareCmd.Flags().BoolVar(&printBool, "print", false, "is print yaml") - return lvscareCmd -} - -func init() { - rootCmd.AddCommand(newStaticPodCmd()) -} diff --git a/cmd/sealctl/cmd/token.go b/cmd/sealctl/cmd/token.go deleted file mode 100755 index 94a1f51c8f4a..000000000000 --- a/cmd/sealctl/cmd/token.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "os" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/labring/sealos/pkg/runtime" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/util/json" -) - -func newTokenCmd() *cobra.Command { - var tokenCmd = &cobra.Command{ - Use: "token", - Short: "token generator", - Run: func(cmd *cobra.Command, args []string) { - t, err := runtime.Default() - if err != nil { - logger.Error("exec token error: " + err.Error()) - os.Exit(1) - } - data, _ := json.Marshal(t) - println(string(data)) - }, - } - return tokenCmd -} - -func init() { - rootCmd.AddCommand(newTokenCmd()) -} diff --git a/cmd/sealctl/cmd/version.go b/cmd/sealctl/cmd/version.go deleted file mode 100644 index aa9d60fb0ad8..000000000000 --- a/cmd/sealctl/cmd/version.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "encoding/json" - "fmt" - - "github.com/labring/sealos/pkg/utils/version" - - "github.com/spf13/cobra" -) - -var shortPrint bool - -func newVersionCmd() *cobra.Command { - var versionCmd = &cobra.Command{ - Use: "version", - Short: "version", - Args: cobra.NoArgs, - Example: `sealctl version`, - RunE: func(cmd *cobra.Command, args []string) error { - marshalled, err := json.Marshal(version.Get()) - if err != nil { - return err - } - if shortPrint { - fmt.Println(version.Get().String()) - } else { - fmt.Println(string(marshalled)) - } - return nil - }, - } - versionCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just the version number.") - return versionCmd -} - -func init() { - rootCmd.AddCommand(newVersionCmd()) -} diff --git a/cmd/sealctl/main.go b/cmd/sealctl/main.go deleted file mode 100644 index 96d7817cc67f..000000000000 --- a/cmd/sealctl/main.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 ( - "github.com/labring/sealos/cmd/sealctl/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/cmd/sealos/cmd/add.go b/cmd/sealos/cmd/add.go deleted file mode 100644 index bd27db0d57bc..000000000000 --- a/cmd/sealos/cmd/add.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright © 2022 cuisongliu@qq.com. -// -// 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 cmd - -import ( - "errors" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/apply" -) - -const exampleAdd = ` -add to nodes : - sealos add --nodes x.x.x.x - -add to default cluster: - sealos add --masters x.x.x.x --nodes x.x.x.x - sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y -` - -// addCmd represents the delete command -func newAddCmd() *cobra.Command { - var addCmd = &cobra.Command{ - Use: "add", - Short: "add some node", - Args: cobra.NoArgs, - Example: exampleAdd, - RunE: func(cmd *cobra.Command, args []string) error { - //return errors.New("add feature no support") - applier, err := apply.NewScaleApplierFromArgs(addArgs, "add") - if err != nil { - return err - } - return applier.Apply() - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if addArgs.Nodes == "" && addArgs.Masters == "" { - return errors.New("node and master not empty in same time") - } - return nil - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - addArgs = &apply.ScaleArgs{} - addCmd.Flags().StringVarP(&addArgs.Masters, "masters", "m", "", "reduce Count or IPList to masters") - addCmd.Flags().StringVarP(&addArgs.Nodes, "nodes", "n", "", "reduce Count or IPList to nodes") - addCmd.Flags().StringVarP(&addArgs.ClusterName, "cluster", "c", "default", "delete a kubernetes cluster with cluster name") - return addCmd -} - -var addArgs *apply.ScaleArgs - -func init() { - rootCmd.AddCommand(newAddCmd()) -} diff --git a/cmd/sealos/cmd/apply.go b/cmd/sealos/cmd/apply.go deleted file mode 100644 index 3eb073ef3305..000000000000 --- a/cmd/sealos/cmd/apply.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright © 2022 cuisongliu@qq.com. -// -// 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 cmd - -import ( - "github.com/labring/sealos/pkg/utils/logger" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/apply" -) - -var clusterFile string - -func newApplyCmd() *cobra.Command { - // applyCmd represents the apply command - var applyCmd = &cobra.Command{ - Use: "apply", - Short: "apply a kubernetes cluster", - Example: `sealos apply -f Clusterfile`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - applier, err := apply.NewApplierFromFile(clusterFile) - if err != nil { - return err - } - return applier.Apply() - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - applyCmd.Flags().StringVarP(&clusterFile, "Clusterfile", "f", "Clusterfile", "apply a kubernetes cluster") - return applyCmd -} - -func init() { - rootCmd.AddCommand(newApplyCmd()) -} diff --git a/cmd/sealos/cmd/build.go b/cmd/sealos/cmd/build.go deleted file mode 100644 index a9bdaf139b6a..000000000000 --- a/cmd/sealos/cmd/build.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "fmt" - "os" - "runtime" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" - "github.com/labring/sealos/pkg/image/types" -) - -func newBuildCmd() *cobra.Command { - var options types.BuildOptions - var buildCmd = &cobra.Command{ - Use: "build [flags] PATH", - Short: "build an cloud image from a Kubefile", - Example: `sealos build -t labring/kubernetes:v1.24.0 .`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - var ( - buildContext = "." - ) - if len(args) != 0 { - buildContext = args[0] - } - - imageSvc, err := image.NewImageService() - if err != nil { - return err - } - return imageSvc.Build(&options, buildContext, options.Tag) - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - buildCmd.Flags().StringVarP(&options.File, "kubefile", "f", "Kubefile", "kubefile filepath") - buildCmd.Flags().StringVarP(&options.Tag, "tag", "t", "", "tagged name to apply to the built image") - buildCmd.Flags().StringVar(&options.Platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), "set the OS/ARCH/VARIANT of the image to the provided value instead of the current operating system and architecture of the host (for example linux/arm)") - buildCmd.Flags().IntVarP(&options.MaxPullProcs, "max-pull-procs", "m", 5, "maximum number of goroutines for pulling") - buildCmd.Flags().BoolVar(&options.BasicAuth, "basic-auth", false, "pull image auth policy,default is token auth") - - if err := buildCmd.MarkFlagRequired("tag"); err != nil { - logger.Error("failed to init flag: %v", err) - os.Exit(1) - } - return buildCmd -} - -func init() { - rootCmd.AddCommand(newBuildCmd()) -} diff --git a/cmd/sealos/cmd/create.go b/cmd/sealos/cmd/create.go deleted file mode 100644 index c1c7b9797c39..000000000000 --- a/cmd/sealos/cmd/create.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright © 2022 sealos. -// -// 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 cmd - -import ( - "github.com/labring/sealos/pkg/utils/logger" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -var exampleCreate = ` -create a mysql cluster: - sealos create mysql:8.0 -with custom cluster name: - sealos create mysql:8.0 -c mysql -` - -func newCreateCmd() *cobra.Command { - var createCmd = &cobra.Command{ - Use: "create", - Short: "Create a cluster without running the CMD", - Long: `sealos create [image name] [args]`, - Example: exampleCreate, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - imageName := args[0] - registrySvc, err := image.NewRegistryService() - if err != nil { - return err - } - - clusterSvc, err := image.NewClusterService() - if err != nil { - return err - } - - err = registrySvc.Pull(imageName) - if err != nil { - return err - } - - manifest, err := clusterSvc.Create(imageName, clusterName) - if err != nil { - return err - } - logger.Info("Mount point: %s", manifest.MountPoint) - - return nil - }, - } - createCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "default", "set custom cluster name") - return createCmd -} - -func init() { - rootCmd.AddCommand(newCreateCmd()) -} diff --git a/cmd/sealos/cmd/delete.go b/cmd/sealos/cmd/delete.go deleted file mode 100644 index a3ee886ca49f..000000000000 --- a/cmd/sealos/cmd/delete.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright © 2021 Alibaba Group Holding Ltd. -// -// 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 cmd - -import ( - "errors" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/apply" - "github.com/labring/sealos/pkg/apply/processor" -) - -var deleteArgs apply.ScaleArgs - -var exampleDelete = ` -delete nodes: - sealos delete --nodes x.x.x.x - if accidentally deleted; - Use 'sealos add' to recover: - sealos add --nodes x.x.x.x - -delete masters: - sealos delete --masters x.x.x.x - -delete masters and nodes: - sealos delete --masters x.x.x.x --nodes x.x.x.x - sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y - -Please note that sealos will delete your master if the --masters parameter is specified. -` - -// deleteCmd represents the delete command -func newDeleteCmd() *cobra.Command { - var deleteCmd = &cobra.Command{ - Use: "delete", - Short: "delete some node", - Args: cobra.NoArgs, - Example: exampleDelete, - RunE: func(cmd *cobra.Command, args []string) error { - if err := processor.ConfirmDeleteNodes(); err != nil { - return err - } - applier, err := apply.NewScaleApplierFromArgs(&deleteArgs, "delete") - if err != nil { - return err - } - return applier.Apply() - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if deleteArgs.Nodes == "" && deleteArgs.Masters == "" { - return errors.New("node and master not empty in same time") - } - return nil - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - deleteCmd.Flags().StringVarP(&deleteArgs.Masters, "masters", "m", "", "reduce Count or IPList to masters") - deleteCmd.Flags().StringVarP(&deleteArgs.Nodes, "nodes", "n", "", "reduce Count or IPList to nodes") - deleteCmd.Flags().StringVarP(&deleteArgs.ClusterName, "cluster", "c", "default", "delete a kubernetes cluster with cluster name") - deleteCmd.Flags().BoolVar(&processor.ForceDelete, "force", false, "We also can input an --force flag to delete cluster by force") - return deleteCmd -} - -func init() { - rootCmd.AddCommand(newDeleteCmd()) -} diff --git a/cmd/sealos/cmd/docs.go b/cmd/sealos/cmd/docs.go deleted file mode 100644 index 3e8b32d13683..000000000000 --- a/cmd/sealos/cmd/docs.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2022 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" -) - -var docsPath string - -func newDocsCmd() *cobra.Command { - var docsCmd = &cobra.Command{ - Use: "docs", - Short: "generate API reference", - Example: `sealos docs`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return doc.GenMarkdownTree(rootCmd, docsPath) - }, - } - docsCmd.Flags().StringVarP(&docsPath, "path", "p", "./docs/api", "path to output docs") - - return docsCmd -} - -func init() { - rootCmd.AddCommand(newDocsCmd()) -} diff --git a/cmd/sealos/cmd/exec.go b/cmd/sealos/cmd/exec.go deleted file mode 100644 index c9063b7d2670..000000000000 --- a/cmd/sealos/cmd/exec.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "github.com/labring/sealos/pkg/ssh" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/clusterfile" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -var roles string -var clusterName string -var ips []string - -var exampleExec = ` -exec to default cluster: default - sealos exec "cat /etc/hosts" -specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ): - sealos exec -c my-cluster "cat /etc/hosts" -set role label to exec cmd: - sealos exec -c my-cluster -r master,slave,node1 "cat /etc/hosts" -set ips to exec cmd: - sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" -` - -func newExecCmd() *cobra.Command { - var cluster *v1beta1.Cluster - var execCmd = &cobra.Command{ - Use: "exec", - Short: "exec a shell command or script on all node.", - Example: exampleExec, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(ips) > 0 { - execIPCmd, err := ssh.NewExecCmdFromIPs(cluster, ips) - if err != nil { - return err - } - return execIPCmd.RunCmd(args[0]) - } - execRoleCmd, err := ssh.NewExecCmdFromRoles(cluster, roles) - if err != nil { - return err - } - return execRoleCmd.RunCmd(args[0]) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - cls, err := clusterfile.GetClusterFromName(clusterName) - if err != nil { - return err - } - cluster = cls - return nil - }, - } - execCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "default", "submit one cluster name") - execCmd.Flags().StringVarP(&roles, "roles", "r", "", "set role label to roles") - execCmd.Flags().StringSliceVar(&ips, "ips", []string{}, "ssh ips list on node") - return execCmd -} - -func init() { - rootCmd.AddCommand(newExecCmd()) -} diff --git a/cmd/sealos/cmd/gen.go b/cmd/sealos/cmd/gen.go deleted file mode 100644 index eb239f7f0080..000000000000 --- a/cmd/sealos/cmd/gen.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "fmt" - - "github.com/labring/sealos/pkg/apply" - "github.com/labring/sealos/pkg/types/v1beta1" - "github.com/spf13/cobra" -) - -var exampleGen = ` -generate a cluster with multi images, specify masters and nodes: - sealos gen labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \ - --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx - -specify server InfraSSH port: - all servers use the same InfraSSH port: - sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx - different InfraSSH port numbers: - sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ - --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx -` -var genArgs apply.RunArgs - -func newGenCmd() *cobra.Command { - var genCmd = &cobra.Command{ - Use: "gen", - Short: "Generate a Clusterfile", - Long: `Generate a Clusterfile of the kubernetes cluster, which can be applied by 'sealos apply' command`, - Example: exampleGen, - RunE: func(cmd *cobra.Command, args []string) error { - cluster, err := apply.NewClusterFromArgs(args, &genArgs) - if err != nil { - return err - } - fmt.Println(cluster.String()) - return nil - }, - } - genCmd.Flags().StringVarP(&genArgs.Masters, "masters", "m", "", "set Count or IPList to masters") - genCmd.Flags().StringVarP(&genArgs.Nodes, "nodes", "n", "", "set Count or IPList to nodes") - genCmd.Flags().StringVarP(&genArgs.User, "user", "u", v1beta1.DefaultUserRoot, "set baremetal server username") - genCmd.Flags().StringVarP(&genArgs.Password, "passwd", "p", "", "set cloud provider or baremetal server password") - genCmd.Flags().Uint16Var(&genArgs.Port, "port", 22, "set the sshd service port number for the server") - genCmd.Flags().StringVar(&genArgs.Pk, "pk", v1beta1.DefaultPKFile, "set baremetal server private key") - genCmd.Flags().StringVar(&genArgs.PkPassword, "pk-passwd", "", "set baremetal server private key password") - genCmd.Flags().StringSliceVar(&genArgs.CustomCMD, "cmd", []string{}, "set cmd for image cmd instruction") - genCmd.Flags().StringSliceVarP(&genArgs.CustomEnv, "env", "e", []string{}, "set custom environment variables") - genCmd.Flags().StringVar(&genArgs.ClusterName, "name", "default", "set cluster name variables") - return genCmd -} - -func init() { - rootCmd.AddCommand(newGenCmd()) -} diff --git a/cmd/sealos/cmd/images.go b/cmd/sealos/cmd/images.go deleted file mode 100644 index d968f0c6134c..000000000000 --- a/cmd/sealos/cmd/images.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newImagesCmd() *cobra.Command { - var imagesCmd = &cobra.Command{ - Use: "images", - Short: "list cloud image", - Example: `sealos images`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.ListImages() - }, - } - return imagesCmd -} - -func init() { - rootCmd.AddCommand(newImagesCmd()) -} diff --git a/cmd/sealos/cmd/inspect.go b/cmd/sealos/cmd/inspect.go deleted file mode 100644 index cd53a9e58c8d..000000000000 --- a/cmd/sealos/cmd/inspect.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2022 chenchuanle6@gmail.com. - -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 cmd - -import ( - "encoding/json" - "os" - - "github.com/labring/sealos/pkg/image" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/term" -) - -var exampleInspect = ` - sealos inspect imageName - sealos inspect -t app applicationName -` - -var inspectType string - -func newInspectCmd() *cobra.Command { - var inspectCmd = &cobra.Command{ - Use: "inspect", - Short: "Inspect the configuration of a application or image", - Example: exampleInspect, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.Errorf("application or image name must be specified") - } - if len(args) > 1 { - return errors.Errorf("too many arguments specified") - } - - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if term.IsTerminal(int(os.Stdout.Fd())) { - enc.SetEscapeHTML(false) - } - - name := args[0] - switch inspectType { - case "image": - imageService, err := image.NewImageService() - if err != nil { - return err - } - imageList, err := imageService.Inspect(name) - if err != nil { - return err - } - if len(imageList) == 0 { - return errors.Errorf("inspect error") - } - return enc.Encode(imageList[0]) - case "app": - clusterService, err := image.NewClusterService() - if err != nil { - return err - } - clusterManifest, err := clusterService.Inspect(name) - if err != nil { - return err - } - return enc.Encode(clusterManifest) - default: - return errors.Errorf("unknown type %s", inspectType) - } - }, - } - - inspectCmd.Flags().StringVarP(&inspectType, "type", "t", "image", "look at the item of the specified type (image or app)") - return inspectCmd -} - -func init() { - rootCmd.AddCommand(newInspectCmd()) -} diff --git a/cmd/sealos/cmd/load.go b/cmd/sealos/cmd/load.go deleted file mode 100644 index bcaca0b7e484..000000000000 --- a/cmd/sealos/cmd/load.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newLoadCmd() *cobra.Command { - var archiveName string - var loadCmd = &cobra.Command{ - Use: "load", - Short: "load cloud image", - Example: `sealos load -i kubernetes.tar`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.Load(archiveName) - }, - } - loadCmd.Flags().StringVarP(&archiveName, "input", "i", "", "read image from tar archive file") - return loadCmd -} - -func init() { - rootCmd.AddCommand(newLoadCmd()) -} diff --git a/cmd/sealos/cmd/login.go b/cmd/sealos/cmd/login.go deleted file mode 100644 index 0545bb71e643..000000000000 --- a/cmd/sealos/cmd/login.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "os" - - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newLoginCmd() *cobra.Command { - var username, password string - var loginCmd = &cobra.Command{ - Use: "login", - Short: "login image repository", - Example: `sealos login registry.cn-qingdao.aliyuncs.com -u [username] -p [password]`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewRegistryService() - if err != nil { - return err - } - return registrySvc.Login(args[0], username, password) - }, - } - loginCmd.Flags().StringVarP(&username, "username", "u", "", "user name for login registry") - loginCmd.Flags().StringVarP(&password, "passwd", "p", "", "password for login registry") - if err := loginCmd.MarkFlagRequired("username"); err != nil { - logger.Error("failed to init flag: %v", err) - os.Exit(1) - } - if err := loginCmd.MarkFlagRequired("passwd"); err != nil { - logger.Error("failed to init flag: %v", err) - os.Exit(1) - } - return loginCmd -} - -func init() { - rootCmd.AddCommand(newLoginCmd()) -} diff --git a/cmd/sealos/cmd/logout.go b/cmd/sealos/cmd/logout.go deleted file mode 100644 index b498a5a6de0a..000000000000 --- a/cmd/sealos/cmd/logout.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newLogoutCmd() *cobra.Command { - var logoutCmd = &cobra.Command{ - Use: "logout", - Short: "logout image repository", - Example: `sealos logout registry.cn-qingdao.aliyuncs.com`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewRegistryService() - if err != nil { - return err - } - return registrySvc.Logout(args[0]) - }, - } - return logoutCmd -} - -func init() { - rootCmd.AddCommand(newLogoutCmd()) -} diff --git a/cmd/sealos/cmd/prune.go b/cmd/sealos/cmd/prune.go deleted file mode 100644 index f16342bc54a3..000000000000 --- a/cmd/sealos/cmd/prune.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newPruneCmd() *cobra.Command { - var pruneCmd = &cobra.Command{ - Use: "prune", - Short: "prune image ", - Example: `sealos prune`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.Prune() - }, - } - return pruneCmd -} - -func init() { - rootCmd.AddCommand(newPruneCmd()) -} diff --git a/cmd/sealos/cmd/pull.go b/cmd/sealos/cmd/pull.go deleted file mode 100644 index 3d9dfa359111..000000000000 --- a/cmd/sealos/cmd/pull.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newPullCmd() *cobra.Command { - var pullCmd = &cobra.Command{ - Use: "pull", - Short: "pull cloud image", - Example: `sealos pull labring/kubernetes:v1.24.0`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewRegistryService() - if err != nil { - return err - } - return registrySvc.Pull(args[0]) - }, - } - return pullCmd -} - -func init() { - rootCmd.AddCommand(newPullCmd()) -} diff --git a/cmd/sealos/cmd/push.go b/cmd/sealos/cmd/push.go deleted file mode 100644 index 661cc9694942..000000000000 --- a/cmd/sealos/cmd/push.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newPushCmd() *cobra.Command { - var pushCmd = &cobra.Command{ - Use: "push", - Short: "push cloud image", - Example: `sealos push labring/kubernetes:v1.24.0`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewRegistryService() - if err != nil { - return err - } - return registrySvc.Push(args[0]) - }, - } - return pushCmd -} - -func init() { - rootCmd.AddCommand(newPushCmd()) -} diff --git a/cmd/sealos/cmd/reset.go b/cmd/sealos/cmd/reset.go deleted file mode 100644 index da4fc41f8275..000000000000 --- a/cmd/sealos/cmd/reset.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/labring/sealos/pkg/utils/logger" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/apply" - "github.com/labring/sealos/pkg/apply/processor" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -var exampleReset = ` -reset you current cluster: - sealos reset --name xxx [--force] -` - -var resetArgs apply.ResetArgs - -func newResetCmd() *cobra.Command { - var resetCmd = &cobra.Command{ - Use: "reset", - Short: "Simplest way to reset your cluster", - Long: `sealos reset --name [arg]`, - Example: exampleReset, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - if err := processor.ConfirmDeleteNodes(); err != nil { - return err - } - applier, err := apply.NewApplierFromResetArgs(&resetArgs) - if err != nil { - return err - } - return applier.Delete() - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - resetCmd.Flags().StringVarP(&resetArgs.Masters, "masters", "m", "", "set Count or IPList to masters") - resetCmd.Flags().StringVarP(&resetArgs.Nodes, "nodes", "n", "", "set Count or IPList to nodes") - resetCmd.Flags().StringVarP(&resetArgs.User, "user", "u", v1beta1.DefaultUserRoot, "set baremetal server username") - resetCmd.Flags().StringVarP(&resetArgs.Password, "passwd", "p", "", "set cloud provider or baremetal server password") - resetCmd.Flags().Uint16Var(&resetArgs.Port, "port", 22, "set the sshd service port number for the server") - resetCmd.Flags().StringVar(&resetArgs.Pk, "pk", v1beta1.DefaultPKFile, "set baremetal server private key") - resetCmd.Flags().StringVar(&resetArgs.PkPassword, "pk-passwd", "", "set baremetal server private key password") - resetCmd.Flags().StringVar(&resetArgs.ClusterName, "name", "default", "set cluster name variables") - resetCmd.Flags().BoolVar(&processor.ForceDelete, "force", false, "we also can input an --force flag to reset cluster by force") - return resetCmd -} - -func init() { - rootCmd.AddCommand(newResetCmd()) -} diff --git a/cmd/sealos/cmd/rmi.go b/cmd/sealos/cmd/rmi.go deleted file mode 100644 index 0caca7603ce0..000000000000 --- a/cmd/sealos/cmd/rmi.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newRMICmd() *cobra.Command { - var force bool - var rmiCmd = &cobra.Command{ - Use: "rmi", - Short: "Remove one or more cloud images", - Example: `sealos rmi [-f] labring/kubernetes:v1.24.0`, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.Remove(force, args...) - }, - } - rmiCmd.Flags().BoolVarP(&force, "force", "f", false, "force removal all of the image") - return rmiCmd -} - -func init() { - rootCmd.AddCommand(newRMICmd()) -} diff --git a/cmd/sealos/cmd/root.go b/cmd/sealos/cmd/root.go deleted file mode 100644 index 55ae1d1e0843..000000000000 --- a/cmd/sealos/cmd/root.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "fmt" - "os" - - "github.com/labring/sealos/pkg/constants" - "github.com/labring/sealos/pkg/utils/file" - "github.com/labring/sealos/pkg/utils/logger" - - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -var ( - debug bool - clusterRootDir string -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "sealos", - Short: "simplest way install kubernetes tools.", - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(onBootOnDie) - - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logger") - rootCmd.PersistentFlags().StringVar(&clusterRootDir, "cluster-root", constants.DefaultClusterRootfsDir, "cluster root directory") -} - -func onBootOnDie() { - constants.DefaultClusterRootfsDir = clusterRootDir - var rootDirs = []string{ - constants.LogPath(), - constants.DataPath(), - constants.Workdir(), - } - if err := file.MkDirs(rootDirs...); err != nil { - logger.Error(err) - panic(1) - } - logger.CfgAndFile(debug, constants.LogPath(), "sealos", false) - setupLogrus() -} - -const logLevel = "warn" - -func setupLogrus() { - logrusLvl, _ := logrus.ParseLevel(logLevel) - logrus.SetLevel(logrusLvl) -} diff --git a/cmd/sealos/cmd/run.go b/cmd/sealos/cmd/run.go deleted file mode 100644 index f48630b711fe..000000000000 --- a/cmd/sealos/cmd/run.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/labring/sealos/pkg/utils/logger" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/apply" - "github.com/labring/sealos/pkg/apply/processor" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -var contact = ` - ___ ___ ___ ___ ___ ___ - /\ \ /\ \ /\ \ /\__\ /\ \ /\ \ - /::\ \ /::\ \ /::\ \ /:/ / /::\ \ /::\ \ - /:/\ \ \ /:/\:\ \ /:/\:\ \ /:/ / /:/\:\ \ /:/\ \ \ - _\:\~\ \ \ /::\~\:\ \ /::\~\:\ \ /:/ / /:/ \:\ \ _\:\~\ \ \ - /\ \:\ \ \__\ /:/\:\ \:\__\ /:/\:\ \:\__\ /:/__/ /:/__/ \:\__\ /\ \:\ \ \__\ - \:\ \:\ \/__/ \:\~\:\ \/__/ \/__\:\/:/ / \:\ \ \:\ \ /:/ / \:\ \:\ \/__/ - \:\ \:\__\ \:\ \:\__\ \::/ / \:\ \ \:\ /:/ / \:\ \:\__\ - \:\/:/ / \:\ \/__/ /:/ / \:\ \ \:\/:/ / \:\/:/ / - \::/ / \:\__\ /:/ / \:\__\ \::/ / \::/ / - \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ - - Website :https://www.sealos.io/ - Address :github.com/labring/sealos -` - -var exampleRun = ` -create cluster to your baremetal server, appoint the iplist: - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx - multi image: - sealos run labring/kubernetes:v1.24.0 calico:v3.22.1 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 - Specify server InfraSSH port : - All servers use the same InfraSSH port (default port: 22): - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx - Different InfraSSH port numbers exist: - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ - --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx - -create a cluster with custom environment variables: - sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx -` -var runArgs apply.RunArgs - -func newRunCmd() *cobra.Command { - var runCmd = &cobra.Command{ - Use: "run", - Short: "Simplest way to run your kubernetes HA cluster", - Long: `sealos run labring/kubernetes:v1.24.0 --masters [arg] --nodes [arg]`, - Example: exampleRun, - RunE: func(cmd *cobra.Command, args []string) error { - applier, err := apply.NewApplierFromArgs(args, &runArgs) - if err != nil { - return err - } - return applier.Apply() - }, - PostRun: func(cmd *cobra.Command, args []string) { - logger.Info(contact) - }, - } - runCmd.Flags().StringVarP(&runArgs.Masters, "masters", "m", "", "set Count or IPList to masters") - runCmd.Flags().StringVarP(&runArgs.Nodes, "nodes", "n", "", "set Count or IPList to nodes") - runCmd.Flags().StringVarP(&runArgs.User, "user", "u", v1beta1.DefaultUserRoot, "set baremetal server username") - runCmd.Flags().StringVarP(&runArgs.Password, "passwd", "p", "", "set cloud provider or baremetal server password") - runCmd.Flags().Uint16Var(&runArgs.Port, "port", 22, "set the sshd service port number for the server") - runCmd.Flags().StringVar(&runArgs.Pk, "pk", v1beta1.DefaultPKFile, "set baremetal server private key") - runCmd.Flags().StringVar(&runArgs.PkPassword, "pk-passwd", "", "set baremetal server private key password") - runCmd.Flags().StringSliceVar(&runArgs.CustomCMD, "cmd", []string{}, "set cmd for image cmd instruction") - runCmd.Flags().StringSliceVarP(&runArgs.CustomEnv, "env", "e", []string{}, "set custom environment variables") - runCmd.Flags().BoolVarP(&processor.ForceOverride, "force", "f", false, "we also can input an --force flag to run app in this cluster by force") - runCmd.Flags().StringVar(&runArgs.ClusterName, "name", "default", "set cluster name variables") - return runCmd -} - -func init() { - rootCmd.AddCommand(newRunCmd()) -} diff --git a/cmd/sealos/cmd/save.go b/cmd/sealos/cmd/save.go deleted file mode 100644 index f7540b07af75..000000000000 --- a/cmd/sealos/cmd/save.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newSaveCmd() *cobra.Command { - var archiveName string - var saveCmd = &cobra.Command{ - Use: "save", - Short: "save cloud image to a tar file", - Example: `sealos save -o kubernetes.tar labring/kubernetes:v1.24.0`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.Save(args[0], archiveName) - }, - } - saveCmd.Flags().StringVarP(&archiveName, "output", "o", "", "read image from tar archive file") - return saveCmd -} - -func init() { - rootCmd.AddCommand(newSaveCmd()) -} diff --git a/cmd/sealos/cmd/scp.go b/cmd/sealos/cmd/scp.go deleted file mode 100644 index b0a5caabd112..000000000000 --- a/cmd/sealos/cmd/scp.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2022 cuisongliu@qq.com. - -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 cmd - -import ( - "github.com/labring/sealos/pkg/ssh" - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/clusterfile" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -// Shared with exec.go -// var roles string -// var clusterName string -// var ips []string - -const exampleScp = ` -copy file to default cluster: default - sealos scp "/root/aa.txt" "/root/dd.txt" -specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ): - sealos scp -c my-cluster "/root/aa.txt" "/root/dd.txt" -set role label to copy file: - sealos scp -c my-cluster -r master,slave,node1 "cat /etc/hosts" -set ips to copy file: - sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" -` - -func newScpCmd() *cobra.Command { - var cluster *v1beta1.Cluster - var scpCmd = &cobra.Command{ - Use: "scp", - // Aliases: []string{"cp"}, - Short: "copy local file to remote on all node.", - Example: exampleScp, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - if len(ips) > 0 { - sshCmd, err := ssh.NewExecCmdFromIPs(cluster, ips) - if err != nil { - return err - } - return sshCmd.RunCopy(args[0], args[1]) - } - sshCmd, err := ssh.NewExecCmdFromRoles(cluster, roles) - if err != nil { - return err - } - return sshCmd.RunCopy(args[0], args[1]) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - cls, err := clusterfile.GetClusterFromName(clusterName) - if err != nil { - return err - } - cluster = cls - return nil - }, - } - scpCmd.Flags().StringVarP(&clusterName, "cluster-name", "c", "default", "submit one cluster name") - scpCmd.Flags().StringVarP(&roles, "roles", "r", "", "set role label to roles") - scpCmd.Flags().StringSliceVar(&ips, "ips", []string{}, "ssh ips list on node") - return scpCmd -} - -func init() { - rootCmd.AddCommand(newScpCmd()) -} diff --git a/cmd/sealos/cmd/tag.go b/cmd/sealos/cmd/tag.go deleted file mode 100644 index 33f7d2be787e..000000000000 --- a/cmd/sealos/cmd/tag.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "github.com/spf13/cobra" - - "github.com/labring/sealos/pkg/image" -) - -func newTagCmd() *cobra.Command { - var tagCmd = &cobra.Command{ - Use: "tag", - Short: "tag a image as a new one", - Example: `sealos tag labring/kubernetes:v1.24.0 oci-kubernetes:v1.24.0`, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - registrySvc, err := image.NewImageService() - if err != nil { - return err - } - return registrySvc.Tag(args[0], args[1]) - }, - } - return tagCmd -} - -func init() { - rootCmd.AddCommand(newTagCmd()) -} diff --git a/cmd/sealos/cmd/version.go b/cmd/sealos/cmd/version.go deleted file mode 100644 index 96a0ab1a1a00..000000000000 --- a/cmd/sealos/cmd/version.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright © 2021 sealos. -// -// 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 cmd - -import ( - "encoding/json" - "fmt" - - "github.com/labring/sealos/pkg/utils/version" - - "github.com/spf13/cobra" -) - -var shortPrint bool - -func newVersionCmd() *cobra.Command { - var versionCmd = &cobra.Command{ - Use: "version", - Short: "version", - Args: cobra.NoArgs, - Example: `sealos version`, - RunE: func(cmd *cobra.Command, args []string) error { - marshalled, err := json.Marshal(version.Get()) - if err != nil { - return err - } - if shortPrint { - fmt.Println(version.Get().String()) - } else { - fmt.Println(string(marshalled)) - } - return nil - }, - } - versionCmd.Flags().BoolVar(&shortPrint, "short", false, "if true, print just the version number.") - return versionCmd -} - -func init() { - rootCmd.AddCommand(newVersionCmd()) -} diff --git a/controllers/account/.dockerignore b/controllers/account/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/account/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/account/.gitignore b/controllers/account/.gitignore new file mode 100644 index 000000000000..3492bd155170 --- /dev/null +++ b/controllers/account/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +paymentSecret.yaml \ No newline at end of file diff --git a/controllers/account/Dockerfile b/controllers/account/Dockerfile new file mode 100644 index 000000000000..4c0401659480 --- /dev/null +++ b/controllers/account/Dockerfile @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-account-$TARGETARCH /manager +ENTRYPOINT ["/manager"] \ No newline at end of file diff --git a/controllers/account/Makefile b/controllers/account/Makefile new file mode 100644 index 000000000000..7f73ffa3624d --- /dev/null +++ b/controllers/account/Makefile @@ -0,0 +1,140 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-account-controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.23 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +build: ## Build manager binary. + LD_FLAGS="-s -w" && CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager . + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: build + mv bin/manager bin/controller-account-${TARGETARCH} + docker build -t $(IMG) . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: docker-build + docker push $(IMG) + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: pre-deploy +pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/account/PROJECT b/controllers/account/PROJECT new file mode 100644 index 000000000000..7ec96ae439b6 --- /dev/null +++ b/controllers/account/PROJECT @@ -0,0 +1,49 @@ +domain: sealos.io +layout: +- go.kubebuilder.io/v3 +projectName: account +repo: github.com/labring/sealos/controllers/account +resources: +- api: + controller: true + domain: sealos.io + group: account + kind: Account + version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: account + kind: Payment + path: github.com/labring/sealos/controllers/account/api/v1 + version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: account + kind: Debt + path: github.com/labring/sealos/controllers/account/api/v1 + version: v1 + webhooks: + conversion: true + webhookVersion: v1 +- controller: true + domain: sealos.io + group: account + kind: Billing + version: v1 +- controller: true + group: core + kind: Pod + path: k8s.io/api/core/v1 + version: v1 +- controller: true + group: core + kind: Namespace + path: k8s.io/api/core/v1 + version: v1 +version: "3" diff --git a/controllers/account/README.md b/controllers/account/README.md new file mode 100644 index 000000000000..37be87128687 --- /dev/null +++ b/controllers/account/README.md @@ -0,0 +1,25 @@ + +创建微信支付所需要的 secret +选择 secret 的方式是因为 WechatPrivateKey 有很多特殊符号很容易解析错误,先base64加密之后通过secret是一种比较合适的办法。 + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: payment-secret + namespace: account-system +type: Opaque +data: + MchID: xxxx + AppID: xxxxx + MchAPIv3Key: xxxx + MchCertificateSerialNumber: xxxx + WechatPrivateKey: xxxx +``` + + +### 部署方式 +``` +sealos run ghcr.io/labring/sealos-account-controller:deploy-cluster --env MONGO_URI="mongodb://username:passwd@ip:port/sealos-resources?authSource=admin" +``` + diff --git a/controllers/account/api/v1/account_types.go b/controllers/account/api/v1/account_types.go new file mode 100644 index 000000000000..365eaf58c926 --- /dev/null +++ b/controllers/account/api/v1/account_types.go @@ -0,0 +1,132 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "github.com/labring/sealos/controllers/pkg/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + AccountSystemNamespaceEnv = "ACCOUNT_SYSTEM_NAMESPACE" +) + +type ( + Status string + Type common.Type +) + +const ( + // Consumption 消费 + Consumption common.Type = iota + // Subconsumption 子消费 + SubConsumption + + TransferIn + TransferOut + ActivityGiving +) + +const QueryAllType Type = -1 + +const ( + Completed Status = "completed" + Create Status = "create" + Failed Status = "failed" +) + +type Costs map[string]int64 + +const ( + Name = "name" + Owner = "owner" +) + +/* + paymentService AccountController wechatpay + | | | + |-----payment order----------------------->| + |<----codeURL------------------------------| + | | | + |-----apply Charge--->| | + | |-------Query------->| + | |<--set status close-| + | | | + +Query the Charging status Order, and get it status, if success, add the Amount to Account Balance the set status to closed. +*/ + +// Account is for user balance, when the balance is 0, the controller will warn the user and clean all +// namespace create by the user. + +type Charge struct { + Amount int64 `json:"balance,omitempty"` + + // deduction info will Record in the Charge + DeductionAmount int64 `json:"deductionAmount,omitempty"` + AccountBalanceName string `json:"accountBalanceName,omitempty"` + + Time metav1.Time `json:"time,omitempty"` + Status string `json:"status,omitempty"` + TradeNO string `json:"tradeNO,omitempty"` + Describe string `json:"describe,omitempty"` +} + +// AccountSpec defines the desired state of Account +type AccountSpec struct{} + +// AccountStatus defines the observed state of Account +type AccountStatus struct { + // EncryptBalance is to encrypt balance + EncryptBalance *string `json:"encryptBalance,omitempty"` + // Recharge amount + Balance int64 `json:"balance,omitempty"` + // ActivityBonus: for demonstration purposes only and does not participate in calculation + ActivityBonus int64 `json:"activityBonus,omitempty"` + // Deduction amount + DeductionBalance int64 `json:"deductionBalance,omitempty"` + // EncryptDeductionBalance is to encrypt DeductionBalance + EncryptDeductionBalance *string `json:"encryptDeductionBalance,omitempty"` + // delete in the future + ChargeList []Charge `json:"chargeList,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Account is the Schema for the accounts API +type Account struct { + // Using userID as Account name + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AccountSpec `json:"spec,omitempty"` + Status AccountStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AccountList contains a list of Account +type AccountList struct { + metav1.TypeMeta ` json:",inline"` + metav1.ListMeta ` json:"metadata,omitempty"` + Items []Account `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Account{}, &AccountList{}) +} diff --git a/controllers/account/api/v1/debt_types.go b/controllers/account/api/v1/debt_types.go new file mode 100644 index 000000000000..2f26e4760d08 --- /dev/null +++ b/controllers/account/api/v1/debt_types.go @@ -0,0 +1,102 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DebtStatusNormal DebtStatusType = "Normal" + DebtStatusSmall DebtStatusType = "Small" + DebtStatusMedium DebtStatusType = "Medium" + DebtStatusLarge DebtStatusType = "Large" + + NormalPeriod DebtStatusType = "NormalPeriod" + WarningPeriod DebtStatusType = "WarningPeriod" + ApproachingDeletionPeriod DebtStatusType = "ApproachingDeletionPeriod" + ImminentDeletionPeriod DebtStatusType = "ImminentDeletionPeriod" + FinalDeletionPeriod DebtStatusType = "FinalDeletionPeriod" + + PreWarningPeriod DebtStatusType = "PreWarningPeriod" + SuspendPeriod DebtStatusType = "SuspendPeriod" + RemovedPeriod DebtStatusType = "RemovedPeriod" + + DebtPrefix = "debt-" + DaySecond = 24 * 60 * 60 +) + +const ( + LowBalancePeriod DebtStatusType = "LowBalancePeriod" + CriticalBalancePeriod DebtStatusType = "CriticalBalancePeriod" + DebtPeriod DebtStatusType = "DebtPeriod" + DebtDeletionPeriod DebtStatusType = "DebtDeletionPeriod" +) + +type DebtStatusType string + +var DefaultDebtConfig = map[DebtStatusType]int64{ + // DebtStatusNormal: NormalPrice, + + // DebtStatusSmall: SmallBlockWaitSecond, +} + +// DebtSpec defines the desired state of Debt +type DebtSpec struct { + UserName string `json:"userName,omitempty"` + UserID string `json:"userID,omitempty"` +} + +// DebtStatus defines the observed state of Debt +type DebtStatus struct { + LastUpdateTimestamp int64 `json:"lastUpdateTimestamp,omitempty"` + DebtStatusRecords []DebtStatusRecord `json:"debtStatusRecords,omitempty"` + AccountDebtStatus DebtStatusType `json:"status,omitempty"` +} + +// DebtStatusRecord defines the observed state of Debt +type DebtStatusRecord struct { + LastStatus DebtStatusType `json:"lastDebtStatus,omitempty"` + CurrentStatus DebtStatusType `json:"currentStatus,omitempty"` + UpdateTime metav1.Time `json:"updateTime,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="status",type=string,JSONPath=".status.status" + +// Debt is the Schema for the debts API +type Debt struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DebtSpec `json:"spec,omitempty"` + Status DebtStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// DebtList contains a list of Debt +type DebtList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Debt `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Debt{}, &DebtList{}) +} diff --git a/controllers/account/api/v1/debt_webhook.go b/controllers/account/api/v1/debt_webhook.go new file mode 100644 index 000000000000..07f82c704636 --- /dev/null +++ b/controllers/account/api/v1/debt_webhook.go @@ -0,0 +1,313 @@ +/* +Copyright 2023. + +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 v1 + +import ( + "context" + "errors" + "fmt" + "os" + "slices" + "strings" + + "github.com/go-logr/logr" + account2 "github.com/labring/sealos/controllers/pkg/account" + "github.com/labring/sealos/controllers/pkg/code" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + pkgtype "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/maps" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "gorm.io/gorm" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +const ( + saPrefix = "system:serviceaccounts" + mastersGroup = "system:masters" + kubeSystemNamespace = "kube-system" + defaultUserSystemNamespace = "user-system" +) + +const ( + DebtSchedulerName = "sealos-debt-scheduler" + PreviousSchedulerName = "sealos-debt/previousScheduler" + PodPhaseSuspended = "Suspended" +) + +var logger = logf.Log.WithName("debt-resource") + +//+kubebuilder:webhook:path=/validate-v1-sealos-cloud,mutating=false,failurePolicy=ignore,groups="*",resources=*,verbs=create;update;delete,versions=*,name=debt.sealos.io,admissionReviewVersions=v1,sideEffects=None,timeoutSeconds=10 +// +kubebuilder:object:generate=false + +type DebtValidate struct { + Client client.Client + AccountV2 *cockroach.Cockroach + TTLUserMap *maps.TTLMap[*pkgtype.UsableBalanceWithCredits] +} + +var kubeSystemGroup string + +func init() { + kubeSystemGroup = fmt.Sprintf("%s:%s", saPrefix, kubeSystemNamespace) +} + +func (d *DebtValidate) Handle(ctx context.Context, req admission.Request) admission.Response { + logger.V(1). + Info("checking user", "userInfo", req.UserInfo, "req.Namespace", req.Namespace, "req.Name", req.Name, "req.gvrk", getGVRK(req), "req.Operation", req.Operation) + // skip delete request (删除quota资源除外) + if req.Operation == admissionv1.Delete && !strings.Contains(getGVRK(req), "quotas") { + return admission.Allowed("") + } + + for _, g := range req.UserInfo.Groups { + switch g { + // if user is kubernetes-admin, pass it + case mastersGroup: + logger.V(1).Info("pass for kubernetes-admin") + return admission.ValidationResponse(true, "") + case kubeSystemGroup: + logger.V(1).Info("pass for kube-system") + return admission.ValidationResponse(true, "") + } + // is user sa + if !strings.HasPrefix(g, saPrefix+":user-system") { + continue + } + if strings.Contains(req.UserInfo.Username, "user-controller-manager") { + break + } + if strings.HasSuffix(req.UserInfo.Username, "user-system:admin") { + logger.V(1).Info("pass for ns-admin") + return admission.ValidationResponse(true, "") + } + if isWhiteList(req) { + return admission.ValidationResponse(true, "") + } + logger.V(1). + Info("check for user", "user", req.UserInfo.Username, "ns: ", req.Namespace, "name", req.Name, "Operation", req.Operation) + // Check if the request is for resourcequota resource + if req.Kind.Kind == "ResourceQuota" && isDefaultQuotaName(req.Name) { + // Check if the operation is UPDATE or DELETE + return admission.Denied( + fmt.Sprintf( + "ns %s request %s %s permission denied", + req.Namespace, + req.Kind.Kind, + req.Operation, + ), + ) + } + if req.Kind.Kind == "Namespace" { + return admission.Denied( + fmt.Sprintf( + "ns %s request %s %s permission denied", + req.Namespace, + req.Kind.Kind, + req.Operation, + ), + ) + } + if req.Kind.Kind == "Payment" && req.Operation == admissionv1.Update { + return admission.Denied( + fmt.Sprintf( + "ns %s request %s %s permission denied", + req.Namespace, + req.Kind.Kind, + req.Operation, + ), + ) + } + return d.checkOption(ctx, logger, d.Client, req.Namespace) + } + logger.V(1).Info("pass ", "req.Namespace", req.Namespace) + return admission.ValidationResponse(true, "") +} + +func getGVRK(req admission.Request) string { + if req.Kind.Group == "" { + return fmt.Sprintf("%s.%s/%s", req.Resource.Resource, req.Kind.Kind, req.Kind.Version) + } + return fmt.Sprintf( + "%s.%s.%s/%s", + req.Resource.Resource, + req.Kind.Kind, + req.Kind.Group, + req.Kind.Version, + ) +} + +func isWhiteList(req admission.Request) bool { + // check if it is in whitelist + // default: "terminals.Terminal.terminal.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1" + whitelists := os.Getenv("WHITELIST") + if whitelists == "" { + return false + } + whitelist := strings.Split(whitelists, ",") + reqGVK := getGVRK(req) + if slices.Contains(whitelist, reqGVK) { + logger.V(1). + Info("pass for whitelists", "gck", req.Kind.String(), "name", req.Name, "namespace", req.Namespace, "userinfo", req.UserInfo) + return true + } + return false +} + +func (d *DebtValidate) checkOption( + ctx context.Context, + logger logr.Logger, + c client.Client, + nsName string, +) admission.Response { + if nsName == "" { + return admission.Allowed("") + } + ns := &corev1.Namespace{} + if err := c.Get(ctx, types.NamespacedName{Name: nsName, Namespace: nsName}, ns); err != nil { + return admission.Allowed("namespace not found") + } + // Check if it is a user namespace + user, ok := ns.Labels[userv1.UserLabelOwnerKey] + if !ok { + return admission.ValidationResponse( + false, + fmt.Sprintf("this namespace is not user namespace %s, or have not created", ns.Name), + ) + } + if suspendedStatus, suspended := getSuspendedNamespaceStatus(ns); suspended { + logger.V(1). + Info("deny request for suspended namespace", "ns", ns.Name, "status", suspendedStatus) + return admission.Denied( + fmt.Sprintf("namespace %s is suspended with status %s", ns.Name, suspendedStatus), + ) + } + logger.V(1).Info("check user namespace", "ns", ns.Name, "user", user) + + // Check cache first + cacheKey := "account:" + user + if cachedAccount, ok := d.TTLUserMap.Get(cacheKey); ok { + logger.Info("cache hit for user", "user", user) + if cachedAccount.Balance+cachedAccount.UsableCredits <= cachedAccount.DeductionBalance { + return admission.ValidationResponse( + false, + fmt.Sprintf( + code.MessageFormat, + code.InsufficientBalance, + fmt.Sprintf( + "(cache) account balance less than 0, now account is %.2f¥. Please recharge the user %s.", + GetAccountDebtBalance(cachedAccount), + user, + ), + ), + ) + } + return admission.Allowed( + fmt.Sprintf("pass user %s, namespace %s (from cache)", user, ns.Name), + ) + } + + if ns.Annotations[pkgtype.WorkspaceSubscriptionStatusAnnoKey] != "" { + workspaceSub, err := d.AccountV2.GetWorkspaceSubscription( + nsName, + d.AccountV2.GetLocalRegion().Domain, + ) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Error(err, "get workspace subscription error", "workspace", nsName) + return admission.ValidationResponse(true, err.Error()) + } + if workspaceSub != nil && workspaceSub.Status != pkgtype.SubscriptionStatusNormal { + return admission.ValidationResponse( + false, + fmt.Sprintf( + "the subscription status of workspace %s is expired, please contact the administrator", + ns.Name, + ), + ) + } + } else { + // Cache miss, query database + userUID, err := d.AccountV2.GetUserUID( + &pkgtype.UserQueryOpts{Owner: user, WithOutCache: true}, + ) + if err != nil { + logger.Error(err, "get user error", "user", user) + return admission.ValidationResponse(true, err.Error()) + } + account, err := d.AccountV2.GetAccountWithCredits(userUID) + if err != nil { + logger.Error(err, "get account error", "user", user) + return admission.ValidationResponse(true, err.Error()) + } + // Store in cache + d.TTLUserMap.Put(cacheKey, account) + logger.V(1).Info("cached account for user", "user", user) + + if account.Balance+account.UsableCredits <= account.DeductionBalance { + return admission.ValidationResponse( + false, + fmt.Sprintf( + code.MessageFormat, + code.InsufficientBalance, + fmt.Sprintf( + "account balance less than 0, now account is %.2f¥. Please recharge the user %s.", + GetAccountDebtBalance(account), + user, + ), + ), + ) + } + } + return admission.Allowed(fmt.Sprintf("pass user %s, namespace %s", user, ns.Name)) +} + +func getSuspendedNamespaceStatus(ns *corev1.Namespace) (string, bool) { + debtStatus := ns.Annotations[pkgtype.DebtNamespaceAnnoStatusKey] + switch debtStatus { + case pkgtype.SuspendDebtNamespaceAnnoStatus, + pkgtype.SuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionCompletedDebtNamespaceAnnoStatus: + return debtStatus, true + } + + networkStatus := ns.Annotations[pkgtype.NetworkStatusAnnoKey] + switch networkStatus { + case pkgtype.NetworkSuspend, pkgtype.NetworkSuspendCompleted: + return networkStatus, true + } + + return "", false +} + +func isDefaultQuotaName(name string) bool { + return strings.HasPrefix(name, "quota-") || name == debtLimit0QuotaName +} + +func GetAccountDebtBalance(account *pkgtype.UsableBalanceWithCredits) float64 { + return account2.GetCurrencyBalance( + account.Balance + account.UsableCredits - account.DeductionBalance, + ) +} + +const debtLimit0QuotaName = "debt-limit0" diff --git a/controllers/account/api/v1/debt_webhook_test.go b/controllers/account/api/v1/debt_webhook_test.go new file mode 100644 index 000000000000..ef38d1eabdfb --- /dev/null +++ b/controllers/account/api/v1/debt_webhook_test.go @@ -0,0 +1,537 @@ +package v1 + +import ( + "context" + "testing" + + pkgtype "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/maps" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// stubClient wraps a fake client and overrides Get for Namespace objects +// to be compatible with the production code's {Name: ns, Namespace: ns} key pattern. +type stubClient struct { + client.WithWatch + nsMap map[string]*corev1.Namespace + scheme *runtime.Scheme +} + +func (s *stubClient) Get( + _ context.Context, + key client.ObjectKey, + obj client.Object, + _ ...client.GetOption, +) error { + if ns, ok := s.nsMap[key.Name]; ok { + namespace, ok := obj.(*corev1.Namespace) + if !ok { + return apierrors.NewBadRequest("stubClient only supports Namespace objects") + } + ns.DeepCopyInto(namespace) + return nil + } + return apierrors.NewNotFound(corev1.Resource("namespaces"), key.Name) +} + +func (s *stubClient) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error { + return nil +} + +func (s *stubClient) Scheme() *runtime.Scheme { return s.scheme } + +func newDebtWithNS(namespaces ...*corev1.Namespace) *DebtValidate { + nsMap := make(map[string]*corev1.Namespace) + objs := make([]client.Object, len(namespaces)) + for i, ns := range namespaces { + nsMap[ns.Name] = ns + objs[i] = ns + } + fc := fake.NewClientBuilder().WithObjects(objs...).Build() + return &DebtValidate{ + Client: &stubClient{WithWatch: fc, nsMap: nsMap, scheme: fc.Scheme()}, + AccountV2: nil, + TTLUserMap: maps.New[*pkgtype.UsableBalanceWithCredits](600), + } +} + +func makeReq(op admissionv1.Operation, kind, group, version, resource string) admission.Request { + return admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: op, + Kind: metav1.GroupVersionKind{Kind: kind, Group: group, Version: version}, + Resource: metav1.GroupVersionResource{ + Resource: resource, + Group: group, + Version: version, + }, + UserInfo: authenticationv1.UserInfo{ + Username: "test-user", + Groups: []string{"system:serviceaccounts:user-system"}, + }, + Namespace: "ns-test", + Name: "test-resource", + }, + } +} + +func makeReqWithGroups(op admissionv1.Operation, groups []string) admission.Request { + req := makeReq(op, "Pod", "", "v1", "pods") + req.UserInfo = authenticationv1.UserInfo{ + Username: "test-user", + Groups: groups, + } + return req +} + +// --- Identity bypass tests (no client/DB needed) --- + +func TestHandle_SystemMastersBypass(t *testing.T) { + d := &DebtValidate{} + resp := d.Handle( + context.Background(), + makeReqWithGroups(admissionv1.Create, []string{"system:masters"}), + ) + if !resp.Allowed { + t.Fatalf("system:masters should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_KubeSystemBypass(t *testing.T) { + d := &DebtValidate{} + resp := d.Handle( + context.Background(), + makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:kube-system"}), + ) + if !resp.Allowed { + t.Fatalf("kube-system SA should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_NonUserSASkipped(t *testing.T) { + d := &DebtValidate{} + resp := d.Handle( + context.Background(), + makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:account-system"}), + ) + if !resp.Allowed { + t.Fatalf("non-user-system SA should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_UserControllerManagerBreaks(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:user-system"}) + req.UserInfo.Username = "system:serviceaccount:user-system:user-controller-manager" + resp := d.Handle(context.Background(), req) + if !resp.Allowed { + t.Fatalf("user-controller-manager should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_UserSA_AdminBypass(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:user-system"}) + req.UserInfo.Username = "system:serviceaccount:user-system:admin" + resp := d.Handle(context.Background(), req) + if !resp.Allowed { + t.Fatalf("ns-admin should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_WhitelistBypass(t *testing.T) { + t.Setenv("WHITELIST", "terminals.Terminal.terminal.sealos.io/v1") + + d := &DebtValidate{} + req := makeReq(admissionv1.Create, "Terminal", "terminal.sealos.io", "v1", "terminals") + req.UserInfo = authenticationv1.UserInfo{ + Username: "test-user", + Groups: []string{"system:serviceaccounts:user-system"}, + } + resp := d.Handle(context.Background(), req) + if !resp.Allowed { + t.Fatalf("whitelisted resource should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_WhitelistNoMatch_EntersCheck(t *testing.T) { + t.Setenv("WHITELIST", "terminals.Terminal.terminal.sealos.io/v1") + + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + req := makeReq(admissionv1.Create, "Pod", "", "v1", "pods") + req.UserInfo = authenticationv1.UserInfo{ + Username: "test-user", + Groups: []string{"system:serviceaccounts:user-system"}, + } + resp := d.Handle(context.Background(), req) + if resp.Allowed { + t.Fatal("non-whitelisted Pod from user SA without owner label namespace should be denied") + } +} + +func TestHandle_DeleteNonQuotaAllowed(t *testing.T) { + d := &DebtValidate{} + resp := d.Handle(context.Background(), makeReq(admissionv1.Delete, "Pod", "", "v1", "pods")) + if !resp.Allowed { + t.Fatalf("DELETE non-quota should be allowed, got: %s", resp.Result.Message) + } +} + +func TestHandle_DeleteQuotaNotBypassed(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + req := makeReq(admissionv1.Delete, "ResourceQuota", "", "v1", "quotas") + resp := d.Handle(context.Background(), req) + if resp.Allowed { + t.Fatal("DELETE quota from user SA without owner label should be denied") + } +} + +func TestHandle_SuspendedNamespace_NonDeleteDenied(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + Annotations: map[string]string{ + pkgtype.DebtNamespaceAnnoStatusKey: pkgtype.SuspendCompletedDebtNamespaceAnnoStatus, + }, + }}) + req := makeReq(admissionv1.Update, "Pod", "", "v1", "pods") + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("non-DELETE request in a suspended namespace should be denied") + } +} + +func TestHandle_SuspendedNamespace_DeleteAllowed(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + Annotations: map[string]string{ + pkgtype.DebtNamespaceAnnoStatusKey: pkgtype.SuspendCompletedDebtNamespaceAnnoStatus, + }, + }}) + req := makeReq(admissionv1.Delete, "Pod", "", "v1", "pods") + if resp := d.Handle(context.Background(), req); !resp.Allowed { + t.Fatalf( + "DELETE request in a suspended namespace should be allowed, got: %s", + resp.Result.Message, + ) + } +} + +// --- Resource-type denial tests (no client/DB needed) --- + +func TestHandle_NamespaceDenied(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:user-system"}) + req.Kind = metav1.GroupVersionKind{Kind: "Namespace", Version: "v1"} + req.Resource = metav1.GroupVersionResource{Resource: "namespaces", Version: "v1"} + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("Namespace should be denied for user SA") + } +} + +func TestHandle_PaymentUpdateDenied(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Update, []string{"system:serviceaccounts:user-system"}) + req.Kind = metav1.GroupVersionKind{Kind: "Payment", Group: "account.sealos.io", Version: "v1"} + req.Resource = metav1.GroupVersionResource{ + Resource: "payments", + Group: "account.sealos.io", + Version: "v1", + } + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("Payment Update should be denied for user SA") + } +} + +func TestHandle_ResourceQuota_Denied(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:user-system"}) + req.Kind = metav1.GroupVersionKind{Kind: "ResourceQuota", Version: "v1"} + req.Resource = metav1.GroupVersionResource{Resource: "resourcequotas", Version: "v1"} + req.Name = "quota-default" + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("ResourceQuota with quota-xxx name should be denied for user SA") + } +} + +func TestHandle_DebtLimit0Quota_Denied(t *testing.T) { + d := &DebtValidate{} + req := makeReqWithGroups(admissionv1.Create, []string{"system:serviceaccounts:user-system"}) + req.Kind = metav1.GroupVersionKind{Kind: "ResourceQuota", Version: "v1"} + req.Resource = metav1.GroupVersionResource{Resource: "resourcequotas", Version: "v1"} + req.Name = debtLimit0QuotaName + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("debt-limit0 ResourceQuota should be denied for user SA") + } +} + +// --- API version coverage: verifies handler treats v1alpha1 same as v1 --- + +func TestHandle_V1Alpha1OpsRequest_EntersDebtCheck(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + req := makeReq( + admissionv1.Create, + "OpsRequest", + "apps.kubeblocks.io", + "v1alpha1", + "opsrequests", + ) + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("v1alpha1 OpsRequest from user SA without owner label namespace should be denied") + } +} + +func TestHandle_V1Pod_EntersDebtCheck(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + req := makeReq(admissionv1.Create, "Pod", "", "v1", "pods") + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("v1 Pod from user SA without owner label namespace should be denied") + } +} + +func TestHandle_V1Beta1Deployment_EntersDebtCheck(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + req := makeReq(admissionv1.Create, "Deployment", "apps", "v1beta1", "deployments") + if resp := d.Handle(context.Background(), req); resp.Allowed { + t.Fatal("v1beta1 Deployment from user SA without owner label namespace should be denied") + } +} + +// --- checkOption --- + +func TestCheckOption_EmptyNamespace(t *testing.T) { + d := &DebtValidate{} + if resp := d.checkOption(context.Background(), logger, nil, ""); !resp.Allowed { + t.Fatal("empty namespace should be allowed") + } +} + +func TestCheckOption_NonUserNamespace_Denied(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-test"}}) + if resp := d.checkOption(context.Background(), logger, d.Client, "ns-test"); resp.Allowed { + t.Fatal("namespace without owner label should be denied") + } +} + +func TestCheckOption_CacheHit_InsufficientBalance(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + }, + }) + d.TTLUserMap.Put("account:user-1", &pkgtype.UsableBalanceWithCredits{ + Balance: 0, + UsableCredits: 0, + DeductionBalance: 100, + }) + if resp := d.checkOption(context.Background(), logger, d.Client, "ns-test"); resp.Allowed { + t.Fatal("insufficient balance from cache should be denied") + } +} + +func TestCheckOption_CacheHit_SufficientBalance(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + }, + }) + d.TTLUserMap.Put("account:user-1", &pkgtype.UsableBalanceWithCredits{ + Balance: 100, + UsableCredits: 0, + DeductionBalance: 0, + }) + if resp := d.checkOption(context.Background(), logger, d.Client, "ns-test"); !resp.Allowed { + t.Fatalf("sufficient balance from cache should be allowed, got: %s", resp.Result.Message) + } +} + +func TestCheckOption_SuspendedDebtStatus_Denied(t *testing.T) { + statuses := []string{ + pkgtype.SuspendDebtNamespaceAnnoStatus, + pkgtype.SuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionCompletedDebtNamespaceAnnoStatus, + } + + for _, status := range statuses { + t.Run(status, func(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + Annotations: map[string]string{pkgtype.DebtNamespaceAnnoStatusKey: status}, + }, + }) + if resp := d.checkOption( + context.Background(), + logger, + d.Client, + "ns-test", + ); resp.Allowed { + t.Fatalf("suspended namespace status %q should be denied", status) + } + }) + } +} + +func TestCheckOption_SuspendedNetworkStatus_Denied(t *testing.T) { + for _, status := range []string{pkgtype.NetworkSuspend, pkgtype.NetworkSuspendCompleted} { + t.Run(status, func(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + Annotations: map[string]string{pkgtype.NetworkStatusAnnoKey: status}, + }, + }) + if resp := d.checkOption( + context.Background(), + logger, + d.Client, + "ns-test", + ); resp.Allowed { + t.Fatalf("suspended network status %q should be denied", status) + } + }) + } +} + +func TestCheckOption_ActiveDebtStatus_AllowsWithSufficientBalance(t *testing.T) { + for _, status := range []string{ + pkgtype.NormalDebtNamespaceAnnoStatus, + pkgtype.ResumeDebtNamespaceAnnoStatus, + pkgtype.ResumeCompletedDebtNamespaceAnnoStatus, + } { + t.Run(status, func(t *testing.T) { + d := newDebtWithNS(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-test", + Labels: map[string]string{"user.sealos.io/owner": "user-1"}, + Annotations: map[string]string{pkgtype.DebtNamespaceAnnoStatusKey: status}, + }, + }) + d.TTLUserMap.Put("account:user-1", &pkgtype.UsableBalanceWithCredits{ + Balance: 100, + }) + if resp := d.checkOption( + context.Background(), + logger, + d.Client, + "ns-test", + ); !resp.Allowed { + t.Fatalf( + "active namespace status %q should be allowed, got: %s", + status, + resp.Result.Message, + ) + } + }) + } +} + +// --- Helpers --- + +func TestGetSuspendedNamespaceStatus(t *testing.T) { + for _, status := range []string{ + pkgtype.SuspendDebtNamespaceAnnoStatus, + pkgtype.SuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendDebtNamespaceAnnoStatus, + pkgtype.TerminateSuspendCompletedDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionDebtNamespaceAnnoStatus, + pkgtype.FinalDeletionCompletedDebtNamespaceAnnoStatus, + } { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{pkgtype.DebtNamespaceAnnoStatusKey: status}, + }} + if got, ok := getSuspendedNamespaceStatus(ns); !ok || got != status { + t.Errorf( + "getSuspendedNamespaceStatus(%q) = (%q, %v), want (%q, true)", + status, + got, + ok, + status, + ) + } + } + for _, status := range []string{pkgtype.NetworkSuspend, pkgtype.NetworkSuspendCompleted} { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{pkgtype.NetworkStatusAnnoKey: status}, + }} + if got, ok := getSuspendedNamespaceStatus(ns); !ok || got != status { + t.Errorf( + "getSuspendedNamespaceStatus(%q) = (%q, %v), want (%q, true)", + status, + got, + ok, + status, + ) + } + } + + activeStatuses := []struct { + key string + status string + }{ + {pkgtype.DebtNamespaceAnnoStatusKey, ""}, + {pkgtype.DebtNamespaceAnnoStatusKey, pkgtype.NormalDebtNamespaceAnnoStatus}, + {pkgtype.DebtNamespaceAnnoStatusKey, pkgtype.ResumeDebtNamespaceAnnoStatus}, + {pkgtype.DebtNamespaceAnnoStatusKey, pkgtype.ResumeCompletedDebtNamespaceAnnoStatus}, + {pkgtype.NetworkStatusAnnoKey, pkgtype.NetworkResume}, + {pkgtype.NetworkStatusAnnoKey, pkgtype.NetworkResumeCompleted}, + } + for _, test := range activeStatuses { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{test.key: test.status}, + }} + if got, ok := getSuspendedNamespaceStatus(ns); ok { + t.Errorf( + "getSuspendedNamespaceStatus(%s=%q) = (%q, true), want no suspension", + test.key, + test.status, + got, + ) + } + } +} + +func TestIsDefaultQuotaName(t *testing.T) { + tests := []struct { + name string + expected bool + }{ + {"quota-default", true}, + {"quota-", true}, + {"debt-limit0", true}, + {"my-quota", false}, + {"default", false}, + {"", false}, + } + for _, tt := range tests { + if got := isDefaultQuotaName(tt.name); got != tt.expected { + t.Errorf("isDefaultQuotaName(%q) = %v, want %v", tt.name, got, tt.expected) + } + } +} + +func TestGetGVRK(t *testing.T) { + reqWithGroup := makeReq(admissionv1.Create, "Pod", "apps", "v1", "deployments") + if got := getGVRK(reqWithGroup); got != "deployments.Pod.apps/v1" { + t.Errorf("getGVRK with group = %q, want deployments.Pod.apps/v1", got) + } + reqWithoutGroup := makeReq(admissionv1.Create, "Pod", "", "v1", "pods") + if got := getGVRK(reqWithoutGroup); got != "pods.Pod/v1" { + t.Errorf("getGVRK without group = %q, want pods.Pod/v1", got) + } +} diff --git a/controllers/account/api/v1/groupversion_info.go b/controllers/account/api/v1/groupversion_info.go new file mode 100644 index 000000000000..e28957fc755d --- /dev/null +++ b/controllers/account/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +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 v1 contains API Schema definitions for the account v1 API group +// +kubebuilder:object:generate=true +// +groupName=account.sealos.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "account.sealos.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/controllers/account/api/v1/payment_types.go b/controllers/account/api/v1/payment_types.go new file mode 100644 index 000000000000..5940f9c3bd57 --- /dev/null +++ b/controllers/account/api/v1/payment_types.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "strconv" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +/* +If user want to recharge, just using kubectl apply -f payment.yaml, he will get the codeURL, convert it to QRcode and scan it. + +apiVersion: user.sealos.io/v1 +kind: Payment +metadata: + name: payment-sample +spec: + UserID: fanux + Amount: 1 + +CodeURL: weixin://wxpay/bizpayurl?pr=aIQrOYOzz + +The controller log will show the QRcode +*/ + +// PaymentSpec defines the desired state of Payment +type PaymentSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // UserID is the user id who want to recharge + UserID string `json:"userID,omitempty"` + // UserCr is the user cr name who want to recharge + UserCR string `json:"userCR,omitempty"` + // Amount is the amount of recharge + Amount int64 `json:"amount,omitempty"` + // e.g. wechat, alipay, creditcard, etc. + //+kubebuilder:default:=wechat + PaymentMethod string `json:"paymentMethod,omitempty"` +} + +// PaymentStatus defines the observed state of Payment +type PaymentStatus struct { + // TradeNO is the tradeNO of wechatpay + TradeNO string `json:"tradeNO,omitempty"` + // CodeURL is the codeURL of wechatpay + CodeURL string `json:"codeURL,omitempty"` + // Status is the status of wechatpay, charging, closed, timeout + Status string `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Payment is the Schema for the payments API +type Payment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PaymentSpec `json:"spec,omitempty"` + Status PaymentStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// PaymentList contains a list of Payment +type PaymentList struct { + metav1.TypeMeta ` json:",inline"` + metav1.ListMeta ` json:"metadata,omitempty"` + Items []Payment `json:"items"` +} + +func (p *Payment) ToJSON() string { + return `{ + "spec": { + "userID": "` + p.Spec.UserID + `", + "amount": ` + strconv.FormatInt(p.Spec.Amount, 10) + `, + "paymentMethod": "` + p.Spec.PaymentMethod + `" + }, + "status": { + "tradeNO": "` + p.Status.TradeNO + `", + "paymentURL": "` + p.Status.CodeURL + `", + "status": "` + p.Status.Status + `" + } +}` +} + +func init() { + SchemeBuilder.Register(&Payment{}, &PaymentList{}) +} diff --git a/controllers/account/api/v1/zz_generated.deepcopy.go b/controllers/account/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..de436afff0d5 --- /dev/null +++ b/controllers/account/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,369 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +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 v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Account) DeepCopyInto(out *Account) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Account. +func (in *Account) DeepCopy() *Account { + if in == nil { + return nil + } + out := new(Account) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Account) 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 *AccountList) DeepCopyInto(out *AccountList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Account, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountList. +func (in *AccountList) DeepCopy() *AccountList { + if in == nil { + return nil + } + out := new(AccountList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AccountList) 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 *AccountSpec) DeepCopyInto(out *AccountSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountSpec. +func (in *AccountSpec) DeepCopy() *AccountSpec { + if in == nil { + return nil + } + out := new(AccountSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccountStatus) DeepCopyInto(out *AccountStatus) { + *out = *in + if in.EncryptBalance != nil { + in, out := &in.EncryptBalance, &out.EncryptBalance + *out = new(string) + **out = **in + } + if in.EncryptDeductionBalance != nil { + in, out := &in.EncryptDeductionBalance, &out.EncryptDeductionBalance + *out = new(string) + **out = **in + } + if in.ChargeList != nil { + in, out := &in.ChargeList, &out.ChargeList + *out = make([]Charge, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccountStatus. +func (in *AccountStatus) DeepCopy() *AccountStatus { + if in == nil { + return nil + } + out := new(AccountStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Charge) DeepCopyInto(out *Charge) { + *out = *in + in.Time.DeepCopyInto(&out.Time) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Charge. +func (in *Charge) DeepCopy() *Charge { + if in == nil { + return nil + } + out := new(Charge) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Costs) DeepCopyInto(out *Costs) { + { + in := &in + *out = make(Costs, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Costs. +func (in Costs) DeepCopy() Costs { + if in == nil { + return nil + } + out := new(Costs) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Debt) DeepCopyInto(out *Debt) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Debt. +func (in *Debt) DeepCopy() *Debt { + if in == nil { + return nil + } + out := new(Debt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Debt) 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 *DebtList) DeepCopyInto(out *DebtList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Debt, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebtList. +func (in *DebtList) DeepCopy() *DebtList { + if in == nil { + return nil + } + out := new(DebtList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DebtList) 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 *DebtSpec) DeepCopyInto(out *DebtSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebtSpec. +func (in *DebtSpec) DeepCopy() *DebtSpec { + if in == nil { + return nil + } + out := new(DebtSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DebtStatus) DeepCopyInto(out *DebtStatus) { + *out = *in + if in.DebtStatusRecords != nil { + in, out := &in.DebtStatusRecords, &out.DebtStatusRecords + *out = make([]DebtStatusRecord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebtStatus. +func (in *DebtStatus) DeepCopy() *DebtStatus { + if in == nil { + return nil + } + out := new(DebtStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DebtStatusRecord) DeepCopyInto(out *DebtStatusRecord) { + *out = *in + in.UpdateTime.DeepCopyInto(&out.UpdateTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebtStatusRecord. +func (in *DebtStatusRecord) DeepCopy() *DebtStatusRecord { + if in == nil { + return nil + } + out := new(DebtStatusRecord) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Payment) DeepCopyInto(out *Payment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Payment. +func (in *Payment) DeepCopy() *Payment { + if in == nil { + return nil + } + out := new(Payment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Payment) 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 *PaymentList) DeepCopyInto(out *PaymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Payment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PaymentList. +func (in *PaymentList) DeepCopy() *PaymentList { + if in == nil { + return nil + } + out := new(PaymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PaymentList) 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 *PaymentSpec) DeepCopyInto(out *PaymentSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PaymentSpec. +func (in *PaymentSpec) DeepCopy() *PaymentSpec { + if in == nil { + return nil + } + out := new(PaymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PaymentStatus) DeepCopyInto(out *PaymentStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PaymentStatus. +func (in *PaymentStatus) DeepCopy() *PaymentStatus { + if in == nil { + return nil + } + out := new(PaymentStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/account/config/certmanager/certificate.yaml b/controllers/account/config/certmanager/certificate.yaml new file mode 100644 index 000000000000..54c78b15e6b3 --- /dev/null +++ b/controllers/account/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/controllers/account/config/certmanager/kustomization.yaml b/controllers/account/config/certmanager/kustomization.yaml new file mode 100644 index 000000000000..27b9b3c07457 --- /dev/null +++ b/controllers/account/config/certmanager/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/controllers/account/config/certmanager/kustomizeconfig.yaml b/controllers/account/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 000000000000..a6f7e14c39fc --- /dev/null +++ b/controllers/account/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml new file mode 100644 index 000000000000..f47c776de17a --- /dev/null +++ b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml @@ -0,0 +1,107 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: accounts.account.sealos.io +spec: + group: account.sealos.io + names: + kind: Account + listKind: AccountList + plural: accounts + singular: account + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Account is the Schema for the accounts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: AccountSpec defines the desired state of Account + type: object + status: + description: AccountStatus defines the observed state of Account + properties: + activityBonus: + description: 'ActivityBonus: for demonstration purposes only and does + not participate in calculation' + format: int64 + type: integer + balance: + description: Recharge amount + format: int64 + type: integer + chargeList: + description: delete in the future + items: + properties: + accountBalanceName: + type: string + balance: + format: int64 + type: integer + deductionAmount: + description: deduction info will Record in the Charge + format: int64 + type: integer + describe: + type: string + status: + type: string + time: + format: date-time + type: string + tradeNO: + type: string + type: object + type: array + deductionBalance: + description: Deduction amount + format: int64 + type: integer + encryptBalance: + description: EncryptBalance is to encrypt balance + type: string + encryptDeductionBalance: + description: EncryptDeductionBalance is to encrypt DeductionBalance + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/account/config/crd/bases/account.sealos.io_debts.yaml b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml new file mode 100644 index 000000000000..262cfc5fd7ef --- /dev/null +++ b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml @@ -0,0 +1,91 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: debts.account.sealos.io +spec: + group: account.sealos.io + names: + kind: Debt + listKind: DebtList + plural: debts + singular: debt + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.status + name: status + type: string + name: v1 + schema: + openAPIV3Schema: + description: Debt is the Schema for the debts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DebtSpec defines the desired state of Debt + properties: + userID: + type: string + userName: + type: string + type: object + status: + description: DebtStatus defines the observed state of Debt + properties: + debtStatusRecords: + items: + description: DebtStatusRecord defines the observed state of Debt + properties: + currentStatus: + type: string + lastDebtStatus: + type: string + updateTime: + format: date-time + type: string + type: object + type: array + lastUpdateTimestamp: + format: int64 + type: integer + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/account/config/crd/bases/account.sealos.io_payments.yaml b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml new file mode 100644 index 000000000000..0d62f42da279 --- /dev/null +++ b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml @@ -0,0 +1,89 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: payments.account.sealos.io +spec: + group: account.sealos.io + names: + kind: Payment + listKind: PaymentList + plural: payments + singular: payment + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Payment is the Schema for the payments API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PaymentSpec defines the desired state of Payment + properties: + amount: + description: Amount is the amount of recharge + format: int64 + type: integer + paymentMethod: + default: wechat + description: e.g. wechat, alipay, creditcard, etc. + type: string + userCR: + description: UserCr is the user cr name who want to recharge + type: string + userID: + description: UserID is the user id who want to recharge + type: string + type: object + status: + description: PaymentStatus defines the observed state of Payment + properties: + codeURL: + description: CodeURL is the codeURL of wechatpay + type: string + status: + description: Status is the status of wechatpay, charging, closed, + timeout + type: string + tradeNO: + description: TradeNO is the tradeNO of wechatpay + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/account/config/crd/kustomization.yaml b/controllers/account/config/crd/kustomization.yaml new file mode 100644 index 000000000000..c3ece071eb59 --- /dev/null +++ b/controllers/account/config/crd/kustomization.yaml @@ -0,0 +1,50 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/account.sealos.io_payments.yaml +- bases/account.sealos.io_debts.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_accounts.yaml +#- patches/webhook_in_payments.yaml +- patches/webhook_in_debts.yaml +#- patches/webhook_in_billingrecordqueries.yaml +#- patches/webhook_in_pricequeries.yaml +#- patches/webhook_in_transfers.yaml +#- patches/webhook_in_namespacebillinghistories.yaml +#- patches/webhook_in_billinginfoqueries.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_accounts.yaml +#- patches/cainjection_in_payments.yaml +- patches/cainjection_in_debts.yaml +#- patches/cainjection_in_billingrecordqueries.yaml +#- patches/cainjection_in_pricequeries.yaml +#- patches/cainjection_in_transfers.yaml +#- patches/cainjection_in_namespacebillinghistories.yaml +#- patches/cainjection_in_billinginfoqueries.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controllers/account/config/crd/kustomizeconfig.yaml b/controllers/account/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..9c99fc52fb20 --- /dev/null +++ b/controllers/account/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controllers/account/config/crd/patches/cainjection_in_accounts.yaml b/controllers/account/config/crd/patches/cainjection_in_accounts.yaml new file mode 100644 index 000000000000..09928ba10602 --- /dev/null +++ b/controllers/account/config/crd/patches/cainjection_in_accounts.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: accounts.account.sealos.io diff --git a/controllers/account/config/crd/patches/cainjection_in_debts.yaml b/controllers/account/config/crd/patches/cainjection_in_debts.yaml new file mode 100644 index 000000000000..d043830010cc --- /dev/null +++ b/controllers/account/config/crd/patches/cainjection_in_debts.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: debts.account.sealos.io diff --git a/controllers/account/config/crd/patches/cainjection_in_payments.yaml b/controllers/account/config/crd/patches/cainjection_in_payments.yaml new file mode 100644 index 000000000000..a92537b0320b --- /dev/null +++ b/controllers/account/config/crd/patches/cainjection_in_payments.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: payments.account.sealos.io diff --git a/controllers/account/config/crd/patches/webhook_in_accounts.yaml b/controllers/account/config/crd/patches/webhook_in_accounts.yaml new file mode 100644 index 000000000000..695982badb6e --- /dev/null +++ b/controllers/account/config/crd/patches/webhook_in_accounts.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: accounts.account.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/account/config/crd/patches/webhook_in_debts.yaml b/controllers/account/config/crd/patches/webhook_in_debts.yaml new file mode 100644 index 000000000000..8140f778e0e5 --- /dev/null +++ b/controllers/account/config/crd/patches/webhook_in_debts.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: debts.account.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/account/config/crd/patches/webhook_in_payments.yaml b/controllers/account/config/crd/patches/webhook_in_payments.yaml new file mode 100644 index 000000000000..dff1cddf5ce1 --- /dev/null +++ b/controllers/account/config/crd/patches/webhook_in_payments.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: payments.account.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/account/config/default/kustomization.yaml b/controllers/account/config/default/kustomization.yaml new file mode 100644 index 000000000000..d04178cc3839 --- /dev/null +++ b/controllers/account/config/default/kustomization.yaml @@ -0,0 +1,88 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: account-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: account- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace +- name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml +- name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace +- name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/controllers/account/config/default/manager_auth_proxy_patch.yaml b/controllers/account/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..4ef430bca332 --- /dev/null +++ b/controllers/account/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,89 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 + imagePullPolicy: IfNotPresent + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + envFrom: + - secretRef: + name: payment-secret + optional: true + - secretRef: + name: account-admin-jwt + - configMapRef: + name: account-manager-env + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + env: + - name: ACCOUNT_NAMESPACE + value: sealos-system + - name: NAMESPACE_NAME + value: user-system + - name: WHITELIST + value: licenses.License.license.sealos.io/v1,notifications.Notification.notification.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,billinginfoqueries.BillingInfoQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1 + - name: ACCOUNT_SYSTEM_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: ghcr.io/labring/sealos-account-controller:latest + imagePullPolicy: Always + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "control-plane" + operator: In + values: + - controller-manager + topologyKey: "kubernetes.io/hostname" diff --git a/controllers/account/config/default/manager_config_patch.yaml b/controllers/account/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..ca4883fc5ce5 --- /dev/null +++ b/controllers/account/config/default/manager_config_patch.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/controllers/account/config/default/manager_webhook_patch.yaml b/controllers/account/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000000..ade33aa9d5f0 --- /dev/null +++ b/controllers/account/config/default/manager_webhook_patch.yaml @@ -0,0 +1,37 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/controllers/account/config/default/webhookcainjection_patch.yaml b/controllers/account/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000000..bcaf743f52ca --- /dev/null +++ b/controllers/account/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + diff --git a/controllers/account/config/manager/controller_manager_config.yaml b/controllers/account/config/manager/controller_manager_config.yaml new file mode 100644 index 000000000000..fd68e304ee2f --- /dev/null +++ b/controllers/account/config/manager/controller_manager_config.yaml @@ -0,0 +1,25 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: a63686c3.sealos.io diff --git a/controllers/account/config/manager/kustomization.yaml b/controllers/account/config/manager/kustomization.yaml new file mode 100644 index 000000000000..1a40d7e4e6bf --- /dev/null +++ b/controllers/account/config/manager/kustomization.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-account-controller + newTag: latest diff --git a/controllers/account/config/manager/manager.yaml b/controllers/account/config/manager/manager.yaml new file mode 100644 index 000000000000..9b8e2bee7b8c --- /dev/null +++ b/controllers/account/config/manager/manager.yaml @@ -0,0 +1,83 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + imagePullPolicy: Always + name: manager + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + startupProbe: + httpGet: + path: /startupz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 60 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 100m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/account/config/prometheus/kustomization.yaml b/controllers/account/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/account/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/account/config/prometheus/monitor.yaml b/controllers/account/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..32401c3d7fdc --- /dev/null +++ b/controllers/account/config/prometheus/monitor.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/account/config/rbac/account_editor_role.yaml b/controllers/account/config/rbac/account_editor_role.yaml new file mode 100644 index 000000000000..925bae1772ee --- /dev/null +++ b/controllers/account/config/rbac/account_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit accounts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: account-editor-role +rules: +- apiGroups: + - account.sealos.io + resources: + - accounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - accounts/status + verbs: + - get diff --git a/controllers/account/config/rbac/account_viewer_role.yaml b/controllers/account/config/rbac/account_viewer_role.yaml new file mode 100644 index 000000000000..7a25a6a23e00 --- /dev/null +++ b/controllers/account/config/rbac/account_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view accounts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: account-viewer-role +rules: +- apiGroups: + - account.sealos.io + resources: + - accounts + verbs: + - get + - list + - watch +- apiGroups: + - account.sealos.io + resources: + - accounts/status + verbs: + - get diff --git a/controllers/account/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/account/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..0e421adaba5d --- /dev/null +++ b/controllers/account/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/account/config/rbac/auth_proxy_role.yaml b/controllers/account/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..7539c6cd1e86 --- /dev/null +++ b/controllers/account/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,31 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/account/config/rbac/auth_proxy_role_binding.yaml b/controllers/account/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..84c16ebd8d6a --- /dev/null +++ b/controllers/account/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/account/config/rbac/auth_proxy_service.yaml b/controllers/account/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..70e47bf67833 --- /dev/null +++ b/controllers/account/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,29 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/account/config/rbac/debt_editor_role.yaml b/controllers/account/config/rbac/debt_editor_role.yaml new file mode 100644 index 000000000000..afc3d5ca66f5 --- /dev/null +++ b/controllers/account/config/rbac/debt_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit debts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: debt-editor-role +rules: +- apiGroups: + - account.sealos.io + resources: + - debts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - debts/status + verbs: + - get diff --git a/controllers/account/config/rbac/debt_viewer_role.yaml b/controllers/account/config/rbac/debt_viewer_role.yaml new file mode 100644 index 000000000000..c2611da02c9e --- /dev/null +++ b/controllers/account/config/rbac/debt_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view debts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: debt-viewer-role +rules: +- apiGroups: + - account.sealos.io + resources: + - debts + verbs: + - get + - list + - watch +- apiGroups: + - account.sealos.io + resources: + - debts/status + verbs: + - get diff --git a/controllers/account/config/rbac/kustomization.yaml b/controllers/account/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/account/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/account/config/rbac/leader_election_role.yaml b/controllers/account/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..7ccba03c9765 --- /dev/null +++ b/controllers/account/config/rbac/leader_election_role.yaml @@ -0,0 +1,51 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/account/config/rbac/leader_election_role_binding.yaml b/controllers/account/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..493b75ed3993 --- /dev/null +++ b/controllers/account/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/account/config/rbac/payment_editor_role.yaml b/controllers/account/config/rbac/payment_editor_role.yaml new file mode 100644 index 000000000000..589d87ae1dcc --- /dev/null +++ b/controllers/account/config/rbac/payment_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit payments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: payment-editor-role +rules: +- apiGroups: + - account.sealos.io + resources: + - payments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - payments/status + verbs: + - get diff --git a/controllers/account/config/rbac/payment_viewer_role.yaml b/controllers/account/config/rbac/payment_viewer_role.yaml new file mode 100644 index 000000000000..936aa75ded0e --- /dev/null +++ b/controllers/account/config/rbac/payment_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view payments. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: payment-viewer-role +rules: +- apiGroups: + - account.sealos.io + resources: + - payments + verbs: + - get + - list + - watch +- apiGroups: + - account.sealos.io + resources: + - payments/status + verbs: + - get diff --git a/controllers/account/config/rbac/role.yaml b/controllers/account/config/rbac/role.yaml new file mode 100644 index 000000000000..c1e49dd74cad --- /dev/null +++ b/controllers/account/config/rbac/role.yaml @@ -0,0 +1,356 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - accounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - accounts/finalizers + verbs: + - update +- apiGroups: + - account.sealos.io + resources: + - accounts/status + verbs: + - get + - patch + - update +- apiGroups: + - account.sealos.io + resources: + - debts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - debts/finalizers + verbs: + - update +- apiGroups: + - account.sealos.io + resources: + - debts/status + verbs: + - get + - patch + - update +- apiGroups: + - account.sealos.io + resources: + - payments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - payments/finalizers + verbs: + - update +- apiGroups: + - account.sealos.io + resources: + - payments/status + verbs: + - get + - patch + - update +- apiGroups: + - apps + resources: + - daemonsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - clusters/status + verbs: + - get + - patch + - update +- apiGroups: + - apps.kubeblocks.io + resources: + - opsrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - opsrequests/status + verbs: + - get + - patch + - update +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - limitranges + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - namespaces/finalizers + verbs: + - update +- apiGroups: + - "" + resources: + - namespaces/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods/finalizers + verbs: + - update +- apiGroups: + - "" + resources: + - pods/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - resourcequotas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metering.common.sealos.io + resources: + - extensionresourceprices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - notification.sealos.io + resources: + - notifications + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - get + - list + - patch + - watch diff --git a/controllers/account/config/rbac/role_binding.yaml b/controllers/account/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..749383ffe1ec --- /dev/null +++ b/controllers/account/config/rbac/role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/account/config/rbac/service_account.yaml b/controllers/account/config/rbac/service_account.yaml new file mode 100644 index 000000000000..b8ca78e010b4 --- /dev/null +++ b/controllers/account/config/rbac/service_account.yaml @@ -0,0 +1,19 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/controllers/account/config/samples/account_v1_account.yaml b/controllers/account/config/samples/account_v1_account.yaml new file mode 100644 index 000000000000..5133a1b896a5 --- /dev/null +++ b/controllers/account/config/samples/account_v1_account.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: account.sealos.io/v1 +kind: Account +metadata: + name: account-sample + namespace: sealos-system +spec: + # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_debt.yaml b/controllers/account/config/samples/account_v1_debt.yaml new file mode 100644 index 000000000000..b359c82ae4e8 --- /dev/null +++ b/controllers/account/config/samples/account_v1_debt.yaml @@ -0,0 +1,20 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: account.sealos.io/v1 +kind: Debt +metadata: + name: debt-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_payment.yaml b/controllers/account/config/samples/account_v1_payment.yaml new file mode 100644 index 000000000000..dfb2e701d47d --- /dev/null +++ b/controllers/account/config/samples/account_v1_payment.yaml @@ -0,0 +1,27 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + name: ns-test +--- +apiVersion: account.sealos.io/v1 +kind: Payment +metadata: + name: payment-sample + namespace: ns-test +spec: + userID: test + amount: 1 \ No newline at end of file diff --git a/controllers/account/config/webhook/kustomization.yaml b/controllers/account/config/webhook/kustomization.yaml new file mode 100644 index 000000000000..1bf9427c2424 --- /dev/null +++ b/controllers/account/config/webhook/kustomization.yaml @@ -0,0 +1,20 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/controllers/account/config/webhook/kustomizeconfig.yaml b/controllers/account/config/webhook/kustomizeconfig.yaml new file mode 100644 index 000000000000..24bbc5440aaa --- /dev/null +++ b/controllers/account/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/controllers/account/config/webhook/manifests.yaml b/controllers/account/config/webhook/manifests.yaml new file mode 100644 index 000000000000..5f0b77fee8b6 --- /dev/null +++ b/controllers/account/config/webhook/manifests.yaml @@ -0,0 +1,55 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: account-webhook-service + namespace: account-system + path: /validate-v1-sealos-cloud + timeoutSeconds: 10 + failurePolicy: Ignore + name: debt.sealos.io + namespaceSelector: + matchExpressions: + - key: user.sealos.io/owner + operator: Exists + rules: + - apiGroups: + - '*' + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + - DELETE + resources: + - '*' + - apiGroups: + - account.sealos.io + apiVersions: + - v1 + operations: + - UPDATE + resources: + - payments/status + scope: '*' + sideEffects: None diff --git a/controllers/account/config/webhook/service.yaml b/controllers/account/config/webhook/service.yaml new file mode 100644 index 000000000000..03198124dd55 --- /dev/null +++ b/controllers/account/config/webhook/service.yaml @@ -0,0 +1,27 @@ +# Copyright © 2023 sealos. +# +# 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. + + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go new file mode 100644 index 000000000000..37013bd652a7 --- /dev/null +++ b/controllers/account/controllers/account_controller.go @@ -0,0 +1,644 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/resources" + pkgtypes "github.com/labring/sealos/controllers/pkg/types" + usernotify "github.com/labring/sealos/controllers/pkg/user_notify" + "github.com/labring/sealos/controllers/pkg/utils" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/pkg/utils/logger" + "github.com/labring/sealos/controllers/pkg/utils/retry" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + gonanoid "github.com/matoous/go-nanoid/v2" + "github.com/sirupsen/logrus" + "go.mongodb.org/mongo-driver/bson/primitive" + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + 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/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +type CVMTaskRunner struct { + DBClient database.Interface + Logger logr.Logger + *AccountReconciler +} + +func (r *CVMTaskRunner) Start(ctx context.Context) error { + ticker := time.NewTicker(env.GetDurationEnvWithDefault("BILLING_CVM_INTERVAL", 10*time.Minute)) + defer func() { + ticker.Stop() + r.Logger.Info("stop billing cvm") + }() + for { + select { + case <-ticker.C: + r.Logger.Info("start billing cvm", "time", time.Now().Format(time.RFC3339)) + err := r.BillingCVM() + if err != nil { + r.Logger.Error(err, "fail to billing cvm") + } + r.Logger.Info("end billing cvm", "time", time.Now().Format(time.RFC3339)) + case <-ctx.Done(): + return nil + } + } +} + +const ( + ACCOUNTNAMESPACEENV = "ACCOUNT_NAMESPACE" + DEFAULTACCOUNTNAMESPACE = "sealos-system" + RECHARGEGIFT = "recharge-gift" + SEALOS = "sealos" + + EnvNonFreeTrialEnabled = "NON_FREE_TRIAL_ENABLED" + EnvJwtSecret = "ACCOUNT_API_JWT_SECRET" + EnvAdminJwtSecret = "ACCOUNT_ADMIN_JWT_SECRET" + EnvDesktopJwtSecret = "DESKTOP_API_JWT_SECRET" + + InitAccountTimeAnnotation = "user.sealos.io/init-account-time" + WorkspaceStatusAnnotation = "user.sealos.io/workspace-status" + WorkspaceStatusSubscription = "subscription" + WorkspaceStatusPAYG = "payg" +) + +var SubscriptionEnabled = false + +// AccountReconciler reconciles an Account object +type AccountReconciler struct { + client.Client + AccountV2 database.AccountV2 + InitUserAccountFunc func(user *pkgtypes.UserQueryOpts) (*pkgtypes.Account, error) + Scheme *runtime.Scheme + Logger logr.Logger + VLogger *logger.Logger + accountSystemNamespace string + DBClient database.Account + CVMDBClient database.CVM + MongoDBURI string + Activities pkgtypes.Activities + DefaultDiscount pkgtypes.RechargeDiscount + SubscriptionQuotaLimit map[string]corev1.ResourceList + SyncNSQuotaFunc func(ctx context.Context, owner, nsName string) error + SkipExpiredUserTimeDuration time.Duration + localDomain string + allRegionDomain []string + jwtManager *utils.JWTManager + adminJwtManager *utils.JWTManager + desktopJwtManager *utils.JWTManager + workspaceSubPlans []pkgtypes.WorkspaceSubscriptionPlan + workspaceSubPlansResourceLimit map[string]corev1.ResourceList + + UserContactProvider usernotify.UserContactProvider + UserNotificationService usernotify.EventNotificationService + NonSupportFreeTrial bool +} + +//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=limitranges,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=user.sealos.io,resources=users,verbs=create;get;list;patch;watch +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete + +func (r *AccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + user := &metav1.PartialObjectMetadata{} + user.SetGroupVersionKind(userv1.GroupVersion.WithKind("User")) + if err := r.Get( + ctx, + client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, + user, + ); err == nil { + if owner := user.Annotations[userv1.UserAnnotationOwnerKey]; owner == "" { + return ctrl.Result{}, errors.New("user owner is empty") + } + if user.Annotations[InitAccountTimeAnnotation] != "" { + return ctrl.Result{}, nil + } + // This is only used to monitor and initialize user resource creation data, + // determine the resource quota created by the owner user and the resource quota initialized by the account user, + // and only the resource quota created by the team user + _, err = r.syncAccount(ctx, user) + if errors.Is(err, gorm.ErrRecordNotFound) && + user.CreationTimestamp.Add(r.SkipExpiredUserTimeDuration).Before(time.Now()) { + return ctrl.Result{}, nil + } + if err == nil { + original := user.DeepCopy() + if user.Annotations == nil { + user.Annotations = make(map[string]string) + } + user.Annotations[InitAccountTimeAnnotation] = time.Now().Format(time.RFC3339) + return ctrl.Result{}, r.Patch(ctx, user, client.MergeFrom(original)) + } + return ctrl.Result{}, err + } else if client.IgnoreNotFound( + err, + ) != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *AccountReconciler) syncAccount( + ctx context.Context, + userCr *metav1.PartialObjectMetadata, +) (account *pkgtypes.Account, err error) { + owner := userCr.Annotations[userv1.UserAnnotationOwnerKey] + userNamespace := "ns-" + userCr.Name + // if err := r.adaptEphemeralStorageLimitRange(ctx, userNamespace); err != nil { + // r.Logger.Error(err, "adapt ephemeral storage limitRange failed") + //} + user, err := r.AccountV2.GetUser(&pkgtypes.UserQueryOpts{Owner: owner, IgnoreEmpty: true}) + if err != nil { + return nil, err + } + if user == nil { + return nil, gorm.ErrRecordNotFound + } + if getUsername(userNamespace) == owner { + account, err = r.InitUserAccountFunc(&pkgtypes.UserQueryOpts{Owner: owner}) + if err != nil { + return nil, err + } + if err := r.syncDebt(ctx, owner); err != nil { + return nil, fmt.Errorf("sync user debt failed: %w", err) + } + } + if userCr.Annotations != nil && + userCr.Annotations[WorkspaceStatusAnnotation] == WorkspaceStatusSubscription { + if err := r.syncSubscriptionWorkspaceResourceQuotaAndLimitRange( + ctx, + user.UID, + userNamespace, + ); err != nil { + return nil, fmt.Errorf( + "sync subscription workspace resource quota and limit range failed: %w", + err, + ) + } + } else { + if err := r.syncResourceQuotaAndLimitRange(ctx, userNamespace); err != nil { + return nil, fmt.Errorf( + "sync user namespace resource quota and limit range failed: %w", + err, + ) + } + } + return account, err +} + +func (r *AccountReconciler) syncDebt(ctx context.Context, owner string) error { + userUID, err := r.AccountV2.GetUserUID(&pkgtypes.UserQueryOpts{Owner: owner}) + if err != nil { + return fmt.Errorf("get userUID failed: %w", err) + } + var count int64 + err = r.AccountV2.GetGlobalDB(). + Model(&pkgtypes.Debt{}). + Where("user_uid = ?", userUID). + Count(&count). + Error + if err != nil { + return fmt.Errorf("check user debt existence failed: %w", err) + } + if count <= 0 { + createDebt, err := r.initializeDebt(ctx, owner, userUID) + if err != nil { + return fmt.Errorf("initialize user debt failed: %w", err) + } + if err = r.AccountV2.GetGlobalDB().Create(createDebt).Error; err != nil { + return fmt.Errorf("create user debt failed: %w", err) + } + } + return nil +} + +func (r *AccountReconciler) initializeDebt( + ctx context.Context, + owner string, + userUID uuid.UUID, +) (*pkgtypes.Debt, error) { + debtCr := &accountv1.Debt{} + err := r.Get( + ctx, + client.ObjectKey{Namespace: r.accountSystemNamespace, Name: "debt-" + owner}, + debtCr, + ) + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("failed to get user debt from CR: %w", err) + } + return &pkgtypes.Debt{ + UserUID: userUID, + AccountDebtStatus: pkgtypes.NormalPeriod, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + }, nil + } + + return convertDebtCrToDebt(debtCr, userUID), nil +} + +func convertDebtCrToDebt(debtCr *accountv1.Debt, userUID uuid.UUID) *pkgtypes.Debt { + debt := &pkgtypes.Debt{ + UserUID: userUID, + AccountDebtStatus: convertDebtStatus(debtCr.Status.AccountDebtStatus), + CreatedAt: debtCr.CreationTimestamp.UTC(), + } + if debtCr.Status.LastUpdateTimestamp > 0 { + debt.UpdatedAt = time.Unix(debtCr.Status.LastUpdateTimestamp, 0).UTC() + } else { + debt.UpdatedAt = debtCr.CreationTimestamp.UTC() + } + statusRecords := make([]pkgtypes.DebtStatusRecord, len(debtCr.Status.DebtStatusRecords)) + for i, record := range debtCr.Status.DebtStatusRecords { + statusRecords[i] = pkgtypes.DebtStatusRecord{ + ID: uuid.New(), + UserUID: userUID, + LastStatus: convertDebtStatus(record.LastStatus), + CurrentStatus: convertDebtStatus(record.CurrentStatus), + CreateAt: record.UpdateTime.UTC(), + } + } + debt.StatusRecords = statusRecords + return debt +} + +func convertDebtStatus(statusType accountv1.DebtStatusType) pkgtypes.DebtStatusType { + switch statusType { + case accountv1.NormalPeriod: + return pkgtypes.NormalPeriod + case accountv1.WarningPeriod: + return pkgtypes.DebtPeriod + case accountv1.ApproachingDeletionPeriod: + return pkgtypes.DebtPeriod + case accountv1.ImminentDeletionPeriod: + return pkgtypes.DebtDeletionPeriod + case accountv1.LowBalancePeriod: + return pkgtypes.LowBalancePeriod + case accountv1.CriticalBalancePeriod: + return pkgtypes.CriticalBalancePeriod + case accountv1.DebtPeriod: + return pkgtypes.DebtPeriod + case accountv1.DebtDeletionPeriod: + return pkgtypes.DebtDeletionPeriod + case accountv1.FinalDeletionPeriod: + return pkgtypes.FinalDeletionPeriod + case "": + return pkgtypes.NormalPeriod + default: + logrus.Errorf("unknown debt status type: %v", statusType) + return "" + } +} + +func (r *AccountReconciler) syncResourceQuotaAndLimitRange( + ctx context.Context, + nsName string, +) error { + objs := []client.Object{ + client.Object(resources.GetDefaultLimitRange(nsName, nsName)), + client.Object(resources.GetDefaultResourceQuota(nsName, ResourceQuotaPrefix+nsName)), + } + for i := range objs { + err := retry.Retry(10, 1*time.Second, func() error { + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, objs[i], func() error { + return nil + }) + return err + }) + if err != nil { + return fmt.Errorf("sync resource %T failed: %w", objs[i], err) + } + } + return nil +} + +func (r *AccountReconciler) syncSubscriptionWorkspaceResourceQuotaAndLimitRange( + ctx context.Context, + userUID uuid.UUID, + nsName string, +) error { + objs := []client.Object{client.Object(resources.GetDefaultLimitRange(nsName, nsName))} + for i := range objs { + err := retry.Retry(10, 1*time.Second, func() error { + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, objs[i], func() error { + return nil + }) + return err + }) + if err != nil { + return fmt.Errorf("sync resource %T failed: %w", objs[i], err) + } + } + // determine that each user subscribes only once + err := r.AccountV2.GetGlobalDB(). + Where(&pkgtypes.WorkspaceSubscription{UserUID: userUID}). + First(&pkgtypes.WorkspaceSubscription{}). + Error + if err == nil { + // If already have other subscription Spaces, just create WorkspaceSubscription and set the quota to 0 + return r.handlerNoTrialInitialWorkspaceSubscription(ctx, userUID, nsName) + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("check user workspace subscription existence failed: %w", err) + } + if r.NonSupportFreeTrial { + return r.handlerNoTrialInitialWorkspaceSubscription(ctx, userUID, nsName) + } else { + err = r.handleProbationPeriodWorkspaceSubscription(ctx, userUID, nsName) + if err != nil { + return fmt.Errorf("handle workspace subscription created failed: %w", err) + } + } + r.Logger.Info("handle workspace subscription created", "namespace", nsName) + return nil +} + +// func (r *AccountReconciler) adaptEphemeralStorageLimitRange(ctx context.Context, nsName string) error { +// limit := resources.GetDefaultLimitRange(nsName, nsName) +// return retry.Retry(10, 1*time.Second, func() error { +// _, err := controllerutil.CreateOrUpdate(ctx, r.Client, limit, func() error { +// if len(limit.Spec.Limits) == 0 { +// limit = resources.GetDefaultLimitRange(nsName, nsName) +// } +// limit.Spec.Limits[0].DefaultRequest[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] +// limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage] = resources.LimitRangeDefault[corev1.ResourceEphemeralStorage] +// //if _, ok := limit.Spec.Limits[0].Default[corev1.ResourceEphemeralStorage]; !ok { +// //} +// return nil +// }) +// return err +// }) +//} + +// SetupWithManager sets up the controller with the Manager. +func (r *AccountReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { + r.Logger = ctrl.Log.WithName("account_controller") + r.accountSystemNamespace = env.GetEnvWithDefault( + accountv1.AccountSystemNamespaceEnv, + "account-system", + ) + regions, err := r.AccountV2.GetRegions() + if err != nil { + return fmt.Errorf("get regions failed: %w", err) + } + r.allRegionDomain = make([]string, len(regions)) + for i, region := range regions { + r.allRegionDomain[i] = region.Domain + } + r.localDomain = r.AccountV2.GetLocalRegion().Domain + jwtSecret := os.Getenv(EnvJwtSecret) + if jwtSecret == "" { + return fmt.Errorf("empty jwt secret env: %s", EnvJwtSecret) + } + adminJwtSecret := os.Getenv(EnvAdminJwtSecret) + if adminJwtSecret == "" { + return fmt.Errorf("empty admin jwt secret env: %s", EnvAdminJwtSecret) + } + if adminJwtSecret == jwtSecret { + return fmt.Errorf("admin jwt secret must differ from %s", EnvJwtSecret) + } + r.jwtManager = utils.NewJWTManager(jwtSecret, 10*time.Minute) + r.adminJwtManager = utils.NewJWTManager(adminJwtSecret, 10*time.Minute) + plans, err := r.AccountV2.GetWorkspaceSubscriptionPlanList() + if err != nil { + return fmt.Errorf("failed to get workspace subscription plans: %w", err) + } + res, err := resources.ParseResourceLimitWithPlans(plans) + if err != nil { + return fmt.Errorf("failed to parse resource limits with plans: %w", err) + } + r.workspaceSubPlans = plans + r.workspaceSubPlansResourceLimit = res + r.InitUserAccountFunc = r.AccountV2.NewAccount + r.NonSupportFreeTrial = os.Getenv(EnvNonFreeTrialEnabled) == trueStatus + r.VLogger = logger.NewFeishuLogger( + nil, + os.Getenv("FEISHU_WEBHOOK"), + logger.INFO, + r.localDomain+"-account-controller", + ) + notifyConfigStr := os.Getenv("NOTIFY_CONFIG") + if notifyConfigStr != "" { + notifyConfig, err := usernotify.ParseConfigsWithJSON(notifyConfigStr) + if err != nil { + return fmt.Errorf("parse notify config error: %w", err) + } + r.UserContactProvider = usernotify.NewMemoryContactProvider() + r.UserNotificationService = usernotify.NewEventNotificationService( + notifyConfig, + r.UserContactProvider, + ) + } else { + r.Logger.Info("NOTIFY_CONFIG is empty") + } + // r.SyncNSQuotaFunc = r.syncResourceQuotaAndLimitRange + return ctrl.NewControllerManagedBy(mgr). + For(&userv1.User{}, builder.WithPredicates(OnlyCreatePredicate{}), builder.OnlyMetadata). + WithOptions(rateOpts). + Complete(r) +} + +type OnlyCreatePredicate struct { + predicate.Funcs +} + +func (OnlyCreatePredicate) Update(_ event.UpdateEvent) bool { + return false +} + +func (OnlyCreatePredicate) Create(_ event.CreateEvent) bool { + return true +} + +func RawParseRechargeConfig() (activities pkgtypes.Activities, discountsteps []int64, discountratios []float64, returnErr error) { + // local test + // config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) + // if err != nil { + // fmt.Printf("Error building kubeconfig: %v\n", err) + // os.Exit(1) + //} + config, err := rest.InClusterConfig() + if err != nil { + returnErr = fmt.Errorf("get in cluster config failed: %w", err) + return activities, discountsteps, discountratios, returnErr + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + returnErr = fmt.Errorf("get clientset failed: %w", err) + return activities, discountsteps, discountratios, returnErr + } + configMap, err := clientset.CoreV1(). + ConfigMaps(SEALOS). + Get(context.TODO(), RECHARGEGIFT, metav1.GetOptions{}) + if err != nil { + returnErr = fmt.Errorf("get configmap failed: %w", err) + return activities, discountsteps, discountratios, returnErr + } + if returnErr = parseConfigList( + configMap.Data["steps"], + &discountsteps, + "steps", + ); returnErr != nil { + return activities, discountsteps, discountratios, returnErr + } + + if returnErr = parseConfigList( + configMap.Data["ratios"], + &discountratios, + "ratios", + ); returnErr != nil { + return activities, discountsteps, discountratios, returnErr + } + + if activityStr := configMap.Data["activities"]; activityStr != "" { + returnErr = json.Unmarshal([]byte(activityStr), &activities) + } + return activities, discountsteps, discountratios, returnErr +} + +func parseConfigList(s string, list any, configName string) error { + for v := range strings.SplitSeq(s, ",") { + switch list := list.(type) { + case *[]int64: + i, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return fmt.Errorf("%s format error: %w", configName, err) + } + *list = append(*list, i) + case *[]float64: + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return fmt.Errorf("%s format error: %w", configName, err) + } + *list = append(*list, f) + } + } + return nil +} + +const BaseUnit = 1_000_000 + +func getFirstRechargeDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) (bool, int64) { + if discount.FirstRechargeSteps != nil && discount.FirstRechargeSteps[amount/BaseUnit] > 0 { + return true, discount.FirstRechargeSteps[amount/BaseUnit] * BaseUnit + } + if discount.DefaultSteps != nil { + return false, discount.DefaultSteps[amount/BaseUnit] * BaseUnit + } + return false, 0 +} + +func (r *AccountReconciler) BillingCVM() error { + cvmMap, err := r.CVMDBClient.GetPendingStateInstance(os.Getenv("LOCAL_REGION")) + if err != nil { + return fmt.Errorf("get pending state instance failed: %w", err) + } + for userUID, cvms := range cvmMap { + fmt.Println("billing cvm", userUID, cvms) + // userUID, namespace := strings.Split(userInfo, "/")[0], strings.Split(userInfo, "/")[1] + appCosts := make([]resources.AppCost, len(cvms)) + cvmTotalAmount := 0.0 + cvmIDs := make([]primitive.ObjectID, len(cvms)) + cvmIDsDetail := make([]string, 0) + for i := range cvms { + appCosts[i] = resources.AppCost{ + Amount: int64(cvms[i].Amount * BaseUnit), + Name: cvms[i].InstanceName, + } + cvmTotalAmount += cvms[i].Amount + cvmIDs[i] = cvms[i].ID + cvmIDsDetail = append(cvmIDsDetail, cvms[i].ID.String()) + } + userQueryOpts := pkgtypes.UserQueryOpts{UID: uuid.MustParse(userUID)} + user, err := r.AccountV2.GetUserCr(&userQueryOpts) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + fmt.Println("user not found", userQueryOpts) + continue + } + return fmt.Errorf("get user failed: %w", err) + } + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("generate billing id error: %w", err) + } + billing := &resources.Billing{ + OrderID: id, + AppCosts: appCosts, + Type: accountv1.Consumption, + Namespace: "ns-" + user.CrName, + AppType: resources.AppType[resources.CVM], + Amount: int64(cvmTotalAmount * BaseUnit), + Owner: user.CrName, + Time: time.Now().UTC(), + Status: resources.Settled, + Detail: "{" + strings.Join(cvmIDsDetail, ",") + "}", + } + err = r.AccountV2.AddDeductionBalanceWithFunc( + &pkgtypes.UserQueryOpts{UID: user.UserUID}, + billing.Amount, + func() error { + if saveErr := r.DBClient.SaveBillings(billing); saveErr != nil { + return fmt.Errorf("save billing failed: %w", saveErr) + } + return nil + }, + func() error { + if saveErr := r.CVMDBClient.SetDoneStateInstance(cvmIDs...); saveErr != nil { + return fmt.Errorf("set done state instance failed: %w", saveErr) + } + return nil + }, + ) + if err != nil { + return fmt.Errorf("add balance failed: %w", err) + } + fmt.Printf("billing cvm success %#+v\n", billing) + } + return nil +} diff --git a/controllers/account/controllers/account_controller_test.go b/controllers/account/controllers/account_controller_test.go new file mode 100644 index 000000000000..6e50a22c3916 --- /dev/null +++ b/controllers/account/controllers/account_controller_test.go @@ -0,0 +1,146 @@ +/* +Copyright 2023. + +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 controllers + +import ( + "context" + "encoding/json" + "os" + "testing" + + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/database/mongo" + ctrl "sigs.k8s.io/controller-runtime" +) + +func TestAccountReconciler_BillingCVM(t *testing.T) { + requireAccountExternalTest(t, + database.CVMMongoURI, + database.MongoURI, + database.GlobalCockroachURI, + database.LocalCockroachURI, + "LOCAL_REGION", + ) + dbCtx := context.Background() + cvmDBClient, err := mongo.NewMongoInterface(dbCtx, os.Getenv(database.CVMMongoURI)) + if err != nil { + t.Fatalf("unable to connect to mongo: %v", err) + } + defer func() { + if cvmDBClient != nil { + err := cvmDBClient.Disconnect(dbCtx) + if err != nil { + t.Errorf("unable to disconnect from mongo: %v", err) + } + } + }() + v2Account, err := cockroach.NewCockRoach( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + t.Fatalf("unable to connect to cockroach: %v", err) + } + defer func() { + err := v2Account.Close() + if err != nil { + t.Errorf("unable to disconnect from cockroach: %v", err) + } + }() + DBClient, err := mongo.NewMongoInterface(dbCtx, os.Getenv(database.MongoURI)) + if err != nil { + t.Fatalf("unable to connect to mongo: %v", err) + } + defer func() { + err := DBClient.Disconnect(dbCtx) + if err != nil { + t.Errorf("unable to disconnect from mongo: %v", err) + } + }() + + r := &AccountReconciler{ + AccountV2: v2Account, + DBClient: DBClient, + CVMDBClient: cvmDBClient, + Logger: ctrl.Log.WithName("controllers").WithName("AccountReconciler"), + } + if err := r.BillingCVM(); err != nil { + t.Errorf("AccountReconciler.BillingCVM() error = %v", err) + } +} + +func TestAccountV2_GetAccountConfig(t *testing.T) { + requireAccountExternalTest(t, + database.GlobalCockroachURI, + database.LocalCockroachURI, + "LOCAL_REGION", + ) + v2Account, err := cockroach.NewCockRoach( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + t.Fatalf("unable to connect to cockroach: %v", err) + } + defer func() { + err := v2Account.Close() + if err != nil { + t.Errorf("unable to disconnect from cockroach: %v", err) + } + }() + err = v2Account.InitTables() + if err != nil { + t.Fatalf("unable to init tables: %v", err) + } + + // if err = v2Account.InsertAccountConfig(&types.AccountConfig{ + // TaskProcessRegion: "192.160.0.55.nip.io", + // FirstRechargeDiscountSteps: map[int64]float64{ + // 8: 100, 32: 100, 128: 100, 256: 100, 512: 100, 1024: 100, + // }, + // DefaultDiscountSteps: map[int64]float64{ + // //128,256,512,1024,2048,4096; 10,15,20,25,30,35 + // 128: 10, 256: 15, 512: 20, 1024: 25, 2048: 30, 4096: 35, + // }, + // }); err != nil { + // t.Fatalf("unable to insert account config: %v", err) + //} + + aa, err := v2Account.GetAccountConfig() + if err != nil { + t.Fatalf("failed to get account config: %v", err) + } + + data, err := json.MarshalIndent(aa, "", " ") + if err != nil { + t.Fatalf("failed to marshal account config: %v", err) + } + t.Logf("success get account config:\n%s", string(data)) +} + +func requireAccountExternalTest(t *testing.T, envNames ...string) { + t.Helper() + if os.Getenv("RUN_ACCOUNT_EXTERNAL_TESTS") != "true" { + t.Skip("set RUN_ACCOUNT_EXTERNAL_TESTS=true to run account external tests") + } + for _, name := range envNames { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } +} diff --git a/controllers/account/controllers/accountv2_test.go b/controllers/account/controllers/accountv2_test.go new file mode 100644 index 000000000000..b70eb741945d --- /dev/null +++ b/controllers/account/controllers/accountv2_test.go @@ -0,0 +1,151 @@ +// Copyright © 2024 sealos. +// +// 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 controllers + +import ( + "os" + "testing" + + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" +) + +func TestAccountV2_CreateAccount(t *testing.T) { + account := newExternalAccountV2(t) + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + aa, err := account.NewAccount(&types.UserQueryOpts{Owner: "eoxwhh80"}) + if err != nil { + t.Errorf("failed to create account: %v", err) + } + t.Logf("success create account: %v", aa) + + aa, err = account.NewAccount(&types.UserQueryOpts{Owner: "1ycieb5b"}) + if err != nil { + t.Errorf("failed to create account: %v", err) + } + t.Logf("success create account: %v", aa) +} + +func TestAccountV2_GetAccount(t *testing.T) { + account := newExternalAccountV2(t) + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "zzxns1si"}) + if err != nil { + t.Errorf("failed to get account: %v", err) + } + t.Logf("success create account: %+v", aa) + + // aa, err = account.GetAccount(&types.UserQueryOpts{Owner: "1ycieb5b"}) + // if err != nil { + // t.Errorf("failed to get account: %v", err) + //} + // t.Logf("success create account: %+v", aa) +} + +func TestAccountV2_GetUser(t *testing.T) { + account := newExternalAccountV2(t) + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + user, err := account.GetUserCr(&types.UserQueryOpts{Owner: "eoxwhh80"}) + if err != nil { + t.Errorf("failed to get user: %v", err) + } + t.Logf("success get user: %v", user) +} + +func TestAccountV2_TransferAccount(t *testing.T) { + account := newExternalAccountV2(t) + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + err := account.TransferAccount( + &types.UserQueryOpts{Owner: "eoxwhh80"}, + &types.UserQueryOpts{Owner: "1ycieb5b"}, + 85*cockroach.BaseUnit, + ) + if err != nil { + t.Errorf("failed to transfer account: %v", err) + } + aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "eoxwhh80"}) + if err != nil { + t.Errorf("failed to get eoxwhh80 account: %v", err) + } + t.Logf("success create eoxwhh80 account: %+v", aa) + + aa, err = account.GetAccount(&types.UserQueryOpts{Owner: "1ycieb5b"}) + if err != nil { + t.Errorf("failed to get 1ycieb5b account: %v", err) + } + t.Logf("success create 1ycieb5b account: %+v", aa) +} + +func TestAccountV2_AddBalance(t *testing.T) { + account := newExternalAccountV2(t) + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + // err = account.AddBalance(&types.UserQueryOpts{Owner: "zzxns1si"}, 100*cockroach.BaseUnit) + // if err != nil { + // t.Errorf("failed to add balance: %v", err) + //} + // err = account.AddDeductionBalance(&types.UserQueryOpts{Owner: "zzxns1si"}, 999*cockroach.BaseUnit) + // if err != nil { + // t.Fatalf("failed to add deduction balance: %v", err) + //} + aa, err := account.GetAccount(&types.UserQueryOpts{Owner: "zzxns1si"}) + if err != nil { + t.Fatalf("failed to get account: %v", err) + } + t.Logf("success create DeductionBalance: %+v", aa.DeductionBalance/cockroach.BaseUnit) + + t.Logf("success create Balance: %+v", aa.Balance/cockroach.BaseUnit) + t.Logf( + "success create accountbalance: %+v", + (aa.Balance-aa.DeductionBalance)/cockroach.BaseUnit, + ) +} + +func newExternalAccountV2(t *testing.T) database.AccountV2 { + t.Helper() + requireAccountExternalTest(t, + database.GlobalCockroachURI, + database.LocalCockroachURI, + "LOCAL_REGION", + ) + account, err := database.NewAccountV2( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + t.Fatalf("failed to create account client: %v", err) + } + return account +} diff --git a/controllers/account/controllers/activity_controller.go b/controllers/account/controllers/activity_controller.go new file mode 100644 index 000000000000..441f724b51be --- /dev/null +++ b/controllers/account/controllers/activity_controller.go @@ -0,0 +1,175 @@ +/* +Copyright 2022 labring. + +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 controllers + +// +// import ( +// "context" +// "fmt" +// "reflect" +// "time" +// +// gonanoid "github.com/matoous/go-nanoid/v2" +// +// "github.com/labring/sealos/controllers/pkg/resources" +// +// "github.com/labring/sealos/controllers/pkg/database" +// +// "github.com/labring/sealos/controllers/pkg/crypto" +// +// "sigs.k8s.io/controller-runtime/pkg/event" +// "sigs.k8s.io/controller-runtime/pkg/predicate" +// +// "sigs.k8s.io/controller-runtime/pkg/controller" +// +// "github.com/go-logr/logr" +// "k8s.io/apimachinery/pkg/runtime" +// +// ctrl "sigs.k8s.io/controller-runtime" +// "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/controller-runtime/pkg/log" +// +// accountv1 "github.com/labring/sealos/controllers/account/api/v1" +// "github.com/labring/sealos/controllers/pkg/types" +//) +// +// type ActivityReconciler struct { +// client.Client +// Scheme *runtime.Scheme +// Logger logr.Logger +// Activity types.Activities +// DBClient database.Account +//} +// +////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create;update;patch;delete +////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get;update;patch +////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/finalizers,verbs=update +// +//// Reconcile is part of the main kubernetes reconciliation loop which aims to +//// move the current state of the cluster closer to the desired state. +//// TODO(user): Modify the Reconcile function to compare the state specified by +//// the Payment object against the actual cluster state, and then +//// perform operations to make the cluster state reflect the state specified by +//// the user. +//// +//// For more details, check Reconcile and its Result here: +//// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +// func (r *ActivityReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// r.Logger = log.FromContext(ctx) +// +// account := &accountv1.Account{} +// if err := r.Get(ctx, req.NamespacedName, account); err != nil { +// r.Logger.Error(err, "get account failed") +// return ctrl.Result{}, client.IgnoreNotFound(err) +// } +// userActivities, err := types.ParseUserActivities(account.Annotations) +// if err != nil { +// r.Logger.Error(err, "parse user activities failed") +// return ctrl.Result{}, err +// } +// anno, amount, err := r.giveAmount(userActivities, account) +// if err != nil { +// return ctrl.Result{}, err +// } +// if anno != nil { +// if err := r.handleBonus(account, anno, amount); err != nil { +// return ctrl.Result{}, fmt.Errorf("handle bonus failed: %v", err) +// } +// } +// return ctrl.Result{}, nil +//} +// +// func (r *ActivityReconciler) handleBonus(account *accountv1.Account, annotations map[string]string, amount int64) error { +// if err := SyncAccountStatus(context.Background(), r.Client, account); err != nil { +// return fmt.Errorf("update account status failed: %v", err) +// } +// +// account.Annotations = annotations +// if err := r.Update(context.Background(), account); err != nil { +// return fmt.Errorf("update account failed: %v", err) +// } +// id, err := gonanoid.New(12) +// if err != nil { +// return fmt.Errorf("create id failed: %v", err) +// } +// +// if err = r.DBClient.SaveBillings(&resources.Billing{ +// Time: time.Now().UTC(), +// OrderID: id, +// Amount: amount, +// Namespace: GetUserNamespace(account.Name), +// Owner: getUsername(account.Name), +// Type: accountv1.ActivityGiving, +// }); err != nil { +// return fmt.Errorf("save billing failed: %v", err) +// } +// r.Logger.Info("update account success", "account", account.Name, "bonus amount", amount, "balance", account.Status.Balance) +// return nil +//} +// +// func (r *ActivityReconciler) giveAmount(userActivities types.UserActivities, account *accountv1.Account) (annotations map[string]string, amount int64, err error) { +// for activityType, userActivity := range userActivities { +// activity, exist := r.Activity[activityType] +// if !exist { +// r.Logger.Error(nil, "activity not exist", "activity", activity) +// continue +// } +// userPhase, exist := userActivity.Phases[userActivity.CurrentPhase] +// if !exist { +// r.Logger.Error(nil, "userPhase not exist", "activity", activityType, "phase", userActivity.CurrentPhase) +// continue +// } +// if userPhase.EndTime.IsZero() { +// continue +// } +// giveAmount := activity.Phases[userActivity.CurrentPhase].GiveAmount +// if giveAmount != 0 && userPhase.GiveAmount == 0 { +// err := crypto.RechargeBalance(account.Status.EncryptBalance, giveAmount) +// if err != nil { +// return nil, 0, fmt.Errorf("give account %s amount failed: %w", account.Name, err) +// } +// account.Status.Balance += giveAmount +// account.Status.ActivityBonus += giveAmount +// return types.SetUserPhaseGiveAmount(account.Annotations, activityType, userActivity.CurrentPhase, giveAmount), giveAmount, nil +// } +// } +// return nil, 0, nil +//} +// +//// SetupWithManager sets up the controller with the Manager. +// func (r *ActivityReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { +// const controllerName = "activity_controller" +// r.Logger = ctrl.Log.WithName(controllerName) +// return ctrl.NewControllerManagedBy(mgr). +// For(&accountv1.Account{}). +// WithEventFilter(predicate.Funcs{ +// CreateFunc: func(e event.CreateEvent) bool { +// return len(e.Object.GetAnnotations()) > 1 +// }, +// UpdateFunc: func(e event.UpdateEvent) bool { +// accountOld := e.ObjectOld.(*accountv1.Account) +// accountNew := e.ObjectNew.(*accountv1.Account) +// if len(accountNew.Annotations) == 0 { +// return false +// } +// return !reflect.DeepEqual(accountOld.Annotations, accountNew.Annotations) +// }, +// DeleteFunc: func(e event.DeleteEvent) bool { +// return false +// }, +// }).WithOptions(rateOpts).Complete(r) +//} diff --git a/controllers/account/controllers/activity_controller_test.go b/controllers/account/controllers/activity_controller_test.go new file mode 100644 index 000000000000..59ca1fac3c03 --- /dev/null +++ b/controllers/account/controllers/activity_controller_test.go @@ -0,0 +1,15 @@ +// Copyright © 2023 sealos. +// +// 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 controllers diff --git a/controllers/account/controllers/billing_controller.go b/controllers/account/controllers/billing_controller.go new file mode 100644 index 000000000000..bd5ee8cb42e1 --- /dev/null +++ b/controllers/account/controllers/billing_controller.go @@ -0,0 +1,926 @@ +/* +Copyright 2023 sealos. + +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 controllers + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/pkg/utils/maps" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "gorm.io/gorm" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type BillingTaskRunner struct { + *BillingReconciler +} + +func (r *BillingTaskRunner) NeedLeaderElection() bool { return true } + +const ( + billingMonitorDelay = 5 * time.Minute + billingRetryDelay = time.Minute + defaultBillingMaxCatchupDuration = 24 * time.Hour + billingMaxCatchupDurationEnv = "BILLING_MAX_CATCHUP_DURATION" +) + +var ( + DebtUserMap *maps.ConcurrentNullValueMap + SubscriptionWorkspaceMap *maps.ConcurrentNullValueMap +) + +func (r *BillingTaskRunner) Start(ctx context.Context) error { + defer func() { + r.Info("stopping billing reconcile", "time", time.Now().Format(time.RFC3339)) + }() + + for { + if err := ctx.Err(); err != nil { + return nil + } + target := latestReadyBillingHour(time.Now()) + if err := r.ExecuteBillingTasksUntil(target); err != nil { + r.Error(err, "failed to execute billing tasks", "target", target) + if !waitForBillingRun(ctx, billingRetryDelay) { + return nil + } + continue + } + + // A long catch-up may make another hour ready while the task is running. + if latestReadyBillingHour(time.Now()).After(target) { + continue + } + nextRun := target.Add(time.Hour).Add(billingMonitorDelay) + r.Info("next billing reconcile time", "time", nextRun.Format(time.RFC3339)) + if !waitForBillingRun(ctx, time.Until(nextRun)) { + return nil + } + } +} + +func latestReadyBillingHour(now time.Time) time.Time { + return now.Add(-billingMonitorDelay).Truncate(time.Hour).UTC() +} + +func billingHoursAfter(checkpoint, target time.Time) []time.Time { + var hours []time.Time + for hour := checkpoint.UTC().Truncate(time.Hour).Add(time.Hour); !hour.After(target); hour = hour.Add(time.Hour) { + hours = append(hours, hour) + } + return hours +} + +func waitForBillingRun(ctx context.Context, delay time.Duration) bool { + if delay < 0 { + delay = 0 + } + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-timer.C: + return true + case <-ctx.Done(): + return false + } +} + +func runBillingReads(tasks ...func() error) error { + errs := make([]error, len(tasks)) + var wg sync.WaitGroup + for i, task := range tasks { + wg.Add(1) + go func(index int, read func() error) { + defer wg.Done() + errs[index] = read() + }(i, task) + } + wg.Wait() + return errors.Join(errs...) +} + +const ( + UserNamespacePrefix = "ns-" + ResourceQuotaPrefix = "quota-" +) + +const BillingAnnotationLastUpdateTime = "account.sealos.io/last-update-time" + +// BillingReconciler reconciles a Billing object +type BillingReconciler struct { + client.Client + Scheme *runtime.Scheme + logr.Logger + DBClient database.Account + AccountV2 database.AccountV2 + Properties *resources.PropertyTypeLS + reconcileBillingFunc func(owner string, billings []*resources.Billing, endHourTime time.Time) error + executeBillingHourFunc func(time.Time) error + concurrentLimit int64 + DebtUserMap *maps.ConcurrentMap + debtOwnerMap *maps.ConcurrentNullValueMap +} + +func (r *BillingReconciler) ExecuteBillingTask() error { + return r.ExecuteBillingTasksUntil(latestReadyBillingHour(time.Now())) +} + +func (r *BillingReconciler) ExecuteBillingTasksUntil(target time.Time) (err error) { + target = target.UTC().Truncate(time.Hour) + setBillingTargetMetrics(target) + defer func() { + setBillingProcessingMetrics(time.Time{}, false) + if err != nil { + billingReconcileFailures.Inc() + } + }() + checkpoint, exists, err := r.DBClient.GetBillingCheckpoint() + if err != nil { + return fmt.Errorf("get billing checkpoint: %w", err) + } + maxCatchup := env.GetDurationEnvWithDefault( + billingMaxCatchupDurationEnv, + defaultBillingMaxCatchupDuration, + ) + if err := validateBillingMaxCatchupDuration(maxCatchup); err != nil { + return err + } + persistedCheckpoint := checkpoint.UTC().Truncate(time.Hour) + if !exists { + checkpoint = target.Add(-time.Hour) + persistedCheckpoint = checkpoint + } else { + checkpoint, err = limitBillingCheckpoint(persistedCheckpoint, target, maxCatchup) + if err != nil { + return err + } + if checkpoint.After(persistedCheckpoint) { + r.Info( + "billing catch-up window truncated", + "persistedCheckpoint", persistedCheckpoint.Format(time.RFC3339), + "replayFrom", checkpoint.Format(time.RFC3339), + "skippedHours", int64(checkpoint.Sub(persistedCheckpoint)/time.Hour), + ) + } + } + setBillingCheckpointMetrics(persistedCheckpoint, target) + setBillingPendingCheckpointMetrics(checkpoint, target) + for _, billingTime := range billingHoursAfter(checkpoint, target) { + setBillingProcessingMetrics(billingTime, true) + execute := r.ExecuteBillingTaskAt + if r.executeBillingHourFunc != nil { + execute = r.executeBillingHourFunc + } + if err := execute(billingTime); err != nil { + return fmt.Errorf( + "reconcile billing hour %s: %w", + billingTime.Format(time.RFC3339), + err, + ) + } + if err := r.DBClient.SaveBillingCheckpoint(billingTime); err != nil { + return fmt.Errorf( + "save billing checkpoint for %s: %w", + billingTime.Format(time.RFC3339), + err, + ) + } + checkpoint = billingTime + setBillingCheckpointMetrics(checkpoint, target) + billingLastSuccessTimestamp.Set(float64(time.Now().UTC().Unix())) + } + return nil +} + +func validateBillingMaxCatchupDuration(maxCatchup time.Duration) error { + if maxCatchup < time.Hour || maxCatchup%time.Hour != 0 { + return fmt.Errorf( + "%s must be a positive whole number of hours: %s", + billingMaxCatchupDurationEnv, + maxCatchup, + ) + } + return nil +} + +func limitBillingCheckpoint( + checkpoint, target time.Time, + maxCatchup time.Duration, +) (time.Time, error) { + if err := validateBillingMaxCatchupDuration(maxCatchup); err != nil { + return time.Time{}, err + } + checkpoint = checkpoint.UTC().Truncate(time.Hour) + target = target.UTC().Truncate(time.Hour) + earliest := target.Add(-maxCatchup) + if checkpoint.Before(earliest) { + return earliest, nil + } + return checkpoint, nil +} + +func (r *BillingReconciler) ExecuteBillingTaskAt(endHourTime time.Time) error { + endHourTime = endHourTime.UTC().Truncate(time.Hour) + taskStartedAt := time.Now() + r.Info("start billing reconcile", "billingTime", endHourTime.Format(time.RFC3339)) + DebtUserMap = maps.NewConcurrentNullValueMap() + SubscriptionWorkspaceMap = maps.NewConcurrentNullValueMap() + r.debtOwnerMap = maps.NewConcurrentNullValueMap() + var ownerListMap map[string][]string + var ownerUserUIDs map[string]uuid.UUID + inputStartedAt := time.Now() + if err := runBillingReads( + func() error { + return r.loadDebtUsersAt(endHourTime) + }, + func() error { + var err error + ownerListMap, ownerUserUIDs, err = r.getRecentUsedOwnersAt(endHourTime) + if err != nil { + return fmt.Errorf("get recently used owners: %w", err) + } + return nil + }, + ); err != nil { + return fmt.Errorf("load billing inputs: %w", err) + } + r.Info("load billing inputs", "duration", time.Since(inputStartedAt)) + r.removeDebtOwners(ownerListMap, ownerUserUIDs) + if err := r.loadSubscriptionWorkspacesAt(endHourTime, ownerListMap); err != nil { + return err + } + if len(ownerListMap) == 0 { + r.Info( + "billing hour has no monitor-backed owners", + "billingTime", + endHourTime.Format(time.RFC3339), + ) + } + if err := r.reconcileOwnerListBatch( + ownerListMap, + env.GetIntEnvWithDefault("BILLING_RECONCILE_BATCH_COUNT", 200), + endHourTime, + r.reconcileOwnerList, + ); err != nil { + return fmt.Errorf("failed to reconcile owner list batch: %w", err) + } + r.Info( + "finish billing reconcile", + "billingTime", endHourTime.Format(time.RFC3339), + "duration", time.Since(taskStartedAt), + ) + return nil +} + +func (r *BillingReconciler) loadDebtUsersAt(endHourTime time.Time) error { + startedAt := time.Now() + db := r.AccountV2.GetGlobalDB() + var debts []types.Debt + // Probe the first transition per debt row so catch-up never materializes + // every status record after the billing boundary. + if err := db.Raw(` + SELECT + d.user_uid, + COALESCE(first_record.last_status, d.account_debt_status) AS account_debt_status + FROM "Debt" AS d + LEFT JOIN LATERAL ( + SELECT r.last_status + FROM "DebtStatusRecord" AS r + WHERE r.user_uid = d.user_uid AND r.create_at >= ? + ORDER BY r.create_at ASC, r.id ASC + LIMIT 1 + ) AS first_record ON TRUE + WHERE d.created_at < ?`, endHourTime, endHourTime). + Scan(&debts).Error; err != nil { + return fmt.Errorf("query consistent billing-period debt state: %w", err) + } + + var users []string + for i := range debts { + if types.ContainDebtStatus(types.DebtStates, debts[i].AccountDebtStatus) { + users = append(users, debts[i].UserUID.String()) + } + } + DebtUserMap.Set(users...) + r.Info( + "load billing-period debt users", + "count", len(users), + "duration", time.Since(startedAt), + ) + return nil +} + +func (r *BillingReconciler) loadSubscriptionWorkspacesAt( + endHourTime time.Time, + ownerListMap map[string][]string, +) error { + startedAt := time.Now() + effectiveTime := endHourTime.Add(-time.Nanosecond) + db := r.AccountV2.GetGlobalDB() + regionDomain := r.AccountV2.GetLocalRegion().Domain + workspaceSet := make(map[string]struct{}) + for _, workspaces := range ownerListMap { + for _, workspace := range workspaces { + workspaceSet[workspace] = struct{}{} + } + } + workspaces := make([]string, 0, len(workspaceSet)) + for workspace := range workspaceSet { + workspaces = append(workspaces, workspace) + } + if len(workspaces) == 0 { + return nil + } + + var subscriptions []types.WorkspaceSubscription + var periodTransactions []types.WorkspaceSubscriptionTransaction + var terminalTransactions []types.WorkspaceSubscriptionTransaction + // The subscription snapshot and completed transactions describe one entitlement state. + if err := db.Transaction( + func(tx *gorm.DB) error { + if err := tx.Model(&types.WorkspaceSubscription{}). + Where( + "region_domain = ? AND workspace IN ? AND create_at <= ?", + regionDomain, + workspaces, + effectiveTime, + ). + Find(&subscriptions).Error; err != nil { + return fmt.Errorf("query billing-period workspace subscriptions: %w", err) + } + if err := tx.Model(&types.WorkspaceSubscriptionTransaction{}). + Where( + "region_domain = ? AND workspace IN ? AND status = ? AND pay_status IN (?, ?) AND updated_at <= ?", + regionDomain, + workspaces, + types.SubscriptionTransactionStatusCompleted, + types.SubscriptionPayStatusPaid, + types.SubscriptionPayStatusNoNeed, + effectiveTime, + ). + Find(&periodTransactions).Error; err != nil { + return fmt.Errorf( + "query billing-period workspace subscription transactions: %w", + err, + ) + } + if err := tx.Model(&types.WorkspaceSubscriptionTransaction{}). + Where( + "region_domain = ? AND workspace IN ? AND status = ? AND operator IN (?, ?) AND updated_at <= ?", + regionDomain, + workspaces, + types.SubscriptionTransactionStatusCompleted, + types.SubscriptionTransactionTypeCanceled, + types.SubscriptionTransactionTypeDeleted, + effectiveTime, + ). + Find(&terminalTransactions).Error; err != nil { + return fmt.Errorf( + "query billing-period terminal subscription transactions: %w", + err, + ) + } + return nil + }, + &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: true}, + ); err != nil { + return fmt.Errorf("query consistent billing-period subscriptions: %w", err) + } + transactions := make( + []types.WorkspaceSubscriptionTransaction, + 0, + len(periodTransactions)+len(terminalTransactions), + ) + transactions = append(transactions, periodTransactions...) + transactions = append(transactions, terminalTransactions...) + + workspaces, err := activeWorkspaceSubscriptionsAt(effectiveTime, subscriptions, transactions) + if err != nil { + return fmt.Errorf("resolve billing-period workspace subscriptions: %w", err) + } + SubscriptionWorkspaceMap.Set(workspaces...) + r.Info( + "load billing-period subscriptions", + "count", len(workspaces), + "duration", time.Since(startedAt), + ) + return nil +} + +func activeWorkspaceSubscriptionsAt( + effectiveTime time.Time, + subscriptions []types.WorkspaceSubscription, + transactions []types.WorkspaceSubscriptionTransaction, +) ([]string, error) { + active := make(map[string]struct{}) + terminalAt := make(map[string]time.Time) + for i := range transactions { + if !isWorkspaceSubscriptionTerminalTransaction(transactions[i], effectiveTime) { + continue + } + previous, exists := terminalAt[transactions[i].Workspace] + if !exists || transactions[i].UpdatedAt.After(previous) { + terminalAt[transactions[i].Workspace] = transactions[i].UpdatedAt + } + } + for i := range subscriptions { + terminatedAt, terminated := terminalAt[subscriptions[i].Workspace] + if workspaceSubscriptionCovers(subscriptions[i], effectiveTime) && + (!terminated || subscriptions[i].UpdateAt.After(terminatedAt)) { + active[subscriptions[i].Workspace] = struct{}{} + } + } + for i := range transactions { + covers, err := workspaceSubscriptionTransactionCovers(transactions[i], effectiveTime) + if err != nil { + return nil, err + } + if !covers { + continue + } + terminatedAt, terminated := terminalAt[transactions[i].Workspace] + if !terminated || transactions[i].UpdatedAt.After(terminatedAt) { + active[transactions[i].Workspace] = struct{}{} + } + } + workspaces := make([]string, 0, len(active)) + for workspace := range active { + workspaces = append(workspaces, workspace) + } + return workspaces, nil +} + +func isWorkspaceSubscriptionTerminalTransaction( + transaction types.WorkspaceSubscriptionTransaction, + effectiveTime time.Time, +) bool { + if transaction.Status != types.SubscriptionTransactionStatusCompleted || + transaction.UpdatedAt.After(effectiveTime) { + return false + } + switch transaction.Operator { + case types.SubscriptionTransactionTypeCanceled, + types.SubscriptionTransactionTypeDeleted: + return true + default: + return false + } +} + +func workspaceSubscriptionCovers( + subscription types.WorkspaceSubscription, + effectiveTime time.Time, +) bool { + if subscription.CreateAt.After(effectiveTime) || subscription.UpdateAt.After(effectiveTime) { + return false + } + endAt := subscription.CurrentPeriodEndAt + if subscription.ExpireAt != nil && subscription.ExpireAt.Before(endAt) { + endAt = *subscription.ExpireAt + } + if !subscription.CancelAtPeriodEnd && !subscription.CancelAt.IsZero() && + subscription.CancelAt.Before(endAt) { + endAt = subscription.CancelAt + } + return !subscription.CurrentPeriodStartAt.After(effectiveTime) && endAt.After(effectiveTime) +} + +func workspaceSubscriptionTransactionCovers( + transaction types.WorkspaceSubscriptionTransaction, + effectiveTime time.Time, +) (bool, error) { + if transaction.Status != types.SubscriptionTransactionStatusCompleted || + (transaction.PayStatus != types.SubscriptionPayStatusPaid && + transaction.PayStatus != types.SubscriptionPayStatusNoNeed) || + transaction.UpdatedAt.After(effectiveTime) { + return false, nil + } + switch transaction.Operator { + case types.SubscriptionTransactionTypeCreated, + types.SubscriptionTransactionTypeUpgraded, + types.SubscriptionTransactionTypeDowngraded, + types.SubscriptionTransactionTypeRenewed: + default: + return false, nil + } + period, err := types.ParsePeriod(transaction.Period) + if err != nil { + return false, fmt.Errorf( + "parse subscription transaction %s period: %w", transaction.ID, err, + ) + } + return !transaction.UpdatedAt.After(effectiveTime) && + transaction.UpdatedAt.Add(period).After(effectiveTime), nil +} + +func (r *BillingReconciler) reconcileOwnerList( + ownerListMap map[string][]string, + now time.Time, +) error { + if r.concurrentLimit <= 0 { + return fmt.Errorf( + "billing concurrent limit must be greater than zero: %d", r.concurrentLimit, + ) + } + endHourTime := now.UTC().Truncate(time.Hour) + startHourTime := endHourTime.Add(-1 * time.Hour) + ownerList := make([]string, 0, len(ownerListMap)) + for owner := range ownerListMap { + ownerList = append(ownerList, owner) + } + generationOwners := make(map[string][]string, len(ownerListMap)) + for owner, namespaces := range ownerListMap { + if r.ownerInDebt(owner) { + continue + } + generationOwners[owner] = namespaces + } + + var existingBillings map[string][]*resources.Billing + var ownerBillings map[string][]*resources.Billing + if err := runBillingReads( + func() error { + startedAt := time.Now() + var err error + existingBillings, err = r.DBClient.GetOwnerBillingsAt(ownerList, endHourTime) + if err != nil { + return fmt.Errorf("get existing owner billings: %w", err) + } + r.Info( + "get existing owner billings", + "owner count", len(existingBillings), + "duration", time.Since(startedAt), + ) + return nil + }, + func() error { + startedAt := time.Now() + var err error + ownerBillings, err = r.DBClient.GenerateBillingData( + startHourTime, + endHourTime, + r.Properties, + generationOwners, + ) + if err != nil { + return fmt.Errorf("generate billing data: %w", err) + } + r.Info( + "generate billing data", + "count", len(ownerBillings), + "duration", time.Since(startedAt), + ) + return nil + }, + ); err != nil { + return err + } + classifyGeneratedBillings(ownerBillings) + ownerBillings = pendingOwnerBillings(ownerBillings, existingBillings) + + type result struct { + owner string + err error + } + workers := make(chan struct{}, r.concurrentLimit) + resultChan := make(chan result, len(ownerBillings)) + var wg sync.WaitGroup + for owner, billings := range ownerBillings { + if len(billings) == 0 { + continue + } + wg.Add(1) + go func(owner string, billings []*resources.Billing) { + defer wg.Done() + workers <- struct{}{} + defer func() { + <-workers + }() + reconcileErr := r.reconcileBillingFunc(owner, billings, endHourTime) + if reconcileErr != nil { + r.Error( + reconcileErr, + "failed to reconcile owner", + "owner", + owner, + "billings", + billings, + ) + } + resultChan <- result{owner: owner, err: reconcileErr} + }(owner, billings) + } + wg.Wait() + close(resultChan) + var failedList []string + for res := range resultChan { + if res.err != nil { + failedList = append(failedList, res.owner) + } + } + if len(failedList) > 0 { + billingFailedOwners.Set(float64(len(failedList))) + return fmt.Errorf("failed to reconcile owners: %s", strings.Join(failedList, ",")) + } + billingFailedOwners.Set(0) + return nil +} + +func billingBusinessKey(billing *resources.Billing) string { + return fmt.Sprintf("%s\x00%d\x00%s", billing.Namespace, billing.AppType, billing.AppName) +} + +func classifyGeneratedBillings(ownerBillings map[string][]*resources.Billing) { + for _, billings := range ownerBillings { + for _, billing := range billings { + billing.Status = resources.Settled + if _, ok := SubscriptionWorkspaceMap.Get(billing.Namespace); ok { + billing.Status = resources.Subscription + } + } + } +} + +func (r *BillingReconciler) isSubscriptionBilling(billing *resources.Billing) bool { + if SubscriptionWorkspaceMap == nil { + return false + } + _, ok := SubscriptionWorkspaceMap.Get(billing.Namespace) + return ok +} + +func pendingOwnerBillings( + generated, existing map[string][]*resources.Billing, +) map[string][]*resources.Billing { + pending := make(map[string][]*resources.Billing) + for owner, billings := range generated { + existingByKey := make(map[string]*resources.Billing, len(existing[owner])) + for _, billing := range existing[owner] { + existingByKey[billingBusinessKey(billing)] = billing + } + for _, billing := range billings { + if _, ok := existingByKey[billingBusinessKey(billing)]; ok { + continue + } + pending[owner] = append(pending[owner], billing) + } + } + return pending +} + +func (r *BillingReconciler) reconcileBilling( + owner string, + billings []*resources.Billing, + _ time.Time, +) error { + amount := int64(0) + orderIDs := make([]string, 0, len(billings)) + for _, billing := range billings { + if billing.Status == resources.Subscription || r.isSubscriptionBilling(billing) { + billing.Status = resources.Subscription + continue + } + amount += billing.Amount + orderIDs = append(orderIDs, billing.OrderID) + } + if err := r.DBClient.SaveBillings(billings...); err != nil { + return fmt.Errorf("save billings failed: %w", err) + } + if amount == 0 { + return nil + } + if err := r.AccountV2.AddDeductionBalance( + &types.UserQueryOpts{Owner: owner}, amount, + ); err != nil { + if updateErr := r.DBClient.UpdateBillingStatus( + orderIDs, resources.Unsettled, + ); updateErr != nil { + r.Error(updateErr, "update billing unsettled status failed", "orderIDs", orderIDs) + } + return fmt.Errorf("deduct owner %s balance: %w", owner, err) + } + return nil +} + +func (r *BillingReconciler) reconcileBillingWithCredits( + owner string, + billings []*resources.Billing, + endHourTime time.Time, +) error { + amount := int64(0) + orderIDs := make([]string, 0, len(billings)) + for _, billing := range billings { + if billing.Status == resources.Subscription || r.isSubscriptionBilling(billing) { + billing.Status = resources.Subscription + continue + } + amount += billing.Amount + orderIDs = append(orderIDs, billing.OrderID) + } + if err := r.DBClient.SaveBillings(billings...); err != nil { + return fmt.Errorf("save billings failed: %w", err) + } + if amount == 0 { + return nil + } + if err := r.AccountV2.AddDeductionBalanceWithCreditsAt( + &types.UserQueryOpts{Owner: owner}, amount, orderIDs, endHourTime, + ); err != nil { + if updateErr := r.DBClient.UpdateBillingStatus( + orderIDs, resources.Unsettled, + ); updateErr != nil { + r.Error(updateErr, "update billing unsettled status failed", "orderIDs", orderIDs) + } + return fmt.Errorf("deduct owner %s with credits: %w", owner, err) + } + return nil +} + +// reconcileOwnerListBatch process ownerlistmap in batch mode +func (r *BillingReconciler) reconcileOwnerListBatch( + ownerListMap map[string][]string, // The owner -> namespaces mapping needs to be handled + batchSize int, // number of owners processed per batch + now time.Time, // current time + reconcileFunc func(map[string][]string, time.Time) error, // processing function +) error { + if batchSize <= 0 { + return errors.New("batch size must be greater than zero") + } + + owners := make([]string, 0, len(ownerListMap)) // store all owners + for owner := range ownerListMap { + owners = append(owners, owner) + } + + total := len(owners) + for i := 0; i < total; i += batchSize { + end := min(i+batchSize, total) + + batchOwners := owners[i:end] // the owner list of the current batch + batchOwnerMap := make(map[string][]string, len(batchOwners)) + for _, owner := range batchOwners { + batchOwnerMap[owner] = ownerListMap[owner] // example retrieve a namespace + } + // call processing logic + if err := reconcileFunc(batchOwnerMap, now); err != nil { + return fmt.Errorf("failed to reconcile batch from %d to %d: %w", i, end, err) + } + r.Info("reconcile batch", "from", i, "to", end) + } + return nil +} + +func (r *BillingReconciler) getRecentUsedOwnersAt( + endHourTime time.Time, +) (map[string][]string, map[string]uuid.UUID, error) { + endHourTime = endHourTime.UTC().Truncate(time.Hour) + startHourTime := endHourTime.Add(-1 * time.Hour) + monitorStartedAt := time.Now() + namespaceList, err := r.DBClient.GetTimeUsedNamespaceList(startHourTime, endHourTime) + if err != nil { + return nil, nil, fmt.Errorf("get recent owners failed: %w", err) + } + r.Info( + "get monitored namespaces", + "namespace count", len(namespaceList), + "duration", time.Since(monitorStartedAt), + ) + if len(namespaceList) == 0 { + r.Info( + "billing source monitor window is empty", + "billingTime", + endHourTime.Format(time.RFC3339), + ) + } + lookupStartedAt := time.Now() + nsToOwnerMap, err := r.getUsersForNamespaces(namespaceList) + if err != nil { + return nil, nil, fmt.Errorf("get users for monitored namespaces failed: %w", err) + } + r.Info( + "get owner and namespace", + "matched user count", + len(nsToOwnerMap), + "namespace count", + len(namespaceList), + "duration", + time.Since(lookupStartedAt), + ) + usedOwnerList := make(map[string][]string) + ownerUserUIDs := make(map[string]uuid.UUID) + for _, ns := range namespaceList { + if owner, ok := nsToOwnerMap[ns]; ok { + if _, ok := usedOwnerList[owner]; !ok { + userUID, err := r.AccountV2.GetUserUID( + &types.UserQueryOpts{Owner: owner, IgnoreEmpty: true}, + ) + if err != nil { + return nil, nil, fmt.Errorf("get user uid failed: %w", err) + } + if userUID == uuid.Nil { + r.Error(errors.New("user uid is nil"), "get user uid failed", "owner", owner) + continue + } + ownerUserUIDs[owner] = userUID + usedOwnerList[owner] = []string{} + } + usedOwnerList[owner] = append(usedOwnerList[owner], ns) + } + } + r.Info("get monitored users", "count", len(usedOwnerList)) + return usedOwnerList, ownerUserUIDs, nil +} + +func (r *BillingReconciler) removeDebtOwners( + ownerListMap map[string][]string, + ownerUserUIDs map[string]uuid.UUID, +) { + for owner, userUID := range ownerUserUIDs { + if _, inDebt := DebtUserMap.Get(userUID.String()); inDebt { + delete(ownerListMap, owner) + r.debtOwnerMap.Set(owner) + } + } +} + +func (r *BillingReconciler) ownerInDebt(owner string) bool { + if r.debtOwnerMap == nil { + return false + } + _, inDebt := r.debtOwnerMap.Get(owner) + return inDebt +} + +func (r *BillingReconciler) getUsersForNamespaces(namespaces []string) (map[string]string, error) { + nsToOwnerMap := make(map[string]string, len(namespaces)) + for _, namespace := range namespaces { + if !strings.HasPrefix(namespace, UserNamespacePrefix) { + continue + } + user := &metav1.PartialObjectMetadata{} + user.SetGroupVersionKind(userv1.GroupVersion.WithKind("User")) + if err := r.Get( + context.Background(), + client.ObjectKey{Name: strings.TrimPrefix(namespace, UserNamespacePrefix)}, + user, + ); err != nil { + if apierrors.IsNotFound(err) { + continue + } + return nil, fmt.Errorf("get user for namespace %s: %w", namespace, err) + } + if owner := user.Annotations[userv1.UserLabelOwnerKey]; owner != "" { + nsToOwnerMap[namespace] = owner + } + } + return nsToOwnerMap, nil +} + +func getUsername(namespace string) string { + return strings.TrimPrefix(namespace, UserNamespacePrefix) +} + +func (r *BillingReconciler) Init() error { + r.Logger = ctrl.Log.WithName("controller").WithName("Billing") + if err := r.DBClient.CreateBillingIfNotExist(); err != nil { + return fmt.Errorf("create billing collection failed: %w", err) + } + r.concurrentLimit = env.GetInt64EnvWithDefault("BILLING_CONCURRENT_LIMIT", 10) + r.reconcileBillingFunc = r.reconcileBilling + if os.Getenv("CREDITS_ENABLED") == trueStatus || + os.Getenv("SUBSCRIPTION_ENABLED") == trueStatus { + r.reconcileBillingFunc = r.reconcileBillingWithCredits + } + return nil +} diff --git a/controllers/account/controllers/billing_controller_runtime_test.go b/controllers/account/controllers/billing_controller_runtime_test.go new file mode 100644 index 000000000000..8af9d8ff4b0f --- /dev/null +++ b/controllers/account/controllers/billing_controller_runtime_test.go @@ -0,0 +1,114 @@ +package controllers + +import ( + "context" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/maps" + "github.com/testcontainers/testcontainers-go" + postgrescontainer "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type debtSnapshotAccountV2 struct { + database.AccountV2 + db *gorm.DB +} + +func (a *debtSnapshotAccountV2) GetGlobalDB() *gorm.DB { + return a.db +} + +func TestLoadDebtUsersAtUsesFirstLaterTransition(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := postgrescontainer.Run(ctx, "postgres:16-alpine", + postgrescontainer.WithDatabase("account"), + postgrescontainer.WithUsername("account"), + postgrescontainer.WithPassword("account"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections").WithOccurrence(2), + ), + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate PostgreSQL: %v", err) + } + }) + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatal(err) + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&types.Debt{}, &types.DebtStatusRecord{}); err != nil { + t.Fatal(err) + } + + endHourTime := time.Now().UTC().Add(-time.Hour).Truncate(time.Hour) + historicalUserUID := uuid.New() + currentDebtUserUID := uuid.New() + createdLaterUserUID := uuid.New() + debts := []types.Debt{ + { + UserUID: historicalUserUID, CreatedAt: endHourTime.Add(-time.Hour), + UpdatedAt: endHourTime.Add(20 * time.Minute), AccountDebtStatus: types.NormalPeriod, + }, + { + UserUID: currentDebtUserUID, CreatedAt: endHourTime.Add(-time.Hour), + UpdatedAt: endHourTime.Add(-time.Hour), AccountDebtStatus: types.DebtPeriod, + }, + { + UserUID: createdLaterUserUID, CreatedAt: endHourTime.Add(time.Minute), + UpdatedAt: endHourTime.Add(time.Minute), AccountDebtStatus: types.DebtPeriod, + }, + } + if err := db.Create(&debts).Error; err != nil { + t.Fatal(err) + } + records := []types.DebtStatusRecord{ + { + ID: uuid.New(), UserUID: historicalUserUID, + LastStatus: types.NormalPeriod, CurrentStatus: types.DebtPeriod, + CreateAt: endHourTime, + }, + { + ID: uuid.New(), UserUID: historicalUserUID, + LastStatus: types.DebtPeriod, CurrentStatus: types.NormalPeriod, + CreateAt: endHourTime.Add(20 * time.Minute), + }, + } + if err := db.Create(&records).Error; err != nil { + t.Fatal(err) + } + + DebtUserMap = maps.NewConcurrentNullValueMap() + reconciler := &BillingReconciler{ + AccountV2: &debtSnapshotAccountV2{db: db}, + Logger: logr.Discard(), + } + if err := reconciler.loadDebtUsersAt(endHourTime); err != nil { + t.Fatal(err) + } + if _, inDebt := DebtUserMap.Get(historicalUserUID.String()); inDebt { + t.Fatal("historical user did not use the first later transition") + } + if _, inDebt := DebtUserMap.Get(currentDebtUserUID.String()); !inDebt { + t.Fatal("current debt user without a later transition was omitted") + } + if _, inDebt := DebtUserMap.Get(createdLaterUserUID.String()); inDebt { + t.Fatal("debt created after the billing hour was included") + } +} diff --git a/controllers/account/controllers/billing_controller_test.go b/controllers/account/controllers/billing_controller_test.go new file mode 100644 index 000000000000..25b70d7c3d34 --- /dev/null +++ b/controllers/account/controllers/billing_controller_test.go @@ -0,0 +1,729 @@ +package controllers + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/maps" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/prometheus/client_golang/prometheus/testutil" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type billingTestAccount struct { + database.Account + mu sync.Mutex + checkpoint time.Time + hasCheckpoint bool + checkpointWrites []time.Time + monitorErr error + existing map[string][]*resources.Billing + unsettled map[string][]*resources.Billing + generated map[string][]*resources.Billing + generateInput map[string][]string + saveErr error + saved []*resources.Billing + statuses map[string]resources.BillingStatus +} + +func (f *billingTestAccount) GetBillingCheckpoint() (time.Time, bool, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.checkpoint, f.hasCheckpoint, nil +} + +func (f *billingTestAccount) SaveBillingCheckpoint(value time.Time) error { + f.mu.Lock() + defer f.mu.Unlock() + f.checkpoint, f.hasCheckpoint = value, true + f.checkpointWrites = append(f.checkpointWrites, value) + return nil +} + +func (f *billingTestAccount) GetTimeUsedNamespaceList(time.Time, time.Time) ([]string, error) { + return nil, f.monitorErr +} + +func (f *billingTestAccount) GetOwnerBillingsAt( + []string, + time.Time, +) (map[string][]*resources.Billing, error) { + return f.existing, nil +} + +func (f *billingTestAccount) GetUnsettledBillingsAt( + time.Time, +) (map[string][]*resources.Billing, error) { + return f.unsettled, nil +} + +func (f *billingTestAccount) GenerateBillingData( + _ time.Time, + _ time.Time, + _ *resources.PropertyTypeLS, + ownerListMap map[string][]string, +) (map[string][]*resources.Billing, error) { + f.mu.Lock() + f.generateInput = make(map[string][]string, len(ownerListMap)) + for owner, namespaces := range ownerListMap { + f.generateInput[owner] = append([]string(nil), namespaces...) + } + f.mu.Unlock() + return f.generated, nil +} + +func (f *billingTestAccount) SaveBillings(billings ...*resources.Billing) error { + if f.saveErr != nil { + return f.saveErr + } + f.mu.Lock() + defer f.mu.Unlock() + f.saved = append(f.saved, billings...) + return nil +} + +func (f *billingTestAccount) UpdateBillingStatus( + orderIDs []string, + status resources.BillingStatus, +) error { + f.mu.Lock() + defer f.mu.Unlock() + if f.statuses == nil { + f.statuses = make(map[string]resources.BillingStatus) + } + for _, orderID := range orderIDs { + f.statuses[orderID] = status + } + return nil +} + +type billingTestAccountV2 struct { + database.AccountV2 + mu sync.Mutex + err error + deductions int + amounts []int64 +} + +func (f *billingTestAccountV2) AddDeductionBalance( + _ *types.UserQueryOpts, amount int64, +) error { + if f.err != nil { + return f.err + } + f.mu.Lock() + defer f.mu.Unlock() + f.deductions++ + f.amounts = append(f.amounts, amount) + return nil +} + +func (f *billingTestAccountV2) AddDeductionBalanceWithCreditsAt( + _ *types.UserQueryOpts, amount int64, _ []string, _ time.Time, +) error { + if f.err != nil { + return f.err + } + f.mu.Lock() + defer f.mu.Unlock() + f.deductions++ + f.amounts = append(f.amounts, amount) + return nil +} + +func initBillingTestGlobals() { + DebtUserMap = maps.NewConcurrentNullValueMap() + SubscriptionWorkspaceMap = maps.NewConcurrentNullValueMap() +} + +func TestRunBillingReadsRunsIndependentTasksConcurrently(t *testing.T) { + started := make(chan struct{}, 2) + release := make(chan struct{}) + result := make(chan error, 1) + wantErr := errors.New("read failed") + + read := func(err error) func() error { + return func() error { + started <- struct{}{} + <-release + return err + } + } + go func() { + result <- runBillingReads(read(nil), read(wantErr)) + }() + + for range 2 { + select { + case <-started: + case <-time.After(time.Second): + close(release) + t.Fatal("independent read did not start concurrently") + } + } + close(release) + if err := <-result; !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want %v", err, wantErr) + } +} + +func TestBillingMetricsTrackCheckpointLag(t *testing.T) { + checkpoint := time.Date(2026, time.July, 7, 9, 0, 0, 0, time.UTC) + target := checkpoint.Add(3 * time.Hour) + setBillingTargetMetrics(target) + setBillingCheckpointMetrics(checkpoint, target) + + wantLag := 3 * float64(time.Hour/time.Second) + if got := testutil.ToFloat64(billingCheckpointLagSeconds); got != wantLag { + t.Fatalf("checkpoint lag seconds = %v, want %v", got, wantLag) + } + if got := testutil.ToFloat64(billingPendingCheckpoints); got != 3 { + t.Fatalf("pending checkpoints = %v, want 3", got) + } + if got := testutil.ToFloat64(billingTargetTimestamp); got != float64(target.Unix()) { + t.Fatalf("target timestamp = %v, want %v", got, target.Unix()) + } + setBillingProcessingMetrics(target, true) + if got := testutil.ToFloat64(billingProcessing); got != 1 { + t.Fatalf("processing = %v, want 1", got) + } + if got := testutil.ToFloat64(billingProcessingStartedTimestamp); got <= 0 { + t.Fatalf("processing started timestamp = %v, want positive timestamp", got) + } + setBillingProcessingMetrics(time.Time{}, false) + if got := testutil.ToFloat64(billingProcessing); got != 0 { + t.Fatalf("processing = %v, want 0", got) + } + if got := testutil.ToFloat64(billingProcessingStartedTimestamp); got != 0 { + t.Fatalf("processing started timestamp = %v, want 0", got) + } +} + +func TestBillingHourScheduling(t *testing.T) { + t.Run("normal hourly execution", func(t *testing.T) { + checkpoint := time.Date(2026, time.July, 7, 9, 0, 0, 0, time.UTC) + target := checkpoint.Add(time.Hour) + hours := billingHoursAfter(checkpoint, target) + if len(hours) != 1 || !hours[0].Equal(target) { + t.Fatalf("hours = %v", hours) + } + }) + + t.Run("start after half hour", func(t *testing.T) { + now := time.Date(2026, time.July, 7, 10, 37, 0, 0, time.UTC) + want := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + if got := latestReadyBillingHour(now); !got.Equal(want) { + t.Fatalf("ready hour = %v, want %v", got, want) + } + }) + + t.Run("resume after several hours", func(t *testing.T) { + start := time.Date(2026, time.July, 7, 7, 0, 0, 0, time.UTC) + hours := billingHoursAfter(start, start.Add(4*time.Hour)) + if len(hours) != 4 { + t.Fatalf("hours = %v", hours) + } + }) + + t.Run("task crosses next hour", func(t *testing.T) { + started := time.Date(2026, time.July, 7, 10, 6, 0, 0, time.UTC) + finished := started.Add(61 * time.Minute) + if !latestReadyBillingHour(finished).After(latestReadyBillingHour(started)) { + t.Fatalf("finished target did not advance") + } + }) +} + +func TestOwnerInDebtUsesOwnerMapping(t *testing.T) { + initBillingTestGlobals() + DebtUserMap.Set("owner-uid") + reconciler := &BillingReconciler{ + debtOwnerMap: maps.NewConcurrentNullValueMap(), + } + if reconciler.ownerInDebt("owner-uid") { + t.Fatal("user UID was treated as an owner") + } + reconciler.debtOwnerMap.Set("owner") + if !reconciler.ownerInDebt("owner") { + t.Fatal("debt owner was not detected") + } +} + +func TestExecuteBillingTasksUntilPersistsEachSuccessfulHour(t *testing.T) { + start := time.Date(2026, time.July, 7, 7, 0, 0, 0, time.UTC) + db := &billingTestAccount{checkpoint: start, hasCheckpoint: true} + var executed []time.Time + reconciler := &BillingReconciler{ + DBClient: db, + executeBillingHourFunc: func(hour time.Time) error { + executed = append(executed, hour) + return nil + }, + } + if err := reconciler.ExecuteBillingTasksUntil(start.Add(3 * time.Hour)); err != nil { + t.Fatal(err) + } + if len(executed) != 3 || len(db.checkpointWrites) != 3 { + t.Fatalf("executed=%v checkpoints=%v", executed, db.checkpointWrites) + } +} + +func TestExecuteBillingTasksUntilFirstStartProcessesLatestHour(t *testing.T) { + t.Setenv(billingMaxCatchupDurationEnv, "72h") + target := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + db := &billingTestAccount{} + var executed []time.Time + reconciler := &BillingReconciler{ + DBClient: db, + executeBillingHourFunc: func(hour time.Time) error { + executed = append(executed, hour) + return nil + }, + } + if err := reconciler.ExecuteBillingTasksUntil(target); err != nil { + t.Fatal(err) + } + if len(executed) != 1 || !executed[0].Equal(target) { + t.Fatalf("executed = %v", executed) + } +} + +func TestExecuteBillingTasksUntilLimitsHistoricalCatchup(t *testing.T) { + t.Setenv(billingMaxCatchupDurationEnv, "48h") + target := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + db := &billingTestAccount{checkpoint: target.Add(-72 * time.Hour), hasCheckpoint: true} + var executed []time.Time + var pendingAtStart, checkpointAtStart float64 + reconciler := &BillingReconciler{ + DBClient: db, + executeBillingHourFunc: func(hour time.Time) error { + if len(executed) == 0 { + pendingAtStart = testutil.ToFloat64(billingPendingCheckpoints) + checkpointAtStart = testutil.ToFloat64(billingCheckpointTimestamp) + } + executed = append(executed, hour) + return nil + }, + } + if err := reconciler.ExecuteBillingTasksUntil(target); err != nil { + t.Fatal(err) + } + if len(executed) != 48 { + t.Fatalf("executed %d hours, want 48", len(executed)) + } + if pendingAtStart != 48 { + t.Fatalf("pending checkpoints at start = %v, want 48", pendingAtStart) + } + if want := float64(target.Add(-72 * time.Hour).Unix()); checkpointAtStart != want { + t.Fatalf("checkpoint metric at start = %v, want %v", checkpointAtStart, want) + } + if !executed[0].Equal(target.Add(-47 * time.Hour)) { + t.Fatalf("first executed hour = %v, want %v", executed[0], target.Add(-47*time.Hour)) + } + if !db.checkpoint.Equal(target) { + t.Fatalf("checkpoint = %v, want %v", db.checkpoint, target) + } +} + +func TestLimitBillingCheckpointValidatesDuration(t *testing.T) { + checkpoint := time.Date(2026, time.July, 4, 10, 0, 0, 0, time.UTC) + target := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + limited, err := limitBillingCheckpoint(checkpoint, target, 48*time.Hour) + if err != nil { + t.Fatal(err) + } + if want := target.Add(-48 * time.Hour); !limited.Equal(want) { + t.Fatalf("limited checkpoint = %v, want %v", limited, want) + } + if _, err := limitBillingCheckpoint(checkpoint, target, 90*time.Minute); err == nil { + t.Fatal("expected non-whole-hour duration error") + } +} + +func TestExecuteBillingTasksUntilStopsAtFailedHour(t *testing.T) { + start := time.Date(2026, time.July, 7, 7, 0, 0, 0, time.UTC) + db := &billingTestAccount{checkpoint: start, hasCheckpoint: true} + reconciler := &BillingReconciler{ + DBClient: db, + executeBillingHourFunc: func(hour time.Time) error { + if hour.Equal(start.Add(2 * time.Hour)) { + return errors.New("monitor unavailable") + } + return nil + }, + } + if err := reconciler.ExecuteBillingTasksUntil(start.Add(3 * time.Hour)); err == nil { + t.Fatal("expected failed hour error") + } + if !db.checkpoint.Equal(start.Add(time.Hour)) { + t.Fatalf("checkpoint = %v", db.checkpoint) + } +} + +func TestExecuteBillingTasksUntilAdvancesCheckpointWhenMonitorDataIsEmpty(t *testing.T) { + start := time.Date(2026, time.July, 7, 7, 0, 0, 0, time.UTC) + db := &billingTestAccount{checkpoint: start, hasCheckpoint: true} + reconciler := &BillingReconciler{DBClient: db, Logger: logr.Discard()} + reconciler.executeBillingHourFunc = func(hour time.Time) error { + owners, _, err := reconciler.getRecentUsedOwnersAt(hour) + if len(owners) != 0 { + t.Fatalf("owners=%v", owners) + } + return err + } + target := start.Add(time.Hour) + if err := reconciler.ExecuteBillingTasksUntil(target); err != nil { + t.Fatal(err) + } + if !db.checkpoint.Equal(target) || len(db.checkpointWrites) != 1 { + t.Fatalf("checkpoint=%v writes=%v", db.checkpoint, db.checkpointWrites) + } +} + +func TestGetRecentUsedOwnersReturnsMonitorError(t *testing.T) { + db := &billingTestAccount{monitorErr: errors.New("query failed")} + reconciler := &BillingReconciler{DBClient: db} + _, _, err := reconciler.getRecentUsedOwnersAt(time.Now()) + if err == nil { + t.Fatal("expected monitor query error") + } +} + +func TestActiveWorkspaceSubscriptionsAtBillingHour(t *testing.T) { + hour := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + activeStart := hour.Add(-24 * time.Hour) + activeEnd := hour.Add(time.Hour) + expiredEnd := hour.Add(-time.Minute) + subscriptions := []types.WorkspaceSubscription{ + { + Workspace: "active", CreateAt: activeStart, + CurrentPeriodStartAt: activeStart, CurrentPeriodEndAt: activeEnd, + }, + { + Workspace: "purchased-later", CreateAt: hour.Add(time.Hour), + CurrentPeriodStartAt: hour.Add(time.Hour), CurrentPeriodEndAt: hour.Add(48 * time.Hour), + }, + { + Workspace: "expired", CreateAt: activeStart, + CurrentPeriodStartAt: activeStart, CurrentPeriodEndAt: expiredEnd, + }, + { + Workspace: "renewed-later", CreateAt: activeStart, UpdateAt: hour.Add(time.Hour), + CurrentPeriodStartAt: activeStart, CurrentPeriodEndAt: hour.Add(48 * time.Hour), + }, + } + transactions := []types.WorkspaceSubscriptionTransaction{ + { + ID: uuid.New(), + Workspace: "previous-period", + Operator: types.SubscriptionTransactionTypeCreated, + Status: types.SubscriptionTransactionStatusCompleted, + PayStatus: types.SubscriptionPayStatusPaid, + StartAt: hour.Add(-12 * time.Hour), + UpdatedAt: hour.Add(-11 * time.Hour), + Period: types.DayPeriod(1), + }, + { + ID: uuid.New(), + Workspace: "delayed-activation", + Operator: types.SubscriptionTransactionTypeUpgraded, + Status: types.SubscriptionTransactionStatusCompleted, + PayStatus: types.SubscriptionPayStatusPaid, + StartAt: hour.Add(-25 * time.Hour), + UpdatedAt: hour.Add(-23 * time.Hour), + Period: types.DayPeriod(1), + }, + { + ID: uuid.New(), + Workspace: "completed-later", + Operator: types.SubscriptionTransactionTypeCreated, + Status: types.SubscriptionTransactionStatusCompleted, + PayStatus: types.SubscriptionPayStatusPaid, + StartAt: hour.Add(-12 * time.Hour), + UpdatedAt: hour.Add(time.Hour), + Period: types.DayPeriod(1), + }, + { + ID: uuid.New(), + Workspace: "canceled", + Operator: types.SubscriptionTransactionTypeCreated, + Status: types.SubscriptionTransactionStatusCompleted, + PayStatus: types.SubscriptionPayStatusPaid, + StartAt: hour.Add(-12 * time.Hour), + UpdatedAt: hour.Add(-11 * time.Hour), + Period: types.DayPeriod(1), + }, + { + ID: uuid.New(), + Workspace: "canceled", + Operator: types.SubscriptionTransactionTypeCanceled, + Status: types.SubscriptionTransactionStatusCompleted, + PayStatus: types.SubscriptionPayStatusCanceled, + StartAt: hour.Add(-time.Hour), + UpdatedAt: hour.Add(-time.Hour), + }, + } + workspaces, err := activeWorkspaceSubscriptionsAt(hour, subscriptions, transactions) + if err != nil { + t.Fatal(err) + } + got := make(map[string]struct{}, len(workspaces)) + for _, workspace := range workspaces { + got[workspace] = struct{}{} + } + if _, exists := got["active"]; !exists { + t.Fatal("active current period is missing") + } + if _, exists := got["previous-period"]; !exists { + t.Fatal("active historical transaction period is missing") + } + if _, exists := got["delayed-activation"]; !exists { + t.Fatal("subscription completion time was not used as its period start") + } + if _, exists := got["purchased-later"]; exists { + t.Fatal("future subscription was applied to historical billing") + } + if _, exists := got["expired"]; exists { + t.Fatal("expired subscription was applied to billing") + } + if _, exists := got["renewed-later"]; exists { + t.Fatal("later subscription snapshot was applied to historical billing") + } + if _, exists := got["completed-later"]; exists { + t.Fatal("later transaction completion was applied to historical billing") + } + if _, exists := got["canceled"]; exists { + t.Fatal("canceled subscription was applied to billing") + } +} + +func TestGetUsersForNamespacesUsesTargetedLookups(t *testing.T) { + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + userA := &userv1.User{} + userA.Name = "a" + userA.Annotations = map[string]string{userv1.UserLabelOwnerKey: "owner-a"} + userB := &userv1.User{} + userB.Name = "b" + userB.Annotations = map[string]string{userv1.UserLabelOwnerKey: "owner-b"} + reconciler := &BillingReconciler{Client: fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(userA, userB). + Build()} + + owners, err := reconciler.getUsersForNamespaces([]string{ + "ns-a", "ns-b", "ns-missing", "workspace-without-user-prefix", + }) + if err != nil { + t.Fatal(err) + } + if len(owners) != 2 || owners["ns-a"] != "owner-a" || owners["ns-b"] != "owner-b" { + t.Fatalf("owners = %#v", owners) + } +} + +func TestReconcileOwnerListReturnsPartialOwnerFailure(t *testing.T) { + initBillingTestGlobals() + end := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + db := &billingTestAccount{ + existing: map[string][]*resources.Billing{}, + generated: map[string][]*resources.Billing{ + "owner-a": {{OrderID: "a", Owner: "owner-a", Namespace: "ns-a", Time: end, Amount: 1}}, + "owner-b": {{OrderID: "b", Owner: "owner-b", Namespace: "ns-b", Time: end, Amount: 1}}, + }, + } + reconciler := &BillingReconciler{ + DBClient: db, + Logger: logr.Discard(), + concurrentLimit: 2, + reconcileBillingFunc: func(owner string, _ []*resources.Billing, _ time.Time) error { + db.mu.Lock() + if db.statuses == nil { + db.statuses = make(map[string]resources.BillingStatus) + } + db.statuses[owner] = resources.Settled + db.mu.Unlock() + if owner == "owner-b" { + return errors.New("save failed") + } + return nil + }, + } + err := reconciler.reconcileOwnerList(map[string][]string{ + "owner-a": {"ns-a"}, "owner-b": {"ns-b"}, + }, end) + if err == nil { + t.Fatal("expected partial owner failure") + } + if len(db.statuses) != 2 { + t.Fatalf("reconciled owner count = %d", len(db.statuses)) + } +} + +func TestReconcileOwnerListRejectsNonPositiveConcurrency(t *testing.T) { + reconciler := &BillingReconciler{concurrentLimit: 0} + if err := reconciler.reconcileOwnerList(nil, time.Now()); err == nil { + t.Fatal("expected invalid concurrency error") + } +} + +func TestReconcileBillingSaveFailure(t *testing.T) { + initBillingTestGlobals() + db := &billingTestAccount{saveErr: errors.New("insert failed")} + reconciler := &BillingReconciler{DBClient: db, AccountV2: &billingTestAccountV2{}} + err := reconciler.reconcileBilling( + "owner", []*resources.Billing{{OrderID: "id", Amount: 10}}, time.Now(), + ) + if err == nil { + t.Fatal("expected save failure") + } +} + +func TestReconcileBillingDeductionFailureMayLeaveSettled(t *testing.T) { + initBillingTestGlobals() + db := &billingTestAccount{} + account := &billingTestAccountV2{err: errors.New("deduction failed")} + billing := &resources.Billing{OrderID: "id", Amount: 10, Status: resources.Settled} + reconciler := &BillingReconciler{DBClient: db, AccountV2: account} + if err := reconciler.reconcileBilling( + "owner", []*resources.Billing{billing}, time.Now(), + ); err == nil { + t.Fatal("expected deduction failure") + } + if len(db.saved) != 1 { + t.Fatalf("saved billing count = %d", len(db.saved)) + } + if db.saved[0].Status != resources.Settled { + t.Fatalf("saved billing status = %v", db.saved[0].Status) + } + if status := db.statuses[billing.OrderID]; status != resources.Unsettled { + t.Fatalf("failed deduction status = %v", status) + } +} + +func TestReconcileBillingReclassifiesSubscriptionBeforeDeduction(t *testing.T) { + initBillingTestGlobals() + SubscriptionWorkspaceMap.Set("ns-subscription") + db := &billingTestAccount{} + account := &billingTestAccountV2{} + billing := &resources.Billing{ + OrderID: "subscription-order", + Namespace: "ns-subscription", + Amount: 10, + Status: resources.Unsettled, + } + reconciler := &BillingReconciler{DBClient: db, AccountV2: account} + if err := reconciler.reconcileBilling( + "owner", []*resources.Billing{billing}, time.Now(), + ); err != nil { + t.Fatal(err) + } + if account.deductions != 0 { + t.Fatalf("subscription deductions = %d", account.deductions) + } + if billing.Status != resources.Subscription { + t.Fatalf("billing status = %v", billing.Status) + } +} + +func TestClassifyGeneratedBillingsDefaultsToSettled(t *testing.T) { + initBillingTestGlobals() + SubscriptionWorkspaceMap.Set("ns-subscription") + generated := map[string][]*resources.Billing{"owner": { + {OrderID: "new-subscription", Namespace: "ns-subscription", AppName: "new"}, + {OrderID: "new-usage", Namespace: "ns-usage", AppName: "usage"}, + }} + existing := &resources.Billing{ + OrderID: "bh_existing", Namespace: "ns-subscription", AppName: "existing", + Status: resources.Unsettled, + } + + classifyGeneratedBillings(generated) + pending := pendingOwnerBillings(generated, map[string][]*resources.Billing{ + "owner": {existing}, + })["owner"] + + statuses := make(map[string]resources.BillingStatus, len(pending)) + for _, billing := range pending { + statuses[billing.OrderID] = billing.Status + } + if statuses["new-subscription"] != resources.Subscription { + t.Fatalf("new subscription status = %v", statuses["new-subscription"]) + } + if statuses["new-usage"] != resources.Settled { + t.Fatalf("new usage status = %v", statuses["new-usage"]) + } + if _, exists := statuses[existing.OrderID]; exists { + t.Fatalf("existing billing was unexpectedly requeued: %v", existing.OrderID) + } +} + +func TestReconcileBillingRetryCanDeductAgain(t *testing.T) { + initBillingTestGlobals() + db := &billingTestAccount{} + account := &billingTestAccountV2{} + billings := []*resources.Billing{ + {OrderID: "stable-a", Amount: 10, Status: resources.Settled}, + {OrderID: "stable-b", Amount: 15, Status: resources.Settled}, + } + reconciler := &BillingReconciler{DBClient: db, AccountV2: account} + for range 2 { + if err := reconciler.reconcileBilling("owner", billings, time.Now()); err != nil { + t.Fatal(err) + } + } + if account.deductions != 2 { + t.Fatalf("deductions = %d", account.deductions) + } + if len(account.amounts) != 2 || account.amounts[0] != 25 || account.amounts[1] != 25 { + t.Fatalf("deduction amounts = %#v", account.amounts) + } +} + +func TestPendingOwnerBillingsSkipsExistingBusinessKeys(t *testing.T) { + generated := map[string][]*resources.Billing{"owner": { + {OrderID: "new-a", Namespace: "ns", AppType: 1, AppName: "a"}, + {OrderID: "new-b", Namespace: "ns", AppType: 1, AppName: "b"}, + {OrderID: "new-c", Namespace: "ns", AppType: 1, AppName: "c"}, + }} + existing := map[string][]*resources.Billing{"owner": { + {OrderID: "legacy-a", Namespace: "ns", AppType: 1, AppName: "a", Status: resources.Settled}, + { + OrderID: "bh_existing-b", + Namespace: "ns", + AppType: 1, + AppName: "b", + Status: resources.Unsettled, + }, + { + OrderID: "bh_orphan-d", + Namespace: "ns", + AppType: 1, + AppName: "d", + Status: resources.Unsettled, + }, + }} + pending := pendingOwnerBillings(generated, existing)["owner"] + got := make(map[string]struct{}, len(pending)) + for _, billing := range pending { + got[billing.OrderID] = struct{}{} + } + if len(got) != 1 { + t.Fatalf("pending = %#v", pending) + } + for _, orderID := range []string{"new-c"} { + if _, exists := got[orderID]; !exists { + t.Fatalf("pending billing %s is missing: %#v", orderID, pending) + } + } +} diff --git a/controllers/account/controllers/billing_metrics.go b/controllers/account/controllers/billing_metrics.go new file mode 100644 index 000000000000..b98edbc03293 --- /dev/null +++ b/controllers/account/controllers/billing_metrics.go @@ -0,0 +1,116 @@ +/* +Copyright 2023 sealos. + +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 controllers + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +var ( + billingCheckpointTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_checkpoint_timestamp_seconds", + Help: "Unix timestamp of the last successfully persisted account billing checkpoint.", + }) + billingTargetTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_target_timestamp_seconds", + Help: "Unix timestamp of the latest account billing hour ready for processing.", + }) + billingCheckpointLagSeconds = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_checkpoint_lag_seconds", + Help: "Duration of ready account billing hours after the persisted checkpoint.", + }) + billingPendingCheckpoints = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_pending_checkpoints", + Help: "Number of ready account billing hours after the persisted checkpoint.", + }) + billingProcessing = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_processing", + Help: "Whether the account billing runner is processing a billing hour.", + }) + billingProcessingTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_processing_timestamp_seconds", + Help: "Unix timestamp of the account billing hour currently being processed.", + }) + billingProcessingStartedTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_processing_started_timestamp_seconds", + Help: "Unix timestamp when processing of the current account billing hour started.", + }) + billingLastSuccessTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_last_success_timestamp_seconds", + Help: "Unix timestamp when the account billing checkpoint was last advanced.", + }) + billingReconcileFailures = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "sealos_account_billing_reconcile_failures_total", + Help: "Total account billing hour reconciliation failures.", + }) + billingFailedOwners = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "sealos_account_billing_failed_owners", + Help: "Number of owners that failed in the most recent account billing batch.", + }) +) + +func init() { + ctrlmetrics.Registry.MustRegister( + billingCheckpointTimestamp, + billingTargetTimestamp, + billingCheckpointLagSeconds, + billingPendingCheckpoints, + billingProcessing, + billingProcessingTimestamp, + billingProcessingStartedTimestamp, + billingLastSuccessTimestamp, + billingReconcileFailures, + billingFailedOwners, + ) +} + +func setBillingTargetMetrics(target time.Time) { + target = target.UTC().Truncate(time.Hour) + billingTargetTimestamp.Set(float64(target.Unix())) +} + +func setBillingCheckpointMetrics(checkpoint, target time.Time) { + checkpoint = checkpoint.UTC().Truncate(time.Hour) + billingCheckpointTimestamp.Set(float64(checkpoint.Unix())) + setBillingPendingCheckpointMetrics(checkpoint, target) +} + +func setBillingPendingCheckpointMetrics(checkpoint, target time.Time) { + checkpoint = checkpoint.UTC().Truncate(time.Hour) + target = target.UTC().Truncate(time.Hour) + lag := target.Sub(checkpoint).Seconds() + if lag < 0 { + lag = 0 + } + billingCheckpointLagSeconds.Set(lag) + billingPendingCheckpoints.Set(lag / float64(time.Hour/time.Second)) +} + +func setBillingProcessingMetrics(hour time.Time, processing bool) { + if !processing { + billingProcessing.Set(0) + billingProcessingTimestamp.Set(0) + billingProcessingStartedTimestamp.Set(0) + return + } + billingProcessing.Set(1) + billingProcessingTimestamp.Set(float64(hour.UTC().Truncate(time.Hour).Unix())) + billingProcessingStartedTimestamp.Set(float64(time.Now().UTC().Unix())) +} diff --git a/controllers/account/controllers/cache/cache.go b/controllers/account/controllers/cache/cache.go new file mode 100644 index 000000000000..c71979739e20 --- /dev/null +++ b/controllers/account/controllers/cache/cache.go @@ -0,0 +1,212 @@ +// Copyright © 2023 sealos. +// +// 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 cache + +import ( + "context" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + accounttypes "github.com/labring/sealos/controllers/pkg/types" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const podSchedulerNameField = "spec.schedulerName" + +// Options limits cached objects to the fields consumed by account controllers. +func Options() ctrlcache.Options { + return ctrlcache.Options{ + DefaultTransform: ctrlcache.TransformStripManagedFields(), + ByObject: map[client.Object]ctrlcache.ByObject{ + &corev1.Namespace{}: {Transform: transformNamespace}, + &corev1.Pod{}: { + Field: fields.OneTermEqualSelector( + podSchedulerNameField, + accountv1.DebtSchedulerName, + ), + Transform: transformPod, + }, + &userv1.User{}: {Transform: transformUser}, + }, + } +} + +// UncachedObjects returns objects whose reads require complete, current API data. +func UncachedObjects() []client.Object { + return []client.Object{ + &corev1.LimitRange{}, + // Other controllers need complete Pods; PodReconciler reads its projection + // directly from the cache. + &corev1.Pod{}, + &corev1.ResourceQuota{}, + &accountv1.Debt{}, + ¬ificationv1.Notification{}, + } +} + +func transformNamespace(obj any) (any, error) { + ns, ok := obj.(*corev1.Namespace) + if !ok { + return obj, nil + } + + metadata := projectObjectMeta(ns.ObjectMeta) + metadata.Labels = copyMapValues(ns.Labels, userv1.UserLabelOwnerKey) + metadata.Annotations = copyMapValues( + ns.Annotations, + accounttypes.DebtNamespaceAnnoStatusKey, + accounttypes.FinalDeletionReplayAnnotationKey, + accounttypes.NetworkStatusAnnoKey, + accounttypes.WorkspaceSubscriptionStatusAnnoKey, + accounttypes.WorkspaceSubscriptionStatusUpdateTimeAnnoKey, + ) + return &corev1.Namespace{ + TypeMeta: ns.TypeMeta, + ObjectMeta: metadata, + Spec: corev1.NamespaceSpec{ + Finalizers: append([]corev1.FinalizerName(nil), ns.Spec.Finalizers...), + }, + Status: corev1.NamespaceStatus{ + Phase: ns.Status.Phase, + }, + }, nil +} + +func transformPod(obj any) (any, error) { + if metadata, ok := obj.(*metav1.PartialObjectMetadata); ok { + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projectObjectMeta(metadata.ObjectMeta), + }, nil + } + pod, ok := obj.(*corev1.Pod) + if !ok { + return obj, nil + } + + return &corev1.Pod{ + TypeMeta: pod.TypeMeta, + ObjectMeta: projectObjectMeta(pod.ObjectMeta), + Spec: corev1.PodSpec{ + SchedulerName: pod.Spec.SchedulerName, + }, + Status: corev1.PodStatus{ + Phase: pod.Status.Phase, + }, + }, nil +} + +func transformUser(obj any) (any, error) { + if metadata, ok := obj.(*metav1.PartialObjectMetadata); ok { + projected := &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projectObjectMeta(metadata.ObjectMeta), + } + projected.Annotations = copyMapValues( + metadata.Annotations, + userv1.UserAnnotationOwnerKey, + "user.sealos.io/init-account-time", + "user.sealos.io/workspace-status", + ) + return projected, nil + } + user, ok := obj.(*userv1.User) + if !ok { + return obj, nil + } + + metadata := projectObjectMeta(user.ObjectMeta) + metadata.Annotations = copyMapValues( + user.Annotations, + userv1.UserAnnotationOwnerKey, + "user.sealos.io/init-account-time", + "user.sealos.io/workspace-status", + ) + return &userv1.User{ + TypeMeta: user.TypeMeta, + ObjectMeta: metadata, + }, nil +} + +func projectObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + out := metav1.ObjectMeta{ + Name: in.Name, + Namespace: in.Namespace, + UID: in.UID, + ResourceVersion: in.ResourceVersion, + Generation: in.Generation, + CreationTimestamp: in.CreationTimestamp, + } + if in.DeletionTimestamp != nil { + out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy() + } + if in.DeletionGracePeriodSeconds != nil { + gracePeriod := *in.DeletionGracePeriodSeconds + out.DeletionGracePeriodSeconds = &gracePeriod + } + return out +} + +func copyMapValues(source map[string]string, keys ...string) map[string]string { + var result map[string]string + for _, key := range keys { + if value, ok := source[key]; ok { + if result == nil { + result = make(map[string]string) + } + result[key] = value + } + } + return result +} + +func SetupCache(mgr ctrl.Manager) error { + ns := &corev1.Namespace{} + nsNameFunc := func(obj client.Object) []string { + _ns, ok := obj.(*corev1.Namespace) + if !ok { + return nil + } + return []string{_ns.Name} + } + nsOwnerFunc := func(obj client.Object) []string { + _ns, ok := obj.(*corev1.Namespace) + if !ok { + return nil + } + return []string{_ns.Labels[userv1.UserLabelOwnerKey]} + } + + for _, idx := range []struct { + obj client.Object + field string + extractValue client.IndexerFunc + }{ + {ns, accountv1.Name, nsNameFunc}, + {ns, accountv1.Owner, nsOwnerFunc}, + } { + if err := mgr.GetFieldIndexer(). + IndexField(context.TODO(), idx.obj, idx.field, idx.extractValue); err != nil { + return err + } + } + return nil +} diff --git a/controllers/account/controllers/cache/cache_test.go b/controllers/account/controllers/cache/cache_test.go new file mode 100644 index 000000000000..24bccf44f096 --- /dev/null +++ b/controllers/account/controllers/cache/cache_test.go @@ -0,0 +1,265 @@ +/* +Copyright 2026. + +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 cache + +import ( + "reflect" + "testing" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" +) + +func TestPodCacheOptions(t *testing.T) { + found := false + for obj, options := range Options().ByObject { + if _, ok := obj.(*corev1.Pod); !ok { + continue + } + found = true + if options.Field == nil { + t.Fatal("pod cache field selector is nil") + } + if !options.Field.Matches(fields.Set{ + podSchedulerNameField: accountv1.DebtSchedulerName, + }) { + t.Fatal("pod cache selector excludes debt scheduler pods") + } + if options.Field.Matches(fields.Set{podSchedulerNameField: "default-scheduler"}) { + t.Fatal("pod cache selector includes regular pods") + } + if options.Transform == nil { + t.Fatal("pod cache transform is nil") + } + } + if !found { + t.Fatal("pod cache options not found") + } +} + +func TestUncachedObjectsIncludesPod(t *testing.T) { + for _, obj := range UncachedObjects() { + if _, ok := obj.(*corev1.Pod); ok { + return + } + } + t.Fatal("pod reads are not configured to bypass the cache") +} + +func TestTransformPodKeepsOnlyReconcileFields(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-a", + Namespace: "ns-a", + ResourceVersion: "42", + Annotations: map[string]string{"unused.example/key": "large-value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.PodSpec{ + SchedulerName: accountv1.DebtSchedulerName, + Containers: []corev1.Container{{Name: "app", Image: "example/app:latest"}}, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + + transformed, err := transformPod(pod) + if err != nil { + t.Fatalf("transform pod: %v", err) + } + got, ok := transformed.(*corev1.Pod) + if !ok { + t.Fatalf("transformed type = %T, want *corev1.Pod", transformed) + } + if got.Name != pod.Name || got.Namespace != pod.Namespace || + got.ResourceVersion != pod.ResourceVersion { + t.Fatalf("metadata was not retained: %#v", got.ObjectMeta) + } + if got.Spec.SchedulerName != pod.Spec.SchedulerName || got.Status.Phase != pod.Status.Phase { + t.Fatalf("required pod fields were not retained: %#v", got) + } + got.Spec.SchedulerName = "" + got.Status.Phase = "" + if !reflect.DeepEqual(got.Spec, corev1.PodSpec{}) || + !reflect.DeepEqual(got.Status, corev1.PodStatus{}) { + t.Fatalf("unused pod data was retained: %#v", got) + } + if len(got.Annotations) != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("unused metadata was retained: %#v", got.ObjectMeta) + } + if len(pod.ManagedFields) == 0 || len(pod.Spec.Containers) == 0 { + t.Fatal("transform mutated the source object") + } +} + +func TestTransformPartialPodKeepsOnlyMetadata(t *testing.T) { + pod := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-a", + Namespace: "ns-a", + ResourceVersion: "42", + Labels: map[string]string{"unused.example/key": "large-value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + } + pod.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Pod")) + + transformed, err := transformPod(pod) + if err != nil { + t.Fatalf("transform partial pod: %v", err) + } + got, ok := transformed.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed) + } + if got.Name != pod.Name || got.Namespace != pod.Namespace || + got.ResourceVersion != pod.ResourceVersion { + t.Fatalf("metadata was not retained: %#v", got.ObjectMeta) + } + if got.GroupVersionKind() != pod.GroupVersionKind() { + t.Fatalf("GVK = %s, want %s", got.GroupVersionKind(), pod.GroupVersionKind()) + } + if len(got.Labels) != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("unused metadata was retained: %#v", got.ObjectMeta) + } + if len(pod.Labels) == 0 || len(pod.ManagedFields) == 0 { + t.Fatal("transform mutated the source object") + } +} + +func TestTransformUserKeepsOnlyMetadata(t *testing.T) { + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-a", + ResourceVersion: "42", + Annotations: map[string]string{ + "user.sealos.io/owner": "owner-a", + "unused.example/key": "large-value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: userv1.UserSpec{CSRExpirationSeconds: 600}, + Status: userv1.UserStatus{ + Phase: userv1.UserActive, + KubeConfig: "large-kubeconfig", + }, + } + + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform user: %v", err) + } + got, ok := transformed.(*userv1.User) + if !ok { + t.Fatalf("transformed type = %T, want *userv1.User", transformed) + } + if got.Name != user.Name || got.ResourceVersion != user.ResourceVersion { + t.Fatalf("metadata was not retained: %#v", got.ObjectMeta) + } + wantAnnotations := map[string]string{"user.sealos.io/owner": "owner-a"} + if !reflect.DeepEqual(got.Annotations, wantAnnotations) { + t.Fatalf("annotations = %#v, want %#v", got.Annotations, wantAnnotations) + } + if len(got.ManagedFields) != 0 { + t.Fatalf("managed fields were retained: %#v", got.ManagedFields) + } + if !reflect.DeepEqual(got.Spec, userv1.UserSpec{}) { + t.Fatalf("spec was retained: %#v", got.Spec) + } + if !reflect.DeepEqual(got.Status, userv1.UserStatus{}) { + t.Fatalf("status was retained: %#v", got.Status) + } + if len(user.ManagedFields) == 0 { + t.Fatal("transform mutated the source object") + } +} + +func TestTransformPartialUserKeepsOnlyMetadata(t *testing.T) { + user := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-a", + Annotations: map[string]string{ + "user.sealos.io/owner": "owner-a", + "unused.example/key": "large-value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + } + user.SetGroupVersionKind(userv1.GroupVersion.WithKind("User")) + + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform partial user: %v", err) + } + got, ok := transformed.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed) + } + if got.Name != user.Name || got.Annotations[userv1.UserAnnotationOwnerKey] != "owner-a" { + t.Fatalf("metadata was not retained: %#v", got.ObjectMeta) + } + if _, ok := got.Annotations["unused.example/key"]; ok || len(got.ManagedFields) != 0 { + t.Fatalf("unused metadata was retained: %#v", got.ObjectMeta) + } +} + +func TestTransformNamespaceKeepsIndexedFields(t *testing.T) { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-a", + Labels: map[string]string{"user.sealos.io/owner": "owner-a"}, + Annotations: map[string]string{ + "debt.sealos/status": "Normal", + "unused.example/key": "large-value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.NamespaceSpec{Finalizers: []corev1.FinalizerName{"kubernetes"}}, + Status: corev1.NamespaceStatus{ + Phase: corev1.NamespaceActive, + Conditions: []corev1.NamespaceCondition{{Type: corev1.NamespaceDeletionContentFailure}}, + }, + } + + transformed, err := transformNamespace(ns) + if err != nil { + t.Fatalf("transform namespace: %v", err) + } + got, ok := transformed.(*corev1.Namespace) + if !ok { + t.Fatalf("transformed type = %T, want *corev1.Namespace", transformed) + } + if got.Labels["user.sealos.io/owner"] != "owner-a" || + got.Annotations["debt.sealos/status"] != "Normal" { + t.Fatalf("indexed metadata was not retained: %#v", got.ObjectMeta) + } + if _, ok := got.Annotations["unused.example/key"]; ok { + t.Fatalf("unused annotation was retained: %#v", got.Annotations) + } + if got.Status.Phase != corev1.NamespaceActive { + t.Fatalf("phase = %q, want %q", got.Status.Phase, corev1.NamespaceActive) + } + if len(got.Spec.Finalizers) != len(ns.Spec.Finalizers) || + len(got.Status.Conditions) != 0 || + len(got.ManagedFields) != 0 { + t.Fatalf("unused namespace fields were retained: %#v", got) + } +} diff --git a/controllers/account/controllers/debt.go b/controllers/account/controllers/debt.go new file mode 100644 index 000000000000..04d5a8dcebf8 --- /dev/null +++ b/controllers/account/controllers/debt.go @@ -0,0 +1,1383 @@ +package controllers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math" + "net/http" + "os" + "reflect" + runtime2 "runtime" + "strconv" + "strings" + "sync" + "text/template" + "time" + + client2 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client" + "github.com/go-logr/logr" + "github.com/google/uuid" + v1 "github.com/labring/sealos/controllers/account/api/v1" + utils2 "github.com/labring/sealos/controllers/account/controllers/utils" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils" + "github.com/labring/sealos/controllers/pkg/utils/env" + dlock "github.com/labring/sealos/controllers/pkg/utils/lock" + "github.com/labring/sealos/controllers/pkg/utils/maps" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/volcengine/volc-sdk-golang/service/vms" + "gorm.io/gorm" + "k8s.io/apimachinery/pkg/runtime" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +const ( + DebtDetectionCycleEnv = "DebtDetectionCycleSeconds" + + finalDeletionDebtNamespaceSyncInterval = time.Hour + finalDeletionDebtNamespaceQueryTimeout = 30 * time.Second + flushDebtResourceStatusRequestTimeout = 30 * time.Second + + SMSAccessKeyIDEnv = "SMS_AK" + SMSAccessKeySecretEnv = "SMS_SK" + VmsAccessKeyIDEnv = "VMS_AK" + VmsAccessKeySecretEnv = "VMS_SK" + SMSEndpointEnv = "SMS_ENDPOINT" + SMSSignNameEnv = "SMS_SIGN_NAME" + SMSCodeMapEnv = "SMS_CODE_MAP" + VmsCodeMapEnv = "VMS_CODE_MAP" + VmsNumberPollEnv = "VMS_NUMBER_POLL" + SMTPHostEnv = "SMTP_HOST" + SMTPPortEnv = "SMTP_PORT" + SMTPFromEnv = "SMTP_FROM" + SMTPUserEnv = "SMTP_USER" + SMTPPasswordEnv = "SMTP_PASSWORD" + SMTPTitleEnv = "SMTP_TITLE" +) + +// DebtReconciler reconciles a Debt object +type DebtReconciler struct { + client.Client + *AccountReconciler + AccountV2 database.AccountV2 + InitUserAccountFunc func(user *types.UserQueryOpts) (*types.Account, error) + Scheme *runtime.Scheme + DebtDetectionCycle time.Duration + LocalRegionID string + logr.Logger + accountSystemNamespace string + SmsConfig *SmsConfig + VmsConfig *VmsConfig + smtpConfig *utils2.SMTPConfig + DebtUserMap *maps.ConcurrentMap + // TODO need init + userLocks *sync.Map + failedUserLocks *sync.Map + processID string + SkipExpiredUserTimeDuration time.Duration + SendDebtStatusEmailBody map[v1.DebtStatusType]string + debtEmailLanguage string +} + +type VmsConfig struct { + TemplateCode map[string]string + NumberPoll string +} + +type SmsConfig struct { + Client *client2.Client + SmsSignName string + SmsCode map[string]string +} + +var DebtConfig = v1.DefaultDebtConfig + +func (r *DebtReconciler) DetermineCurrentStatus( + oweamount int64, + _ uuid.UUID, + updateIntervalSeconds int64, + lastStatus v1.DebtStatusType, +) (v1.DebtStatusType, error) { + return determineCurrentStatus(oweamount, updateIntervalSeconds, lastStatus), nil +} + +func determineCurrentStatus( + oweamount, updateIntervalSeconds int64, + lastStatus v1.DebtStatusType, +) v1.DebtStatusType { + if oweamount > 0 { + if oweamount > 10*BaseUnit { + return v1.NormalPeriod + } else if oweamount > 5*BaseUnit { + return v1.LowBalancePeriod + } + return v1.CriticalBalancePeriod + } + if lastStatus == v1.NormalPeriod || lastStatus == v1.LowBalancePeriod || + lastStatus == v1.CriticalBalancePeriod { + return v1.DebtPeriod + } + if lastStatus == v1.DebtPeriod && updateIntervalSeconds >= DebtConfig[v1.DebtDeletionPeriod] { + return v1.DebtDeletionPeriod + } + if lastStatus == v1.DebtDeletionPeriod && + updateIntervalSeconds >= DebtConfig[v1.FinalDeletionPeriod] { + return v1.FinalDeletionPeriod + } + return lastStatus // Maintain current debt state if no transition +} + +const ( + // fromEn = "Debt-System" + // fromZh = "欠费系统" + // languageEn = "en" + // languageZh = "zh" + // debtChoicePrefix = "debt-choice-" + // readStatusLabel = "isRead" + // falseStatus = "false" + trueStatus = "true" +) + +var ( + TitleTemplateZHMap = map[v1.DebtStatusType]string{ + v1.LowBalancePeriod: "余额不足", + v1.CriticalBalancePeriod: "余额即将耗尽", + v1.DebtPeriod: "余额耗尽", + v1.DebtDeletionPeriod: "即将资源释放", + v1.FinalDeletionPeriod: "彻底资源释放", + } + TitleTemplateENMap = map[v1.DebtStatusType]string{ + v1.LowBalancePeriod: "Low Balance", + v1.CriticalBalancePeriod: "Critical Balance", + v1.DebtPeriod: "Debt", + v1.DebtDeletionPeriod: "Imminent Resource Release", + v1.FinalDeletionPeriod: "Radical resource release", + } + NoticeTemplateENMap map[v1.DebtStatusType]string + NoticeTemplateZHMap map[v1.DebtStatusType]string + EmailTemplateENMap map[v1.DebtStatusType]string + EmailTemplateZHMap map[v1.DebtStatusType]string +) + +var ( + forbidTimes = []string{"00:00-10:00", "20:00-24:00"} + UTCPlus8 = time.FixedZone("UTC+8", 8*3600) +) + +// GetSendVmsTimeInUTCPlus8 send vms time in UTC+8 10:00-20:00 +func GetSendVmsTimeInUTCPlus8(t time.Time) time.Time { + nowInUTCPlus8 := t.In(UTCPlus8) + hour := nowInUTCPlus8.Hour() + if hour >= 10 && hour < 20 { + return t + } + var next10AM time.Time + if hour < 10 { + next10AM = time.Date( + nowInUTCPlus8.Year(), + nowInUTCPlus8.Month(), + nowInUTCPlus8.Day(), + 10, + 0, + 0, + 0, + UTCPlus8, + ) + } else { + next10AM = time.Date( + nowInUTCPlus8.Year(), + nowInUTCPlus8.Month(), + nowInUTCPlus8.Day()+1, + 10, + 0, + 0, + 0, + UTCPlus8, + ) + } + return next10AM.In(time.Local) +} + +// convert "1:code1,2:code2" to map[int]string +func splitSmsCodeMap(codeStr string) (map[string]string, error) { + codeMap := make(map[string]string) + for code := range strings.SplitSeq(codeStr, ",") { + split := strings.SplitN(code, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("invalid sms code map: %s", codeStr) + } + codeMap[split[0]] = split[1] + } + return codeMap, nil +} + +func (r *DebtReconciler) setupSmsConfig() error { + if err := env.CheckEnvSetting( + []string{ + SMSAccessKeyIDEnv, + SMSAccessKeySecretEnv, + SMSEndpointEnv, + SMSSignNameEnv, + SMSCodeMapEnv, + }, + ); err != nil { + return fmt.Errorf("check env setting error: %w", err) + } + + smsCodeMap, err := splitSmsCodeMap(os.Getenv(SMSCodeMapEnv)) + if err != nil { + return fmt.Errorf("split sms code map error: %w", err) + } + for key := range smsCodeMap { + if _, ok := types.StatusMap[types.DebtStatusType(key)]; !ok { + return fmt.Errorf("invalid sms code map key: %s", key) + } + } + r.Info("set sms code map", "smsCodeMap", smsCodeMap, "smsSignName", os.Getenv(SMSSignNameEnv)) + smsClient, err := utils2.CreateSMSClient( + os.Getenv(SMSAccessKeyIDEnv), + os.Getenv(SMSAccessKeySecretEnv), + os.Getenv(SMSEndpointEnv), + ) + if err != nil { + return fmt.Errorf("create sms client error: %w", err) + } + r.SmsConfig = &SmsConfig{ + Client: smsClient, + SmsSignName: os.Getenv(SMSSignNameEnv), + SmsCode: smsCodeMap, + } + return nil +} + +func (r *DebtReconciler) setupVmsConfig() error { + if err := env.CheckEnvSetting( + []string{VmsAccessKeyIDEnv, VmsAccessKeySecretEnv, VmsNumberPollEnv}, + ); err != nil { + return fmt.Errorf("check env setting error: %w", err) + } + vms.DefaultInstance.SetAccessKey(os.Getenv(VmsAccessKeyIDEnv)) + vms.DefaultInstance.SetSecretKey(os.Getenv(VmsAccessKeySecretEnv)) + + vmsCodeMap, err := splitSmsCodeMap(os.Getenv(VmsCodeMapEnv)) + if err != nil { + return fmt.Errorf("split vms code map error: %w", err) + } + for key := range vmsCodeMap { + if _, ok := types.StatusMap[types.DebtStatusType(key)]; !ok { + return fmt.Errorf("invalid sms code map key: %s", key) + } + } + r.Info("set vms code map", "vmsCodeMap", vmsCodeMap) + r.VmsConfig = &VmsConfig{ + TemplateCode: vmsCodeMap, + NumberPoll: os.Getenv(VmsNumberPollEnv), + } + return nil +} + +func (r *DebtReconciler) setupSMTPConfig() error { + if err := env.CheckEnvSetting( + []string{SMTPHostEnv, SMTPFromEnv, SMTPPasswordEnv, SMTPTitleEnv}, + ); err != nil { + return fmt.Errorf("check env setting error: %w", err) + } + serverPort, err := strconv.Atoi(env.GetEnvWithDefault(SMTPPortEnv, "465")) + if err != nil { + return fmt.Errorf("invalid smtp port: %w", err) + } + r.smtpConfig = &utils2.SMTPConfig{ + ServerHost: os.Getenv(SMTPHostEnv), + ServerPort: serverPort, + Username: env.GetEnvWithDefault(SMTPUserEnv, os.Getenv(SMTPFromEnv)), + FromEmail: os.Getenv(SMTPFromEnv), + Passwd: os.Getenv(SMTPPasswordEnv), + EmailTitle: os.Getenv(SMTPTitleEnv), + } + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { + r.Init() + /* + {"DebtConfig":{ + "ApproachingDeletionPeriod":345600, + "FinalDeletionPeriod":604800, + "ImminentDeletionPeriod":259200,"WarningPeriod":0}, + "DebtDetectionCycle": "1m0s", + "accountSystemNamespace": "account-system", + "accountNamespace": "sealos-system"} + */ + r.Info("set config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle, + "accountSystemNamespace", r.accountSystemNamespace) + return ctrl.NewControllerManagedBy(mgr). + For(&userv1.User{}, builder.WithPredicates(predicate.And(UserOwnerPredicate{})), builder.OnlyMetadata). + Watches(&v1.Payment{}, &handler.EnqueueRequestForObject{}). + WithOptions(rateOpts). + Complete(r) +} + +func (r *DebtReconciler) Init() { + r.Logger = ctrl.Log.WithName("DebtController") + r.accountSystemNamespace = env.GetEnvWithDefault(v1.AccountSystemNamespaceEnv, "account-system") + r.LocalRegionID = os.Getenv(cockroach.EnvLocalRegion) + debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 1800) + r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second + r.userLocks = &sync.Map{} + r.failedUserLocks = &sync.Map{} + r.processID = uuid.NewString() + + currency := strings.ToLower(strings.TrimSpace(os.Getenv("STRIPE_CURRENCY"))) + if currency == "usd" { + r.debtEmailLanguage = "en" + } else { + r.debtEmailLanguage = "zh" + } + + setupList := []func() error{ + r.setupSmsConfig, + r.setupVmsConfig, + r.setupSMTPConfig, + } + for i := range setupList { + if err := setupList[i](); err != nil { + r.Error( + err, + "failed to set up "+runtime2.FuncForPC(reflect.ValueOf(setupList[i]).Pointer()). + Name(), + ) + } + } + setDefaultDebtPeriodWaitSecond() + r.SendDebtStatusEmailBody = make(map[v1.DebtStatusType]string) + for _, status := range []v1.DebtStatusType{v1.LowBalancePeriod, v1.CriticalBalancePeriod, v1.DebtPeriod, v1.DebtDeletionPeriod, v1.FinalDeletionPeriod} { + email := os.Getenv(string(status) + "EmailBody") + if email == "" { + email = EmailTemplateZHMap[status] + "\n" + EmailTemplateENMap[status] + } else { + r.Info("set email body", "status", status, "body", email) + } + r.SendDebtStatusEmailBody[status] = email + } + r.Info( + "debt config", + "DebtConfig", + DebtConfig, + "DebtDetectionCycle", + r.DebtDetectionCycle, + "debtEmailLanguage", + r.debtEmailLanguage, + ) +} + +func setDefaultDebtPeriodWaitSecond() { + DebtConfig[v1.DebtDeletionPeriod] = env.GetInt64EnvWithDefault( + string(v1.DebtDeletionPeriod), + 7*v1.DaySecond, + ) + DebtConfig[v1.FinalDeletionPeriod] = env.GetInt64EnvWithDefault( + string(v1.FinalDeletionPeriod), + 7*v1.DaySecond, + ) + domain := os.Getenv("DOMAIN") + NoticeTemplateZHMap = map[v1.DebtStatusType]string{ + v1.LowBalancePeriod: "当前工作空间所属账户余额过低,请及时充值,以免影响您的正常使用。", + v1.CriticalBalancePeriod: "当前工作空间所属账户余额即将耗尽,请及时充值,以免影响您的正常使用。", + v1.DebtPeriod: "当前工作空间所属账户余额已耗尽,系统将为您暂停服务,请及时充值,以免影响您的正常使用。", + v1.DebtDeletionPeriod: "系统即将释放当前空间的资源,请及时充值,以免影响您的正常使用。", + v1.FinalDeletionPeriod: "系统将随时彻底释放当前工作空间所属账户下的所有资源,请及时充值,以免影响您的正常使用。", + } + NoticeTemplateENMap = map[v1.DebtStatusType]string{ + v1.LowBalancePeriod: "Your account balance is too low, please recharge in time to avoid affecting your normal use.", + v1.CriticalBalancePeriod: "Your account balance is about to run out, please recharge in time to avoid affecting your normal use.", + v1.DebtPeriod: "Your account balance has been exhausted, and services will be suspended for you. Please recharge in time to avoid affecting your normal use.", + v1.DebtDeletionPeriod: "The system will release the resources of the current space soon. Please recharge in time to avoid affecting your normal use.", + v1.FinalDeletionPeriod: "The system will completely release all resources under the current account at any time. Please recharge in time to avoid affecting your normal use.", + } + EmailTemplateZHMap, EmailTemplateENMap = make( + map[v1.DebtStatusType]string, + ), make( + map[v1.DebtStatusType]string, + ) + for _, i := range []v1.DebtStatusType{v1.LowBalancePeriod, v1.CriticalBalancePeriod, v1.DebtPeriod, v1.DebtDeletionPeriod, v1.FinalDeletionPeriod} { + EmailTemplateENMap[i] = TitleTemplateENMap[i] + ":" + NoticeTemplateENMap[i] + "(" + domain + ")" + EmailTemplateZHMap[i] = TitleTemplateZHMap[i] + ":" + NoticeTemplateZHMap[i] + "(" + domain + ")" + } +} + +type UserOwnerPredicate struct { + predicate.Funcs +} + +func (UserOwnerPredicate) Create(e event.CreateEvent) bool { + owner := e.Object.GetAnnotations()[userv1.UserAnnotationOwnerKey] + return owner != "" && owner == e.Object.GetName() +} + +func (UserOwnerPredicate) Update(_ event.UpdateEvent) bool { + return false +} + +func (r *DebtReconciler) Start(ctx context.Context) error { + lock := dlock.NewDistributedLock(r.AccountV2.GetGlobalDB(), "debt_reconciler", r.processID) + if err := lock.TryLock(context.Background(), 15*time.Second); err != nil { + if errors.Is(err, dlock.ErrLockNotAcquired) { + time.Sleep(5 * time.Second) + return r.Start(ctx) + } + } + defer func() { + if err := lock.Unlock(); err != nil { + log.Printf("failed to unlock: %v", err) + } + }() + log.Printf("debt reconciler lock acquired, process ID: %s", r.processID) + r.start(ctx) + log.Printf("debt reconciler started") + return nil +} + +func (r *DebtReconciler) start(ctx context.Context) { + db := r.AccountV2.GetGlobalDB() + var wg sync.WaitGroup + + // 1.1 account update processing + wg.Add(1) + go func() { + defer wg.Done() + r.processWithTimeRange( + &types.Account{}, + "updated_at", + 1*time.Minute, + 24*time.Hour, + func(db *gorm.DB, start, end time.Time) error { + users, err := getUniqueUsers(db, &types.Account{}, "updated_at", start, end) + if err != nil { + return fmt.Errorf("failed to get unique users: %w", err) + } + if len(users) > 0 { + r.Info( + "processed account updates", + "count", + len(users), + "start", + start, + "end", + end, + ) + r.processUsersInParallel(users) + } + return nil + }, + ) + }() + + // 1.2 the arrears are transferred to the clearing state + wg.Add(1) + go func() { + defer wg.Done() + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + var users []uuid.UUID + if err := db.Model(&types.Debt{}). + Where("account_debt_status = ? AND updated_at < ?", types.DebtPeriod, time.Now().UTC().Add(-7*24*time.Hour)). + Distinct("user_uid"). + Pluck("user_uid", &users). + Error; err != nil { + r.Error( + err, + "failed to query unique users", + "account_debt_status", + types.DebtPeriod, + "updated_at", + time.Now().Add(-7*24*time.Hour), + ) + continue + } + if len(users) > 0 { + r.processUsersInParallel(users) + r.Info( + "processed debt status", + "count", + len(users), + "updated_at", + time.Now().Add(-7*24*time.Hour), + ) + } + } + }() + + // 1.3 clearing changes to delete state + wg.Add(1) + go func() { + defer wg.Done() + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + var users []uuid.UUID + if err := db.Model(&types.Debt{}). + Where("account_debt_status = ? AND updated_at < ?", types.DebtDeletionPeriod, time.Now().UTC().Add(-7*24*time.Hour)). + Distinct("user_uid"). + Pluck("user_uid", &users). + Error; err != nil { + r.Error( + err, + "failed to query unique users", + "account_debt_status", + types.DebtPeriod, + "updated_at", + time.Now().Add(-7*24*time.Hour), + ) + continue + } + if len(users) > 0 { + r.processUsersInParallel(users) + r.Info( + "processed debt status", + "count", + len(users), + "updated_at", + time.Now().Add(-7*24*time.Hour), + ) + } + } + }() + + // 1.4 replay final deletion status for accounts that are already in the terminal debt state. + wg.Add(1) + go func() { + defer wg.Done() + r.syncFinalDeletionDebtNamespacesLoop(ctx, db) + }() + + // 2.1 recharge record processing + // wg.Add(1) + // go func() { + // defer wg.Done() + // r.processWithTimeRange( + // &types.Payment{}, + // "created_at", + // 10*time.Second, + // 1*time.Hour, + // func(db *gorm.DB, start, end time.Time) error { + // users, err := getUniqueUsers(db, &types.Payment{}, "created_at", start, end) + // if err != nil { + // return fmt.Errorf("failed to get unique users: %w", err) + // } + // if len(users) > 0 { + // r.processUsersInParallel(users) + // r.Info( + // "processed payment records", + // "count", + // len(users), + // "start", + // start, + // "end", + // end, + // ) + // } + // return nil + // }, + // ) + // }() + + // 2.2 subscription change processing + // wg.Add(1) + // go func() { + // defer wg.Done() + // r.processWithTimeRange( + // &types.Subscription{}, + // "update_at", + // 1*time.Minute, + // 24*time.Hour, + // func(db *gorm.DB, start, end time.Time) error { + // users, err := getUniqueUsers(db, &types.Subscription{}, "update_at", start, end) + // if err != nil { + // return fmt.Errorf("failed to get unique users: %w", err) + // } + // if len(users) > 0 { + // r.processUsersInParallel(users) + // r.Info( + // "processed subscription changes", + // "count", + // len(users), + // "users", + // users, + // "start", + // start, + // "end", + // end, + // ) + // } + // return nil + // }, + // ) + // }() + + // 2.3 credits refresh processing + // wg.Add(1) + // go func() { + // defer wg.Done() + // r.processWithTimeRange( + // &types.Credits{}, + // "updated_at", + // 1*time.Minute, + // 24*time.Hour, + // func(db *gorm.DB, start, end time.Time) error { + // users, err := getUniqueUsers(db, &types.Credits{}, "updated_at", start, end) + // if err != nil { + // return fmt.Errorf("failed to get unique users: %w", err) + // } + // if len(users) > 0 { + // r.processUsersInParallel(users) + // r.Info( + // "processed credits refresh", + // "count", + // len(users), + // "users", + // users, + // "start", + // start, + // "end", + // end, + // ) + // } + // return nil + // }, + // ) + // }() + + // 3 retry failed users + wg.Add(1) + go func() { + defer wg.Done() + r.retryFailedUsers() + }() + + wg.Wait() +} + +func (r *DebtReconciler) syncFinalDeletionDebtNamespacesLoop(ctx context.Context, db *gorm.DB) { + run := func() { + if err := r.syncFinalDeletionDebtNamespaces(ctx, db); err != nil { + r.Error(err, "failed to sync final deletion debt namespaces") + } + } + + run() + ticker := time.NewTicker(finalDeletionDebtNamespaceSyncInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + run() + } + } +} + +func (r *DebtReconciler) syncFinalDeletionDebtNamespaces(ctx context.Context, db *gorm.DB) error { + var users []uuid.UUID + queryCtx, cancel := context.WithTimeout(ctx, finalDeletionDebtNamespaceQueryTimeout) + defer cancel() + + if err := db.WithContext(queryCtx).Model(&types.Debt{}). + Where("account_debt_status = ?", types.FinalDeletionPeriod). + Distinct("user_uid"). + Pluck("user_uid", &users).Error; err != nil { + return fmt.Errorf("failed to query final deletion debt users: %w", err) + } + + var errs []error + for _, userUID := range users { + if err := r.syncFinalDeletionDebtNamespacesForUser(ctx, userUID); err != nil { + errMsg := "failed to sync final deletion debt namespaces " + + "for user %s: %w" + errs = append(errs, fmt.Errorf(errMsg, userUID, err)) + } + } + if len(users) > 0 { + r.Info("synced final deletion debt namespaces", "users", len(users)) + } + return errors.Join(errs...) +} + +func (r *DebtReconciler) syncFinalDeletionDebtNamespacesForUser( + ctx context.Context, + userUID uuid.UUID, +) error { + mutex, err := r.getUserMutex(userUID) + if err != nil { + return err + } + mutex.Lock() + defer mutex.Unlock() + + // The hourly query can race with a successful recharge. Recompute the + // current debt state while holding the same lock as normal refreshes before + // issuing another destructive cleanup request. + var debt types.Debt + if err := r.AccountV2.GetGlobalDB(). + Where("user_uid = ?", userUID). + First(&debt).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return fmt.Errorf("failed to reload debt %s: %w", userUID, err) + } + if debt.AccountDebtStatus != types.FinalDeletionPeriod { + return nil + } + + if err := r.refreshDebtStatus(userUID, true); err != nil { + return fmt.Errorf("failed to refresh debt status before final deletion replay: %w", err) + } + + if err := r.AccountV2.GetGlobalDB(). + Where("user_uid = ?", userUID). + First(&debt).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return fmt.Errorf("failed to reload debt %s after refresh: %w", userUID, err) + } + if debt.AccountDebtStatus != types.FinalDeletionPeriod { + return nil + } + + req := finalDeletionDebtNamespaceFlushReq(userUID) + req.ReplayFinalDeletion = true + return r.sendFlushDebtResourceStatusRequestWithContext( + ctx, + req, + ) +} + +func finalDeletionDebtNamespaceFlushReq(userUID uuid.UUID) AdminFlushResourceStatusReq { + return AdminFlushResourceStatusReq{ + UserUID: userUID, + LastDebtStatus: types.DebtDeletionPeriod, + CurrentDebtStatus: types.FinalDeletionPeriod, + } +} + +func (r *DebtReconciler) RefreshDebtStatus(userUID uuid.UUID) error { + return r.refreshDebtStatus(userUID, false) +} + +func (r *DebtReconciler) refreshDebtStatus(userUID uuid.UUID, skipSendMsg bool) error { + account, err := r.AccountV2.GetAccountWithCredits(userUID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get account %s: %w", userUID, err) + } + if account == nil { + return fmt.Errorf("account %s not found", userUID) + } + debt := types.Debt{} + err = r.AccountV2.GetGlobalDB(). + Model(&types.Debt{}). + Where("user_uid = ?", userUID). + First(&debt). + Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get debt %s: %w", userUID, err) + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + lastStatus := debt.AccountDebtStatus + update := false + if lastStatus == "" { + lastStatus = types.NormalPeriod + update = true + } + // A user can still need debt recovery after ResumeBalance has normalized deduction_balance. + if account.DeductionBalance == 0 && !types.ContainDebtStatus(types.DebtStates, lastStatus) { + return nil + } + isBasicUser := account.Balance <= 10*BaseUnit + oweamount := account.Balance - account.DeductionBalance + account.UsableCredits + // update interval seconds + updateIntervalSeconds := time.Now().UTC().Unix() - debt.UpdatedAt.UTC().Unix() + currentStatusRaw, err := r.DetermineCurrentStatus( + oweamount, + account.UserUID, + updateIntervalSeconds, + v1.DebtStatusType(lastStatus), + ) + if err != nil { + return fmt.Errorf("failed to determine current status for user %s: %w", userUID, err) + } + currentStatus := types.DebtStatusType(currentStatusRaw) + if lastStatus == currentStatus && !update { + return nil + } + if lastStatus != currentStatus { + if err := r.sendFlushDebtResourceStatusRequest(AdminFlushResourceStatusReq{ + UserUID: userUID, + LastDebtStatus: lastStatus, + CurrentDebtStatus: currentStatus, + IsBasicUser: isBasicUser, + }); err != nil { + return fmt.Errorf("failed to send flush resource status request: %w", err) + } + } + + switch lastStatus { + case types.NormalPeriod, types.LowBalancePeriod, types.CriticalBalancePeriod: + if types.ContainDebtStatus(types.DebtStates, currentStatus) { + // resume user account + if err = r.ResumeBalance(userUID); err != nil { + return fmt.Errorf("failed to resume balance: %w", err) + } + if !skipSendMsg { + if err := r.SendUserDebtMsg( + userUID, + oweamount, + currentStatus, + isBasicUser, + ); err != nil { + return NewErrSendMsg(err, userUID) + } + } + break + } + if types.StatusMap[currentStatus] > types.StatusMap[lastStatus] { + // TODO send sms + if !skipSendMsg && account.Balance > 0 { + if err := r.SendUserDebtMsg( + userUID, + oweamount, + currentStatus, + isBasicUser, + ); err != nil { + return NewErrSendMsg(err, userUID) + } + } + } + case types.DebtPeriod, + types.DebtDeletionPeriod, + types.FinalDeletionPeriod: // The current status may be: (Normal, LowBalance, CriticalBalance) Period [Service needs to be restored], DebtDeletionPeriod [Service suspended] + if types.ContainDebtStatus(types.DebtStates, currentStatus) { + if err = r.ResumeBalance(userUID); err != nil { + return fmt.Errorf("failed to resume balance: %w", err) + } + } + if currentStatus != types.FinalDeletionPeriod { + if !skipSendMsg && types.StatusMap[currentStatus] > types.StatusMap[lastStatus] { + if err := r.SendUserDebtMsg( + userUID, + oweamount, + currentStatus, + isBasicUser, + ); err != nil { + return NewErrSendMsg(err, userUID) + } + } + } + } + + r.Logger.V(1).Info("update debt status", "account", debt.UserUID, + "last status", lastStatus, "last update time", debt.UpdatedAt.Format(time.RFC3339), + "current status", debt.AccountDebtStatus, "time", time.Now().UTC().Format(time.RFC3339)) + + debt.AccountDebtStatus = currentStatus + debt.UpdatedAt = time.Now() + + debtRecord := types.DebtStatusRecord{ + ID: uuid.New(), + UserUID: userUID, + LastStatus: lastStatus, + CurrentStatus: currentStatus, + CreateAt: time.Now().UTC(), + } + err = r.AccountV2.GlobalTransactionHandler(func(tx *gorm.DB) error { + dErr := tx.Model(&types.Debt{}).Where("user_uid = ?", userUID).Save(debt).Error + if dErr != nil { + return fmt.Errorf("failed to save debt: %w", dErr) + } + sErr := tx.Model(&types.DebtStatusRecord{}).Create(&debtRecord).Error + if sErr != nil { + return fmt.Errorf("failed to save debt status record: %w", sErr) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to save debt status: %w", err) + } + return nil +} + +func (r *DebtReconciler) ResumeBalance(userUID uuid.UUID) error { + account, err := r.AccountV2.GetAccount(&types.UserQueryOpts{UID: userUID}) + if err != nil { + return fmt.Errorf("failed to get account %s: %w", userUID, err) + } + if account.DeductionBalance <= account.Balance { + return nil + } + err = r.AccountV2.GlobalTransactionHandler(func(tx *gorm.DB) error { + result := tx.Model(&types.Account{}). + Where(`"userUid" = ?`, userUID). + Where(`"deduction_balance" > "balance"`). + Updates(map[string]any{ + "deduction_balance": gorm.Expr("balance"), + }) + if result.Error != nil { + return fmt.Errorf("failed to update account balance: %w", result.Error) + } + if result.RowsAffected > 0 { + return tx.Create(&types.DebtResumeDeductionBalanceTransaction{ + UserUID: userUID, + BeforeDeductionBalance: account.DeductionBalance, + AfterDeductionBalance: account.Balance, + BeforeBalance: account.Balance, + }).Error + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to update account balance: %w", err) + } + return nil +} + +type SendMsgError struct { + UserUID uuid.UUID `json:"userUID" bson:"userUID"` + Err error `json:"err" bson:"err"` +} + +func NewErrSendMsg(err error, userUID uuid.UUID) error { + return SendMsgError{ + UserUID: userUID, + Err: err, + } +} + +func (e SendMsgError) Error() string { + return fmt.Sprintf("failed to send message to user %s: %v", e.UserUID, e.Err) +} + +func (r *DebtReconciler) SendUserDebtMsg( + userUID uuid.UUID, + oweamount int64, + currentStatus types.DebtStatusType, + isBasicUser bool, +) error { + if r.SmsConfig == nil && r.VmsConfig == nil && r.smtpConfig == nil { + return nil + } + if isBasicUser && currentStatus == types.LowBalancePeriod { + return nil + } + _user, err := r.AccountV2.GetUser(&types.UserQueryOpts{UID: userUID}) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + // skip abnormal user + if _user.Status != types.UserStatusNormal { + return nil + } + outh, err := r.AccountV2.GetUserOauthProvider( + &types.UserQueryOpts{UID: _user.UID, ID: _user.ID}, + ) + if err != nil { + return fmt.Errorf("failed to get user oauth provider: %w", err) + } + + // Collect all phone numbers and emails from OAuth providers + var phones []string + var emails []string + for i := range outh { + switch outh[i].ProviderType { + case types.OauthProviderTypePhone: + phones = append(phones, outh[i].ProviderID) + case types.OauthProviderTypeEmail: + emails = append(emails, outh[i].ProviderID) + } + } + fmt.Printf("user: %s, phones: %v, emails: %v\n", userUID, phones, emails) + + if len(phones) > 0 { + if r.SmsConfig != nil && r.SmsConfig.SmsCode[string(currentStatus)] != "" { + oweamount := strconv.FormatInt( + int64(math.Abs(math.Ceil(float64(oweamount)/1_000_000))), + 10, + ) + // Use SendSmsMultiple to send to all phone numbers + err = utils2.SendSmsMultiple( + r.SmsConfig.Client, + phones, + r.SmsConfig.SmsSignName, + r.SmsConfig.SmsCode[string(currentStatus)], + "{\"user_id\":\""+userUID.String()+"\",\"oweamount\":\""+oweamount+"\"}", + ) + if err != nil { + return fmt.Errorf("failed to send sms notice: %w", err) + } + } + if r.VmsConfig != nil && types.ContainDebtStatus(types.DebtStates, currentStatus) && + r.VmsConfig.TemplateCode[string(currentStatus)] != "" { + // Use SendVmsMultiple to send to all phone numbers + err = utils2.SendVmsMultiple( + phones, + r.VmsConfig.TemplateCode[string(currentStatus)], + r.VmsConfig.NumberPoll, + GetSendVmsTimeInUTCPlus8(time.Now()), + forbidTimes, + ) + if err != nil { + return fmt.Errorf("failed to send vms notice: %w", err) + } + } + } + if r.smtpConfig != nil && len(emails) > 0 { + var emailBody string + var emailSubject string + var emailTmpl string + + emailRender := &utils.EmailDebtRender{ + Type: string(currentStatus), + CurrentStatus: currentStatus, + Domain: r.AccountV2.GetLocalRegion().Domain, + } + emailRender.SetLanguage(r.debtEmailLanguage) + emailRender.SetBalance(oweamount) + + if types.ContainDebtStatus(types.DebtStates, currentStatus) { + if r.debtEmailLanguage == "zh" { + emailRender.GraceReason = []string{"余额不足"} + } else { + emailRender.GraceReason = []string{string(utils.GraceReasonNoBalance)} + } + } + + if _user != nil { + emailRender.SetUserName(_user.Name) + } + + emailTmpl = utils.GetDebtEmailTemplate(r.debtEmailLanguage) + + tmp, err := template.New("debt-reconcile").Parse(emailTmpl) + if err != nil { + return fmt.Errorf("failed to parse email template: %w", err) + } + var rendered bytes.Buffer + if err = tmp.Execute(&rendered, emailRender.Build()); err != nil { + return fmt.Errorf("failed to render email template: %w", err) + } + emailBody = rendered.String() + emailSubject = emailRender.GetSubject() + + // 发送邮件 + if err = r.smtpConfig.SendEmailWithTitleMultiple( + emailSubject, + emailBody, + emails, + ); err != nil { + return fmt.Errorf("failed to send email notice: %w", err) + } + } + return nil +} + +type AdminFlushResourceStatusReq struct { + UserUID uuid.UUID `json:"userUID" bson:"userUID"` + LastDebtStatus types.DebtStatusType `json:"lastDebtStatus" bson:"lastDebtStatus"` + CurrentDebtStatus types.DebtStatusType `json:"currentDebtStatus" bson:"currentDebtStatus"` + IsBasicUser bool `json:"isBasicUser" bson:"isBasicUser"` + ReplayFinalDeletion bool `json:"replayFinalDeletion,omitempty" bson:"replayFinalDeletion,omitempty"` +} + +// TODO flush desktop message (send or read) && flush resource quota (suspend or resume or delete) +func (r *DebtReconciler) sendFlushDebtResourceStatusRequest( + quotaReq AdminFlushResourceStatusReq, +) error { + return r.sendFlushDebtResourceStatusRequestWithContext(context.Background(), quotaReq) +} + +func (r *DebtReconciler) sendFlushDebtResourceStatusRequestWithContext( + ctx context.Context, + quotaReq AdminFlushResourceStatusReq, +) error { + client := http.Client{ + Timeout: flushDebtResourceStatusRequestTimeout, + } + + for _, domain := range r.allRegionDomain { + token, err := r.adminJwtManager.GenerateAdminToken(utils.JwtUser{ + Requester: AdminUserName, + }) + if err != nil { + return fmt.Errorf("failed to generate token: %w", err) + } + + prefix := "https://" + if strings.Contains(domain, "nip.io") { + prefix = "http://" + } + url := fmt.Sprintf( + prefix+"account-api.%s/admin/v1alpha1/flush-debt-resource-status", + domain, + ) + + quotaReqBody, err := json.Marshal(quotaReq) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + var lastErr error + backoffTime := time.Second + + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + url, + bytes.NewBuffer(quotaReqBody), + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to send request: %w", err) + } else { + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + lastErr = nil + break + } + body, err := io.ReadAll(resp.Body) + if err != nil { + lastErr = fmt.Errorf( + "unexpected status code: %d, failed to read response body: %w", + resp.StatusCode, + err, + ) + } else { + lastErr = fmt.Errorf( + "unexpected status code: %d, response body: %s", + resp.StatusCode, + string(body), + ) + } + } + + // 进行重试 + if attempt < maxRetries { + fmt.Printf( + "Attempt %d failed: %v. Retrying in %v...\n", + attempt, + lastErr, + backoffTime, + ) + time.Sleep(backoffTime) + backoffTime *= 2 // 指数增长退避时间 + } + } + if lastErr != nil { + return fmt.Errorf( + "failed to send %s request after %d attempts: %w", + url, + maxRetries, + lastErr, + ) + } + } + return nil +} + +// 获取时间范围内的不重复用户 UUID +func getUniqueUsers( + db *gorm.DB, + table any, + timeField string, + startTime, endTime time.Time, +) ([]uuid.UUID, error) { + var users []uuid.UUID + switch table.(type) { + case *types.Account: + if err := db.Model(table).Where(timeField+" BETWEEN ? AND ?", startTime, endTime). + // Where("deduction_balance > ?", 0). + Distinct(`"userUid"`).Pluck(`"userUid"`, &users).Error; err != nil { + return nil, fmt.Errorf("failed to query unique users: %w", err) + } + case *types.AccountTransaction, *types.Payment: + if err := db.Model(table).Where(timeField+" BETWEEN ? AND ?", startTime, endTime). + Distinct(`"userUid"`).Pluck(`"userUid"`, &users).Error; err != nil { + return nil, fmt.Errorf("failed to query unique users: %w", err) + } + default: + if err := db.Model(table).Where(timeField+" BETWEEN ? AND ?", startTime, endTime). + Distinct("user_uid").Pluck("user_uid", &users).Error; err != nil { + return nil, fmt.Errorf("failed to query unique users: %w", err) + } + } + return users, nil +} + +func (r *DebtReconciler) retryFailedUsers() { + ticker := time.NewTicker(1 * time.Minute) + for range ticker.C { + var failedUsers []uuid.UUID + r.failedUserLocks.Range(func(key, value any) bool { + userUID, ok := key.(uuid.UUID) + if ok { + failedUsers = append(failedUsers, userUID) + } + return true + }) + if len(failedUsers) > 0 { + r.Info("retrying failed users", "count", len(failedUsers), "users", failedUsers) + r.processUsersInParallel(failedUsers) + } + } +} + +func (r *DebtReconciler) getUserMutex(userUID uuid.UUID) (*sync.Mutex, error) { + if r.userLocks == nil { + return nil, errors.New("user locks are not initialized") + } + lock, _ := r.userLocks.LoadOrStore(userUID, &sync.Mutex{}) + mutex, ok := lock.(*sync.Mutex) + if !ok { + return nil, fmt.Errorf("invalid mutex for user %s", userUID) + } + return mutex, nil +} + +// Parallel processing of user debt status, the same user simultaneously through the lock to implement a debt refresh processing. +func (r *DebtReconciler) processUsersInParallel(users []uuid.UUID) { + var ( + wg sync.WaitGroup + semaphore = make(chan struct{}, 1000) + ) + + for _, user := range users { + wg.Add(1) + semaphore <- struct{}{} + go func(u uuid.UUID) { + defer wg.Done() + defer func() { <-semaphore }() + mutex, err := r.getUserMutex(u) + if err != nil { + r.Error(err, "failed to load user mutex", "userUID", u) + return + } + if !mutex.TryLock() { + // r.Logger.V(1).Info("user debt processing skipped due to existing lock", + // "userUID", u) + r.failedUserLocks.Store(u, 0) + return + } + defer mutex.Unlock() + if err := r.RefreshDebtStatus(u); err != nil { + r.Error(err, fmt.Sprintf("failed to refresh debt status for user %s", u)) + sendMsgNumber := 1 + if value, ok := r.failedUserLocks.LoadOrStore(u, sendMsgNumber); ok { + if sendMsgNumber, ok = value.(int); ok { + if sendMsgNumber >= 3 { + if err = r.refreshDebtStatus(u, true); err != nil { + r.Error( + err, + fmt.Sprintf("failed to refresh debt status for user %s", u), + ) + } else { + r.failedUserLocks.Delete(u) + } + return + } + sendMsgNumber++ + r.failedUserLocks.Store(u, sendMsgNumber) + } + } + } else { + r.failedUserLocks.Delete(u) + } + }(user) + } + wg.Wait() +} + +// 时间区间轮询处理 +func (r *DebtReconciler) processWithTimeRange( + table any, + timeField string, + interval, initialDuration time.Duration, + processFunc func(*gorm.DB, time.Time, time.Time) error, +) { + // 首次处理 + startTime := time.Now().Add(-initialDuration) + endTime := time.Now().Add(-2 * time.Minute) + users, err := getUniqueUsers(r.AccountV2.GetGlobalDB(), table, timeField, startTime, endTime) + if err != nil { + r.Error( + err, + "failed to get unique users", + "table", + fmt.Sprintf("%T", table), + "start", + startTime, + "end", + endTime, + ) + endTime = startTime + } else if len(users) > 0 { + r.processUsersInParallel(users) + r.Info( + "processed table updates", + "table", + fmt.Sprintf("%T", table), + "count", + len(users), + "start", + startTime, + "end", + endTime, + ) + } + + // 后续按时间区间轮询 + lastEndTime := endTime + ticker := time.NewTicker(interval) + for range ticker.C { + startTime = lastEndTime + endTime = time.Now().Add(-interval) + // if error occurs, the start time of the next execution is the start time of the last one + if err := processFunc(r.AccountV2.GetGlobalDB(), startTime, endTime); err != nil { + r.Error( + err, + "failed to process time range", + "start", + startTime, + "end", + endTime, + "table", + fmt.Sprintf("%T", table), + ) + continue + } + lastEndTime = endTime + } +} diff --git a/controllers/account/controllers/debt_controller.go b/controllers/account/controllers/debt_controller.go new file mode 100644 index 000000000000..acc2c675fb06 --- /dev/null +++ b/controllers/account/controllers/debt_controller.go @@ -0,0 +1,356 @@ +/* +Copyright 2023. + +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 controllers + +// const ( +// DebtDetectionCycleEnv = "DebtDetectionCycleSeconds" +// +// SMSAccessKeyIDEnv = "SMS_AK" +// SMSAccessKeySecretEnv = "SMS_SK" +// VmsAccessKeyIDEnv = "VMS_AK" +// VmsAccessKeySecretEnv = "VMS_SK" +// SMSEndpointEnv = "SMS_ENDPOINT" +// SMSSignNameEnv = "SMS_SIGN_NAME" +// SMSCodeMapEnv = "SMS_CODE_MAP" +// VmsCodeMapEnv = "VMS_CODE_MAP" +// VmsNumberPollEnv = "VMS_NUMBER_POLL" +// SMTPHostEnv = "SMTP_HOST" +// SMTPPortEnv = "SMTP_PORT" +// SMTPFromEnv = "SMTP_FROM" +// SMTPUserEnv = "SMTP_USER" +// SMTPPasswordEnv = "SMTP_PASSWORD" +// SMTPTitleEnv = "SMTP_TITLE" +//) +// +//// DebtReconciler reconciles a Debt object +// type DebtReconciler struct { +// client.Client +// *AccountReconciler +// AccountV2 database.AccountV2 +// InitUserAccountFunc func(user *pkgtypes.UserQueryOpts) (*pkgtypes.Account, error) +// Scheme *runtime.Scheme +// DebtDetectionCycle time.Duration +// LocalRegionID string +// logr.Logger +// accountSystemNamespace string +// SmsConfig *SmsConfig +// VmsConfig *VmsConfig +// smtpConfig *utils.SMTPConfig +// DebtUserMap *maps.ConcurrentMap +// // TODO need init +// userLocks *sync.Map +// failedUserLocks *sync.Map +// processID string +// SkipExpiredUserTimeDuration time.Duration +// SendDebtStatusEmailBody map[accountv1.DebtStatusType]string +//} +// +// type VmsConfig struct { +// TemplateCode map[string]string +// NumberPoll string +//} +// +// type SmsConfig struct { +// Client *client2.Client +// SmsSignName string +// SmsCode map[string]string +//} +// +// var DebtConfig = accountv1.DefaultDebtConfig +// +// func (r *DebtReconciler) DetermineCurrentStatus(oweamount int64, userUID uuid.UUID, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) (accountv1.DebtStatusType, error) { +// if SubscriptionEnabled { +// return r.determineCurrentStatusWithSubscription(oweamount, userUID, updateIntervalSeconds, lastStatus) +// } +// return determineCurrentStatus(oweamount, updateIntervalSeconds, lastStatus), nil +//} +// +// func (r *DebtReconciler) determineCurrentStatusWithSubscription(oweamount int64, userUID uuid.UUID, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) (accountv1.DebtStatusType, error) { +// userSubscription, err := r.AccountV2.GetSubscription(&pkgtypes.UserQueryOpts{UID: userUID}) +// if err != nil { +// return accountv1.NormalPeriod, fmt.Errorf("failed to get user subscription: %w", err) +// } +// +// if oweamount > 0 && userSubscription.Status == pkgtypes.SubscriptionStatusNormal { +// if oweamount >= 5*BaseUnit { +// return accountv1.NormalPeriod, nil +// } else if oweamount > 1*BaseUnit { +// return accountv1.LowBalancePeriod, nil +// } +// return accountv1.CriticalBalancePeriod, nil +// } +// if lastStatus == accountv1.NormalPeriod || lastStatus == accountv1.LowBalancePeriod || lastStatus == accountv1.CriticalBalancePeriod { +// return accountv1.DebtPeriod, nil +// } +// if lastStatus == accountv1.DebtPeriod && updateIntervalSeconds >= DebtConfig[accountv1.DebtDeletionPeriod] { +// return accountv1.DebtDeletionPeriod, nil +// } +// if lastStatus == accountv1.DebtDeletionPeriod && updateIntervalSeconds >= DebtConfig[accountv1.FinalDeletionPeriod] { +// return accountv1.FinalDeletionPeriod, nil +// } +// return lastStatus, nil // Maintain current debt state if no transition +//} +// +// func determineCurrentStatus(oweamount int64, updateIntervalSeconds int64, lastStatus accountv1.DebtStatusType) accountv1.DebtStatusType { +// if oweamount > 0 { +// if oweamount > 10*BaseUnit { +// return accountv1.NormalPeriod +// } else if oweamount > 5*BaseUnit { +// return accountv1.LowBalancePeriod +// } +// return accountv1.CriticalBalancePeriod +// } +// if lastStatus == accountv1.NormalPeriod || lastStatus == accountv1.LowBalancePeriod || lastStatus == accountv1.CriticalBalancePeriod { +// return accountv1.DebtPeriod +// } +// if lastStatus == accountv1.DebtPeriod && updateIntervalSeconds >= DebtConfig[accountv1.DebtDeletionPeriod] { +// return accountv1.DebtDeletionPeriod +// } +// if lastStatus == accountv1.DebtDeletionPeriod && updateIntervalSeconds >= DebtConfig[accountv1.FinalDeletionPeriod] { +// return accountv1.FinalDeletionPeriod +// } +// return lastStatus // Maintain current debt state if no transition +//} +// +// const ( +// fromEn = "Debt-System" +// fromZh = "欠费系统" +// //languageEn = "en" +// languageZh = "zh" +// debtChoicePrefix = "debt-choice-" +// readStatusLabel = "isRead" +// falseStatus = "false" +// trueStatus = "true" +//) +// +// var ( +// TitleTemplateZHMap = map[accountv1.DebtStatusType]string{ +// accountv1.LowBalancePeriod: "余额不足", +// accountv1.CriticalBalancePeriod: "余额即将耗尽", +// accountv1.DebtPeriod: "余额耗尽", +// accountv1.DebtDeletionPeriod: "即将资源释放", +// accountv1.FinalDeletionPeriod: "彻底资源释放", +// } +// TitleTemplateENMap = map[accountv1.DebtStatusType]string{ +// accountv1.LowBalancePeriod: "Low Balance", +// accountv1.CriticalBalancePeriod: "Critical Balance", +// accountv1.DebtPeriod: "Debt", +// accountv1.DebtDeletionPeriod: "Imminent Resource Release", +// accountv1.FinalDeletionPeriod: "Radical resource release", +// } +// NoticeTemplateENMap map[accountv1.DebtStatusType]string +// NoticeTemplateZHMap map[accountv1.DebtStatusType]string +// EmailTemplateENMap map[accountv1.DebtStatusType]string +// EmailTemplateZHMap map[accountv1.DebtStatusType]string +//) +// +// var ( +// forbidTimes = []string{"00:00-10:00", "20:00-24:00"} +// UTCPlus8 = time.FixedZone("UTC+8", 8*3600) +//) +// +//// GetSendVmsTimeInUTCPlus8 send vms time in UTC+8 10:00-20:00 +// func GetSendVmsTimeInUTCPlus8(t time.Time) time.Time { +// nowInUTCPlus8 := t.In(UTCPlus8) +// hour := nowInUTCPlus8.Hour() +// if hour >= 10 && hour < 20 { +// return t +// } +// var next10AM time.Time +// if hour < 10 { +// next10AM = time.Date(nowInUTCPlus8.Year(), nowInUTCPlus8.Month(), nowInUTCPlus8.Day(), 10, 0, 0, 0, UTCPlus8) +// } else { +// next10AM = time.Date(nowInUTCPlus8.Year(), nowInUTCPlus8.Month(), nowInUTCPlus8.Day()+1, 10, 0, 0, 0, UTCPlus8) +// } +// return next10AM.In(time.Local) +//} +// +//// convert "1:code1,2:code2" to map[int]string +// func splitSmsCodeMap(codeStr string) (map[string]string, error) { +// codeMap := make(map[string]string) +// for _, code := range strings.Split(codeStr, ",") { +// split := strings.SplitN(code, ":", 2) +// if len(split) != 2 { +// return nil, fmt.Errorf("invalid sms code map: %s", codeStr) +// } +// codeMap[split[0]] = split[1] +// } +// return codeMap, nil +//} +// +// func (r *DebtReconciler) setupSmsConfig() error { +// if err := env.CheckEnvSetting([]string{SMSAccessKeyIDEnv, SMSAccessKeySecretEnv, SMSEndpointEnv, SMSSignNameEnv, SMSCodeMapEnv}); err != nil { +// return fmt.Errorf("check env setting error: %w", err) +// } +// +// smsCodeMap, err := splitSmsCodeMap(os.Getenv(SMSCodeMapEnv)) +// if err != nil { +// return fmt.Errorf("split sms code map error: %w", err) +// } +// for key := range smsCodeMap { +// if _, ok := pkgtypes.StatusMap[pkgtypes.DebtStatusType(key)]; !ok { +// return fmt.Errorf("invalid sms code map key: %s", key) +// } +// } +// r.Logger.Info("set sms code map", "smsCodeMap", smsCodeMap, "smsSignName", os.Getenv(SMSSignNameEnv)) +// smsClient, err := utils.CreateSMSClient(os.Getenv(SMSAccessKeyIDEnv), os.Getenv(SMSAccessKeySecretEnv), os.Getenv(SMSEndpointEnv)) +// if err != nil { +// return fmt.Errorf("create sms client error: %w", err) +// } +// r.SmsConfig = &SmsConfig{ +// Client: smsClient, +// SmsSignName: os.Getenv(SMSSignNameEnv), +// SmsCode: smsCodeMap, +// } +// return nil +//} +// +// func (r *DebtReconciler) setupVmsConfig() error { +// if err := env.CheckEnvSetting([]string{VmsAccessKeyIDEnv, VmsAccessKeySecretEnv, VmsNumberPollEnv}); err != nil { +// return fmt.Errorf("check env setting error: %w", err) +// } +// vms.DefaultInstance.Client.SetAccessKey(os.Getenv(VmsAccessKeyIDEnv)) +// vms.DefaultInstance.Client.SetSecretKey(os.Getenv(VmsAccessKeySecretEnv)) +// +// vmsCodeMap, err := splitSmsCodeMap(os.Getenv(VmsCodeMapEnv)) +// if err != nil { +// return fmt.Errorf("split vms code map error: %w", err) +// } +// for key := range vmsCodeMap { +// if _, ok := pkgtypes.StatusMap[pkgtypes.DebtStatusType(key)]; !ok { +// return fmt.Errorf("invalid sms code map key: %s", key) +// } +// } +// r.Logger.Info("set vms code map", "vmsCodeMap", vmsCodeMap) +// r.VmsConfig = &VmsConfig{ +// TemplateCode: vmsCodeMap, +// NumberPoll: os.Getenv(VmsNumberPollEnv), +// } +// return nil +//} +// +// func (r *DebtReconciler) setupSMTPConfig() error { +// if err := env.CheckEnvSetting([]string{SMTPHostEnv, SMTPFromEnv, SMTPPasswordEnv, SMTPTitleEnv}); err != nil { +// return fmt.Errorf("check env setting error: %w", err) +// } +// serverPort, err := strconv.Atoi(env.GetEnvWithDefault(SMTPPortEnv, "465")) +// if err != nil { +// return fmt.Errorf("invalid smtp port: %w", err) +// } +// r.smtpConfig = &utils.SMTPConfig{ +// ServerHost: os.Getenv(SMTPHostEnv), +// ServerPort: serverPort, +// Username: env.GetEnvWithDefault(SMTPUserEnv, os.Getenv(SMTPFromEnv)), +// FromEmail: os.Getenv(SMTPFromEnv), +// Passwd: os.Getenv(SMTPPasswordEnv), +// EmailTitle: os.Getenv(SMTPTitleEnv), +// } +// return nil +//} +// +//// SetupWithManager sets up the controller with the Manager. +// func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { +// r.Init() +// /* +// {"DebtConfig":{ +// "ApproachingDeletionPeriod":345600, +// "FinalDeletionPeriod":604800, +// "ImminentDeletionPeriod":259200,"WarningPeriod":0}, +// "DebtDetectionCycle": "1m0s", +// "accountSystemNamespace": "account-system", +// "accountNamespace": "sealos-system"} +// */ +// r.Logger.Info("set config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle, +// "accountSystemNamespace", r.accountSystemNamespace) +// return ctrl.NewControllerManagedBy(mgr). +// For(&userv1.User{}, builder.WithPredicates(predicate.And(UserOwnerPredicate{})), builder.OnlyMetadata). +// Watches(&accountv1.Payment{}, &handler.EnqueueRequestForObject{}). +// WithOptions(rateOpts). +// Complete(r) +//} +// +// func (r *DebtReconciler) Init() { +// r.Logger = ctrl.Log.WithName("DebtController") +// r.accountSystemNamespace = env.GetEnvWithDefault(accountv1.AccountSystemNamespaceEnv, "account-system") +// r.LocalRegionID = os.Getenv(cockroach.EnvLocalRegion) +// debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 1800) +// r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second +// r.userLocks = &sync.Map{} +// r.failedUserLocks = &sync.Map{} +// r.processID = uuid.NewString() +// +// setupList := []func() error{ +// r.setupSmsConfig, +// r.setupVmsConfig, +// r.setupSMTPConfig, +// } +// for i := range setupList { +// if err := setupList[i](); err != nil { +// r.Logger.Error(err, fmt.Sprintf("failed to set up %s", runtime2.FuncForPC(reflect.ValueOf(setupList[i]).Pointer()).Name())) +// } +// } +// setDefaultDebtPeriodWaitSecond() +// r.SendDebtStatusEmailBody = make(map[accountv1.DebtStatusType]string) +// for _, status := range []accountv1.DebtStatusType{accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod, accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod} { +// email := os.Getenv(string(status) + "EmailBody") +// if email == "" { +// email = EmailTemplateZHMap[status] + "\n" + EmailTemplateENMap[status] +// } else { +// r.Logger.Info("set email body", "status", status, "body", email) +// } +// r.SendDebtStatusEmailBody[status] = email +// } +// r.Logger.Info("debt config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle) +//} +// +// func setDefaultDebtPeriodWaitSecond() { +// DebtConfig[accountv1.DebtDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.DebtDeletionPeriod), 7*accountv1.DaySecond) +// DebtConfig[accountv1.FinalDeletionPeriod] = env.GetInt64EnvWithDefault(string(accountv1.FinalDeletionPeriod), 7*accountv1.DaySecond) +// domain := os.Getenv("DOMAIN") +// NoticeTemplateZHMap = map[accountv1.DebtStatusType]string{ +// accountv1.LowBalancePeriod: "当前工作空间所属账户余额过低,请及时充值,以免影响您的正常使用。", +// accountv1.CriticalBalancePeriod: "当前工作空间所属账户余额即将耗尽,请及时充值,以免影响您的正常使用。", +// accountv1.DebtPeriod: "当前工作空间所属账户余额已耗尽,系统将为您暂停服务,请及时充值,以免影响您的正常使用。", +// accountv1.DebtDeletionPeriod: "系统即将释放当前空间的资源,请及时充值,以免影响您的正常使用。", +// accountv1.FinalDeletionPeriod: "系统将随时彻底释放当前工作空间所属账户下的所有资源,请及时充值,以免影响您的正常使用。", +// } +// NoticeTemplateENMap = map[accountv1.DebtStatusType]string{ +// accountv1.LowBalancePeriod: "Your account balance is too low, please recharge in time to avoid affecting your normal use.", +// accountv1.CriticalBalancePeriod: "Your account balance is about to run out, please recharge in time to avoid affecting your normal use.", +// accountv1.DebtPeriod: "Your account balance has been exhausted, and services will be suspended for you. Please recharge in time to avoid affecting your normal use.", +// accountv1.DebtDeletionPeriod: "The system will release the resources of the current space soon. Please recharge in time to avoid affecting your normal use.", +// accountv1.FinalDeletionPeriod: "The system will completely release all resources under the current account at any time. Please recharge in time to avoid affecting your normal use.", +// } +// EmailTemplateZHMap, EmailTemplateENMap = make(map[accountv1.DebtStatusType]string), make(map[accountv1.DebtStatusType]string) +// for _, i := range []accountv1.DebtStatusType{accountv1.LowBalancePeriod, accountv1.CriticalBalancePeriod, accountv1.DebtPeriod, accountv1.DebtDeletionPeriod, accountv1.FinalDeletionPeriod} { +// EmailTemplateENMap[i] = TitleTemplateENMap[i] + ":" + NoticeTemplateENMap[i] + "(" + domain + ")" +// EmailTemplateZHMap[i] = TitleTemplateZHMap[i] + ":" + NoticeTemplateZHMap[i] + "(" + domain + ")" +// } +//} +// +// type UserOwnerPredicate struct { +// predicate.Funcs +//} +// +// func (UserOwnerPredicate) Create(e event.CreateEvent) bool { +// owner := e.Object.GetAnnotations()[userv1.UserAnnotationOwnerKey] +// return owner != "" && owner == e.Object.GetName() +//} +// +// func (UserOwnerPredicate) Update(_ event.UpdateEvent) bool { +// return false +//} diff --git a/controllers/account/controllers/debt_controller_test.go b/controllers/account/controllers/debt_controller_test.go new file mode 100644 index 000000000000..cc3e932f29ad --- /dev/null +++ b/controllers/account/controllers/debt_controller_test.go @@ -0,0 +1,506 @@ +// Copyright © 2023 sealos. +// +// 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 controllers + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/google/uuid" + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils" + "gorm.io/gorm" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Test_splitSmsCodeMap(t *testing.T) { + codeMap, err := splitSmsCodeMap("0:SMS_123456,1:SMS_654321,2:SMS_987654") + if err != nil { + t.Fatal(err) + } + t.Logf("codeMap: %v", codeMap) + if len(codeMap) != 3 { + t.Fatal("invalid codeMap") + } + if codeMap["0"] != "SMS_123456" { + t.Fatal("invalid codeMap") + } + if codeMap["1"] != "SMS_654321" { + t.Fatal("invalid codeMap") + } + if codeMap["2"] != "SMS_987654" { + t.Fatal("invalid codeMap") + } +} + +func TestGetTimeInUTCPlus8(t *testing.T) { + t1 := time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC) + t2 := time.Date(2023, time.January, 1, 1, 0, 0, 0, time.UTC) + t3 := time.Date(2023, time.January, 1, 9, 0, 0, 0, time.UTC) + t4 := time.Date(2023, time.January, 1, 11, 0, 0, 0, time.UTC) + t5 := time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC) + t6 := time.Date(2023, time.January, 1, 13, 0, 0, 0, time.UTC) + t7 := time.Date(2023, time.January, 1, 23, 0, 0, 0, time.UTC) + for _, _t := range []time.Time{t1, t2, t3, t4, t5, t6, t7} { + t.Logf("time: %v, timeInUTCPlus8: %v", _t, GetSendVmsTimeInUTCPlus8(_t)) + } +} + +const ( + processedUsersFile = "processed_users.txt" +) + +// TestReconcileAllFinalUser 测试方法 +func TestReconcileAllFinalUser(t *testing.T) { + requireAccountMaintenanceTest( + t, + database.GlobalCockroachURI, + database.LocalCockroachURI, + "LOCAL_REGION", + EnvJwtSecret, + ) + account, err := database.NewAccountV2( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + t.Fatalf("failed to new account: %v", err) + } + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + + regions, err := account.GetRegions() + if err != nil { + t.Fatalf("failed to get regions: %v", err) + } + allRegionDomain := make([]string, 0) + for _, region := range regions { + if region.Domain != "" { + allRegionDomain = append(allRegionDomain, region.Domain) + } + } + + jwtManager := utils.NewJWTManager(os.Getenv(EnvJwtSecret), time.Hour*24) + + // 获取全部 Debt 状态为 FinalDeletionPeriod 的用户 + allUserUID := make([]uuid.UUID, 0) + err = account.GetGlobalDB(). + Model(&types.Debt{}). + Where("account_debt_status = ?", types.FinalDeletionPeriod). + Pluck("user_uid", &allUserUID). + Error + if err != nil { + t.Fatalf("failed to get all user: %v", err) + } + if len(allUserUID) == 0 { + t.Logf("no user need to flush") + return + } + + // 创建临时文件路径 + tempDir := t.TempDir() + processedFilePath := filepath.Join(tempDir, processedUsersFile) + + // 加载已处理的用户 UID + processedUsers, err := loadProcessedUsers(processedFilePath) + if err != nil { + t.Fatalf("failed to load processed users: %v", err) + } + + for _, user := range allUserUID { + if processedUsers[user] { + t.Logf("user %s already processed, skipping", user) + continue + } + + err = sendFlushDebtResourceStatusRequest( + allRegionDomain, + jwtManager, + user, + processedFilePath, + ) + if err != nil { + t.Fatalf("failed to send flush debt resource status request for user %s: %v", user, err) + } + + if err := recordProcessedUser(processedFilePath, user); err != nil { + t.Fatalf("failed to record processed user %s: %v", user, err) + } + } + t.Logf("all users processed successfully") + // 删除临时文件 + // if err := os.Remove(processedFilePath); err != nil { + // t.Fatalf("failed to remove processed users file: %v", err) + //} +} + +// loadProcessedUsers 从文件中加载已处理的用户 UID +func loadProcessedUsers(filePath string) (map[uuid.UUID]bool, error) { + processed := make(map[uuid.UUID]bool) + data, err := os.ReadFile(filePath) + if os.IsNotExist(err) { + return processed, nil + } + if err != nil { + return nil, fmt.Errorf("failed to read processed users file: %w", err) + } + + lines := strings.SplitSeq(string(data), "\n") + for line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + uid, err := uuid.Parse(line) + if err != nil { + return nil, fmt.Errorf("invalid UUID in processed users file: %s", line) + } + processed[uid] = true + } + return processed, nil +} + +// recordProcessedUser 将处理成功的用户 UID 追加到文件中 +func recordProcessedUser(filePath string, userUID uuid.UUID) error { + f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("failed to open processed users file: %w", err) + } + defer f.Close() + + if _, err := f.WriteString(userUID.String() + "\n"); err != nil { + return fmt.Errorf("failed to write user UID to file: %w", err) + } + return nil +} + +// sendFlushDebtResourceStatusRequest 发送请求并记录成功处理的用户 +func sendFlushDebtResourceStatusRequest( + allRegionDomain []string, + jwtManager *utils.JWTManager, + userUID uuid.UUID, + _ string, +) error { + for _, domain := range allRegionDomain { + fmt.Println("domain:", domain, " userUID:", userUID) + token, err := jwtManager.GenerateToken(utils.JwtUser{ + Requester: AdminUserName, + }) + if err != nil { + return fmt.Errorf("failed to generate token: %w", err) + } + + url := fmt.Sprintf( + "https://account-api.%s/admin/v1alpha1/flush-debt-resource-status", + domain, + ) + + quotaReqBody, err := json.Marshal(AdminFlushResourceStatusReq{ + LastDebtStatus: types.DebtDeletionPeriod, + CurrentDebtStatus: types.FinalDeletionPeriod, + UserUID: userUID, + }) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + var lastErr error + backoffTime := time.Second + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + req, err := http.NewRequestWithContext( + context.Background(), + http.MethodPost, + url, + bytes.NewBuffer(quotaReqBody), + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + client := http.Client{Timeout: 30 * time.Second} + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to send request: %w", err) + } else { + if resp.StatusCode == http.StatusOK { + lastErr = nil + _ = resp.Body.Close() + break + } + lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode) + _ = resp.Body.Close() + } + + if attempt < maxRetries { + fmt.Printf( + "Attempt %d failed: %v. Retrying in %v...\n", + attempt, + lastErr, + backoffTime, + ) + time.Sleep(backoffTime) + backoffTime *= 2 + } + } + if lastErr != nil { + return lastErr + } + } + return nil +} + +type regionConfig struct { + Region string `json:"region"` + KCPath string `json:"kc_path"` + GlobalDB string `json:"global_db"` + LocalDB string `json:"local_db"` + RegionUID string `json:"region_uid"` +} + +// 1. pause account controller +// 2. convert all region debt +// 3. upgrade and restore the account controller +func TestConvertDebt(t *testing.T) { + requireAccountMaintenanceTest(t, "ACCOUNT_DEBT_MIGRATION_REGIONS") + var regions []regionConfig + regionsJSON := os.Getenv("ACCOUNT_DEBT_MIGRATION_REGIONS") + if err := json.Unmarshal([]byte(regionsJSON), ®ions); err != nil { + t.Fatalf("failed to parse ACCOUNT_DEBT_MIGRATION_REGIONS: %v", err) + } + if len(regions) == 0 { + t.Fatal("ACCOUNT_DEBT_MIGRATION_REGIONS must contain at least one region") + } + for i := range regions { + // 先获取全部的debt crd + fmt.Printf( + "Start converting debts for region %s at %s\n", + regions[i].Region, + time.Now().Format(time.RFC3339), + ) + config, err := clientcmd.BuildConfigFromFlags("", regions[i].KCPath) + if err != nil { + t.Fatalf("failed to get in cluster config: %v", err) + } + + emptyScheme := runtime.NewScheme() + utilruntime.Must(accountv1.AddToScheme(emptyScheme)) + clt, err := client.New(config, client.Options{Scheme: emptyScheme}) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + t.Setenv("LOCAL_REGION", regions[i].RegionUID) + account, err := database.NewAccountV2(regions[i].GlobalDB, regions[i].LocalDB) + if err != nil { + t.Fatalf("failed to new account: %v", err) + } + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + if !account.GetGlobalDB().Migrator().HasTable(&types.Debt{}) { + err = account.GetGlobalDB().Migrator().AutoMigrate(&types.Debt{}) + if err != nil { + t.Fatalf("failed to migrate debt table: %v", err) + } + } + if !account.GetGlobalDB().Migrator().HasTable(&types.DebtStatusRecord{}) { + err = account.GetGlobalDB().Migrator().AutoMigrate(&types.DebtStatusRecord{}) + if err != nil { + t.Fatalf("failed to migrate debt status record table: %v", err) + } + } + + err = convertAllDebtCr(account, clt) + if err != nil { + t.Fatalf("failed to convert all debt cr: %v", err) + } + fmt.Printf( + "Finished converting debts for region %s at %s\n", + regions[i].Region, + time.Now().Format(time.RFC3339), + ) + } +} + +func requireAccountMaintenanceTest(t *testing.T, envNames ...string) { + t.Helper() + if os.Getenv("RUN_ACCOUNT_MAINTENANCE_TESTS") != "true" { + t.Skip("set RUN_ACCOUNT_MAINTENANCE_TESTS=true to run account maintenance tests") + } + for _, name := range envNames { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } +} + +func convertAllDebtCr(account database.AccountV2, clt client.Client) error { + // 1. 获取已存在的 user_uid + // 2. 预加载所有 userID -> userUID + // 3. 拉取 CRs 并过滤掉已存在的 userUID // 添加 userUID 以避免重复(主线程去重) + // 4. worker pool 执行写入 + // 5. 转换并推入任务队列 + const ( + workerCount = 10 + batchSize = 100 + maxRetries = 3 + ) + existing := make(map[uuid.UUID]struct{}) + var existingUIDs []uuid.UUID + if err := account.GetGlobalDB(). + Model(&types.Debt{}). + Pluck("user_uid", &existingUIDs). + Error; err != nil { + return fmt.Errorf("failed to preload existing debts: %w", err) + } + for _, uid := range existingUIDs { + existing[uid] = struct{}{} + } + + userIDToUIDMap, err := GetUserIDToUIDMap(account.GetGlobalDB()) + if err != nil { + return fmt.Errorf("failed to preload user UID map: %w", err) + } + + var allDebts []accountv1.Debt + listOpts := &client.ListOptions{Limit: 1000} + for { + debtCRList := &accountv1.DebtList{} + if err := clt.List(context.Background(), debtCRList, listOpts); err != nil { + return fmt.Errorf("failed to list debts: %w", err) + } + + for _, debt := range debtCRList.Items { + if debt.Spec.UserID == "" { + continue + } + userUID, ok := userIDToUIDMap[debt.Spec.UserID] + if !ok || userUID == uuid.Nil { + continue + } + if _, exists := existing[userUID]; exists { + continue + } + existing[userUID] = struct{}{} + allDebts = append(allDebts, debt) + } + + if cont := debtCRList.GetContinue(); cont == "" { + break + } else { + listOpts.Continue = cont + } + } + + fmt.Printf("Total debts to insert: %d\n", len(allDebts)) + + var wg sync.WaitGroup + tasks := make(chan *types.Debt, len(allDebts)) + var firstErr error + var errMu sync.Mutex + + for range workerCount { + wg.Add(1) + go func() { + defer wg.Done() + var batch []*types.Debt + + for debt := range tasks { + batch = append(batch, debt) + if len(batch) >= batchSize { + if err := insertDebts(account, batch); err != nil { + fmt.Printf("failed to insert debts: %v", err) + errMu.Lock() + if firstErr == nil { + firstErr = err + } + errMu.Unlock() + } + batch = batch[:0] + } + } + + if len(batch) > 0 { + if err := insertDebts(account, batch); err != nil { + fmt.Printf("failed to insert debts: %v", err) + errMu.Lock() + if firstErr == nil { + firstErr = err + } + errMu.Unlock() + } + } + }() + } + + for _, debt := range allDebts { + userUID := userIDToUIDMap[debt.Spec.UserID] + tasks <- convertDebtCrToDebt(&debt, userUID) + } + close(tasks) + + wg.Wait() + return firstErr +} + +func GetUserIDToUIDMap(db *gorm.DB) (map[string]uuid.UUID, error) { + type user struct { + ID string `gorm:"column:id"` + UserUID uuid.UUID `gorm:"column:uid"` + } + var users []user + if err := db.Model(&types.User{}).Select("id, uid").Find(&users).Error; err != nil { + return nil, fmt.Errorf("failed to preload users: %w", err) + } + + result := make(map[string]uuid.UUID, len(users)) + for _, u := range users { + result[u.ID] = u.UserUID + } + return result, nil +} + +func insertDebts(account database.AccountV2, debts []*types.Debt) error { + return cockroach.RetryableTransaction( + account.GetGlobalDB().Session(&gorm.Session{PrepareStmt: true}), + 3, + func(tx *gorm.DB) error { + return tx.Create(&debts).Error + }, + ) +} diff --git a/controllers/account/controllers/debt_final_deletion_sync_test.go b/controllers/account/controllers/debt_final_deletion_sync_test.go new file mode 100644 index 000000000000..819bc50e76cf --- /dev/null +++ b/controllers/account/controllers/debt_final_deletion_sync_test.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "testing" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +func TestFinalDeletionDebtNamespaceFlushReq(t *testing.T) { + userUID := uuid.New() + + req := finalDeletionDebtNamespaceFlushReq(userUID) + + if req.UserUID != userUID { + t.Fatalf("expected user uid %s, got %s", userUID, req.UserUID) + } + if req.LastDebtStatus != types.DebtDeletionPeriod { + t.Fatalf( + "expected last debt status %s, got %s", + types.DebtDeletionPeriod, + req.LastDebtStatus, + ) + } + if req.CurrentDebtStatus != types.FinalDeletionPeriod { + t.Fatalf( + "expected current debt status %s, got %s", + types.FinalDeletionPeriod, + req.CurrentDebtStatus, + ) + } + if req.ReplayFinalDeletion { + t.Fatal("normal final deletion request should not be marked as replay") + } +} + +func TestFinalDeletionDebtNamespaceReplayReq(t *testing.T) { + userUID := uuid.New() + req := finalDeletionDebtNamespaceFlushReq(userUID) + req.ReplayFinalDeletion = true + + if !req.ReplayFinalDeletion { + t.Fatal("final deletion replay request should be marked as replay") + } +} diff --git a/controllers/account/controllers/namespace_controller.go b/controllers/account/controllers/namespace_controller.go new file mode 100644 index 000000000000..aa33ca87b603 --- /dev/null +++ b/controllers/account/controllers/namespace_controller.go @@ -0,0 +1,3266 @@ +package controllers + +import ( + "context" + "encoding/json" + errors2 "errors" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-logr/logr" + v1 "github.com/labring/sealos/controllers/account/api/v1" + "github.com/labring/sealos/controllers/pkg/objectstorage" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/minio/madmin-go/v3" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// NamespaceReconciler reconciles a Namespace object +type NamespaceReconciler struct { + Client client.WithWatch + dynamicClient dynamic.Interface + Log logr.Logger + Scheme *runtime.Scheme + OSAdminClient *madmin.AdminClient + OSNamespace string + OSAdminSecret string + InternalEndpoint string + deleteResourceSemaphore chan struct{} + deleteBackupSemaphore chan struct{} + // devboxVersion stores the detected devbox API version (v1alpha1 or v1alpha2) + // empty string if devbox CRD is not installed + devboxVersion string +} + +const ( + DebtLimit0Name = "debt-limit0" + OSAccessKey = "CONSOLE_ACCESS_KEY" + OSSecretKey = "CONSOLE_SECRET_KEY" + Disabled = "disabled" + Enabled = "enabled" + OSInternalEndpointEnv = "OSInternalEndpoint" + OSNamespace = "OSNamespace" + OSAdminSecret = "OSAdminSecret" + + // App deploy annotations + PauseKey = "deploy.cloud.sealos.io/pause" + MinReplicasKey = "deploy.cloud.sealos.io/minReplicas" + MaxReplicasKey = "deploy.cloud.sealos.io/maxReplicas" + DeployPVCResizeKey = "deploy.cloud.sealos.io/resize" +) + +var errFinalDeletionCancelled = errors2.New("final deletion cancelled by namespace status") + +//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=namespaces/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core,resources=namespaces/finalizers,verbs=update +//+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=opsrequests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps.kubeblocks.io,resources=opsrequests/status,verbs=get;update;watch +//+kubebuilder:rbac:groups=app.sealos.io,resources=apps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=app.sealos.io,resources=instances,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete + +//nolint:gocyclo +func (r *NamespaceReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + logger := r.Log.WithValues("Namespace", req.Namespace, "Name", req.NamespacedName) + + ns := corev1.Namespace{} + if err := r.Client.Get(ctx, req.NamespacedName, &ns); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if ns.Status.Phase == corev1.NamespaceTerminating { + logger.V(1).Info("namespace is terminating") + return ctrl.Result{}, nil + } + + if ns.Annotations == nil { + logger.V(1).Info("No debt or network status annotations found") + return ctrl.Result{}, nil + } + + debtStatus, debtExists := ns.Annotations[types.DebtNamespaceAnnoStatusKey] + networkStatus, networkExists := ns.Annotations[types.NetworkStatusAnnoKey] + if !debtExists && !networkExists { + logger.V(1).Info("No debt or network status annotations found") + return ctrl.Result{}, nil + } + + debtCompletedStates := map[string]bool{ + types.SuspendCompletedDebtNamespaceAnnoStatus: true, + types.FinalDeletionCompletedDebtNamespaceAnnoStatus: true, + types.ResumeCompletedDebtNamespaceAnnoStatus: true, + types.TerminateSuspendCompletedDebtNamespaceAnnoStatus: true, + } + networkCompletedStates := map[string]bool{ + types.NetworkSuspendCompleted: true, + types.NetworkResumeCompleted: true, + } + deleteConst := "delete" + + // auxiliary function update annotations + updateAnnotations := func(debtStatus, networkStatus string) (ctrl.Result, error) { + original := ns.DeepCopy() + if debtStatus != "" { + ns.Annotations[types.DebtNamespaceAnnoStatusKey] = debtStatus + } + if networkStatus != "" { + ns.Annotations[types.NetworkStatusAnnoKey] = networkStatus + } + if err := r.Client.Patch(ctx, &ns, client.MergeFrom(original)); err != nil { + logger.Error(err, "failed to update namespace annotations") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // auxiliary function handles resource operations + performAction := func(action func(context.Context, string) error, actionName string) (ctrl.Result, error) { + if err := action(ctx, req.Name); err != nil { + if errors2.Is(err, errFinalDeletionCancelled) { + return ctrl.Result{}, err + } + logger.Error(err, actionName+" namespace resources failed") + return ctrl.Result{ + Requeue: actionName == deleteConst, + RequeueAfter: 10 * time.Minute, + }, err + } + return ctrl.Result{}, nil + } + + // state transition table + type stateTransition struct { + condition func() bool + newDebt string + newNetwork string + action func(context.Context, string) error + actionName string + } + + transitions := []stateTransition{ + // Case 1: only the debt status + { + condition: func() bool { return debtExists && !networkExists && !debtCompletedStates[debtStatus] }, + newDebt: func() string { + switch debtStatus { + case types.SuspendDebtNamespaceAnnoStatus: + return types.SuspendCompletedDebtNamespaceAnnoStatus + case types.TerminateSuspendDebtNamespaceAnnoStatus: + return types.TerminateSuspendCompletedDebtNamespaceAnnoStatus + case types.ResumeDebtNamespaceAnnoStatus: + return types.ResumeCompletedDebtNamespaceAnnoStatus + case types.FinalDeletionDebtNamespaceAnnoStatus: + return types.FinalDeletionCompletedDebtNamespaceAnnoStatus + default: + return types.NormalDebtNamespaceAnnoStatus + } + }(), + newNetwork: "", + action: func(ctx context.Context, name string) error { + switch debtStatus { + case types.SuspendDebtNamespaceAnnoStatus, + types.TerminateSuspendDebtNamespaceAnnoStatus: + return r.SuspendUserResource(ctx, name) + case types.ResumeDebtNamespaceAnnoStatus: + return r.ResumeUserResource(ctx, name) + case types.FinalDeletionDebtNamespaceAnnoStatus: + return r.DeleteUserResource(ctx, name) + default: + return nil + } + }, + actionName: func() string { + switch debtStatus { + case types.FinalDeletionDebtNamespaceAnnoStatus: + return deleteConst + default: + return "suspend/resume" + } + }(), + }, + // Case 2: only in the network state + { + condition: func() bool { return !debtExists && networkExists && !networkCompletedStates[networkStatus] }, + newDebt: "", + newNetwork: func() string { + switch networkStatus { + case types.NetworkSuspend: + return types.NetworkSuspendCompleted + case types.NetworkResume: + return types.NetworkResumeCompleted + default: + return "" + } + }(), + action: func(ctx context.Context, name string) error { + switch networkStatus { + case types.NetworkSuspend: + return r.SuspendUserResource(ctx, name) + case types.NetworkResume: + return r.ResumeUserResource(ctx, name) + default: + return nil + } + }, + actionName: "suspend/resume", + }, + // Case 3: both the debt and network states exist + { + condition: func() bool { + if debtExists && networkExists { + if debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus { + return true + } + } + return debtExists && networkExists && !debtCompletedStates[debtStatus] && + !networkCompletedStates[networkStatus] + }, + newDebt: func() string { + switch debtStatus { + case types.NormalDebtNamespaceAnnoStatus: + return types.NormalDebtNamespaceAnnoStatus + case types.SuspendDebtNamespaceAnnoStatus: + return types.SuspendCompletedDebtNamespaceAnnoStatus + case types.TerminateSuspendDebtNamespaceAnnoStatus: + return types.TerminateSuspendCompletedDebtNamespaceAnnoStatus + case types.ResumeDebtNamespaceAnnoStatus: + return types.ResumeCompletedDebtNamespaceAnnoStatus + case types.FinalDeletionDebtNamespaceAnnoStatus: + return types.FinalDeletionCompletedDebtNamespaceAnnoStatus + default: + return types.NormalDebtNamespaceAnnoStatus + } + }(), + newNetwork: func() string { + switch networkStatus { + case types.NetworkSuspend: + return types.NetworkSuspendCompleted + case types.NetworkResume: + return types.NetworkResumeCompleted + default: + return networkStatus + } + }(), + action: func(ctx context.Context, name string) error { + if debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus { + return r.DeleteUserResource(ctx, name) + } + if networkStatus == types.NetworkSuspend || + debtStatus == types.SuspendDebtNamespaceAnnoStatus || + debtStatus == types.TerminateSuspendDebtNamespaceAnnoStatus { + return r.SuspendUserResource(ctx, name) + } + if networkStatus == types.NetworkResume || + debtStatus == types.ResumeDebtNamespaceAnnoStatus { + return r.ResumeUserResource(ctx, name) + } + return nil + }, + actionName: func() string { + if debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus { + return deleteConst + } + return "suspend/resume" + }(), + }, + // Case 4: debt completion status handling network OR network completion status handling debt + { + condition: func() bool { + // Subcase 4a: debt completed but network not completed + if debtExists && networkExists && debtCompletedStates[debtStatus] && + !networkCompletedStates[networkStatus] { + return true + } + // Subcase 4b: network completed but debt not completed + if debtExists && networkExists && networkCompletedStates[networkStatus] && + !debtCompletedStates[debtStatus] { + return true + } + return false + }, + newDebt: func() string { + // If debt is not completed, transition it to completed state + if !debtCompletedStates[debtStatus] { + switch debtStatus { + case types.SuspendDebtNamespaceAnnoStatus: + return types.SuspendCompletedDebtNamespaceAnnoStatus + case types.TerminateSuspendDebtNamespaceAnnoStatus: + return types.TerminateSuspendCompletedDebtNamespaceAnnoStatus + case types.ResumeDebtNamespaceAnnoStatus: + return types.ResumeCompletedDebtNamespaceAnnoStatus + case types.FinalDeletionDebtNamespaceAnnoStatus: + return types.FinalDeletionCompletedDebtNamespaceAnnoStatus + default: + return debtStatus + } + } + return debtStatus + }(), + newNetwork: func() string { + // If network is not completed, transition it to completed state + if !networkCompletedStates[networkStatus] { + switch networkStatus { + case types.NetworkSuspend: + return types.NetworkSuspendCompleted + case types.NetworkResume: + return types.NetworkResumeCompleted + default: + return networkStatus + } + } + return networkStatus + }(), + action: func(ctx context.Context, name string) error { + // Prioritize debt action (suspend/delete) over network action when both need action + // If debt needs action and is suspend/delete type, execute it + if !debtCompletedStates[debtStatus] { + switch debtStatus { + case types.SuspendDebtNamespaceAnnoStatus, + types.TerminateSuspendDebtNamespaceAnnoStatus: + return r.SuspendUserResource(ctx, name) + case types.FinalDeletionDebtNamespaceAnnoStatus: + return r.DeleteUserResource(ctx, name) + case types.ResumeDebtNamespaceAnnoStatus: + return r.ResumeUserResource(ctx, name) + } + } + // Otherwise execute network action if needed + if !networkCompletedStates[networkStatus] { + switch networkStatus { + case types.NetworkSuspend: + return r.SuspendUserResource(ctx, name) + case types.NetworkResume: + return r.ResumeUserResource(ctx, name) + } + } + return nil + }, + actionName: func() string { + if !debtCompletedStates[debtStatus] { + if debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus { + return deleteConst + } + return "suspend/resume" + } + return "suspend/resume" + }(), + }, + } + + // perform state transition + for _, t := range transitions { + if t.condition() { + if t.action != nil { + if result, err := performAction(t.action, t.actionName); err != nil { + if errors2.Is(err, errFinalDeletionCancelled) { + logger.Info("final deletion cancelled by current namespace status") + return ctrl.Result{}, nil + } + return result, err + } + } + if t.newDebt != "" || t.newNetwork != "" { + r.Log.Info( + "update namespace anno ", + "old debt status", + debtStatus, + "new debt status", + t.newDebt, + "old network status", + networkStatus, + "network status", + t.newNetwork, + ) + return updateAnnotations(t.newDebt, t.newNetwork) + } + return ctrl.Result{}, nil + } + } + + // Default: The status is completed or does not require processing + logger.Info("No action required", "debtStatus", debtStatus, "networkStatus", networkStatus) + return ctrl.Result{}, nil +} + +func (r *NamespaceReconciler) SuspendUserResource(ctx context.Context, namespace string) error { + // IMPORTANT: The order of operations matters! + // 1. suspendOrphanPod must run FIRST because it needs to recreate pods with the debt scheduler, + // which requires pod creation permissions. + // 2. limitResourceQuotaCreate runs immediately after to quickly block all new resource creation, + // preventing any new workloads from being created during the suspension process. + pipelines := []func(context.Context, string) error{ + r.suspendOrphanPod, // Recreate orphan pods with debt scheduler (requires pod creation) + r.limitResourceQuotaCreate, // Create resource quota to block all new resources (must be after suspendOrphanPod) + r.suspendKBCluster, // Stop KubeBlocks clusters and disable backup + r.suspendDevboxes, // Stop devboxes and save original state + r.suspendCertificates, // Disable cert-manager certificate renewal + r.suspendIngresses, // Pause ingresses by changing ingress class to "pause" + r.suspendOrphanDeployments, // Scale orphan deployments to 0 replicas + r.suspendOrphanStatefulSets, // Scale orphan statefulsets to 0 replicas + r.suspendOrphanReplicaSets, // Scale orphan replicasets to 0 replicas + r.suspendOrphanCronJob, // Suspend orphan cronjobs + r.suspendOrphanJob, // Suspend orphan jobs + r.deleteControlledPod, // Delete controlled pods + r.suspendObjectStorage, // Disable object storage access + } + var errs []error + for _, fn := range pipelines { + if err := fn(ctx, namespace); err != nil { + errs = append(errs, err) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) deleteBackup(ctx context.Context, namespace string) error { + gvr := schema.GroupVersionResource{ + Group: "dataprotection.kubeblocks.io", + Version: "v1alpha1", + Resource: "backups", + } + return deleteResourceListAndWait( + ctx, + r.dynamicClient, + gvr, + namespace, + r.deleteBackupSemaphore, + func(ctx context.Context) error { + return r.ensureFinalDeletionActive(ctx, namespace) + }, + ) +} + +func (r *NamespaceReconciler) ensureFinalDeletionActive( + ctx context.Context, + namespace string, +) error { + current := &corev1.Namespace{} + if err := r.Client.Get(ctx, client.ObjectKey{Name: namespace}, current); err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf( + "%w: namespace %s no longer exists", + errFinalDeletionCancelled, + namespace, + ) + } + return fmt.Errorf( + "failed to verify final deletion status for namespace %s: %w", + namespace, + err, + ) + } + if current.Status.Phase == corev1.NamespaceTerminating { + return fmt.Errorf( + "%w: namespace %s is terminating", + errFinalDeletionCancelled, + namespace, + ) + } + if current.Annotations[types.DebtNamespaceAnnoStatusKey] != types.FinalDeletionDebtNamespaceAnnoStatus { + return fmt.Errorf( + "%w: namespace %s has debt status %q", + errFinalDeletionCancelled, + namespace, + current.Annotations[types.DebtNamespaceAnnoStatusKey], + ) + } + return nil +} + +func (r *NamespaceReconciler) DeleteUserResource(ctx context.Context, namespace string) error { + if err := r.ensureFinalDeletionActive(ctx, namespace); err != nil { + return err + } + + // Delete backup first and wait for completion + if err := r.deleteBackup(ctx, namespace); err != nil { + return err + } + + // Delete other resources with rate limiting using semaphore + deleteResources := []string{ + "cluster.apps.kubeblocks.io", "backupschedules", "devboxes", "devboxreleases", "cronjob", + "objectstorageuser", "deploy", "sts", "ds", "rs", "pvc", "Service", "Ingress", + "Issuer", "Certificate", "HorizontalPodAutoscaler", "instance", + "job", "app", "pod", + } + errChan := make(chan error, len(deleteResources)) + for _, rs := range deleteResources { + go func(resource string) { + // Acquire semaphore to limit concurrent resource deletions + select { + case r.deleteResourceSemaphore <- struct{}{}: + defer func() { + <-r.deleteResourceSemaphore // Release semaphore when done + }() + errChan <- r.deleteResource(ctx, resource, namespace) + case <-ctx.Done(): + errChan <- ctx.Err() + } + }(rs) + } + // Cancellation is expected when recharge changes the namespace status while + // deletion workers are still running. Record it and collect sibling results + // so this expected cancellation does not become a reconcile failure. + wasCancelled := false + var allErrors []error + for range deleteResources { + if err := <-errChan; err != nil { + if errors2.Is(err, errFinalDeletionCancelled) { + wasCancelled = true + continue + } + allErrors = append(allErrors, err) + } + } + if len(allErrors) > 0 { + return errors2.Join(allErrors...) + } + if wasCancelled { + return errFinalDeletionCancelled + } + return nil +} + +func (r *NamespaceReconciler) ResumeUserResource(ctx context.Context, namespace string) error { + pipelines := []func(context.Context, string) error{ + r.limitResourceQuotaDelete, // Remove resource quota + r.resumeOrphanPod, // Resume orphan pods + r.resumeKBCluster, // Start KubeBlocks clusters and restore backup + r.resumeDevboxes, // Restore devboxes to original state + r.resumeOrphanReplicaSets, // Restore orphan replicaset replicas + r.resumeOrphanDeployments, // Restore orphan deployment replicas + r.resumeOrphanStatefulSets, // Restore orphan statefulset replicas + r.resumeOrphanCronJob, // Restore orphan cronjob suspend state + r.resumeOrphanJob, // Restore orphan job suspend state + r.resumeCertificates, // Restore certificate renewal + r.resumeIngresses, // Restore ingresses by changing ingress class back + r.resumeObjectStorage, // Enable object storage access + } + var errs []error + for _, fn := range pipelines { + if err := fn(ctx, namespace); err != nil { + errs = append(errs, err) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) limitResourceQuotaCreate( + ctx context.Context, + namespace string, +) error { + limitQuota := GetLimit0ResourceQuota(namespace) + _, err := ctrl.CreateOrUpdate(ctx, r.Client, limitQuota, func() error { + return nil + }) + return err +} + +func (r *NamespaceReconciler) limitResourceQuotaDelete( + ctx context.Context, + namespace string, +) error { + limitQuota := GetLimit0ResourceQuota(namespace) + err := r.Client.Delete(ctx, limitQuota) + return client.IgnoreNotFound(err) +} + +func GetLimit0ResourceQuota(namespace string) *corev1.ResourceQuota { + quota := corev1.ResourceQuota{} + quota.Name = "debt-limit0" + quota.Namespace = namespace + quota.Spec.Hard = corev1.ResourceList{ + corev1.ResourceLimitsCPU: resource.MustParse("0"), + corev1.ResourceLimitsMemory: resource.MustParse("0"), + corev1.ResourceRequestsStorage: resource.MustParse("0"), + } + return "a +} + +func (r *NamespaceReconciler) suspendKBCluster(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendKBCluster") + + // Define the GroupVersionResource for KubeBlocks clusters + clusterGVR := schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "clusters", + } + + // List all clusters in the namespace + clusterList, err := r.dynamicClient.Resource(clusterGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list clusters in namespace %s: %w", namespace, err) + } + + // Define the GroupVersionResource for OpsRequests + opsGVR := schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "opsrequests", + } + + // Iterate through each cluster + for _, cluster := range clusterList.Items { + clusterName := cluster.GetName() + logger.V(1).Info("Processing cluster", "Cluster", clusterName) + + annotations := cluster.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Check if cluster is already stopped + isAlreadyStopped := false + status, exists := cluster.Object["status"] + if exists && status != nil { + phase, _ := status.(map[string]any)["phase"].(string) + if phase == "Stopped" || phase == "Stopping" { + isAlreadyStopped = true + logger.V(1). + Info("Cluster already stopped or stopping", "Cluster", clusterName) + } + } + + // Get current backup configuration (only once, used for both saving state and disabling backup) + backupEnabled := false + backup, hasBackup, err := unstructured.NestedMap(cluster.Object, "spec", "backup") + if err != nil { + logger.Error(err, "failed to get backup config", "cluster", clusterName) + } else if hasBackup && backup != nil { + enabled, found, _ := unstructured.NestedBool( + cluster.Object, + "spec", + "backup", + "enabled", + ) + if found { + backupEnabled = enabled + } + // If enabled field doesn't exist, default is false (backup disabled) + } + + // Track if cluster needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + // Determine if cluster was running (not Stopped or Stopping) + wasRunning := !isAlreadyStopped + + originalState := &KBClusterOriginalState{ + WasRunning: wasRunning, + BackupEnabled: backupEnabled, + } + stateJSON, err := encodeKBClusterState(originalState) + if err != nil { + logger.Error(err, "failed to encode cluster state", "cluster", clusterName) + return fmt.Errorf("failed to encode cluster state for %s: %w", clusterName, err) + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved cluster state", + "cluster", + clusterName, + "wasRunning", + wasRunning, + "backupEnabled", + backupEnabled, + ) + } else { + logger.V(1). + Info("Cluster already has original state, skipping state save", "Cluster", clusterName) + } + + // Disable backup if it exists and is enabled + if hasBackup && backup != nil && backupEnabled { + if err := unstructured.SetNestedField( + cluster.Object, + false, + "spec", + "backup", + "enabled", + ); err != nil { + logger.Error(err, "failed to set backup.enabled=false", "cluster", clusterName) + } else { + logger.Info("Disabled backup for cluster", "cluster", clusterName) + needsUpdate = true + } + } + + // Update cluster only if there are actual changes + if needsUpdate { + cluster.SetAnnotations(annotations) + _, err = r.dynamicClient.Resource(clusterGVR). + Namespace(namespace). + Update(ctx, &cluster, v12.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update cluster", "cluster", clusterName) + return fmt.Errorf("failed to update cluster %s: %w", clusterName, err) + } + } else { + logger.V(1). + Info("No changes needed for cluster, skipping update", "Cluster", clusterName) + } + + // Skip OpsRequest creation if cluster is already stopped or stopping + if isAlreadyStopped { + logger.V(1). + Info("Skipping OpsRequest creation for already stopped cluster", "Cluster", clusterName) + continue + } + + // Create OpsRequest resource to stop the cluster + opsName := fmt.Sprintf("stop-%s-%s", clusterName, time.Now().Format("2006-01-02-15")) + opsRequest := &unstructured.Unstructured{} + opsRequest.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Kind: "OpsRequest", + }) + opsRequest.SetNamespace(namespace) + opsRequest.SetName(opsName) + + // Set OpsRequest spec + opsSpec := map[string]any{ + "clusterRef": clusterName, + "type": "Stop", + "ttlSecondsAfterSucceed": int64(1), + "ttlSecondsBeforeAbort": int64(60 * 60), + } + if err := unstructured.SetNestedField(opsRequest.Object, opsSpec, "spec"); err != nil { + return fmt.Errorf( + "failed to set spec for OpsRequest %s in namespace %s: %w", + opsName, + namespace, + err, + ) + } + + _, err = r.dynamicClient.Resource(opsGVR). + Namespace(namespace). + Create(ctx, opsRequest, v12.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf( + "failed to create OpsRequest %s in namespace %s: %w", + opsName, + namespace, + err, + ) + } + if errors.IsAlreadyExists(err) { + logger.V(1).Info("OpsRequest already exists, skipping creation", "OpsRequest", opsName) + } + } + return nil +} + +// func (r *NamespaceReconciler) suspendKBCluster(ctx context.Context, namespace string) error { +// kbClusterList := kbv1alpha1.ClusterList{} +// if err := r.Client.List(ctx, &kbClusterList, client.InNamespace(namespace)); err != nil { +// return err +// } +// for _, kbCluster := range kbClusterList.Items { +// if kbCluster.Status.Phase == kbv1alpha1.StoppedClusterPhase || kbCluster.Status.Phase == kbv1alpha1.StoppingClusterPhase { +// continue +// } +// ops := kbv1alpha1.OpsRequest{} +// ops.Namespace = kbCluster.Namespace +// ops.ObjectMeta.Name = "stop-" + kbCluster.Name + "-" + time.Now().Format("2006-01-02-15") +// ops.Spec.TTLSecondsAfterSucceed = 1 +// abort := int32(60 * 60) +// ops.Spec.TTLSecondsBeforeAbort = &abort +// ops.Spec.ClusterRef = kbCluster.Name +// ops.Spec.Type = "Stop" +// _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &ops, func() error { +// return nil +// }) +// if err != nil { +// r.Log.Error(err, "create ops request failed", "ops", ops.Name, "namespace", ops.Namespace) +// } +// } +// return nil +//} + +// hasController checks if a resource has a controller owner reference +func hasController(ownerRefs []v12.OwnerReference) bool { + for _, ref := range ownerRefs { + if ref.Controller != nil && *ref.Controller { + return true + } + } + return false +} + +// deployViaFrontend checks if a deployment/statefulset was created via frontend +// by checking for the presence of frontend-specific annotations +func deployViaFrontend(annotations map[string]string) bool { + if annotations == nil { + return false + } + hasMinReplicas := annotations[MinReplicasKey] != "" + hasMaxReplicas := annotations[MaxReplicasKey] != "" + hasResize := annotations[DeployPVCResizeKey] != "" + return hasMinReplicas || hasMaxReplicas || hasResize +} + +// suspendHPA handles HPA suspension logic: reads HPA config, saves to pause annotation, and deletes HPA +// Returns true if annotations were modified, false otherwise +func (r *NamespaceReconciler) suspendHPA( + ctx context.Context, + namespace string, + resourceName string, + resourceType string, + annotations map[string]string, + logger logr.Logger, +) (bool, error) { + // Try to read HPA + hpa := &autoscalingv2.HorizontalPodAutoscaler{} + hpaKey := client.ObjectKey{Name: resourceName, Namespace: namespace} + err := r.Client.Get(ctx, hpaKey, hpa) + + if err == nil { + // HPA exists, save its configuration and delete it + pauseData := &PauseData{ + Target: "", + Value: "", + } + + // Extract HPA configuration + if len(hpa.Spec.Metrics) > 0 && hpa.Spec.Metrics[0].Resource != nil { + pauseData.Target = string(hpa.Spec.Metrics[0].Resource.Name) + if hpa.Spec.Metrics[0].Resource.Target.AverageUtilization != nil { + pauseData.Value = strconv.Itoa( + int(*hpa.Spec.Metrics[0].Resource.Target.AverageUtilization), + ) + } + } + + // Save pause data to annotation + pauseJSON, err := json.Marshal(pauseData) + if err != nil { + logger.Error(err, "failed to marshal pause data", resourceType, resourceName) + return false, fmt.Errorf("failed to marshal pause data for %s: %w", resourceName, err) + } + annotations[PauseKey] = string(pauseJSON) + + // Delete HPA + if err := r.Client.Delete(ctx, hpa); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "failed to delete HPA", resourceType, resourceName) + return false, fmt.Errorf("failed to delete HPA for %s: %w", resourceName, err) + } + logger.Info( + "Deleted HPA and saved pause data", + resourceType, + resourceName, + "pauseData", + string(pauseJSON), + ) + return true, nil + } else if !errors.IsNotFound(err) { + // Error reading HPA (not just not found) + logger.Error(err, "failed to get HPA", resourceType, resourceName) + return false, fmt.Errorf("failed to get HPA for %s: %w", resourceName, err) + } + + // HPA not found, set empty pause annotation + pauseData := &PauseData{ + Target: "", + Value: "", + } + //nolint:errchkjson + pauseJSON, _ := json.Marshal(pauseData) + annotations[PauseKey] = string(pauseJSON) + logger.V(1).Info("HPA not found, setting empty pause annotation", resourceType, resourceName) + return true, nil +} + +// resumeHPA handles HPA restoration logic: reads pause annotation and creates HPA if needed +// Returns true if annotations were modified, false otherwise +func (r *NamespaceReconciler) resumeHPA( + ctx context.Context, + namespace string, + resourceName string, + resourceType string, + resourceKind string, + annotations map[string]string, + logger logr.Logger, +) (bool, error) { + pauseJSON, hasPause := annotations[PauseKey] + if !hasPause { + return false, nil + } + + var pauseData PauseData + if err := json.Unmarshal([]byte(pauseJSON), &pauseData); err != nil { + logger.Error(err, "failed to unmarshal pause data", resourceType, resourceName) + return false, nil + } + + if pauseData.Target == "" { + // No HPA config to restore, just remove pause annotation + delete(annotations, PauseKey) + return true, nil + } + + // Restore HPA + minReplicasStr := annotations[MinReplicasKey] + maxReplicasStr := annotations[MaxReplicasKey] + + if minReplicasStr == "" || maxReplicasStr == "" { + // Missing replica configuration, just remove pause annotation + delete(annotations, PauseKey) + return true, nil + } + + var minReplicas, maxReplicas, targetValue int32 + if val, err := strconv.Atoi(minReplicasStr); err == nil { + //nolint:gosec + minReplicas = int32(val) + } + if val, err := strconv.Atoi(maxReplicasStr); err == nil { + //nolint:gosec + maxReplicas = int32(val) + } + if val, err := strconv.Atoi(pauseData.Value); err == nil { + //nolint:gosec + targetValue = int32(val) + } + + // Create HPA + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v12.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: resourceKind, + Name: resourceName, + }, + MinReplicas: &minReplicas, + MaxReplicas: maxReplicas, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceName(pauseData.Target), + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: &targetValue, + }, + }, + }, + }, + }, + } + + if err := r.Client.Create(ctx, hpa); err != nil && !errors.IsAlreadyExists(err) { + logger.Error(err, "failed to create HPA", resourceType, resourceName) + return false, fmt.Errorf("failed to create HPA for %s: %w", resourceName, err) + } + logger.Info( + "Restored HPA", + resourceType, + resourceName, + "target", + pauseData.Target, + "value", + pauseData.Value, + ) + + // Remove pause annotation + delete(annotations, PauseKey) + return true, nil +} + +func (r *NamespaceReconciler) suspendOrphanPod(ctx context.Context, namespace string) error { + podList := corev1.PodList{} + if err := r.Client.List(ctx, &podList, client.InNamespace(namespace)); err != nil { + return err + } + for _, pod := range podList.Items { + if pod.Spec.SchedulerName == v1.DebtSchedulerName || hasController(pod.OwnerReferences) { + continue + } + clone := pod.DeepCopy() + clone.ResourceVersion = "" + clone.Spec.NodeName = "" + clone.Status = corev1.PodStatus{} + clone.Spec.SchedulerName = v1.DebtSchedulerName + if clone.Annotations == nil { + clone.Annotations = make(map[string]string) + } + clone.Annotations[v1.PreviousSchedulerName] = pod.Spec.SchedulerName + err := r.recreatePod(ctx, pod, clone) + if err != nil { + return fmt.Errorf("recreate unowned pod `%s` failed: %w", pod.Name, err) + } + } + return nil +} + +func (r *NamespaceReconciler) deleteControlledPod(ctx context.Context, namespace string) error { + podList := corev1.PodList{} + if err := r.Client.List(ctx, &podList, client.InNamespace(namespace)); err != nil { + return err + } + for _, pod := range podList.Items { + if pod.Spec.SchedulerName == v1.DebtSchedulerName || len(pod.OwnerReferences) == 0 { + r.Log.Info("skip pod", "pod", pod.Name) + continue + } + r.Log.Info("delete pod", "pod", pod.Name) + err := r.Client.Delete(ctx, &pod) + if err != nil { + return err + } + } + return nil +} + +func (r *NamespaceReconciler) resumeOrphanPod(ctx context.Context, namespace string) error { + var list corev1.PodList + if err := r.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil { + return err + } + deleteCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + for _, pod := range list.Items { + if pod.Status.Phase != v1.PodPhaseSuspended || + pod.Spec.SchedulerName != v1.DebtSchedulerName { + continue + } + + // Skip if this pod has a controller (not an orphan) + if hasController(pod.OwnerReferences) { + continue + } + + // Only resume orphan pods + clone := pod.DeepCopy() + clone.ResourceVersion = "" + clone.Spec.NodeName = "" + clone.Status = corev1.PodStatus{} + if scheduler, ok := clone.Annotations[v1.PreviousSchedulerName]; ok { + clone.Spec.SchedulerName = scheduler + delete(clone.Annotations, v1.PreviousSchedulerName) + } else { + clone.Spec.SchedulerName = "" + } + err := r.recreatePod(deleteCtx, pod, clone) + if err != nil { + return fmt.Errorf("recreate orphan pod %s failed: %w", pod.Name, err) + } + } + return nil +} + +func (r *NamespaceReconciler) recreatePod( + ctx context.Context, + oldPod corev1.Pod, + newPod *corev1.Pod, +) error { + list := corev1.PodList{} + watcher, err := r.Client.Watch(ctx, &list, client.InNamespace(oldPod.Namespace)) + if err != nil { + return fmt.Errorf("failed to start watch stream for pod %s: %w", oldPod.Name, err) + } + ch := watcher.ResultChan() + err = r.Client.Delete(ctx, &oldPod) + if err != nil { + return fmt.Errorf("failed to delete pod %s: %w", oldPod.Name, err) + } + for event := range ch { + if event.Type == watch.Deleted { + if val, ok := event.Object.(*corev1.Pod); ok && val.Name == oldPod.Name { + err = r.Client.Create(ctx, newPod) + if err != nil { + return fmt.Errorf("failed to recreate pod %s: %w", newPod.Name, err) + } + watcher.Stop() + break + } + } + } + return nil +} + +func (r *NamespaceReconciler) resumeKBCluster(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeKBCluster") + + clusterGVR := schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "clusters", + } + + clusterList, err := r.dynamicClient.Resource(clusterGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list clusters in namespace %s: %w", namespace, err) + } + + opsGVR := schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "opsrequests", + } + + for _, cluster := range clusterList.Items { + clusterName := cluster.GetName() + annotations := cluster.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *KBClusterOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore to running state + logger.Info( + "Cluster has no suspend state, using defaults to restore", + "Cluster", + clusterName, + ) + } else { + // Decode original state + var err error + originalState, err = decodeKBClusterState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode cluster state", "cluster", clusterName) + } + } + if originalState == nil { + originalState = getDefaultKBClusterState() + } + + logger.Info( + "Resuming cluster", + "cluster", + clusterName, + "wasRunning", + originalState.WasRunning, + "originalBackupEnabled", + originalState.BackupEnabled, + ) + + // Track if cluster needs update + needsUpdate := false + + // Restore backup configuration only if it was originally enabled + if originalState.BackupEnabled { + backup, hasBackup, err := unstructured.NestedMap(cluster.Object, "spec", "backup") + if err != nil { + logger.Error(err, "failed to get backup config", "cluster", clusterName) + } else if hasBackup && backup != nil { + if err := unstructured.SetNestedField( + cluster.Object, + true, + "spec", + "backup", + "enabled", + ); err != nil { + logger.Error(err, "failed to restore backup.enabled", "cluster", clusterName) + } else { + logger.Info( + "Restored backup enabled state", + "cluster", + clusterName, + "enabled", + true, + ) + needsUpdate = true + } + } + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update cluster only if there are actual changes + if needsUpdate { + cluster.SetAnnotations(annotations) + _, err := r.dynamicClient.Resource(clusterGVR). + Namespace(namespace). + Update(ctx, &cluster, v12.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update cluster", "cluster", clusterName) + return fmt.Errorf("failed to update cluster %s: %w", clusterName, err) + } + } + + // Start the cluster if it was running before suspension + // No need to check current phase since we're resuming from suspended state + if originalState.WasRunning { + opsName := fmt.Sprintf( + "start-%s-%s", + clusterName, + time.Now().Format("2006-01-02-15"), + ) + opsRequest := &unstructured.Unstructured{} + opsRequest.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Kind: "OpsRequest", + }) + opsRequest.SetNamespace(namespace) + opsRequest.SetName(opsName) + + opsSpec := map[string]any{ + "clusterRef": clusterName, + "type": "Start", + "ttlSecondsAfterSucceed": int64(1), + "ttlSecondsBeforeAbort": int64(60 * 60), + } + if err := unstructured.SetNestedField(opsRequest.Object, opsSpec, "spec"); err != nil { + return fmt.Errorf( + "failed to set spec for OpsRequest %s in namespace %s: %w", + opsName, + namespace, + err, + ) + } + + _, err := r.dynamicClient.Resource(opsGVR). + Namespace(namespace). + Create(ctx, opsRequest, v12.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf( + "failed to create OpsRequest %s in namespace %s: %w", + opsName, + namespace, + err, + ) + } + if errors.IsAlreadyExists(err) { + logger.V(1). + Info("OpsRequest already exists, skipping creation", "OpsRequest", opsName) + } else { + logger.Info( + "Created start OpsRequest for cluster", + "cluster", + clusterName, + "opsRequest", + opsName, + ) + } + } + } + return nil +} + +func (r *NamespaceReconciler) suspendObjectStorage(ctx context.Context, namespace string) error { + split := strings.Split(namespace, "-") + user := split[1] + err := r.setOSUserStatus(ctx, user, Disabled) + if err != nil { + r.Log.Error(err, "failed to suspend object storage", "user", user) + return err + } + return nil +} + +func (r *NamespaceReconciler) resumeObjectStorage(ctx context.Context, namespace string) error { + split := strings.Split(namespace, "-") + user := split[1] + err := r.setOSUserStatus(ctx, user, Enabled) + if err != nil { + r.Log.Error(err, "failed to resume object storage", "user", user) + return err + } + return nil +} + +func (r *NamespaceReconciler) setOSUserStatus(ctx context.Context, user, status string) error { + if r.InternalEndpoint == "" || r.OSNamespace == "" || r.OSAdminSecret == "" { + r.Log.V(1).Info("the endpoint or namespace or admin secret env of object storage is nil") + return nil + } + if r.OSAdminClient == nil { + secret := &corev1.Secret{} + if err := r.Client.Get( + ctx, + client.ObjectKey{Name: r.OSAdminSecret, Namespace: r.OSNamespace}, + secret, + ); err != nil { + r.Log.Error( + err, + "failed to get secret", + "name", + r.OSAdminSecret, + "namespace", + r.OSNamespace, + ) + return err + } + accessKey := string(secret.Data[OSAccessKey]) + secretKey := string(secret.Data[OSSecretKey]) + oSAdminClient, err := objectstorage.NewOSAdminClient( + r.InternalEndpoint, + accessKey, + secretKey, + ) + if err != nil { + r.Log.Error(err, "failed to new object storage admin client") + return err + } + r.OSAdminClient = oSAdminClient + } + users, err := r.OSAdminClient.ListUsers(ctx) + if err != nil { + r.Log.Error(err, "failed to list minio user", "user", user) + return err + } + if _, ok := users[user]; !ok { + return nil + } + err = r.OSAdminClient.SetUserStatus(ctx, user, madmin.AccountStatus(status)) + if err != nil { + r.Log.Error(err, "failed to set user status", "user", user, "status", status) + return err + } + return nil +} + +func (r *NamespaceReconciler) SetupWithManager( + mgr ctrl.Manager, + limitOps controller.Options, + deleteResourceConcurrent int, + deleteBackupConcurrent int, +) error { + r.Log = ctrl.Log.WithName("controllers").WithName("Namespace") + r.OSAdminSecret = os.Getenv(OSAdminSecret) + r.InternalEndpoint = os.Getenv(OSInternalEndpointEnv) + r.OSNamespace = os.Getenv(OSNamespace) + + // Initialize semaphore for resource deletion rate limiting + if deleteResourceConcurrent <= 0 { + deleteResourceConcurrent = 3 + } + r.deleteResourceSemaphore = make(chan struct{}, deleteResourceConcurrent) + r.Log.Info("Initialized resource deletion semaphore", "concurrency", deleteResourceConcurrent) + + // Initialize semaphore for backup deletion rate limiting + if deleteBackupConcurrent <= 0 { + deleteBackupConcurrent = 30 + } + r.deleteBackupSemaphore = make(chan struct{}, deleteBackupConcurrent) + r.Log.Info("Initialized backup deletion semaphore", "concurrency", deleteBackupConcurrent) + + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to load in-cluster config: %w", err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create dynamic client: %w", err) + } + r.dynamicClient = dynamicClient + + // Detect devbox CRD version (v1alpha2 preferred, fallback to v1alpha1) + r.devboxVersion = r.detectDevboxVersion() + if r.devboxVersion != "" { + r.Log.Info("Detected devbox CRD", "version", r.devboxVersion) + } else { + r.Log.Info("Devbox CRD not detected, devbox suspend/resume will be skipped") + } + + if r.OSAdminSecret == "" || r.InternalEndpoint == "" || r.OSNamespace == "" { + r.Log.V(1). + Info("failed to get the endpoint or namespace or admin secret env of object storage") + } + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Namespace{}, builder.WithPredicates(AnnotationChangedPredicate{})). + WithEventFilter(&AnnotationChangedPredicate{}). + WithOptions(limitOps). + Complete(r) +} + +type AnnotationChangedPredicate struct { + predicate.Funcs +} + +func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool { + oldObj, ok1 := e.ObjectOld.(*corev1.Namespace) + newObj, ok2 := e.ObjectNew.(*corev1.Namespace) + if !ok1 || !ok2 || newObj.Annotations == nil { + return false + } + + oldDebtStatus := oldObj.Annotations[types.DebtNamespaceAnnoStatusKey] + newDebtStatus := newObj.Annotations[types.DebtNamespaceAnnoStatusKey] + oldNetworkStatus := oldObj.Annotations[types.NetworkStatusAnnoKey] + newNetworkStatus := newObj.Annotations[types.NetworkStatusAnnoKey] + oldFinalDeletionReplay := oldObj.Annotations[types.FinalDeletionReplayAnnotationKey] + newFinalDeletionReplay := newObj.Annotations[types.FinalDeletionReplayAnnotationKey] + + debtChanged := oldDebtStatus != newDebtStatus && !isDebtCompleted(newDebtStatus) + networkChanged := oldNetworkStatus != newNetworkStatus && !isNetworkCompleted(newNetworkStatus) + replayChanged := oldFinalDeletionReplay != newFinalDeletionReplay && + newFinalDeletionReplay != "" + + return debtChanged || networkChanged || replayChanged +} + +func (AnnotationChangedPredicate) Create(e event.CreateEvent) bool { + annotations := e.Object.GetAnnotations() + debtStatus, debtExists := annotations[types.DebtNamespaceAnnoStatusKey] + networkStatus, networkExists := annotations[types.NetworkStatusAnnoKey] + + return (debtExists && !isDebtCompleted(debtStatus)) || + (networkExists && !isNetworkCompleted(networkStatus)) +} + +// Helper functions to check completed states +func isDebtCompleted(status string) bool { + return status == types.SuspendCompletedDebtNamespaceAnnoStatus || + status == types.FinalDeletionCompletedDebtNamespaceAnnoStatus || + status == types.ResumeCompletedDebtNamespaceAnnoStatus || + status == types.TerminateSuspendCompletedDebtNamespaceAnnoStatus +} + +func isNetworkCompleted(status string) bool { + return status == types.NetworkSuspendCompleted || status == types.NetworkResumeCompleted +} + +func (r *NamespaceReconciler) suspendOrphanCronJob(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendOrphanCronJob") + + cronJobList := batchv1.CronJobList{} + if err := r.Client.List(ctx, &cronJobList, client.InNamespace(namespace)); err != nil { + return err + } + + var errs []error + for _, cronJob := range cronJobList.Items { + // Skip if this cronjob has a controller (not an orphan) + if hasController(cronJob.OwnerReferences) { + logger.V(1).Info("CronJob has controller, skipping", "CronJob", cronJob.Name) + continue + } + + annotations := cronJob.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current suspend state + currentlySuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend + + // Track if cronjob needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + originalState := &CronJobOriginalState{ + Suspend: currentlySuspended, + } + stateJSON, err := encodeCronJobState(originalState) + if err != nil { + logger.Error(err, "failed to encode cronjob state", "cronjob", cronJob.Name) + errs = append( + errs, + fmt.Errorf("failed to encode cronjob state for %s: %w", cronJob.Name, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved cronjob state", + "cronjob", + cronJob.Name, + "originalSuspend", + currentlySuspended, + ) + } else { + logger.V(1). + Info("CronJob already has original state, skipping state save", "CronJob", cronJob.Name) + } + + // Set suspend to true if not already suspended + if !currentlySuspended { + cronJob.Spec.Suspend = ptr.To(true) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + cronJob.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &cronJob); err != nil { + errs = append( + errs, + fmt.Errorf("failed to suspend cronjob %s: %w", cronJob.Name, err), + ) + continue + } + logger.V(1).Info("Suspended cronjob", "cronjob", cronJob.Name) + } else { + logger.V(1).Info("CronJob already suspended, skipping update", "cronjob", cronJob.Name) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeOrphanCronJob(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeOrphanCronJob") + + cronJobList := batchv1.CronJobList{} + if err := r.Client.List(ctx, &cronJobList, client.InNamespace(namespace)); err != nil { + return err + } + + var errs []error + for _, cronJob := range cronJobList.Items { + // Skip if this cronjob has a controller (not an orphan) + if hasController(cronJob.OwnerReferences) { + logger.V(1).Info("CronJob has controller, skipping", "CronJob", cronJob.Name) + continue + } + + annotations := cronJob.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *CronJobOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: resume to not suspended + logger.Info( + "CronJob has no suspend state, using defaults to restore", + "CronJob", + cronJob.Name, + ) + } else { + // Decode original state + var err error + originalState, err = decodeCronJobState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode cronjob state", "cronjob", cronJob.Name) + } + } + if originalState == nil { + originalState = getDefaultCronJobState() + } + + logger.Info( + "Resuming cronjob", + "cronjob", + cronJob.Name, + "originalSuspend", + originalState.Suspend, + ) + + // Track if cronjob needs update + needsUpdate := false + + // Restore original suspend state + if !originalState.Suspend { + cronJob.Spec.Suspend = ptr.To(originalState.Suspend) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + cronJob.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &cronJob); err != nil { + errs = append( + errs, + fmt.Errorf("failed to resume cronjob %s: %w", cronJob.Name, err), + ) + continue + } + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) suspendOrphanDeployments( + ctx context.Context, + namespace string, +) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendOrphanDeployments") + + deployList := appsv1.DeploymentList{} + if err := r.Client.List(ctx, &deployList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list deployments: %w", err) + } + + var errs []error + for _, deploy := range deployList.Items { + // Skip if this deployment has a controller (not an orphan) + if hasController(deploy.OwnerReferences) { + logger.V(1).Info("Deployment has controller, skipping", "Deployment", deploy.Name) + continue + } + + annotations := deploy.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current replicas + var replicas int32 + if deploy.Spec.Replicas != nil { + replicas = *deploy.Spec.Replicas + } else { + // If replicas not specified, Kubernetes defaults to 1 + replicas = 1 + } + + // Skip if already 0 replicas and state already saved + if replicas == 0 && hasOriginalState { + logger.V(1).Info("Deployment already suspended, skipping", "Deployment", deploy.Name) + continue + } + + // Track if deployment needs update + needsUpdate := false + + // Handle HPA if this deployment was created via frontend + if deployViaFrontend(annotations) { + hpaModified, err := r.suspendHPA( + ctx, + namespace, + deploy.Name, + "deployment", + annotations, + logger, + ) + if err != nil { + logger.Error( + err, + "failed to suspend HPA, continuing with next deployment", + "deployment", + deploy.Name, + ) + continue + } + if hpaModified { + needsUpdate = true + } + } + + // Save original state only if not already saved + if !hasOriginalState { + originalState := &DeploymentOriginalState{ + Replicas: replicas, + } + stateJSON, err := encodeDeploymentState(originalState) + if err != nil { + logger.Error(err, "failed to encode deployment state", "deployment", deploy.Name) + errs = append( + errs, + fmt.Errorf("failed to encode deployment state for %s: %w", deploy.Name, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved orphan deployment state", + "deployment", + deploy.Name, + "originalReplicas", + replicas, + ) + } else { + logger.V(1). + Info("Deployment already has original state, skipping state save", "Deployment", deploy.Name) + } + + // Set replicas to 0 if not already 0 + if replicas != 0 { + deploy.Spec.Replicas = ptr.To(int32(0)) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + deploy.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &deploy); err != nil { + errs = append( + errs, + fmt.Errorf("failed to update deployment %s: %w", deploy.Name, err), + ) + continue + } + logger.V(1).Info("Suspended orphan deployment", "deployment", deploy.Name) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeOrphanDeployments(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeOrphanDeployments") + + deployList := appsv1.DeploymentList{} + if err := r.Client.List(ctx, &deployList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list deployments: %w", err) + } + + var errs []error + for _, deploy := range deployList.Items { + // Skip if this deployment has a controller (not an orphan) + if hasController(deploy.OwnerReferences) { + logger.V(1).Info("Deployment has controller, skipping", "Deployment", deploy.Name) + continue + } + + annotations := deploy.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *DeploymentOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore to 1 replica + logger.Info( + "Deployment has no suspend state, using defaults to restore", + "Deployment", + deploy.Name, + ) + } else { + // Decode original state + var err error + originalState, err = decodeDeploymentState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode deployment state", "deployment", deploy.Name) + } + } + if originalState == nil { + originalState = getDefaultDeploymentState() + } + + logger.Info( + "Resuming deployment", + "deployment", + deploy.Name, + "originalReplicas", + originalState.Replicas, + ) + + // Track if deployment needs update + needsUpdate := false + + // Restore original replicas only if not 0 + if originalState.Replicas != 0 { + deploy.Spec.Replicas = ptr.To(originalState.Replicas) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Restore HPA only if original replicas was not 0 (meaning it was actually running before suspension) + // Call resumeHPA before update to modify annotations in one go + if originalState.Replicas != 0 { + hpaModified, err := r.resumeHPA( + ctx, + namespace, + deploy.Name, + "deployment", + "Deployment", + annotations, + logger, + ) + if err != nil { + logger.Error( + err, + "failed to resume HPA, continuing with next resource", + "deployment", + deploy.Name, + ) + continue + } + if hpaModified { + needsUpdate = true + } + } + + // Update only if there are actual changes (single update with all changes) + if needsUpdate { + deploy.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &deploy); err != nil { + errs = append( + errs, + fmt.Errorf("failed to update deployment %s: %w", deploy.Name, err), + ) + continue + } + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) suspendOrphanStatefulSets( + ctx context.Context, + namespace string, +) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendOrphanStatefulSets") + + stsList := appsv1.StatefulSetList{} + if err := r.Client.List(ctx, &stsList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list statefulsets: %w", err) + } + + var errs []error + for _, sts := range stsList.Items { + // Skip if this statefulset has a controller (not an orphan) + if hasController(sts.OwnerReferences) { + logger.V(1).Info("StatefulSet has controller, skipping", "StatefulSet", sts.Name) + continue + } + + annotations := sts.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current replicas + var replicas int32 + if sts.Spec.Replicas != nil { + replicas = *sts.Spec.Replicas + } else { + // If replicas not specified, Kubernetes defaults to 1 + replicas = 1 + } + + // Skip if already 0 replicas and state already saved + if replicas == 0 && hasOriginalState { + logger.V(1).Info("StatefulSet already suspended, skipping", "StatefulSet", sts.Name) + continue + } + + // Track if statefulset needs update + needsUpdate := false + + // Handle HPA if this statefulset was created via frontend + if deployViaFrontend(annotations) { + hpaModified, err := r.suspendHPA( + ctx, + namespace, + sts.Name, + "statefulset", + annotations, + logger, + ) + if err != nil { + logger.Error( + err, + "failed to suspend HPA, continuing with next statefulset", + "statefulset", + sts.Name, + ) + continue + } + if hpaModified { + needsUpdate = true + } + } + + // Save original state only if not already saved + if !hasOriginalState { + originalState := &DeploymentOriginalState{ + Replicas: replicas, + } + stateJSON, err := encodeDeploymentState(originalState) + if err != nil { + logger.Error(err, "failed to encode statefulset state", "statefulset", sts.Name) + errs = append( + errs, + fmt.Errorf("failed to encode statefulset state for %s: %w", sts.Name, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved orphan statefulset state", + "statefulset", + sts.Name, + "originalReplicas", + replicas, + ) + } else { + logger.V(1). + Info("StatefulSet already has original state, skipping state save", "StatefulSet", sts.Name) + } + + // Set replicas to 0 if not already 0 + if replicas != 0 { + sts.Spec.Replicas = ptr.To(int32(0)) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + sts.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &sts); err != nil { + errs = append( + errs, + fmt.Errorf("failed to update statefulset %s: %w", sts.Name, err), + ) + continue + } + logger.V(1).Info("Suspended orphan statefulset", "statefulset", sts.Name) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeOrphanStatefulSets( + ctx context.Context, + namespace string, +) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeOrphanStatefulSets") + + stsList := appsv1.StatefulSetList{} + if err := r.Client.List(ctx, &stsList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list statefulsets: %w", err) + } + + var errs []error + for _, sts := range stsList.Items { + // Skip if this statefulset has a controller (not an orphan) + if hasController(sts.OwnerReferences) { + logger.V(1).Info("StatefulSet has controller, skipping", "StatefulSet", sts.Name) + continue + } + + annotations := sts.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *DeploymentOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore to 1 replica + logger.Info( + "StatefulSet has no suspend state, using defaults to restore", + "StatefulSet", + sts.Name, + ) + } else { + // Decode original state + var err error + originalState, err = decodeDeploymentState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode statefulset state", "statefulset", sts.Name) + } + } + if originalState == nil { + originalState = getDefaultDeploymentState() + } + + logger.Info( + "Resuming statefulset", + "statefulset", + sts.Name, + "originalReplicas", + originalState.Replicas, + ) + + // Track if statefulset needs update + needsUpdate := false + + // Restore original replicas only if not 0 + if originalState.Replicas != 0 { + sts.Spec.Replicas = ptr.To(originalState.Replicas) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Restore HPA only if original replicas was not 0 (meaning it was actually running before suspension) + // Call resumeHPA before update to modify annotations in one go + if originalState.Replicas != 0 { + hpaModified, err := r.resumeHPA( + ctx, + namespace, + sts.Name, + "statefulset", + "StatefulSet", + annotations, + logger, + ) + if err != nil { + logger.Error( + err, + "failed to resume HPA, continuing with next resource", + "statefulset", + sts.Name, + ) + continue + } + if hpaModified { + needsUpdate = true + } + } + + // Update only if there are actual changes (single update with all changes) + if needsUpdate { + sts.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &sts); err != nil { + errs = append( + errs, + fmt.Errorf("failed to update statefulset %s: %w", sts.Name, err), + ) + continue + } + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) suspendOrphanReplicaSets( + ctx context.Context, + namespace string, +) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendOrphanReplicaSets") + + rsList := appsv1.ReplicaSetList{} + if err := r.Client.List(ctx, &rsList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list replicasets: %w", err) + } + + var errs []error + for _, rs := range rsList.Items { + // Skip if this replicaset has a controller (not an orphan) + if hasController(rs.OwnerReferences) { + logger.V(1).Info("ReplicaSet has controller, skipping", "ReplicaSet", rs.Name) + continue + } + + annotations := rs.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current replicas + var replicas int32 + if rs.Spec.Replicas != nil { + replicas = *rs.Spec.Replicas + } else { + // If replicas not specified, Kubernetes defaults to 1 + replicas = 1 + } + + // Skip if already 0 replicas and state already saved + if replicas == 0 && hasOriginalState { + logger.V(1).Info("ReplicaSet already suspended, skipping", "ReplicaSet", rs.Name) + continue + } + + // Track if replicaset needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + originalState := &DeploymentOriginalState{ + Replicas: replicas, + } + stateJSON, err := encodeDeploymentState(originalState) + if err != nil { + logger.Error(err, "failed to encode replicaset state", "replicaset", rs.Name) + errs = append( + errs, + fmt.Errorf("failed to encode replicaset state for %s: %w", rs.Name, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved orphan replicaset state", + "replicaset", + rs.Name, + "originalReplicas", + replicas, + ) + } else { + logger.V(1). + Info("ReplicaSet already has original state, skipping state save", "ReplicaSet", rs.Name) + } + + // Set replicas to 0 if not already 0 + if replicas != 0 { + rs.Spec.Replicas = ptr.To(int32(0)) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + rs.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &rs); err != nil { + errs = append(errs, fmt.Errorf("failed to update replicaset %s: %w", rs.Name, err)) + continue + } + logger.V(1).Info("Suspended orphan replicaset", "replicaset", rs.Name) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeOrphanReplicaSets(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeOrphanReplicaSets") + + rsList := appsv1.ReplicaSetList{} + if err := r.Client.List(ctx, &rsList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list replicasets: %w", err) + } + + var errs []error + for _, rs := range rsList.Items { + // Skip if this replicaset has a controller (not an orphan) + if hasController(rs.OwnerReferences) { + logger.V(1).Info("ReplicaSet has controller, skipping", "ReplicaSet", rs.Name) + continue + } + + annotations := rs.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *DeploymentOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore to 1 replica + logger.Info( + "ReplicaSet has no suspend state, using defaults to restore", + "ReplicaSet", + rs.Name, + ) + } else { + // Decode original state + var err error + originalState, err = decodeDeploymentState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode replicaset state", "replicaset", rs.Name) + } + } + if originalState == nil { + originalState = getDefaultDeploymentState() + } + + logger.Info( + "Resuming replicaset", + "replicaset", + rs.Name, + "originalReplicas", + originalState.Replicas, + ) + + // Track if replicaset needs update + needsUpdate := false + + // Restore original replicas only if not 0 + if originalState.Replicas != 0 { + rs.Spec.Replicas = ptr.To(originalState.Replicas) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + rs.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &rs); err != nil { + errs = append(errs, fmt.Errorf("failed to update replicaset %s: %w", rs.Name, err)) + continue + } + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) suspendCertificates(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendCertificates") + + certGVR := schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "certificates", + } + + certList, err := r.dynamicClient.Resource(certGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list certificates: %w", err) + } + + for _, cert := range certList.Items { + certName := cert.GetName() + annotations := cert.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current disable-reissue annotation value + currentDisableReissue := annotations[CertManagerDisableReissueAnnotation] + + // Skip if already suspended (has state and disable-reissue is true) + if hasOriginalState && currentDisableReissue == "true" { + logger.V(1).Info("Certificate already suspended, skipping", "Certificate", certName) + continue + } + + // Track if certificate needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + // Check if disable-reissue was originally enabled (annotation exists and is "true") + wasDisabled := currentDisableReissue == "true" + + originalState := &CertificateOriginalState{ + DisableReissue: wasDisabled, + } + stateJSON, err := encodeCertificateState(originalState) + if err != nil { + logger.Error(err, "failed to encode certificate state", "certificate", certName) + return fmt.Errorf("failed to encode certificate state for %s: %w", certName, err) + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved certificate state", + "certificate", + certName, + "wasDisabled", + wasDisabled, + ) + } else { + logger.V(1). + Info("Certificate already has original state, skipping state save", "Certificate", certName) + } + + // Disable certificate reissue if not already set to true + if currentDisableReissue != "true" { + annotations[CertManagerDisableReissueAnnotation] = "true" + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + cert.SetAnnotations(annotations) + _, err := r.dynamicClient.Resource(certGVR). + Namespace(namespace). + Update(ctx, &cert, v12.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to suspend certificate %s: %w", certName, err) + } + + logger.V(1).Info("Suspended certificate reissue", "certificate", certName) + } + } + return nil +} + +func (r *NamespaceReconciler) resumeCertificates(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeCertificates") + + certGVR := schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "certificates", + } + + certList, err := r.dynamicClient.Resource(certGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list certificates: %w", err) + } + + for _, cert := range certList.Items { + certName := cert.GetName() + annotations := cert.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *CertificateOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore without the annotation + logger.Info( + "Certificate has no suspend state, using defaults to restore", + "Certificate", + certName, + ) + } else { + // Decode original state + var err error + originalState, err = decodeCertificateState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode certificate state", "certificate", certName) + } + } + if originalState == nil { + originalState = getDefaultCertificateState() + } + + logger.Info( + "Resuming certificate", + "certificate", + certName, + "wasDisabled", + originalState.DisableReissue, + ) + + // Track if certificate needs update + needsUpdate := false + + // Remove the annotation if it wasn't disabled before + if !originalState.DisableReissue { + delete(annotations, CertManagerDisableReissueAnnotation) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + cert.SetAnnotations(annotations) + _, err = r.dynamicClient.Resource(certGVR). + Namespace(namespace). + Update(ctx, &cert, v12.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to resume certificate %s: %w", certName, err) + } + } + } + return nil +} + +func (r *NamespaceReconciler) deleteResource( + ctx context.Context, + resource, namespace string, +) error { + if err := r.ensureFinalDeletionActive(ctx, namespace); err != nil { + return err + } + + deletePolicy := v12.DeletePropagationForeground + var gvr schema.GroupVersionResource + switch resource { + case "cluster.apps.kubeblocks.io": + gvr = schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "clusters", + } + case "backupschedules": + gvr = schema.GroupVersionResource{ + Group: "dataprotection.kubeblocks.io", + Version: "v1alpha1", + Resource: "backupschedules", + } + case "cronjob": + gvr = schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjobs"} + case "objectstorageuser": + gvr = schema.GroupVersionResource{ + Group: "objectstorage.sealos.io", + Version: "v1", + Resource: "objectstorageusers", + } + case "deploy": + gvr = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} + case "sts": + gvr = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"} + case "ds": + gvr = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"} + case "rs": + gvr = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"} + case "pvc": + gvr = schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "persistentvolumeclaims", + } + case "Service": + gvr = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + case "Ingress": + gvr = schema.GroupVersionResource{ + Group: "networking.k8s.io", + Version: "v1", + Resource: "ingresses", + } + case "Issuer": + gvr = schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "issuers", + } + case "Certificate": + gvr = schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "certificates", + } + case "HorizontalPodAutoscaler": + gvr = schema.GroupVersionResource{ + Group: "autoscaling", + Version: "v1", + Resource: "horizontalpodautoscalers", + } + case "instance": + gvr = schema.GroupVersionResource{ + Group: "app.sealos.io", + Version: "v1", + Resource: "instances", + } + case "job": + gvr = schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"} + case "app": + gvr = schema.GroupVersionResource{Group: "app.sealos.io", Version: "v1", Resource: "apps"} + case "devboxes": + if r.devboxVersion == "" { + return nil // skip if devbox CRD not installed + } + gvr = schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: r.devboxVersion, + Resource: "devboxes", + } + case "devboxreleases": + if r.devboxVersion == "" { + return nil // skip if devbox CRD not installed + } + gvr = schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: r.devboxVersion, + Resource: "devboxreleases", + } + case "pod": + gvr = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + default: + return fmt.Errorf("unknown resource: %s", resource) + } + err := r.dynamicClient.Resource(gvr). + Namespace(namespace). + DeleteCollection(ctx, v12.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }, v12.ListOptions{}) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete %s: %w", resource, err) + } + return nil +} + +func (r *NamespaceReconciler) suspendIngresses(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendIngresses") + + ingressList := networkingv1.IngressList{} + if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list ingresses: %w", err) + } + + var errs []error + for _, ingress := range ingressList.Items { + ingressName := ingress.Name + annotations := ingress.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current ingress class + currentIngressClass, hasIngressClass := annotations[IngressClassAnnotation] + + // Skip if already suspended (has state and ingress class is "pause") + if hasOriginalState && currentIngressClass == IngressClassPause { + logger.V(1).Info("Ingress already suspended, skipping", "Ingress", ingressName) + continue + } + + // Track if ingress needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + // Save the original ingress class (empty string if not set) + originalIngressClass := "" + if hasIngressClass { + originalIngressClass = currentIngressClass + } + + originalState := &IngressOriginalState{ + IngressClass: originalIngressClass, + } + stateJSON, err := encodeIngressState(originalState) + if err != nil { + logger.Error(err, "failed to encode ingress state", "ingress", ingressName) + errs = append( + errs, + fmt.Errorf("failed to encode ingress state for %s: %w", ingressName, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved ingress state", + "ingress", + ingressName, + "originalIngressClass", + originalIngressClass, + ) + } else { + logger.V(1). + Info("Ingress already has original state, skipping state save", "Ingress", ingressName) + } + + // Change ingress class to "pause" if not already set + if currentIngressClass != IngressClassPause { + annotations[IngressClassAnnotation] = IngressClassPause + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + ingress.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &ingress); err != nil { + errs = append( + errs, + fmt.Errorf("failed to suspend ingress %s: %w", ingressName, err), + ) + continue + } + + logger.V(1).Info("Suspended ingress", "ingress", ingressName) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeIngresses(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeIngresses") + + ingressList := networkingv1.IngressList{} + if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list ingresses: %w", err) + } + + var errs []error + for _, ingress := range ingressList.Items { + ingressName := ingress.Name + annotations := ingress.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *IngressOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default: restore to nginx + logger.Info( + "Ingress has no suspend state, using defaults to restore", + "Ingress", + ingressName, + ) + } else { + // Decode original state + var err error + originalState, err = decodeIngressState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode ingress state", "ingress", ingressName) + } + } + if originalState == nil { + originalState = getDefaultIngressState() + } + + logger.Info( + "Resuming ingress", + "ingress", + ingressName, + "originalIngressClass", + originalState.IngressClass, + ) + + // Track if ingress needs update + needsUpdate := false + + // Restore original ingress class + if originalState.IngressClass != IngressClassPause { + // Restore to original ingress class + annotations[IngressClassAnnotation] = originalState.IngressClass + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + ingress.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &ingress); err != nil { + errs = append(errs, fmt.Errorf("failed to resume ingress %s: %w", ingressName, err)) + continue + } + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) suspendOrphanJob(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendOrphanJob") + + jobList := batchv1.JobList{} + if err := r.Client.List(ctx, &jobList, client.InNamespace(namespace)); err != nil { + return err + } + + var errs []error + for _, job := range jobList.Items { + // Skip if this job has a controller (not an orphan) + if hasController(job.OwnerReferences) { + logger.V(1).Info("Job has controller, skipping", "Job", job.Name) + continue + } + + annotations := job.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current suspend state + currentlySuspended := job.Spec.Suspend != nil && *job.Spec.Suspend + + // Track if job needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + originalState := &JobOriginalState{ + Suspend: currentlySuspended, + } + stateJSON, err := encodeJobState(originalState) + if err != nil { + logger.Error(err, "failed to encode job state", "job", job.Name) + errs = append( + errs, + fmt.Errorf("failed to encode job state for %s: %w", job.Name, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved job state", + "job", + job.Name, + "originalSuspend", + currentlySuspended, + ) + } else { + logger.V(1).Info("Job already has original state, skipping state save", "Job", job.Name) + } + + // Set suspend to true if not already suspended + if !currentlySuspended { + job.Spec.Suspend = ptr.To(true) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + job.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &job); err != nil { + errs = append(errs, fmt.Errorf("failed to suspend job %s: %w", job.Name, err)) + continue + } + logger.V(1).Info("Suspended job", "job", job.Name) + } else { + logger.V(1).Info("Job already suspended, skipping update", "job", job.Name) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeOrphanJob(ctx context.Context, namespace string) error { + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeOrphanJob") + + jobList := batchv1.JobList{} + if err := r.Client.List(ctx, &jobList, client.InNamespace(namespace)); err != nil { + return err + } + + var errs []error + for _, job := range jobList.Items { + // Skip if this job has a controller (not an orphan) + if hasController(job.OwnerReferences) { + logger.V(1).Info("Job has controller, skipping", "Job", job.Name) + continue + } + + annotations := job.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *JobOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: resume to not suspended + logger.Info( + "Job has no suspend state, using defaults to restore", + "Job", + job.Name, + ) + } else { + // Decode original state + var err error + originalState, err = decodeJobState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode job state", "job", job.Name) + } + } + if originalState == nil { + originalState = getDefaultJobState() + } + + logger.Info( + "Resuming job", + "job", + job.Name, + "originalSuspend", + originalState.Suspend, + ) + + // Track if job needs update + needsUpdate := false + + // Restore original suspend state + if !originalState.Suspend { + job.Spec.Suspend = ptr.To(originalState.Suspend) + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + job.SetAnnotations(annotations) + if err := r.Client.Update(ctx, &job); err != nil { + errs = append(errs, fmt.Errorf("failed to resume job %s: %w", job.Name, err)) + continue + } + } + } + return errors2.Join(errs...) +} + +func deleteResourceListAndWait( + ctx context.Context, + dynamicClient dynamic.Interface, + gvr schema.GroupVersionResource, + namespace string, + semaphore chan struct{}, + guard func(context.Context) error, +) error { + if err := guard(ctx); err != nil { + return err + } + + // List all resources + list, err := dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, v12.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list %s in namespace %s: %w", gvr, namespace, err) + } + + if len(list.Items) == 0 { + return nil // No resources to delete + } + + // Concurrent deletion with rate limiting: use semaphore to limit concurrent deletions + var wg sync.WaitGroup + errCh := make(chan error, len(list.Items)) // Buffered channel to avoid blocking + allErrors := []error{} + + for _, item := range list.Items { + name := item.GetName() + wg.Add(1) + go func(resName string) { + defer wg.Done() + // Acquire semaphore + select { + case semaphore <- struct{}{}: + defer func() { <-semaphore }() // Release semaphore when done + if guardErr := guard(ctx); guardErr != nil { + errCh <- guardErr + return + } + if deleteErr := deleteResourceAndWait( + ctx, + dynamicClient, + gvr, + namespace, + resName, + ); deleteErr != nil { + errCh <- fmt.Errorf("failed to delete %s/%s: %w", gvr, resName, deleteErr) + } + case <-ctx.Done(): + errCh <- ctx.Err() + } + }(name) + } + + // Wait for all goroutines to complete and collect errors + go func() { + wg.Wait() + close(errCh) + }() + + // Cancellation is an expected result when recharge changes the namespace + // status during cleanup. It must be separated from real deletion errors so + // the caller can stop cleanly after every worker has finished. + wasCancelled := false + for deleteErr := range errCh { + if errors2.Is(deleteErr, errFinalDeletionCancelled) { + wasCancelled = true + continue + } + allErrors = append(allErrors, deleteErr) + } + + if len(allErrors) > 0 { + return fmt.Errorf("failed to delete some %s resources: %v", gvr, allErrors) + } + if wasCancelled { + return errFinalDeletionCancelled + } + + return nil +} + +func deleteResourceAndWait( + ctx context.Context, + dynamicClient dynamic.Interface, + gvr schema.GroupVersionResource, + namespace, name string, +) error { + deletePolicy := v12.DeletePropagationForeground // Foreground deletion, wait for child resources + if gvr.Resource == "backups" { + deletePolicy = v12.DeletePropagationBackground + } + // Execute deletion (for single resource) + err := dynamicClient.Resource(gvr).Namespace(namespace).Delete(ctx, name, v12.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete %s/%s: %w", gvr, name, err) + } + if errors.IsNotFound(err) { + return nil // Already deleted, no need to wait + } + + // Wait for deletion to complete: poll Get until NotFound + pollInterval := 5 * time.Second + timeout := time.Minute + err = wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, + func(ctx context.Context) (bool, error) { + _, getErr := dynamicClient.Resource(gvr). + Namespace(namespace). + Get(ctx, name, v12.GetOptions{}) + if errors.IsNotFound(getErr) { + return true, nil // Success: resource has been deleted + } + if getErr != nil { + // API call error (not NotFound), need to retry but may need logging + if errors.IsServerTimeout(getErr) || errors.IsServiceUnavailable(getErr) { + // Temporary server error, retry silently + return false, nil + } + // Continue polling for other errors + return false, nil + } + // Resource still exists: continue polling, don't return error (avoid logging) + return false, nil + }) + if err != nil { + if errors2.Is(ctx.Err(), context.DeadlineExceeded) { + return fmt.Errorf("timeout waiting for %s/%s to delete after %v", gvr, name, timeout) + } + return fmt.Errorf("error waiting for %s/%s to delete: %w", gvr, name, err) + } + return nil +} + +// detectDevboxVersion detects the installed devbox CRD version +// Returns "v1alpha2" if available, otherwise "v1alpha1", or empty string if not installed +func (r *NamespaceReconciler) detectDevboxVersion() string { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Try v1alpha2 first (preferred) + gvrV2 := schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: "v1alpha2", + Resource: "devboxes", + } + _, err := r.dynamicClient.Resource(gvrV2).List(ctx, v12.ListOptions{Limit: 1}) + if err == nil { + return "v1alpha2" + } + + // Try v1alpha1 as fallback + gvrV1 := schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: "v1alpha1", + Resource: "devboxes", + } + _, err = r.dynamicClient.Resource(gvrV1).List(ctx, v12.ListOptions{Limit: 1}) + if err == nil { + return "v1alpha1" + } + + // Devbox CRD not installed + return "" +} + +func (r *NamespaceReconciler) suspendDevboxes(ctx context.Context, namespace string) error { + // Skip if devbox CRD is not installed + if r.devboxVersion == "" { + return nil + } + + logger := r.Log.WithValues("Namespace", namespace, "Function", "suspendDevboxes") + + devboxGVR := schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: r.devboxVersion, + Resource: "devboxes", + } + devboxList, err := r.dynamicClient.Resource(devboxGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list devboxes in namespace %s: %w", namespace, err) + } + + var errs []error + for _, devbox := range devboxList.Items { + devboxName := devbox.GetName() + logger.V(1).Info("Processing devbox", "Devbox", devboxName) + + annotations := devbox.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Check if already has original state saved + _, hasOriginalState := annotations[OriginalSuspendStateAnnotation] + + // Get current state from spec.state + currentState, _, _ := unstructured.NestedString(devbox.Object, "spec", "state") + + // Skip if already not running and state already saved + if currentState != "Running" && hasOriginalState { + logger.V(1).Info("Devbox already suspended, skipping", "Devbox", devboxName) + continue + } + + // Track if devbox needs update + needsUpdate := false + + // Save original state only if not already saved + if !hasOriginalState { + // Determine if devbox was running (state is "Running") + wasRunning := currentState == "Running" + + originalState := &DevboxOriginalState{ + WasRunning: wasRunning, + } + stateJSON, err := encodeDevboxState(originalState) + if err != nil { + logger.Error(err, "failed to encode devbox state", "devbox", devboxName) + errs = append( + errs, + fmt.Errorf("failed to encode devbox state for %s: %w", devboxName, err), + ) + continue + } + annotations[OriginalSuspendStateAnnotation] = stateJSON + needsUpdate = true + + logger.Info( + "Saved devbox state", + "devbox", + devboxName, + "wasRunning", + wasRunning, + ) + } else { + logger.V(1). + Info("Devbox already has original state, skipping state save", "Devbox", devboxName) + } + + // Set state to Shutdown if currently running + if currentState == "Running" { + if err := unstructured.SetNestedField( + devbox.Object, + "Shutdown", + "spec", + "state", + ); err != nil { + logger.Error(err, "failed to set devbox state", "devbox", devboxName) + errs = append( + errs, + fmt.Errorf("failed to set devbox state for %s: %w", devboxName, err), + ) + continue + } + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + devbox.SetAnnotations(annotations) + _, err := r.dynamicClient.Resource(devboxGVR). + Namespace(namespace). + Update(ctx, &devbox, v12.UpdateOptions{}) + if err != nil { + errs = append(errs, fmt.Errorf("failed to suspend devbox %s: %w", devboxName, err)) + continue + } + logger.V(1).Info("Suspended devbox", "devbox", devboxName) + } + } + return errors2.Join(errs...) +} + +func (r *NamespaceReconciler) resumeDevboxes(ctx context.Context, namespace string) error { + // Skip if devbox CRD is not installed + if r.devboxVersion == "" { + return nil + } + + logger := r.Log.WithValues("Namespace", namespace, "Function", "resumeDevboxes") + + devboxGVR := schema.GroupVersionResource{ + Group: "devbox.sealos.io", + Version: r.devboxVersion, + Resource: "devboxes", + } + devboxList, err := r.dynamicClient.Resource(devboxGVR). + Namespace(namespace). + List(ctx, v12.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to list devboxes in namespace %s: %w", namespace, err) + } + + var errs []error + for _, devbox := range devboxList.Items { + devboxName := devbox.GetName() + annotations := devbox.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + // Get or create original state with defaults + var originalState *DevboxOriginalState + stateJSON, exists := annotations[OriginalSuspendStateAnnotation] + if !exists { + // If no state annotation, use default values: restore to Running + logger.Info( + "Devbox has no suspend state, using defaults to restore", + "Devbox", + devboxName, + ) + } else { + // Decode original state + var err error + originalState, err = decodeDevboxState(stateJSON) + if err != nil { + logger.Error(err, "failed to decode devbox state", "devbox", devboxName) + } + } + if originalState == nil { + originalState = getDefaultDevboxState() + } + + logger.Info( + "Resuming devbox", + "devbox", + devboxName, + "wasRunning", + originalState.WasRunning, + ) + + // Track if devbox needs update + needsUpdate := false + + // Restore devbox to Running state only if it was running before suspension + if originalState.WasRunning { + if err := unstructured.SetNestedField( + devbox.Object, + "Running", + "spec", + "state", + ); err != nil { + logger.Error(err, "failed to set devbox state", "devbox", devboxName) + errs = append( + errs, + fmt.Errorf("failed to set devbox state for %s: %w", devboxName, err), + ) + continue + } + needsUpdate = true + } + + // Remove original state annotation if it existed + if exists { + delete(annotations, OriginalSuspendStateAnnotation) + needsUpdate = true + } + + // Update only if there are actual changes + if needsUpdate { + devbox.SetAnnotations(annotations) + _, err := r.dynamicClient.Resource(devboxGVR). + Namespace(namespace). + Update(ctx, &devbox, v12.UpdateOptions{}) + if err != nil { + errs = append(errs, fmt.Errorf("failed to resume devbox %s: %w", devboxName, err)) + continue + } + } + } + return errors2.Join(errs...) +} diff --git a/controllers/account/controllers/namespace_controller_test.go b/controllers/account/controllers/namespace_controller_test.go new file mode 100644 index 000000000000..66197c4afaca --- /dev/null +++ b/controllers/account/controllers/namespace_controller_test.go @@ -0,0 +1,2333 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/labring/sealos/controllers/pkg/types" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/fake" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestAnnotationChangedPredicateFinalDeletionReplay(t *testing.T) { + oldNamespace := &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + types.DebtNamespaceAnnoStatusKey: types.FinalDeletionDebtNamespaceAnnoStatus, + }, + }, + } + newNamespace := oldNamespace.DeepCopy() + newNamespace.Annotations[types.FinalDeletionReplayAnnotationKey] = "2026-08-17T00:00:00Z" + + if !(AnnotationChangedPredicate{}).Update(event.UpdateEvent{ + ObjectOld: oldNamespace, + ObjectNew: newNamespace, + }) { + t.Fatal("final deletion replay annotation should trigger reconciliation") + } + + oldNamespace = newNamespace + newNamespace = oldNamespace.DeepCopy() + newNamespace.Annotations[types.DebtNamespaceAnnoStatusKey] = types.ResumeCompletedDebtNamespaceAnnoStatus + if (AnnotationChangedPredicate{}).Update(event.UpdateEvent{ + ObjectOld: oldNamespace, + ObjectNew: newNamespace, + }) { + t.Fatal("completed resume transition should not trigger deletion reconciliation") + } +} + +func TestDeleteUserResourceStopsAfterRecharge(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add core scheme: %v", err) + } + + namespace := &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ns", + Annotations: map[string]string{ + types.DebtNamespaceAnnoStatusKey: types.FinalDeletionDebtNamespaceAnnoStatus, + }, + }, + } + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(namespace). + Build() + + ctx := context.Background() + var recharged corev1.Namespace + if err := fakeClient.Get(ctx, client.ObjectKey{Name: "test-ns"}, &recharged); err != nil { + t.Fatalf("failed to get namespace: %v", err) + } + recharged.Annotations[types.DebtNamespaceAnnoStatusKey] = types.ResumeDebtNamespaceAnnoStatus + if err := fakeClient.Update(ctx, &recharged); err != nil { + t.Fatalf("failed to update namespace after recharge: %v", err) + } + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + err := reconciler.DeleteUserResource(ctx, "test-ns") + if !errors.Is(err, errFinalDeletionCancelled) { + t.Fatalf("expected final deletion cancellation, got %v", err) + } +} + +type deleteCollectionTestDynamicClient struct { + dynamic.Interface + onDeleteCollection func(schema.GroupVersionResource) error +} + +func (c *deleteCollectionTestDynamicClient) Resource( + gvr schema.GroupVersionResource, +) dynamic.NamespaceableResourceInterface { + return &deleteCollectionTestNamespaceableResource{ + NamespaceableResourceInterface: c.Interface.Resource(gvr), + gvr: gvr, + onDeleteCollection: c.onDeleteCollection, + } +} + +type deleteCollectionTestNamespaceableResource struct { + dynamic.NamespaceableResourceInterface + gvr schema.GroupVersionResource + onDeleteCollection func(schema.GroupVersionResource) error +} + +func (r *deleteCollectionTestNamespaceableResource) Namespace( + namespace string, +) dynamic.ResourceInterface { + return &deleteCollectionTestResource{ + ResourceInterface: r.NamespaceableResourceInterface.Namespace(namespace), + gvr: r.gvr, + onDeleteCollection: r.onDeleteCollection, + } +} + +type deleteCollectionTestResource struct { + dynamic.ResourceInterface + gvr schema.GroupVersionResource + onDeleteCollection func(schema.GroupVersionResource) error +} + +func (r *deleteCollectionTestResource) DeleteCollection( + _ context.Context, + _ v1.DeleteOptions, + _ v1.ListOptions, +) error { + return r.onDeleteCollection(r.gvr) +} + +func TestDeleteUserResourceWaitsForSiblingWorkersAfterError(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add core scheme: %v", err) + } + + namespace := &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ns", + Annotations: map[string]string{ + types.DebtNamespaceAnnoStatusKey: types.FinalDeletionDebtNamespaceAnnoStatus, + }, + }, + } + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(namespace). + Build() + + fakeDynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds( + scheme, + map[schema.GroupVersionResource]string{ + { + Group: "dataprotection.kubeblocks.io", + Version: "v1alpha1", + Resource: "backups", + }: "BackupList", + }, + ) + deleteError := errors.New("delete clusters failed") + deleteErrorStarted := make(chan struct{}) + siblingDeleteStarted := make(chan struct{}) + releaseSiblingDelete := make(chan struct{}) + dynamicClient := &deleteCollectionTestDynamicClient{ + Interface: fakeDynamicClient, + onDeleteCollection: func(gvr schema.GroupVersionResource) error { + resource := gvr.Resource + switch resource { + case "clusters": + close(deleteErrorStarted) + return deleteError + case "pods": + close(siblingDeleteStarted) + <-releaseSiblingDelete + } + return nil + }, + } + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + dynamicClient: dynamicClient, + Log: logr.Discard(), + deleteResourceSemaphore: make(chan struct{}, 20), + } + + result := make(chan error, 1) + go func() { + result <- reconciler.DeleteUserResource(context.Background(), "test-ns") + }() + + select { + case <-deleteErrorStarted: + case <-time.After(time.Second): + t.Fatal("timed out waiting for the failing worker") + } + select { + case <-siblingDeleteStarted: + case <-time.After(time.Second): + t.Fatal("timed out waiting for the sibling worker") + } + + select { + case err := <-result: + t.Fatalf("DeleteUserResource returned before sibling worker completed: %v", err) + case <-time.After(50 * time.Millisecond): + } + + close(releaseSiblingDelete) + select { + case err := <-result: + if !errors.Is(err, deleteError) { + t.Fatalf("expected delete error, got %v", err) + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for DeleteUserResource") + } +} + +// Test suspendOrphanDeployments and resumeOrphanDeployments +func TestSuspendResumeOrphanDeployments(t *testing.T) { + tests := []struct { + name string + initialReplicas int32 + expectSuspended bool + expectResumed int32 + skipIfZero bool + }{ + { + name: "deployment with 3 replicas", + initialReplicas: 3, + expectSuspended: true, + expectResumed: 3, + }, + { + name: "deployment with 1 replica", + initialReplicas: 1, + expectSuspended: true, + expectResumed: 1, + }, + { + name: "deployment with 0 replicas", + initialReplicas: 0, + expectSuspended: true, + expectResumed: 0, + skipIfZero: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(tt.initialReplicas), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanDeployments failed: %v", err) + } + + // Verify suspended state + var suspendedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &suspendedDeploy, + ) + if err != nil { + t.Fatalf("failed to get suspended deployment: %v", err) + } + + if tt.expectSuspended { + if *suspendedDeploy.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedDeploy.Spec.Replicas) + } + + // Check if original state was saved + annotations := suspendedDeploy.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; !hasState { + t.Error("expected original state annotation to be saved") + } + } + + // Test resume + err = reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + + // Verify resumed state + var resumedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if err != nil { + t.Fatalf("failed to get resumed deployment: %v", err) + } + + if tt.skipIfZero && tt.initialReplicas == 0 { + // If original was 0, replicas should still be 0 + if *resumedDeploy.Spec.Replicas != 0 { + t.Errorf("expected replicas to remain 0, got %d", *resumedDeploy.Spec.Replicas) + } + } else { + // Otherwise, replicas should be restored + if *resumedDeploy.Spec.Replicas != tt.expectResumed { + t.Errorf( + "expected replicas to be %d, got %d", + tt.expectResumed, + *resumedDeploy.Spec.Replicas, + ) + } + } + + // Check if original state annotation was removed + annotations := resumedDeploy.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("expected original state annotation to be removed") + } + }) + } +} + +// Test suspendOrphanStatefulSets and resumeOrphanStatefulSets +func TestSuspendResumeOrphanStatefulSets(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + sts := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(3)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sts). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanStatefulSets failed: %v", err) + } + + // Verify suspended + var suspendedSts appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &suspendedSts, + ) + if err != nil { + t.Fatalf("failed to get suspended statefulset: %v", err) + } + + if *suspendedSts.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedSts.Spec.Replicas) + } + + // Test resume + err = reconciler.resumeOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanStatefulSets failed: %v", err) + } + + // Verify resumed + var resumedSts appsv1.StatefulSet + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, &resumedSts) + if err != nil { + t.Fatalf("failed to get resumed statefulset: %v", err) + } + + if *resumedSts.Spec.Replicas != 3 { + t.Errorf("expected replicas to be 3, got %d", *resumedSts.Spec.Replicas) + } +} + +// Test suspendOrphanReplicaSets and resumeOrphanReplicaSets +func TestSuspendResumeOrphanReplicaSets(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + rs := &appsv1.ReplicaSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-rs", + Namespace: "test-ns", + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: ptr.To(int32(2)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(rs). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanReplicaSets(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanReplicaSets failed: %v", err) + } + + // Verify suspended + var suspendedRs appsv1.ReplicaSet + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-rs", Namespace: "test-ns"}, &suspendedRs) + if err != nil { + t.Fatalf("failed to get suspended replicaset: %v", err) + } + + if *suspendedRs.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedRs.Spec.Replicas) + } + + // Test resume + err = reconciler.resumeOrphanReplicaSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanReplicaSets failed: %v", err) + } + + // Verify resumed + var resumedRs appsv1.ReplicaSet + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-rs", Namespace: "test-ns"}, &resumedRs) + if err != nil { + t.Fatalf("failed to get resumed replicaset: %v", err) + } + + if *resumedRs.Spec.Replicas != 2 { + t.Errorf("expected replicas to be 2, got %d", *resumedRs.Spec.Replicas) + } +} + +// Test suspendOrphanCronJob and resumeOrphanCronJob +func TestSuspendResumeOrphanCronJob(t *testing.T) { + tests := []struct { + name string + initiallySuspended bool + expectSuspended bool + }{ + { + name: "active cronjob", + initiallySuspended: false, + expectSuspended: true, + }, + { + name: "already suspended cronjob", + initiallySuspended: true, + expectSuspended: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = batchv1.AddToScheme(scheme) + + cronJob := &batchv1.CronJob{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-cronjob", + Namespace: "test-ns", + }, + Spec: batchv1.CronJobSpec{ + Schedule: "*/5 * * * *", + Suspend: ptr.To(tt.initiallySuspended), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(cronJob). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanCronJob(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanCronJob failed: %v", err) + } + + // Verify suspended + var suspendedCronJob batchv1.CronJob + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-cronjob", Namespace: "test-ns"}, + &suspendedCronJob, + ) + if err != nil { + t.Fatalf("failed to get suspended cronjob: %v", err) + } + + if !*suspendedCronJob.Spec.Suspend { + t.Error("expected cronjob to be suspended") + } + + // Test resume + err = reconciler.resumeOrphanCronJob(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanCronJob failed: %v", err) + } + + // Verify resumed + var resumedCronJob batchv1.CronJob + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-cronjob", Namespace: "test-ns"}, + &resumedCronJob, + ) + if err != nil { + t.Fatalf("failed to get resumed cronjob: %v", err) + } + + // Should restore to original state + if *resumedCronJob.Spec.Suspend != tt.initiallySuspended { + t.Errorf( + "expected suspend to be %v, got %v", + tt.initiallySuspended, + *resumedCronJob.Spec.Suspend, + ) + } + }) + } +} + +// Test suspendKBCluster and resumeKBCluster +func TestSuspendResumeKBCluster(t *testing.T) { + tests := []struct { + name string + clusterPhase string + backupEnabled bool + expectBackupDisabled bool + shouldCreateOps bool + }{ + { + name: "running cluster with backup", + clusterPhase: "Running", + backupEnabled: true, + expectBackupDisabled: true, + shouldCreateOps: true, + }, + { + name: "running cluster without backup", + clusterPhase: "Running", + backupEnabled: false, + expectBackupDisabled: false, + shouldCreateOps: true, + }, + { + name: "stopped cluster with backup", + clusterPhase: "Stopped", + backupEnabled: true, + expectBackupDisabled: true, + shouldCreateOps: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + + cluster := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "apps.kubeblocks.io/v1alpha1", + "kind": "Cluster", + "metadata": map[string]any{ + "name": "test-cluster", + "namespace": "test-ns", + }, + "spec": map[string]any{}, + "status": map[string]any{ + "phase": tt.clusterPhase, + }, + }, + } + + if tt.backupEnabled { + _ = unstructured.SetNestedField(cluster.Object, true, "spec", "backup", "enabled") + } + + dynamicClient := fake.NewSimpleDynamicClient(scheme, cluster) + + reconciler := &NamespaceReconciler{ + dynamicClient: dynamicClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendKBCluster(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendKBCluster failed: %v", err) + } + + // Verify suspension + clusterGVR := schema.GroupVersionResource{ + Group: "apps.kubeblocks.io", + Version: "v1alpha1", + Resource: "clusters", + } + + suspendedCluster, err := dynamicClient.Resource(clusterGVR). + Namespace("test-ns"). + Get(ctx, "test-cluster", v1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get suspended cluster: %v", err) + } + + // Check if original state was saved + annotations := suspendedCluster.GetAnnotations() + stateJSON, hasState := annotations[OriginalSuspendStateAnnotation] + if !hasState { + t.Fatal("expected original state annotation to be saved") + } + + // Decode and verify state + state, err := decodeKBClusterState(stateJSON) + if err != nil { + t.Fatalf("failed to decode state: %v", err) + } + + expectedWasRunning := tt.clusterPhase != "Stopped" && tt.clusterPhase != "Stopping" + if state.WasRunning != expectedWasRunning { + t.Errorf("expected wasRunning %v, got %v", expectedWasRunning, state.WasRunning) + } + + if state.BackupEnabled != tt.backupEnabled { + t.Errorf("expected backupEnabled %v, got %v", tt.backupEnabled, state.BackupEnabled) + } + + // Check if backup was disabled + if tt.expectBackupDisabled { + enabled, found, _ := unstructured.NestedBool( + suspendedCluster.Object, + "spec", + "backup", + "enabled", + ) + if !found { + t.Error("backup.enabled field not found") + } else if enabled { + t.Error("expected backup to be disabled") + } + } + + // Simulate cluster being stopped + _ = unstructured.SetNestedField(suspendedCluster.Object, "Stopped", "status", "phase") + _, err = dynamicClient.Resource(clusterGVR). + Namespace("test-ns"). + Update(ctx, suspendedCluster, v1.UpdateOptions{}) + if err != nil { + t.Fatalf("failed to update cluster status: %v", err) + } + + // Test resume + err = reconciler.resumeKBCluster(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeKBCluster failed: %v", err) + } + + // Verify resume + resumedCluster, err := dynamicClient.Resource(clusterGVR). + Namespace("test-ns"). + Get(ctx, "test-cluster", v1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get resumed cluster: %v", err) + } + + // Check if annotation was removed + annotations = resumedCluster.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("expected original state annotation to be removed") + } + + // Check if backup was restored + if tt.backupEnabled { + enabled, found, _ := unstructured.NestedBool( + resumedCluster.Object, + "spec", + "backup", + "enabled", + ) + if !found { + t.Error("backup.enabled field not found after resume") + } else if !enabled { + t.Error("expected backup to be restored to enabled") + } + } + }) + } +} + +// Test suspendCertificates and resumeCertificates +func TestSuspendResumeCertificates(t *testing.T) { + tests := []struct { + name string + initialDisableReissue string + hasInitialAnnotation bool + expectDisabledAfterSuspend bool + }{ + { + name: "certificate without disable annotation", + initialDisableReissue: "", + hasInitialAnnotation: false, + expectDisabledAfterSuspend: true, + }, + { + name: "certificate with disable=true", + initialDisableReissue: "true", + hasInitialAnnotation: true, + expectDisabledAfterSuspend: true, + }, + { + name: "certificate with disable=false", + initialDisableReissue: "false", + hasInitialAnnotation: true, + expectDisabledAfterSuspend: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + + cert := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": map[string]any{ + "name": "test-cert", + "namespace": "test-ns", + }, + "spec": map[string]any{ + "secretName": "test-secret", + "dnsNames": []any{"example.com"}, + }, + }, + } + + if tt.hasInitialAnnotation { + annotations := map[string]any{ + CertManagerDisableReissueAnnotation: tt.initialDisableReissue, + } + _ = unstructured.SetNestedMap(cert.Object, annotations, "metadata", "annotations") + } + + dynamicClient := fake.NewSimpleDynamicClient(scheme, cert) + + reconciler := &NamespaceReconciler{ + dynamicClient: dynamicClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendCertificates(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendCertificates failed: %v", err) + } + + // Verify suspension + certGVR := schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "certificates", + } + + suspendedCert, err := dynamicClient.Resource(certGVR). + Namespace("test-ns"). + Get(ctx, "test-cert", v1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get suspended certificate: %v", err) + } + + // Check annotations + annotations := suspendedCert.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; !hasState { + t.Error("expected original state annotation to be saved") + } + + disableValue, hasDisable := annotations[CertManagerDisableReissueAnnotation] + if !hasDisable || disableValue != "true" { + t.Error("expected disable-reissue annotation to be set to true") + } + + // Test resume + err = reconciler.resumeCertificates(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeCertificates failed: %v", err) + } + + // Verify resume + resumedCert, err := dynamicClient.Resource(certGVR). + Namespace("test-ns"). + Get(ctx, "test-cert", v1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get resumed certificate: %v", err) + } + + // Check if annotation was removed + annotations = resumedCert.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("expected original state annotation to be removed") + } + + // Check if disable-reissue was restored correctly + disableValue, hasDisable = annotations[CertManagerDisableReissueAnnotation] + wasDisabled := tt.hasInitialAnnotation && tt.initialDisableReissue == "true" + + if wasDisabled { + // Should still have the annotation set to "true" + if !hasDisable || disableValue != "true" { + t.Error("expected disable-reissue annotation to remain true") + } + } else { + // Should not have the annotation + if hasDisable { + t.Error("expected disable-reissue annotation to be removed") + } + } + }) + } +} + +// Test roundtrip: suspend -> resume +func TestSuspendResumeRoundtrip(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = batchv1.AddToScheme(scheme) + + // Create resources + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(3)), + }, + } + + cronJob := &batchv1.CronJob{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-cronjob", + Namespace: "test-ns", + }, + Spec: batchv1.CronJobSpec{ + Schedule: "*/5 * * * *", + Suspend: ptr.To(false), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy, cronJob). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Suspend all + if err := reconciler.suspendOrphanDeployments(ctx, "test-ns"); err != nil { + t.Fatalf("suspendOrphanDeployments failed: %v", err) + } + if err := reconciler.suspendOrphanCronJob(ctx, "test-ns"); err != nil { + t.Fatalf("suspendOrphanCronJob failed: %v", err) + } + + // Verify all suspended + var suspendedDeploy appsv1.Deployment + _ = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &suspendedDeploy, + ) + if *suspendedDeploy.Spec.Replicas != 0 { + t.Error("deployment should be suspended") + } + + var suspendedCronJob batchv1.CronJob + _ = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-cronjob", Namespace: "test-ns"}, + &suspendedCronJob, + ) + if !*suspendedCronJob.Spec.Suspend { + t.Error("cronjob should be suspended") + } + + // Resume all + if err := reconciler.resumeOrphanDeployments(ctx, "test-ns"); err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + if err := reconciler.resumeOrphanCronJob(ctx, "test-ns"); err != nil { + t.Fatalf("resumeOrphanCronJob failed: %v", err) + } + + // Verify all resumed + var resumedDeploy appsv1.Deployment + _ = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if *resumedDeploy.Spec.Replicas != 3 { + t.Error("deployment should be resumed to 3 replicas") + } + + var resumedCronJob batchv1.CronJob + _ = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-cronjob", Namespace: "test-ns"}, + &resumedCronJob, + ) + if *resumedCronJob.Spec.Suspend { + t.Error("cronjob should be resumed") + } +} + +// Test HPA suspend and resume with frontend deployment +func TestSuspendResumeHPAWithFrontendDeployment(t *testing.T) { + tests := []struct { + name string + hasHPA bool + hpaTarget string + hpaValue int32 + hpaMinReplicas int32 + hpaMaxReplicas int32 + deploymentReplicas int32 + expectPauseKey bool + expectHPADeleted bool + expectHPARestored bool + }{ + { + name: "deployment with HPA", + hasHPA: true, + hpaTarget: "cpu", + hpaValue: 80, + hpaMinReplicas: 2, + hpaMaxReplicas: 10, + deploymentReplicas: 3, + expectPauseKey: true, + expectHPADeleted: true, + expectHPARestored: true, + }, + { + name: "deployment without HPA", + hasHPA: false, + deploymentReplicas: 3, + expectPauseKey: true, + expectHPADeleted: false, + expectHPARestored: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create deployment with frontend annotations + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "2", + MaxReplicasKey: "10", + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(tt.deploymentReplicas), + }, + } + + objects := []client.Object{deploy} + + // Create HPA if needed + if tt.hasHPA { + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "test-deploy", + }, + MinReplicas: ptr.To(tt.hpaMinReplicas), + MaxReplicas: tt.hpaMaxReplicas, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceName(tt.hpaTarget), + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To(tt.hpaValue), + }, + }, + }, + }, + }, + } + objects = append(objects, hpa) + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objects...). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanDeployments failed: %v", err) + } + + // Verify suspended state + var suspendedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &suspendedDeploy, + ) + if err != nil { + t.Fatalf("failed to get suspended deployment: %v", err) + } + + // Check replicas are 0 + if *suspendedDeploy.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedDeploy.Spec.Replicas) + } + + // Check pause annotation + annotations := suspendedDeploy.GetAnnotations() + if tt.expectPauseKey { + pauseJSON, hasPause := annotations[PauseKey] + if !hasPause { + t.Error("expected pause annotation to be set") + } + + // Verify pause data + var pauseData PauseData + if err := json.Unmarshal([]byte(pauseJSON), &pauseData); err != nil { + t.Fatalf("failed to unmarshal pause data: %v", err) + } + + if tt.hasHPA { + if pauseData.Target != tt.hpaTarget { + t.Errorf("expected target %s, got %s", tt.hpaTarget, pauseData.Target) + } + expectedValue := "80" + if pauseData.Value != expectedValue { + t.Errorf("expected value %s, got %s", expectedValue, pauseData.Value) + } + } else if pauseData.Target != "" || pauseData.Value != "" { + t.Error("expected empty pause data when no HPA exists") + } + } + + // Check if HPA was deleted + if tt.expectHPADeleted { + var hpa autoscalingv2.HorizontalPodAutoscaler + err := fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &hpa, + ) + if err == nil { + t.Error("expected HPA to be deleted") + } + } + + // Test resume + err = reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + + // Verify resumed state + var resumedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if err != nil { + t.Fatalf("failed to get resumed deployment: %v", err) + } + + // Check replicas restored + if *resumedDeploy.Spec.Replicas != tt.deploymentReplicas { + t.Errorf( + "expected replicas to be %d, got %d", + tt.deploymentReplicas, + *resumedDeploy.Spec.Replicas, + ) + } + + // Check pause annotation removed + annotations = resumedDeploy.GetAnnotations() + if _, hasPause := annotations[PauseKey]; hasPause { + t.Error("expected pause annotation to be removed") + } + + // Check if HPA was restored + if tt.expectHPARestored { + var restoredHPA autoscalingv2.HorizontalPodAutoscaler + err := fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &restoredHPA, + ) + if err != nil { + t.Fatalf("expected HPA to be restored, got error: %v", err) + } + + // Verify HPA configuration + if *restoredHPA.Spec.MinReplicas != tt.hpaMinReplicas { + t.Errorf( + "expected minReplicas %d, got %d", + tt.hpaMinReplicas, + *restoredHPA.Spec.MinReplicas, + ) + } + if restoredHPA.Spec.MaxReplicas != tt.hpaMaxReplicas { + t.Errorf( + "expected maxReplicas %d, got %d", + tt.hpaMaxReplicas, + restoredHPA.Spec.MaxReplicas, + ) + } + + if len(restoredHPA.Spec.Metrics) > 0 && + restoredHPA.Spec.Metrics[0].Resource != nil { + resourceName := string(restoredHPA.Spec.Metrics[0].Resource.Name) + if resourceName != tt.hpaTarget { + t.Errorf("expected target %s, got %s", tt.hpaTarget, resourceName) + } + + if restoredHPA.Spec.Metrics[0].Resource.Target.AverageUtilization != nil { + value := *restoredHPA.Spec.Metrics[0].Resource.Target.AverageUtilization + if value != tt.hpaValue { + t.Errorf("expected value %d, got %d", tt.hpaValue, value) + } + } + } + } + }) + } +} + +// Test HPA suspend and resume with statefulset +func TestSuspendResumeHPAWithFrontendStatefulSet(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create statefulset with frontend annotations + sts := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "1", + MaxReplicasKey: "5", + }, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(3)), + }, + } + + // Create HPA + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "test-sts", + }, + MinReplicas: ptr.To(int32(1)), + MaxReplicas: 5, + Metrics: []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To(int32(70)), + }, + }, + }, + }, + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sts, hpa). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanStatefulSets failed: %v", err) + } + + // Verify HPA was deleted + var deletedHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, &deletedHPA) + if err == nil { + t.Error("expected HPA to be deleted") + } + + // Verify pause annotation + var suspendedSts appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &suspendedSts, + ) + if err != nil { + t.Fatalf("failed to get suspended statefulset: %v", err) + } + + annotations := suspendedSts.GetAnnotations() + pauseJSON, hasPause := annotations[PauseKey] + if !hasPause { + t.Fatal("expected pause annotation to be set") + } + + var pauseData PauseData + if err := json.Unmarshal([]byte(pauseJSON), &pauseData); err != nil { + t.Fatalf("failed to unmarshal pause data: %v", err) + } + + if pauseData.Target != "cpu" { + t.Errorf("expected target cpu, got %s", pauseData.Target) + } + if pauseData.Value != "70" { + t.Errorf("expected value 70, got %s", pauseData.Value) + } + + // Test resume + err = reconciler.resumeOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanStatefulSets failed: %v", err) + } + + // Verify HPA was restored + var restoredHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &restoredHPA, + ) + if err != nil { + t.Fatalf("expected HPA to be restored, got error: %v", err) + } + + // Verify HPA kind is StatefulSet + if restoredHPA.Spec.ScaleTargetRef.Kind != "StatefulSet" { + t.Errorf("expected kind StatefulSet, got %s", restoredHPA.Spec.ScaleTargetRef.Kind) + } +} + +// Test deployViaFrontend helper function +func TestDeployViaFrontend(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected bool + }{ + { + name: "has minReplicas", + annotations: map[string]string{ + MinReplicasKey: "1", + }, + expected: true, + }, + { + name: "has maxReplicas", + annotations: map[string]string{ + MaxReplicasKey: "5", + }, + expected: true, + }, + { + name: "has resize", + annotations: map[string]string{ + DeployPVCResizeKey: "true", + }, + expected: true, + }, + { + name: "has all annotations", + annotations: map[string]string{ + MinReplicasKey: "1", + MaxReplicasKey: "5", + DeployPVCResizeKey: "true", + }, + expected: true, + }, + { + name: "has no frontend annotations", + annotations: map[string]string{}, + expected: false, + }, + { + name: "nil annotations", + annotations: nil, + expected: false, + }, + { + name: "has other annotations only", + annotations: map[string]string{ + "app": "test", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deployViaFrontend(tt.annotations) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Test deployment without frontend annotations (should not touch HPA) +func TestSuspendResumeNonFrontendDeployment(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create deployment WITHOUT frontend annotations + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + // No frontend annotations + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(3)), + }, + } + + // Create HPA + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "test-deploy", + }, + MinReplicas: ptr.To(int32(2)), + MaxReplicas: 10, + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy, hpa). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanDeployments failed: %v", err) + } + + // Verify HPA was NOT touched (should still exist) + var existingHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &existingHPA, + ) + if err != nil { + t.Error("HPA should not be deleted for non-frontend deployment") + } + + // Verify suspended deployment state + var suspendedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &suspendedDeploy, + ) + if err != nil { + t.Fatalf("failed to get deployment: %v", err) + } + + // Check replicas set to 0 + if *suspendedDeploy.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedDeploy.Spec.Replicas) + } + + // Verify no pause annotation + annotations := suspendedDeploy.GetAnnotations() + if _, hasPause := annotations[PauseKey]; hasPause { + t.Error("pause annotation should not be set for non-frontend deployment") + } + + // Verify original suspend state annotation exists + stateJSON, hasState := annotations[OriginalSuspendStateAnnotation] + if !hasState { + t.Fatal("original suspend state annotation should be set") + } + + // Verify the saved state + var state DeploymentOriginalState + if err := json.Unmarshal([]byte(stateJSON), &state); err != nil { + t.Fatalf("failed to unmarshal state: %v", err) + } + if state.Replicas != 3 { + t.Errorf("expected original replicas to be 3, got %d", state.Replicas) + } + + // Test resume + err = reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + + // Verify resumed deployment + var resumedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if err != nil { + t.Fatalf("failed to get resumed deployment: %v", err) + } + + // Check replicas restored + if *resumedDeploy.Spec.Replicas != 3 { + t.Errorf("expected replicas to be 3, got %d", *resumedDeploy.Spec.Replicas) + } + + // Verify original suspend state annotation removed + annotations = resumedDeploy.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("original suspend state annotation should be removed after resume") + } + + // Verify HPA still exists and unchanged + var finalHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &finalHPA, + ) + if err != nil { + t.Error("HPA should still exist after resume for non-frontend deployment") + } + + // Verify HPA configuration unchanged + if *finalHPA.Spec.MinReplicas != 2 { + t.Errorf("expected HPA minReplicas to remain 2, got %d", *finalHPA.Spec.MinReplicas) + } + if finalHPA.Spec.MaxReplicas != 10 { + t.Errorf("expected HPA maxReplicas to remain 10, got %d", finalHPA.Spec.MaxReplicas) + } +} + +// Test statefulset without frontend annotations (should not touch HPA) +func TestSuspendResumeNonFrontendStatefulSet(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create statefulset WITHOUT frontend annotations + sts := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + // No frontend annotations + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(5)), + }, + } + + // Create HPA + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "test-sts", + }, + MinReplicas: ptr.To(int32(3)), + MaxReplicas: 15, + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sts, hpa). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test suspend + err := reconciler.suspendOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("suspendOrphanStatefulSets failed: %v", err) + } + + // Verify suspended statefulset + var suspendedSts appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &suspendedSts, + ) + if err != nil { + t.Fatalf("failed to get statefulset: %v", err) + } + + // Check replicas set to 0 + if *suspendedSts.Spec.Replicas != 0 { + t.Errorf("expected replicas to be 0, got %d", *suspendedSts.Spec.Replicas) + } + + // Verify no pause annotation + annotations := suspendedSts.GetAnnotations() + if _, hasPause := annotations[PauseKey]; hasPause { + t.Error("pause annotation should not be set for non-frontend statefulset") + } + + // Verify original suspend state annotation exists + if _, hasState := annotations[OriginalSuspendStateAnnotation]; !hasState { + t.Error("original suspend state annotation should be set") + } + + // Verify HPA was NOT touched + var existingHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &existingHPA, + ) + if err != nil { + t.Error("HPA should not be deleted for non-frontend statefulset") + } + + // Test resume + err = reconciler.resumeOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanStatefulSets failed: %v", err) + } + + // Verify resumed statefulset + var resumedSts appsv1.StatefulSet + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, &resumedSts) + if err != nil { + t.Fatalf("failed to get resumed statefulset: %v", err) + } + + // Check replicas restored + if *resumedSts.Spec.Replicas != 5 { + t.Errorf("expected replicas to be 5, got %d", *resumedSts.Spec.Replicas) + } + + // Verify HPA still exists + var finalHPA autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get(ctx, client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, &finalHPA) + if err != nil { + t.Error("HPA should still exist after resume for non-frontend statefulset") + } +} + +// Test that originally paused deployments (replicas=0) don't restore HPA +func TestResumeAlreadyPausedDeploymentWithHPA(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create deployment that was originally paused (replicas=0) with frontend annotations + originalState := &DeploymentOriginalState{ + Replicas: 0, // Originally paused + } + stateJSON, _ := encodeDeploymentState(originalState) + + pauseData := &PauseData{ + Target: "cpu", + Value: "80", + } + //nolint:errchkjson + pauseJSON, _ := json.Marshal(pauseData) + + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "1", + MaxReplicasKey: "5", + OriginalSuspendStateAnnotation: stateJSON, + PauseKey: string(pauseJSON), + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test resume + err := reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + + // Verify resumed deployment + var resumedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if err != nil { + t.Fatalf("failed to get resumed deployment: %v", err) + } + + // Check replicas should remain 0 (since original was 0) + if *resumedDeploy.Spec.Replicas != 0 { + t.Errorf("expected replicas to remain 0, got %d", *resumedDeploy.Spec.Replicas) + } + + // Check original state annotation was removed + annotations := resumedDeploy.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("original suspend state annotation should be removed") + } + + // Check pause annotation was NOT removed (since replicas was 0) + if _, hasPause := annotations[PauseKey]; !hasPause { + t.Error("pause annotation should NOT be removed when original replicas was 0") + } + + // Verify HPA was NOT created (since original replicas was 0) + var hpa autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &hpa, + ) + if err == nil { + t.Error("HPA should NOT be created when original replicas was 0") + } +} + +// Test that originally paused statefulsets (replicas=0) don't restore HPA +func TestResumeAlreadyPausedStatefulSetWithHPA(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create statefulset that was originally paused (replicas=0) with frontend annotations + originalState := &DeploymentOriginalState{ + Replicas: 0, // Originally paused + } + stateJSON, _ := encodeDeploymentState(originalState) + + pauseData := &PauseData{ + Target: "memory", + Value: "70", + } + //nolint:errchkjson + pauseJSON, _ := json.Marshal(pauseData) + + sts := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "2", + MaxReplicasKey: "10", + OriginalSuspendStateAnnotation: stateJSON, + PauseKey: string(pauseJSON), + }, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sts). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test resume + err := reconciler.resumeOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanStatefulSets failed: %v", err) + } + + // Verify resumed statefulset + var resumedSts appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &resumedSts, + ) + if err != nil { + t.Fatalf("failed to get resumed statefulset: %v", err) + } + + // Check replicas should remain 0 (since original was 0) + if *resumedSts.Spec.Replicas != 0 { + t.Errorf("expected replicas to remain 0, got %d", *resumedSts.Spec.Replicas) + } + + // Check original state annotation was removed + annotations := resumedSts.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("original suspend state annotation should be removed") + } + + // Check pause annotation was NOT removed (since replicas was 0) + if _, hasPause := annotations[PauseKey]; !hasPause { + t.Error("pause annotation should NOT be removed when original replicas was 0") + } + + // Verify HPA was NOT created (since original replicas was 0) + var hpa autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts", Namespace: "test-ns"}, + &hpa, + ) + if err == nil { + t.Error("HPA should NOT be created when original replicas was 0") + } +} + +// Test that HPA restore errors don't block other deployments from being resumed +func TestResumeDeploymentContinuesOnHPAError(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + // Create first deployment with original replicas > 0 and invalid pause data + originalState1 := &DeploymentOriginalState{ + Replicas: 3, + } + stateJSON1, _ := encodeDeploymentState(originalState1) + + // Invalid pause data that will cause error during HPA restoration + invalidPauseData := `{invalid json}` + + deploy1 := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy-1", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "2", + MaxReplicasKey: "8", + OriginalSuspendStateAnnotation: stateJSON1, + PauseKey: invalidPauseData, // Invalid JSON + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + // Create second deployment with valid data + originalState2 := &DeploymentOriginalState{ + Replicas: 2, + } + stateJSON2, _ := encodeDeploymentState(originalState2) + + deploy2 := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy-2", + Namespace: "test-ns", + Annotations: map[string]string{ + OriginalSuspendStateAnnotation: stateJSON2, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy1, deploy2). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test resume - should not fail even though first deployment has invalid pause data + err := reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments should not fail on HPA error, got: %v", err) + } + + // Verify first deployment was still updated (replicas restored despite HPA error) + var resumedDeploy1 appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy-1", Namespace: "test-ns"}, + &resumedDeploy1, + ) + if err != nil { + t.Fatalf("failed to get first deployment: %v", err) + } + + if *resumedDeploy1.Spec.Replicas != 3 { + t.Errorf( + "expected first deployment replicas to be 3, got %d", + *resumedDeploy1.Spec.Replicas, + ) + } + + // Verify second deployment was also updated + var resumedDeploy2 appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy-2", Namespace: "test-ns"}, + &resumedDeploy2, + ) + if err != nil { + t.Fatalf("failed to get second deployment: %v", err) + } + + if *resumedDeploy2.Spec.Replicas != 2 { + t.Errorf( + "expected second deployment replicas to be 2, got %d", + *resumedDeploy2.Spec.Replicas, + ) + } + + // Verify both original state annotations were removed + annotations1 := resumedDeploy1.GetAnnotations() + if _, hasState := annotations1[OriginalSuspendStateAnnotation]; hasState { + t.Error("first deployment should have state annotation removed") + } + + annotations2 := resumedDeploy2.GetAnnotations() + if _, hasState := annotations2[OriginalSuspendStateAnnotation]; hasState { + t.Error("second deployment should have state annotation removed") + } +} + +// Test that HPA restore errors don't block other statefulsets from being resumed +func TestResumeStatefulSetContinuesOnHPAError(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + + // Create first statefulset with original replicas > 0 and invalid pause data + originalState1 := &DeploymentOriginalState{ + Replicas: 5, + } + stateJSON1, _ := encodeDeploymentState(originalState1) + + invalidPauseData := `{invalid json}` + + sts1 := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts-1", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "3", + MaxReplicasKey: "10", + OriginalSuspendStateAnnotation: stateJSON1, + PauseKey: invalidPauseData, // Invalid JSON + }, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + // Create second statefulset with valid data + originalState2 := &DeploymentOriginalState{ + Replicas: 3, + } + stateJSON2, _ := encodeDeploymentState(originalState2) + + sts2 := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-sts-2", + Namespace: "test-ns", + Annotations: map[string]string{ + OriginalSuspendStateAnnotation: stateJSON2, + }, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sts1, sts2). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test resume - should not fail even though first statefulset has invalid pause data + err := reconciler.resumeOrphanStatefulSets(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanStatefulSets should not fail on HPA error, got: %v", err) + } + + // Verify first statefulset was still updated (replicas restored despite HPA error) + var resumedSts1 appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts-1", Namespace: "test-ns"}, + &resumedSts1, + ) + if err != nil { + t.Fatalf("failed to get first statefulset: %v", err) + } + + if *resumedSts1.Spec.Replicas != 5 { + t.Errorf("expected first statefulset replicas to be 5, got %d", *resumedSts1.Spec.Replicas) + } + + // Verify second statefulset was also updated + var resumedSts2 appsv1.StatefulSet + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-sts-2", Namespace: "test-ns"}, + &resumedSts2, + ) + if err != nil { + t.Fatalf("failed to get second statefulset: %v", err) + } + + if *resumedSts2.Spec.Replicas != 3 { + t.Errorf("expected second statefulset replicas to be 3, got %d", *resumedSts2.Spec.Replicas) + } + + // Verify both original state annotations were removed + annotations1 := resumedSts1.GetAnnotations() + if _, hasState := annotations1[OriginalSuspendStateAnnotation]; hasState { + t.Error("first statefulset should have state annotation removed") + } + + annotations2 := resumedSts2.GetAnnotations() + if _, hasState := annotations2[OriginalSuspendStateAnnotation]; hasState { + t.Error("second statefulset should have state annotation removed") + } +} + +// Test HPA is restored AFTER deployment is updated +func TestHPARestoredAfterDeploymentUpdate(t *testing.T) { + scheme := runtime.NewScheme() + _ = appsv1.AddToScheme(scheme) + _ = autoscalingv2.AddToScheme(scheme) + + // Create deployment with original replicas > 0 + originalState := &DeploymentOriginalState{ + Replicas: 3, // Originally running + } + + stateJSON, _ := encodeDeploymentState(originalState) + + pauseData := &PauseData{ + Target: "cpu", + Value: "75", + } + //nolint:errchkjson + pauseJSON, _ := json.Marshal(pauseData) + + deploy := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deploy", + Namespace: "test-ns", + Annotations: map[string]string{ + MinReplicasKey: "2", + MaxReplicasKey: "8", + OriginalSuspendStateAnnotation: stateJSON, + PauseKey: string(pauseJSON), + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(0)), + }, + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(deploy). + Build() + + reconciler := &NamespaceReconciler{ + Client: fakeClient, + Log: logr.Discard(), + } + + ctx := context.Background() + + // Test resume + err := reconciler.resumeOrphanDeployments(ctx, "test-ns") + if err != nil { + t.Fatalf("resumeOrphanDeployments failed: %v", err) + } + + // Verify deployment was updated with correct replicas + var resumedDeploy appsv1.Deployment + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &resumedDeploy, + ) + if err != nil { + t.Fatalf("failed to get resumed deployment: %v", err) + } + + // Check replicas restored to original value + if *resumedDeploy.Spec.Replicas != 3 { + t.Errorf("expected replicas to be 3, got %d", *resumedDeploy.Spec.Replicas) + } + + // Check original state annotation was removed + annotations := resumedDeploy.GetAnnotations() + if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { + t.Error("original suspend state annotation should be removed") + } + + // Check pause annotation was removed (since replicas > 0) + if _, hasPause := annotations[PauseKey]; hasPause { + t.Error("pause annotation should be removed when original replicas was > 0") + } + + // Verify HPA was created (since original replicas > 0) + var hpa autoscalingv2.HorizontalPodAutoscaler + err = fakeClient.Get( + ctx, + client.ObjectKey{Name: "test-deploy", Namespace: "test-ns"}, + &hpa, + ) + if err != nil { + t.Fatalf("HPA should be created when original replicas was > 0, got error: %v", err) + } + + // Verify HPA configuration + if *hpa.Spec.MinReplicas != 2 { + t.Errorf("expected HPA minReplicas to be 2, got %d", *hpa.Spec.MinReplicas) + } + if hpa.Spec.MaxReplicas != 8 { + t.Errorf("expected HPA maxReplicas to be 8, got %d", hpa.Spec.MaxReplicas) + } + if len(hpa.Spec.Metrics) > 0 && hpa.Spec.Metrics[0].Resource != nil { + if string(hpa.Spec.Metrics[0].Resource.Name) != "cpu" { + t.Errorf("expected HPA target to be cpu, got %s", hpa.Spec.Metrics[0].Resource.Name) + } + if hpa.Spec.Metrics[0].Resource.Target.AverageUtilization != nil { + if *hpa.Spec.Metrics[0].Resource.Target.AverageUtilization != 75 { + t.Errorf( + "expected HPA value to be 75, got %d", + *hpa.Spec.Metrics[0].Resource.Target.AverageUtilization, + ) + } + } + } +} + +// Test suspendDevboxes and resumeDevboxes +// func TestSuspendResumeDevboxes(t *testing.T) { +// tests := []struct { +// name string +// initialState devboxv1alpha1.DevboxState +// expectSuspended bool +// }{ +// { +// name: "devbox in Running state", +// initialState: devboxv1alpha1.DevboxStateRunning, +// expectSuspended: true, +// }, +// { +// name: "devbox in Stopped state", +// initialState: devboxv1alpha1.DevboxStateStopped, +// expectSuspended: true, +// }, +// } + +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// scheme := runtime.NewScheme() +// _ = devboxv1alpha1.AddToScheme(scheme) + +// devbox := &devboxv1alpha1.Devbox{ +// ObjectMeta: v1.ObjectMeta{ +// Name: "test-devbox", +// Namespace: "test-ns", +// }, +// Spec: devboxv1alpha1.DevboxSpec{ +// State: tt.initialState, +// }, +// } + +// fakeClient := clientfake.NewClientBuilder(). +// WithScheme(scheme). +// WithObjects(devbox). +// Build() + +// reconciler := &NamespaceReconciler{ +// Client: fakeClient, +// Log: logr.Discard(), +// } + +// ctx := context.Background() + +// // Test suspend +// err := reconciler.suspendDevboxes(ctx, "test-ns") +// if err != nil { +// t.Fatalf("suspendDevboxes failed: %v", err) +// } + +// // Verify suspension +// var suspendedDevbox devboxv1alpha1.Devbox +// err = fakeClient.Get( +// ctx, +// client.ObjectKey{Name: "test-devbox", Namespace: "test-ns"}, +// &suspendedDevbox, +// ) +// if err != nil { +// t.Fatalf("failed to get suspended devbox: %v", err) +// } + +// // Check if original state was saved +// annotations := suspendedDevbox.GetAnnotations() +// stateJSON, hasState := annotations[OriginalSuspendStateAnnotation] +// if !hasState { +// t.Fatal("expected original state annotation to be saved") +// } + +// // Decode and verify state +// state, err := decodeDevboxState(stateJSON) +// if err != nil { +// t.Fatalf("failed to decode state: %v", err) +// } + +// // Check wasRunning matches expectation (only Running is considered running) +// expectedWasRunning := tt.initialState == devboxv1alpha1.DevboxStateRunning +// if state.WasRunning != expectedWasRunning { +// t.Errorf("expected wasRunning %v, got %v", expectedWasRunning, state.WasRunning) +// } + +// // Check if devbox state is Stopped +// if suspendedDevbox.Spec.State != devboxv1alpha1.DevboxStateStopped { +// t.Errorf("expected state to be Stopped, got %s", suspendedDevbox.Spec.State) +// } + +// // Test resume +// err = reconciler.resumeDevboxes(ctx, "test-ns") +// if err != nil { +// t.Fatalf("resumeDevboxes failed: %v", err) +// } + +// // Verify resume +// var resumedDevbox devboxv1alpha1.Devbox +// err = fakeClient.Get( +// ctx, +// client.ObjectKey{Name: "test-devbox", Namespace: "test-ns"}, +// &resumedDevbox, +// ) +// if err != nil { +// t.Fatalf("failed to get resumed devbox: %v", err) +// } + +// // Check if annotation was removed +// annotations = resumedDevbox.GetAnnotations() +// if _, hasState := annotations[OriginalSuspendStateAnnotation]; hasState { +// t.Error("expected original state annotation to be removed") +// } + +// // Check if state was restored correctly +// // If original state was running (not Stopped), it should be restored to "Running" +// // If original state was Stopped, it should remain "Stopped" +// expectedRestoredState := devboxv1alpha1.DevboxStateStopped +// if tt.initialState != devboxv1alpha1.DevboxStateStopped { +// expectedRestoredState = devboxv1alpha1.DevboxStateRunning +// } +// if resumedDevbox.Spec.State != expectedRestoredState { +// t.Errorf( +// "expected state %s, got %s", +// expectedRestoredState, +// resumedDevbox.Spec.State, +// ) +// } +// }) +// } +// } diff --git a/controllers/account/controllers/operationrequest_monitor_controller.go b/controllers/account/controllers/operationrequest_monitor_controller.go new file mode 100644 index 000000000000..d7009189acf8 --- /dev/null +++ b/controllers/account/controllers/operationrequest_monitor_controller.go @@ -0,0 +1,255 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "github.com/labring/sealos/controllers/pkg/types" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +const ( + userNamespacePrefix = "ns-" +) + +// OperationRequestMonitorReconciler monitors OperationRequest objects for owner transfers +type OperationRequestMonitorReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OperationRequestMonitorReconciler) SetupWithManager(mgr ctrl.Manager) error { + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName("operationrequest_monitor_controller") + + return ctrl.NewControllerManagedBy(mgr). + For(&userv1.Operationrequest{}, builder.WithPredicates( + predicate.And( + namespaceOnlyPredicate(config.GetUserSystemNamespace()), + operationRequestCompletedPredicate{}, + ), + )). + WithOptions(controller.Options{ + MaxConcurrentReconciles: 10, + }). + Complete(r) +} + +// operationRequestCompletedPredicate filters for completed OperationRequests +type operationRequestCompletedPredicate struct { + predicate.Funcs +} + +func (operationRequestCompletedPredicate) Create(event.CreateEvent) bool { + return false // We don't need to handle create events +} + +func (operationRequestCompletedPredicate) Update(e event.UpdateEvent) bool { + oldObj, okOld := e.ObjectOld.(*userv1.Operationrequest) + newObj, okNew := e.ObjectNew.(*userv1.Operationrequest) + if !okOld || !okNew { + return false + } + + // Only process when status changes to Completed + return oldObj.Status.Phase != userv1.RequestCompleted && + newObj.Status.Phase == userv1.RequestCompleted +} + +func (operationRequestCompletedPredicate) Delete(event.DeleteEvent) bool { + return false +} + +func (operationRequestCompletedPredicate) Generic(event.GenericEvent) bool { + return false +} + +// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests,verbs=get;list;watch + +func (r *OperationRequestMonitorReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + operationRequest := &userv1.Operationrequest{} + if err := r.Get(ctx, req.NamespacedName, operationRequest); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Only process completed requests + if operationRequest.Status.Phase != userv1.RequestCompleted { + return ctrl.Result{}, nil + } + + // Check if this is an owner transfer request + if operationRequest.Spec.Role != userv1.OwnerRoleType { + r.Logger.V(1).Info("Skipping non-owner role operation request", + "request.name", operationRequest.Name, + "request.namespace", operationRequest.Namespace, + "request.role", operationRequest.Spec.Role, + "request.user", operationRequest.Spec.User, + ) + return ctrl.Result{}, nil + } + + // Only process Grant and Update actions for owner role + if operationRequest.Spec.Action != userv1.Grant && + operationRequest.Spec.Action != userv1.Update { + r.Logger.V(1).Info("Skipping non-grant/update action for owner role", + "request.name", operationRequest.Name, + "request.namespace", operationRequest.Namespace, + "request.action", operationRequest.Spec.Action, + "request.user", operationRequest.Spec.User, + ) + return ctrl.Result{}, nil + } + + // Handle namespace status synchronization based on owner's namespace status + if err := r.syncNamespaceStatus( + ctx, + operationRequest.Spec.User, + operationRequest.Spec.Namespace, + ); err != nil { + r.Logger.Error(err, "Failed to sync namespace status", + "newOwner", operationRequest.Spec.User, + "targetNamespace", operationRequest.Spec.Namespace, + ) + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func namespaceOnlyPredicate(namespace string) predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return e.Object.GetNamespace() == namespace + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return e.Object.GetNamespace() == namespace + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return e.ObjectNew.GetNamespace() == namespace + }, + GenericFunc: func(e event.GenericEvent) bool { + return e.Object.GetNamespace() == namespace + }, + } +} + +// syncNamespaceStatus synchronizes the target namespace status based on owner's namespace status +// 1. Gets the owner's namespace (ns-owner) +// 2. Checks owner's namespace status +// 3. Synchronizes the target namespace status accordingly +func (r *OperationRequestMonitorReconciler) syncNamespaceStatus( + ctx context.Context, + owner, targetNamespace string, +) error { + // Get the owner's namespace + ownerNamespace, err := r.getOwnerNamespace(ctx, owner) + if err != nil { + return fmt.Errorf("failed to get owner namespace for %s: %w", owner, err) + } + + // Get the target namespace + targetNs := &corev1.Namespace{} + if err := r.Get(ctx, client.ObjectKey{Name: targetNamespace}, targetNs); err != nil { + return fmt.Errorf("failed to get target namespace %s: %w", targetNamespace, err) + } + + // Synchronize target namespace status based on owner's namespace status + return r.synchronizeNamespaceStatus(ctx, ownerNamespace, targetNs) +} + +// getOwnerNamespace finds the owner's namespace by owner label +func (r *OperationRequestMonitorReconciler) getOwnerNamespace( + ctx context.Context, + owner string, +) (*corev1.Namespace, error) { + // Get the owner's namespace using the naming convention + namespace := &corev1.Namespace{} + err := r.Get(ctx, client.ObjectKey{Name: userNamespacePrefix + owner}, namespace) + if err != nil { + return nil, fmt.Errorf( + "failed to get owner namespace %s%s: %w", + userNamespacePrefix, + owner, + err, + ) + } + return namespace, nil +} + +// synchronizeNamespaceStatus synchronizes target namespace annotations based on owner namespace status +// If owner debt status is empty/normal/resume/resume-completed and target is suspended, it will resume the target namespace +func (r *OperationRequestMonitorReconciler) synchronizeNamespaceStatus( + ctx context.Context, + ownerNamespace, targetNamespace *corev1.Namespace, +) error { + if targetNamespace.Annotations == nil { + targetNamespace.Annotations = make(map[string]string) + } + + // Get owner debt status only + var ownerDebtStatus string + if ownerNamespace.Annotations != nil { + ownerDebtStatus = ownerNamespace.Annotations[types.DebtNamespaceAnnoStatusKey] + } + + targetDebtStatus := targetNamespace.Annotations[types.DebtNamespaceAnnoStatusKey] + + // Check if owner debt status allows resuming and target is in suspended state + if isOwnerEligibleForResume(ownerDebtStatus) && isSuspendedDebtStatus(targetDebtStatus) { + original := targetNamespace.DeepCopy() + // Resume the target namespace + targetNamespace.Annotations[types.DebtNamespaceAnnoStatusKey] = types.ResumeDebtNamespaceAnnoStatus + + // Update the target namespace + if err := r.Patch(ctx, targetNamespace, client.MergeFrom(original)); err != nil { + return fmt.Errorf( + "failed to update target namespace %s annotations: %w", + targetNamespace.Name, + err, + ) + } + + r.Logger.Info("Successfully resumed target namespace", + "targetNamespace", targetNamespace.Name, + "ownerNamespace", ownerNamespace.Name, + ) + } + + return nil +} + +// isOwnerEligibleForResume checks if owner debt status allows resuming target namespaces +func isOwnerEligibleForResume(ownerStatus string) bool { + // Owner is eligible if status is empty, normal, resume, or resume-completed + return ownerStatus == "" || + ownerStatus == types.NormalDebtNamespaceAnnoStatus || + ownerStatus == types.ResumeDebtNamespaceAnnoStatus || + ownerStatus == types.ResumeCompletedDebtNamespaceAnnoStatus +} + +// isSuspendedDebtStatus checks if the debt status indicates a suspended state +func isSuspendedDebtStatus(status string) bool { + return status == types.SuspendDebtNamespaceAnnoStatus || + status == types.SuspendCompletedDebtNamespaceAnnoStatus || + status == types.TerminateSuspendDebtNamespaceAnnoStatus || + status == types.TerminateSuspendCompletedDebtNamespaceAnnoStatus || + status == types.FinalDeletionDebtNamespaceAnnoStatus || + status == types.FinalDeletionCompletedDebtNamespaceAnnoStatus +} diff --git a/controllers/account/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go new file mode 100644 index 000000000000..905d26ddd996 --- /dev/null +++ b/controllers/account/controllers/payment_controller.go @@ -0,0 +1,368 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + "github.com/labring/sealos/controllers/pkg/pay" + pkgtypes "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// PaymentReconciler reconciles a Payment object +type PaymentReconciler struct { + client.Client + Account *AccountReconciler + DebtReconciler *DebtReconciler + WatchClient client.WithWatch + Scheme *runtime.Scheme + Logger logr.Logger + reconcileDuration time.Duration + createDuration time.Duration + userLock map[uuid.UUID]*sync.Mutex + domain string +} + +var ( + // Ensure PaymentReconciler implements the LeaderElectionRunnable and Runnable interface + _ manager.LeaderElectionRunnable = &PaymentReconciler{} + _ manager.Runnable = &PaymentReconciler{} +) + +const ( + EnvPaymentReconcileDuration = "PAYMENT_RECONCILE_DURATION" + EnvPaymentCreateDuration = "PAYMENT_CREATE_DURATION" + + defaultReconcileDuration = 10 * time.Second + defaultCreateDuration = 5 * time.Second +) + +//+kubebuilder:rbac:groups=account.sealos.io,resources=payments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=account.sealos.io,resources=payments/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=account.sealos.io,resources=payments/finalizers,verbs=update + +// SetupWithManager sets up the controller with the Manager. +func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) (err error) { + const controllerName = "payment_controller" + r.Logger = ctrl.Log.WithName(controllerName) + r.Logger.V(1).Info("init reconcile controller payment") + r.domain = os.Getenv("DOMAIN") + r.reconcileDuration = defaultReconcileDuration + r.createDuration = defaultCreateDuration + r.userLock = make(map[uuid.UUID]*sync.Mutex) + if duration := os.Getenv(EnvPaymentReconcileDuration); duration != "" { + reconcileDuration, err := time.ParseDuration(duration) + if err == nil { + r.reconcileDuration = reconcileDuration + } + } + if duration := os.Getenv(EnvPaymentCreateDuration); duration != "" { + createDuration, err := time.ParseDuration(duration) + if err == nil { + r.createDuration = createDuration + } + } + r.Logger.V(1). + Info("reconcile duration", "reconcileDuration", r.reconcileDuration, "createDuration", r.createDuration) + if err := mgr.Add(r); err != nil { + return fmt.Errorf("add payment controller failed: %w", err) + } + return nil +} + +// LeaderElectionRunnable knows if a Runnable needs to be run in the leader election mode. +func (r *PaymentReconciler) NeedLeaderElection() bool { + return true +} + +func (r *PaymentReconciler) Start(ctx context.Context) error { + var wg sync.WaitGroup + defer wg.Wait() + fc := func(wg *sync.WaitGroup, t *time.Ticker, reconcileFunc func(ctx context.Context) []error) { + wg.Add(1) + defer wg.Done() + for { + select { + case <-t.C: + if errs := reconcileFunc(ctx); len(errs) > 0 { + for _, err := range errs { + r.Logger.Error(err, "reconcile payments failed") + } + } + case <-ctx.Done(): + return + } + } + } + tickerReconcilePayment := time.NewTicker(r.reconcileDuration) + tickerNewPayment := time.NewTicker(r.createDuration) + go fc(&wg, tickerReconcilePayment, r.reconcilePayments) + go fc(&wg, tickerNewPayment, r.reconcileCreatePayments) + return nil +} + +func (r *PaymentReconciler) reconcilePayments(_ context.Context) (errs []error) { + paymentList := &accountv1.PaymentList{} + err := r.List(context.Background(), paymentList, &client.ListOptions{}) + if err != nil { + errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) + return errs + } + for _, payment := range paymentList.Items { + if err := r.reconcilePayment(&payment); err != nil { + errs = append( + errs, + fmt.Errorf( + "reconcile payment failed: payment: %s, user: %s, err: %w", + payment.Name, + payment.Spec.UserID, + err, + ), + ) + } + } + return errs +} + +func (r *PaymentReconciler) reconcileCreatePayments(ctx context.Context) (errs []error) { + watcher, err := r.WatchClient.Watch( + context.Background(), + &accountv1.PaymentList{}, + &client.ListOptions{}, + ) + if err != nil { + errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) + return errs + } + select { + case <-ctx.Done(): + return errs + case event := <-watcher.ResultChan(): + if event.Object == nil { + break + } + payment, ok := event.Object.(*accountv1.Payment) + if !ok { + errs = append(errs, fmt.Errorf("convert payment failed: %v", event.Object)) + break + } + if err := r.reconcileNewPayment(payment); err != nil { + errs = append( + errs, + fmt.Errorf( + "reconcile payment failed: payment: %s, user: %s, err: %w", + payment.Name, + payment.Spec.UserID, + err, + ), + ) + } + } + return errs +} + +func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error { + if payment.Status.TradeNO == "" { + if err := r.reconcileNewPayment(payment); err != nil { + return fmt.Errorf("reconcile new payment failed: %w", err) + } + return nil + } + if payment.Status.Status == pay.PaymentSuccess { + if err := r.expiredOvertimePayment(payment); err != nil { + return fmt.Errorf("expired payment failed: %w", err) + } + return nil + } + // get payment handler + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + return fmt.Errorf("get payment Interface failed: %w", err) + } + // TODO The GetPaymentDetails may cause issues when using Stripe + status, orderAmount, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) + if err != nil { + return fmt.Errorf("get payment details failed: %w", err) + } + switch status { + case pay.PaymentSuccess: + userUID, err := r.Account.AccountV2.GetUserUID( + &pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}, + ) + if err != nil { + return fmt.Errorf("get user UID failed: %w", err) + } + if r.userLock[userUID] == nil { + r.userLock[userUID] = &sync.Mutex{} + } + r.userLock[userUID].Lock() + defer r.userLock[userUID].Unlock() + debtMutex, err := r.DebtReconciler.getUserMutex(userUID) + if err != nil { + return fmt.Errorf("get debt user lock failed: %w", err) + } + debtMutex.Lock() + shouldRefreshDebt := false + defer func() { + debtMutex.Unlock() + if shouldRefreshDebt { + go r.DebtReconciler.processUsersInParallel([]uuid.UUID{userUID}) + } + }() + userDiscount, err := r.Account.AccountV2.GetUserRechargeDiscount( + &pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}, + ) + if err != nil { + return fmt.Errorf("get user discount failed: %w", err) + } + payAmount := orderAmount + isFirstRecharge, gift := getFirstRechargeDiscount(payAmount, userDiscount) + paymentRaw := pkgtypes.PaymentRaw{ + UserUID: userUID, + Amount: payAmount, + Gift: gift, + CreatedAt: payment.CreationTimestamp.Time, + RegionUserOwner: getUsername(payment.Namespace), + Method: pkgtypes.PaymentMethod(payment.Spec.PaymentMethod), + TradeNO: payment.Status.TradeNO, + CodeURL: payment.Status.CodeURL, + } + if isFirstRecharge { + paymentRaw.ActivityType = pkgtypes.ActivityTypeFirstRecharge + } else if gift > 0 { + paymentRaw.ActivityType = userDiscount.DefaultActiveType + } + + if err = r.Account.AccountV2.Payment(&pkgtypes.Payment{ + PaymentRaw: paymentRaw, + }); err != nil { + return fmt.Errorf("payment failed: %w", err) + } + shouldRefreshDebt = true + payment.Status.Status = pay.PaymentSuccess + if err := r.Status().Update(context.Background(), payment); err != nil { + return fmt.Errorf("update payment failed: %w", err) + } + // case pay.PaymentFailed, pay.PaymentExpired: + default: + if err := r.expiredOvertimePayment(payment); err != nil { + return fmt.Errorf("expired payment failed: %w", err) + } + } + return nil +} + +func (r *PaymentReconciler) expiredOvertimePayment(payment *accountv1.Payment) error { + if payment.CreationTimestamp.Time.Add(12 * time.Minute).After(time.Now()) { + return nil + } + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + return fmt.Errorf("get payment Interface failed: %w", err) + } + currentStatus, _, err := payHandler.GetPaymentDetails(payment.Status.TradeNO) + if err != nil { + return fmt.Errorf("get payment details failed: %w", err) + } + + if payment.Status.Status != pay.PaymentSuccess { + // skip if payment is success paid + if currentStatus == pay.PaymentSuccess { + return nil + } + if err = payHandler.ExpireSession(payment.Status.TradeNO); err != nil { + return fmt.Errorf("expire session failed: %w", err) + } + } + if err = r.Delete(context.Background(), payment); err != nil { + r.Logger.Error(err, "delete payment failed") + } + r.Logger.Info("payment expired", "payment", payment) + return nil +} + +func (r *PaymentReconciler) reconcileNewPayment(payment *accountv1.Payment) error { + if payment.Status.TradeNO != "" { + return nil + } + // backward compatibility + if payment.Spec.UserCR == "" { + if payment.Spec.UserID == "" { + return errors.New("user ID is empty") + } + payment.Spec.UserCR = payment.Spec.UserID + id, err := r.Account.AccountV2.GetUserID( + &pkgtypes.UserQueryOpts{Owner: payment.Spec.UserCR, WithOutCache: true}, + ) + if err != nil { + return fmt.Errorf("get user ID failed: %w", err) + } + payment.Spec.UserID = id + } + if err := r.Update(context.Background(), payment); err != nil { + return fmt.Errorf("create payment failed: %w", err) + } + // get user ID + account, err := r.Account.AccountV2.GetAccount( + &pkgtypes.UserQueryOpts{ID: payment.Spec.UserID, IgnoreEmpty: true}, + ) + if err != nil { + return fmt.Errorf("get account failed: %w", err) + } + if account == nil { + _, err := r.Account.InitUserAccountFunc(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}) + if err != nil { + return fmt.Errorf("create account failed: %w", err) + } + } + // get payment handler + payHandler, err := pay.NewPayHandler(payment.Spec.PaymentMethod) + if err != nil { + return fmt.Errorf("get payment Interface failed: %w", err) + } + tradeNO, codeURL, err := payHandler.CreatePayment( + payment.Spec.Amount, + payment.Spec.UserID, + fmt.Sprintf( + env.GetEnvWithDefault("PAY_DESCRIBE_FORMAT", `sealos cloud pay [domain="%s"]`), + r.domain, + ), + ) + if err != nil { + return fmt.Errorf("get tradeNO and codeURL failed: %w", err) + } + payment.Status.CodeURL = codeURL + payment.Status.TradeNO = tradeNO + payment.Status.Status = pay.PaymentProcessing + if err = r.Status().Update(context.Background(), payment); err != nil { + return fmt.Errorf("update payment failed: %w", err) + } + return nil +} diff --git a/controllers/account/controllers/pod_controller.go b/controllers/account/controllers/pod_controller.go new file mode 100644 index 000000000000..0b3ee9c72060 --- /dev/null +++ b/controllers/account/controllers/pod_controller.go @@ -0,0 +1,114 @@ +/* +Copyright 2023. + +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 controllers + +import ( + "context" + "os" + "strconv" + + "github.com/go-logr/logr" + v1 "github.com/labring/sealos/controllers/account/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// PodReconciler reconciles a Pod object +type PodReconciler struct { + client.Client + CacheReader client.Reader + logr.Logger + Scheme *runtime.Scheme +} + +type PodReconcilerOptions struct { + MaxConcurrentReconciles int +} + +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Pod object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile +func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // Fetch the pod + pod := corev1.Pod{} + + reader := r.CacheReader + if reader == nil { + reader = r.Client + } + err := reader.Get(ctx, req.NamespacedName, &pod) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + if pod.Spec.SchedulerName != v1.DebtSchedulerName { + return reconcile.Result{}, nil + } + original := pod.DeepCopy() + pod.Status.Phase = v1.PodPhaseSuspended + + // Update status after reconciliation. + if err = r.patchStatus(ctx, &pod, original); err != nil { + return ctrl.Result{Requeue: true}, client.IgnoreNotFound(err) + } + return ctrl.Result{}, nil +} + +func (r *PodReconciler) patchStatus( + ctx context.Context, + pod, original *corev1.Pod, +) error { + return r.Client.Status().Patch(ctx, pod, client.MergeFrom(original)) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { + maxConcurrentReconciles, _ := strconv.Atoi(os.Getenv("MAX_POD_CONCURRENT_RECONCILES")) + if maxConcurrentReconciles == 0 { + maxConcurrentReconciles = 2 + } + if r.CacheReader == nil { + r.CacheReader = mgr.GetCache() + } + r.Logger = ctrl.Log.WithName("controllers").WithName("Pod") + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Pod{}). + WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}). + Complete(r) +} diff --git a/controllers/account/controllers/pod_controller_cache_test.go b/controllers/account/controllers/pod_controller_cache_test.go new file mode 100644 index 000000000000..b538a9a43adf --- /dev/null +++ b/controllers/account/controllers/pod_controller_cache_test.go @@ -0,0 +1,82 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "testing" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestPatchPodStatusPreservesUncachedFields(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + stored := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-a", Namespace: "default"}, + Spec: corev1.PodSpec{ + SchedulerName: accountv1.DebtSchedulerName, + Containers: []corev1.Container{ + {Name: "app", Image: "example/app:latest"}, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + Conditions: []corev1.PodCondition{ + {Type: corev1.PodReady, Status: corev1.ConditionTrue}, + }, + }, + } + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&corev1.Pod{}). + WithObjects(stored). + Build() + + projected := &corev1.Pod{ + ObjectMeta: stored.ObjectMeta, + Spec: corev1.PodSpec{ + SchedulerName: stored.Spec.SchedulerName, + }, + Status: corev1.PodStatus{Phase: stored.Status.Phase}, + } + original := projected.DeepCopy() + projected.Status.Phase = accountv1.PodPhaseSuspended + + reconciler := &PodReconciler{Client: cli} + if err := reconciler.patchStatus(context.Background(), projected, original); err != nil { + t.Fatalf("patch pod status: %v", err) + } + + got := &corev1.Pod{} + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), got); err != nil { + t.Fatalf("get updated pod: %v", err) + } + if got.Status.Phase != accountv1.PodPhaseSuspended { + t.Fatalf("phase = %q, want %q", got.Status.Phase, accountv1.PodPhaseSuspended) + } + if len(got.Status.Conditions) != len(stored.Status.Conditions) { + t.Fatalf("conditions = %#v, want %#v", got.Status.Conditions, stored.Status.Conditions) + } +} diff --git a/controllers/account/controllers/property_reload_handler.go b/controllers/account/controllers/property_reload_handler.go new file mode 100644 index 000000000000..5fb266407cec --- /dev/null +++ b/controllers/account/controllers/property_reload_handler.go @@ -0,0 +1,122 @@ +// Copyright © 2023 sealos. +// +// 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 controllers + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/utils" + ctrl "sigs.k8s.io/controller-runtime" +) + +var reloadLogger = ctrl.Log.WithName("property-reload-handler") + +// PropertyReloadHandler is an HTTP handler to reload property types from database +type PropertyReloadHandler struct { + DBClient database.Interface + AccountReconciler *AccountReconciler + JwtSecret string + AdminJwtSecret string +} + +func (h *PropertyReloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Authenticate admin request + if err := authenticateAdminRequest(r, h.AdminJwtSecret, h.JwtSecret); err != nil { + reloadLogger.Error(err, "admin authentication failed") + http.Error(w, "Unauthorized: "+err.Error(), http.StatusUnauthorized) + return + } + + reloadLogger.Info("received request to reload property types from authenticated admin") + + // Reload property types from database + if err := h.DBClient.ReloadPropertyTypeLS(); err != nil { + reloadLogger.Error(err, "failed to reload property types") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var err error + if _, err = h.AccountReconciler.AccountV2.ReloadAccountConfig(); err != nil { + reloadLogger.Error(err, "failed to reload account config") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get the reloaded properties count + propertyCount := 0 + if resources.DefaultPropertyTypeLS != nil { + propertyCount = len(resources.DefaultPropertyTypeLS.Types) + } + + response := map[string]any{ + "status": "success", + "message": "Property types reloaded successfully", + "count": propertyCount, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + reloadLogger.Error(err, "failed to encode response") + } +} + +// authenticateAdminRequest validates that the request is from an admin user +func authenticateAdminRequest(r *http.Request, adminJwtSecret, legacyJwtSecret string) error { + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + return errors.New("authorization header is required") + } + + // Remove "Bearer " prefix if present + token := strings.TrimPrefix(tokenString, "Bearer ") + if token == "" || token == tokenString { + return errors.New("invalid authorization token format") + } + + if adminJwtSecret != "" { + jwtMgr := utils.NewJWTManager(adminJwtSecret, 0) + if _, err := jwtMgr.ParseAdminUser(token); err == nil { + return nil + } + } + + // Keep accepting tokens signed with the legacy account secret during the + // admin-secret migration. Remove this fallback after all callers migrate. + jwtMgr := utils.NewJWTManager(legacyJwtSecret, 0) + user, err := jwtMgr.ParseUser(token) + if err != nil { + return err + } + if user == nil { + return errors.New("user not found in token") + } + + if user.Requester != AdminUserName { + return errors.New("user is not admin") + } + + return nil +} diff --git a/controllers/account/controllers/property_reload_handler_test.go b/controllers/account/controllers/property_reload_handler_test.go new file mode 100644 index 000000000000..fcf7112d613d --- /dev/null +++ b/controllers/account/controllers/property_reload_handler_test.go @@ -0,0 +1,72 @@ +package controllers + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/labring/sealos/controllers/pkg/utils" +) + +func TestAuthenticateAdminRequestUsesDedicatedSecret(t *testing.T) { + adminManager := utils.NewJWTManager("admin-secret", time.Hour) + legacyManager := utils.NewJWTManager("api-secret", time.Hour) + + tests := []struct { + name string + manager *utils.JWTManager + requester string + adminToken bool + wantErr bool + }{ + { + name: "dedicated admin token", + manager: adminManager, + requester: AdminUserName, + adminToken: true, + }, + { + name: "admin key token without admin claims", + manager: adminManager, + requester: AdminUserName, + wantErr: true, + }, + { + name: "legacy admin token", + manager: legacyManager, + requester: AdminUserName, + }, + { + name: "ordinary api token", + manager: legacyManager, + requester: "user", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var token string + var err error + if tt.adminToken { + token, err = tt.manager.GenerateAdminToken(utils.JwtUser{Requester: tt.requester}) + } else { + token, err = tt.manager.GenerateToken(utils.JwtUser{Requester: tt.requester}) + } + if err != nil { + t.Fatalf("generate token: %v", err) + } + + req := httptest.NewRequestWithContext( + context.Background(), http.MethodPost, "/reload-property-types", nil, + ) + req.Header.Set("Authorization", "Bearer "+token) + err = authenticateAdminRequest(req, "admin-secret", "api-secret") + if (err != nil) != tt.wantErr { + t.Fatalf("authenticateAdminRequest() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/controllers/account/controllers/subscription.go b/controllers/account/controllers/subscription.go new file mode 100644 index 000000000000..4457228c8f3b --- /dev/null +++ b/controllers/account/controllers/subscription.go @@ -0,0 +1,742 @@ +package controllers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "sync" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils" + "gorm.io/gorm" +) + +// SubscriptionProcessor 处理订阅事务的处理器 +type SubscriptionProcessor struct { + db *gorm.DB + pollInterval time.Duration + wg sync.WaitGroup + stopChan chan struct{} + *AccountReconciler +} + +// NewSubscriptionProcessor 创建新的处理器实例 +func NewSubscriptionProcessor(reconciler *AccountReconciler) *SubscriptionProcessor { + return &SubscriptionProcessor{ + db: reconciler.AccountV2.GetGlobalDB(), + pollInterval: time.Second, + stopChan: make(chan struct{}), + AccountReconciler: reconciler, + } +} + +// Start 开始监听和处理订阅事务 +func (sp *SubscriptionProcessor) Start(ctx context.Context) error { + sp.wg.Add(1) + go func() { + defer sp.wg.Done() + ticker := time.NewTicker(sp.pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-sp.stopChan: + return + case <-ticker.C: + if err := sp.processPendingTransactions(ctx); err != nil { + log.Printf("Failed to process pending transactions: %v", err) + } + } + } + }() + return nil +} + +// Stop 停止处理器 +func (sp *SubscriptionProcessor) Stop() { + close(sp.stopChan) + sp.wg.Wait() +} + +// processPendingTransactions 处理待处理的事务 +func (sp *SubscriptionProcessor) processPendingTransactions(ctx context.Context) error { + var transactions []types.SubscriptionTransaction + now := time.Now() + + // 查询待处理事务并加锁 + err := sp.db.WithContext(ctx).Model(&types.SubscriptionTransaction{}). + Where("pay_status IN (?, ?) AND start_at <= ? AND status NOT IN (?, ?)", + types.SubscriptionPayStatusPaid, + types.SubscriptionPayStatusNoNeed, + now, + types.SubscriptionTransactionStatusCompleted, + types.SubscriptionTransactionStatusFailed). + Find(&transactions).Error + if err != nil { + return fmt.Errorf("failed to query pending transactions: %w", err) + } + + for i := range transactions { + acc := &types.Account{} + dErr := sp.db.Model(&types.Account{}). + Where(&types.Account{UserUID: transactions[i].UserUID}). + Find(acc). + Error + if dErr != nil { + sp.Logger.Error( + fmt.Errorf("failed to fetch account: %w", dErr), + "", + "user_uid", + transactions[i].UserUID, + ) + continue + } + if acc.CreateRegionID != sp.AccountV2.GetLocalRegion().UID.String() { + continue + } + sp.Logger.Info( + "Processing transaction", + "id", + transactions[i].SubscriptionID, + "operator", + transactions[i].Operator, + "status", + transactions[i].Status, + "plan", + transactions[i].NewPlanName, + ) + if err := sp.processTransaction(ctx, &transactions[i]); err != nil { + sp.Logger.Error( + fmt.Errorf("failed to process transaction: %w", err), + "", + "id", + transactions[i].ID, + ) + } + } + return nil +} + +// processTransaction 处理单个事务 +func (sp *SubscriptionProcessor) processTransaction( + ctx context.Context, + tx *types.SubscriptionTransaction, +) error { + return sp.db.Transaction(func(dbTx *gorm.DB) error { + // var latestTx types.SubscriptionTransaction + // if err := dbTx.Clauses(clause.Locking{Strength: "UPDATE"}). + // Find(&latestTx, "subscription_id = ?", tx.SubscriptionID).Error; err != nil { + // return fmt.Errorf("failed to lock transaction %s: %w", tx.SubscriptionID, err) + //} + latestTx := *tx + // 检查是否仍需处理 + if !sp.shouldProcessTransaction(&latestTx) { + sp.Logger.Info("Transaction needn't processed", "id", latestTx.ID) + return nil + } + + // 根据操作类型分发处理 + handler, exists := map[types.SubscriptionOperator]func(context.Context, *gorm.DB, *types.SubscriptionTransaction) error{ + types.SubscriptionTransactionTypeCreated: sp.handleCreated, + types.SubscriptionTransactionTypeUpgraded: sp.handleUpgrade, + types.SubscriptionTransactionTypeDowngraded: sp.handleDowngrade, + types.SubscriptionTransactionTypeRenewed: sp.handleRenewal, + }[latestTx.Operator] + if !exists { + sp.Logger.Info("Unknown operator", "operator", latestTx.Operator) + return nil // 未知操作类型,跳过 + } + + return handler(ctx, dbTx, &latestTx) + }) +} + +// shouldProcessTransaction 检查事务是否需要处理 +func (sp *SubscriptionProcessor) shouldProcessTransaction(tx *types.SubscriptionTransaction) bool { + now := time.Now() + return (tx.PayStatus == types.SubscriptionPayStatusPaid || tx.PayStatus == types.SubscriptionPayStatusNoNeed) && + !tx.StartAt.After(now) && + tx.Status != types.SubscriptionTransactionStatusCompleted && + tx.Status != types.SubscriptionTransactionStatusFailed +} + +// If the account service network is too slow, can synchronize the database +// func (sp *SubscriptionProcessor) flushOtherDomainQuota(_ context.Context, userUID uuid.UUID) error { +// var regionTaskList []*types.AccountRegionUserTask +// for _, domain := range sp.allRegionDomain { +// if domain == sp.localDomain { +// continue +// } +// regionTaskList = append(regionTaskList, &types.AccountRegionUserTask{ +// UserUID: userUID, +// RegionDomain: domain, +// Type: types.AccountRegionUserTaskTypeFlushQuota, +// StartAt: time.Now().UTC(), +// Status: types.AccountRegionUserTaskStatusPending, +// }) +// } +// err := sp.AccountV2.GetGlobalDB().Transaction(func(tx *gorm.DB) error { +// for _, task := range regionTaskList { +// if err := tx.Create(task).Error; err != nil { +// return err +// } +// } +// return nil +// }) +// if err != nil { +// return fmt.Errorf("failed to create account region user task: %w", err) +// } +// return nil +//} + +// updateQuota 更新用户的资源配额 +func (sp *SubscriptionProcessor) updateQuota( + _ context.Context, + userUID, planID uuid.UUID, + planName string, +) error { + // if err := sp.flushOtherDomainQuota(ctx, userUID); err != nil { + // return fmt.Errorf("failed to flush other domain quota: %w", err) + //} + if err := sp.sendFlushQuotaRequest(userUID, planID, planName); err != nil { + return fmt.Errorf("failed to send flush quota request: %w", err) + } + return nil +} + +const AdminUserName = utils.AdminJWTRequester + +type AdminFlushSubscriptionQuotaReq struct { + UserUID uuid.UUID `json:"userUID" bson:"userUID"` + PlanName string `json:"planName" bson:"planName"` + PlanID uuid.UUID `json:"planID" bson:"planID"` +} + +// 延迟过高 +func (sp *SubscriptionProcessor) sendFlushQuotaRequest( + userUID, planID uuid.UUID, + planName string, +) error { + return sendFlushQuotaRequest(sp.allRegionDomain, sp.adminJwtManager, userUID, planID, planName) +} + +func sendFlushQuotaRequest( + allRegion []string, + jwtManager *utils.JWTManager, + userUID, planID uuid.UUID, + planName string, +) error { + for _, domain := range allRegion { + token, err := jwtManager.GenerateAdminToken(utils.JwtUser{ + Requester: AdminUserName, + }) + if err != nil { + return fmt.Errorf("failed to generate token: %w", err) + } + + url := fmt.Sprintf("https://account-api.%s/admin/v1alpha1/flush-sub-quota", domain) + + quotaReq := AdminFlushSubscriptionQuotaReq{ + UserUID: userUID, + PlanID: planID, + PlanName: planName, + } + quotaReqBody, err := json.Marshal(quotaReq) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + var lastErr error + backoffTime := time.Second + + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + // #nosec G704 -- domains come from operator-managed region records. + req, err := http.NewRequestWithContext( + context.Background(), + http.MethodPost, + url, + bytes.NewBuffer(quotaReqBody), + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + client := http.Client{} + + // #nosec G704 -- domains come from operator-managed region records. + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to send request: %w", err) + } else { + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + lastErr = nil + break + } + body, err := io.ReadAll(resp.Body) + if err != nil { + lastErr = fmt.Errorf( + "unexpected status code: %d, failed to read response body: %w", + resp.StatusCode, + err, + ) + } else { + lastErr = fmt.Errorf( + "unexpected status code: %d, response body: %s", + resp.StatusCode, + string(body), + ) + } + } + + // 进行重试 + if attempt < maxRetries { + fmt.Printf( + "Attempt %d failed: %v. Retrying in %v...\n", + attempt, + lastErr, + backoffTime, + ) + time.Sleep(backoffTime) + backoffTime *= 2 // 指数增长退避时间 + } + } + if lastErr != nil { + return lastErr + } + } + return nil +} + +// handleCreated 处理创建订阅 +func (sp *SubscriptionProcessor) handleCreated( + ctx context.Context, + dbTx *gorm.DB, + tx *types.SubscriptionTransaction, +) error { + var sub types.Subscription + if err := dbTx.Model(&types.Subscription{}). + Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}). + Find(&sub). + Error; err != nil { + return fmt.Errorf("failed to fetch subscription: %w", err) + } + + now := time.Now().UTC() + sub.PlanID = tx.NewPlanID + sub.PlanName = tx.NewPlanName + sub.Status = types.SubscriptionStatusNormal + sub.StartAt = now + sub.UpdateAt = now + sub.ExpireAt = now.AddDate(0, 1, 0) + sub.NextCycleDate = sub.ExpireAt + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update subscription: %w", err) + } + + // 更新配额 + if err := sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName); err != nil { + return fmt.Errorf("failed to update quota: %w", err) + } + + // 创建积分 + plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName) + if err != nil { + return fmt.Errorf("failed to get subscription plan: %w", err) + } + if err := cockroach.CreateCredits(dbTx, &types.Credits{ + UserUID: sub.UserUID, + Amount: plan.GiftAmount, + FromID: sub.PlanID.String(), + FromType: types.CreditsFromTypeSubscription, + ExpireAt: sub.ExpireAt, + CreatedAt: now, + StartAt: now, + Status: types.CreditsStatusActive, + }); err != nil { + return fmt.Errorf("failed to create credits: %w", err) + } + // TODO create Credits Transaction + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + return dbTx.Save(tx).Error +} + +// handleUpgrade 处理升级 +func (sp *SubscriptionProcessor) handleUpgrade( + ctx context.Context, + dbTx *gorm.DB, + tx *types.SubscriptionTransaction, +) error { + var sub types.Subscription + if err := dbTx.Model(&types.Subscription{}). + Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}). + Find(&sub). + Error; err != nil { + return fmt.Errorf("failed to fetch subscription: %w", err) + } + now := time.Now() + // 更新订阅信息 + sub.PlanID = tx.NewPlanID + sub.PlanName = tx.NewPlanName + sub.Status = types.SubscriptionStatusNormal + sub.StartAt = now + sub.UpdateAt = now + sub.ExpireAt = now.AddDate(0, 1, 0) + sub.NextCycleDate = sub.ExpireAt + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update subscription: %w", err) + } + + // 更新配额 + if err := sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName); err != nil { + return err + } + + err := dbTx.Model(&types.Credits{}).Where(&types.Credits{ + UserUID: sub.UserUID, + FromID: tx.OldPlanID.String(), + FromType: types.CreditsFromTypeSubscription, + }).Where("expire_at > ? AND status = ?", now, types.CreditsStatusActive).Update("status", types.CreditsStatusExpired).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to update credits: %w", err) + } + // 更新积分 + plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName) + if err != nil { + return fmt.Errorf("failed to get subscription plan: %w", err) + } + credits := types.Credits{ + UserUID: sub.UserUID, + FromType: types.CreditsFromTypeSubscription, + FromID: sub.PlanID.String(), + Status: types.CreditsStatusActive, + Amount: plan.GiftAmount, + ExpireAt: sub.NextCycleDate, + CreatedAt: now, + StartAt: now, + } + if err := dbTx.Save(&credits).Error; err != nil { + return fmt.Errorf("failed to update credits: %w", err) + } + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = time.Now().UTC() + return dbTx.Save(tx).Error +} + +// handleDowngrade 处理降级 +func (sp *SubscriptionProcessor) handleDowngrade( + ctx context.Context, + dbTx *gorm.DB, + tx *types.SubscriptionTransaction, +) error { + var sub types.Subscription + if err := dbTx.Model(&types.Subscription{}). + Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}). + Find(&sub). + Error; err != nil { + return fmt.Errorf("failed to fetch subscription: %w", err) + } + if ok, err := sp.checkDowngradeConditions(ctx, &sub, tx.NewPlanID); err != nil { + return fmt.Errorf("failed to check downgrade conditions: %w", err) + } else if !ok { + tx.Status = types.SubscriptionTransactionStatusFailed + return dbTx.Save(tx).Error + } + if ok, err := sp.checkQuotaConditions( + ctx, + sub.UserUID, + tx.NewPlanID, + tx.NewPlanName, + ); err != nil { + return fmt.Errorf("failed to check quota conditions: %w", err) + } else if !ok { + tx.Status = types.SubscriptionTransactionStatusFailed + return dbTx.Save(tx).Error + } + + now := time.Now() + sub.PlanID = tx.NewPlanID + sub.PlanName = tx.NewPlanName + sub.Status = types.SubscriptionStatusNormal + sub.StartAt = now + sub.UpdateAt = now + sub.ExpireAt = now.AddDate(0, 1, 0) + sub.NextCycleDate = sub.ExpireAt + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update subscription: %w", err) + } + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + if err := dbTx.Save(tx).Error; err != nil { + return fmt.Errorf("failed to update transaction: %w", err) + } + // 更新配额 + return sp.updateQuota(ctx, sub.UserUID, tx.NewPlanID, tx.NewPlanName) +} + +// handleRenewal 处理续订 +func (sp *SubscriptionProcessor) handleRenewal( + _ context.Context, + dbTx *gorm.DB, + tx *types.SubscriptionTransaction, +) error { + var sub types.Subscription + if err := dbTx.Model(&types.Subscription{}). + Where(&types.Subscription{UserUID: tx.UserUID, ID: tx.SubscriptionID}). + Find(&sub). + Error; err != nil { + return fmt.Errorf("failed to fetch subscription: %w", err) + } + + // 更新订阅时间 + now := time.Now() + sub.Status = types.SubscriptionStatusNormal + sub.StartAt = now + sub.UpdateAt = now + sub.ExpireAt = now.AddDate(0, 1, 0) + sub.NextCycleDate = sub.ExpireAt + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update subscription: %w", err) + } + + // //TODO: 续费赠送 credits + plan, err := sp.AccountV2.GetSubscriptionPlan(tx.NewPlanName) + if err != nil { + return fmt.Errorf("failed to get subscription plan: %w", err) + } + if plan.GiftAmount > 0 { + // 过期之前的 credits + err := dbTx.Model(&types.Credits{}).Where(&types.Credits{ + UserUID: sub.UserUID, + FromID: sub.PlanID.String(), + FromType: types.CreditsFromTypeSubscription, + }).Update("status", types.CreditsStatusExpired).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to update credits: %w", err) + } + if err := cockroach.CreateCredits(dbTx, &types.Credits{ + UserUID: sub.UserUID, + Amount: plan.GiftAmount, + FromID: sub.PlanID.String(), + FromType: types.CreditsFromTypeSubscription, + ExpireAt: sub.NextCycleDate, + CreatedAt: now, + StartAt: now, + Status: types.CreditsStatusActive, + }); err != nil { + return fmt.Errorf("failed to create credits: %w", err) + } + } + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + return dbTx.Save(tx).Error +} + +// checkDowngradeConditions 检查降级条件 +func (sp *SubscriptionProcessor) checkDowngradeConditions( + ctx context.Context, + subscription *types.Subscription, + planID uuid.UUID, +) (bool, error) { + // //TODO: 检查 disk、namespace、seat 等条件 + token, err := sp.desktopJwtManager.GenerateToken(utils.JwtUser{ + UserUID: subscription.UserUID, + RegionUID: sp.AccountV2.GetLocalRegion().UID.String(), + }) + if err != nil { + return false, fmt.Errorf("failed to generate token: %w", err) + } + + url := "http://desktop-frontend.sealos.svc.cluster.local:3000/api/v1alpha/downGrade/check" + + var lastErr error + backoffTime := time.Second + + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + url, + bytes.NewBufferString(fmt.Sprintf(`{"subscriptionPlanId": "%s"}`, planID)), + ) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", token) + req.Header.Set("Content-Type", "application/json") + + client := http.Client{ + Timeout: 10 * time.Minute, + } + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to send request: %w", err) + } else { + defer resp.Body.Close() + + // 读取响应 + body, err := io.ReadAll(resp.Body) + switch { + case err != nil: + return false, fmt.Errorf("failed to read response: %w", err) + case resp.StatusCode == http.StatusOK: + var response APIResponse + if err = json.Unmarshal(body, &response); err != nil { + return false, fmt.Errorf("failed to unmarshal response: %w", err) + } + if response.Code != 200 { + return false, fmt.Errorf("response code is not 200: %v", response) + } + return response.Data.AllWorkspaceReady && response.Data.SeatReady, nil + case resp.StatusCode >= 500: + lastErr = fmt.Errorf( + "unexpected status code: %d; %s", + resp.StatusCode, + string(body), + ) + default: + return false, fmt.Errorf("client error: %d; %s", resp.StatusCode, string(body)) + } + } + + // 进行重试 + if attempt < maxRetries { + fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, lastErr, backoffTime) + time.Sleep(backoffTime) + backoffTime *= 2 // 指数增长退避时间 + } + } + return false, lastErr +} + +type SubscriptionQuotaCheckReq struct { + // @Summary PlanID + // @Description PlanID + PlanID uuid.UUID `json:"planID" bson:"planID" example:"123e4567-e89b-12d3-a456-426614174000"` + + // @Summary PlanName + // @Description PlanName + PlanName string `json:"planName" bson:"planName" example:"planName"` +} + +type SubscriptionQuotaCheckResp struct { + // allWorkspaceReady + AllWorkspaceReady bool `json:"allWorkspaceReady" bson:"allWorkspaceReady" example:"true"` + + ReadyWorkspace []string `json:"readyWorkspace" bson:"readyWorkspace" example:"workspace1,workspace2"` + + UnReadyWorkspace []string `json:"unReadyWorkspace" bson:"unReadyWorkspace" example:"workspace3,workspace4"` +} + +// checkDowngradeConditions 检查降级条件 +func (sp *SubscriptionProcessor) checkQuotaConditions( + ctx context.Context, + userUID, planID uuid.UUID, + planName string, +) (bool, error) { + for _, domain := range sp.allRegionDomain { + token, err := sp.jwtManager.GenerateToken(utils.JwtUser{ + UserUID: userUID, + // RegionUID: sp.AccountV2.GetLocalRegion().UID.String(), + }) + if err != nil { + return false, fmt.Errorf("failed to generate token: %w", err) + } + url := fmt.Sprintf( + "http://account-api.%s/payment/v1alpha1/subscription/quota-check", + domain, + ) + quotaReq := SubscriptionQuotaCheckReq{ + PlanID: planID, + PlanName: planName, + } + quotaReqBody, err := json.Marshal(quotaReq) + if err != nil { + return false, fmt.Errorf("failed to marshal request: %w", err) + } + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + url, + bytes.NewBuffer(quotaReqBody), + ) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Authorization", token) + req.Header.Set("Content-Type", "application/json") + + client := http.Client{ + Timeout: 10 * time.Minute, + } + resp, err := client.Do(req) + if err != nil { + return false, fmt.Errorf("failed to send request: %w", err) + } else { + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("failed to read response: %w", err) + } else if resp.StatusCode == http.StatusOK { + var response SubscriptionQuotaCheckResp + if err = json.Unmarshal(body, &response); err != nil { + return false, fmt.Errorf("failed to unmarshal response: %w", err) + } + if !response.AllWorkspaceReady { + return false, nil + } + continue + } + return false, fmt.Errorf("client error: %d; %s", resp.StatusCode, string(body)) + } + } + return true, nil +} + +type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data Data `json:"data"` +} + +// Data 数据部分结构体 +type Data struct { + AllWorkspaceReady bool `json:"allWorkspaceReady"` + SeatReady bool `json:"seatReady"` + MaxWorkspace int `json:"max_workspace"` + MaxSeat int `json:"max_seat"` + GroupedWorkspaceUsage map[string]WorkspaceGroup `json:"groupedWorkspaceUsage"` +} + +// WorkspaceGroup 工作空间组结构体 +type WorkspaceGroup struct { + Workspaces []Workspace `json:"workspaces"` +} + +// Workspace 单个工作空间结构体 +type Workspace struct { + RegionUID string `json:"regionUid"` + WorkspaceUID string `json:"workspaceUid"` + Seat int `json:"seat"` +} diff --git a/controllers/account/controllers/subscription_test.go b/controllers/account/controllers/subscription_test.go new file mode 100644 index 000000000000..9354554730c9 --- /dev/null +++ b/controllers/account/controllers/subscription_test.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils" + "gorm.io/gorm" +) + +func Test_sendFlushQuotaRequest(t *testing.T) { + requireAccountExternalTest(t, + database.GlobalCockroachURI, + database.LocalCockroachURI, + "LOCAL_REGION", + "ACCOUNT_API_JWT_SECRET", + "ACCOUNT_TEST_REGION_DOMAINS", + ) + regions := strings.Split(os.Getenv("ACCOUNT_TEST_REGION_DOMAINS"), ",") + jwtManager := utils.NewJWTManager(os.Getenv("ACCOUNT_API_JWT_SECRET"), time.Hour) + account, err := database.NewAccountV2( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + t.Fatalf("failed to new account: %v", err) + } + defer func() { + if err := account.Close(); err != nil { + t.Errorf("failed close connection: %v", err) + } + }() + err = FetchAndFlushSubscriptions(account.GetGlobalDB(), regions, jwtManager) + if err != nil { + t.Fatalf("failed to fetch and flush subscriptions: %v", err) + } + t.Logf("successfully fetch and flush subscriptions") +} + +func FetchAndFlushSubscriptions( + db *gorm.DB, + allRegion []string, + jwtManager *utils.JWTManager, +) error { + var subscriptions []types.Subscription + + // Query subscriptions where PlanName is not "Free" + result := db.Where("plan_name != ? AND status != ?", "Free", "DEBT").Find(&subscriptions) + if result.Error != nil { + return fmt.Errorf("failed to fetch subscriptions: %w", result.Error) + } + + fmt.Printf("fetched %d subscriptions\n", len(subscriptions)) + + // Iterate through each subscription and call sendFlushQuotaRequest + for _, sub := range subscriptions { + if sub.PlanName == "Free" || sub.Status == "DEBT" { + // Skip subscriptions with PlanName "Free" or status "DEBT" + fmt.Printf( + "Skipping subscription for user %s with plan %s and status %s\n", + sub.UserUID, + sub.PlanName, + sub.Status, + ) + continue + } + err := sendFlushQuotaRequest(allRegion, jwtManager, sub.UserUID, sub.PlanID, sub.PlanName) + if err != nil { + // Log the error but continue processing other subscriptions + fmt.Printf("Failed to flush quota %s for user %s: %v\n", sub.PlanName, sub.UserUID, err) + continue + } + fmt.Printf("Successfully flushed quota %s for user %s\n", sub.PlanName, sub.UserUID) + } + return nil +} diff --git a/controllers/account/controllers/suspend_state.go b/controllers/account/controllers/suspend_state.go new file mode 100644 index 000000000000..cf2eebefae1c --- /dev/null +++ b/controllers/account/controllers/suspend_state.go @@ -0,0 +1,220 @@ +package controllers + +import ( + "encoding/json" +) + +// Annotation keys +const ( + // Stores the original state before suspension in JSON format + OriginalSuspendStateAnnotation = "sealos.io/original-suspend-state" + CertManagerDisableReissueAnnotation = "cert-manager.io/disable-reissue" +) + +// Ingress class constants +const ( + IngressClassAnnotation = "kubernetes.io/ingress.class" + IngressClassPause = "pause" + IngressClassNginx = "nginx" +) + +// DeploymentOriginalState stores the original state of Deployment/StatefulSet/ReplicaSet +type DeploymentOriginalState struct { + Replicas int32 `json:"replicas"` +} + +// CronJobOriginalState stores the original suspend state of CronJob +type CronJobOriginalState struct { + Suspend bool `json:"suspend"` +} + +// JobOriginalState stores the original suspend state of Job +type JobOriginalState struct { + Suspend bool `json:"suspend"` +} + +// KBClusterOriginalState stores the original state of KubeBlocks Cluster +type KBClusterOriginalState struct { + WasRunning bool `json:"wasRunning"` // Whether the cluster was running before suspension + BackupEnabled bool `json:"backupEnabled"` // Whether backup was enabled +} + +// CertificateOriginalState stores the original state of cert-manager Certificate +type CertificateOriginalState struct { + DisableReissue bool `json:"disableReissue"` // Whether reissue was disabled (annotation existed and was "true") +} + +// IngressOriginalState stores the original state of Ingress +type IngressOriginalState struct { + IngressClass string `json:"ingressClass"` // Original ingress class (e.g., "nginx") +} + +// PauseData stores HPA configuration when pausing deployment/statefulset +type PauseData struct { + Target string `json:"target"` // Resource target (e.g., "cpu", "memory") + Value string `json:"value"` // Target utilization value +} + +// DevboxOriginalState stores the original state of Devbox +type DevboxOriginalState struct { + WasRunning bool `json:"wasRunning"` // Whether the devbox was running before suspension +} + +// Encoding functions for each type + +func encodeDeploymentState(state *DeploymentOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeDeploymentState(data string) (*DeploymentOriginalState, error) { + var state DeploymentOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func encodeCronJobState(state *CronJobOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeCronJobState(data string) (*CronJobOriginalState, error) { + var state CronJobOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func encodeJobState(state *JobOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeJobState(data string) (*JobOriginalState, error) { + var state JobOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func encodeKBClusterState(state *KBClusterOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeKBClusterState(data string) (*KBClusterOriginalState, error) { + var state KBClusterOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func encodeCertificateState(state *CertificateOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeCertificateState(data string) (*CertificateOriginalState, error) { + var state CertificateOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func encodeIngressState(state *IngressOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeIngressState(data string) (*IngressOriginalState, error) { + var state IngressOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +// Default state constructors - used when original state is missing or decode fails + +func getDefaultDeploymentState() *DeploymentOriginalState { + return &DeploymentOriginalState{ + Replicas: 1, // Default: restore to 1 replica + } +} + +func getDefaultCronJobState() *CronJobOriginalState { + return &CronJobOriginalState{ + Suspend: false, // Default: resume to not suspended + } +} + +func getDefaultJobState() *JobOriginalState { + return &JobOriginalState{ + Suspend: false, // Default: resume to not suspended + } +} + +func getDefaultKBClusterState() *KBClusterOriginalState { + return &KBClusterOriginalState{ + WasRunning: true, // Default: restore to running + BackupEnabled: false, // Default: don't modify backup + } +} + +func getDefaultCertificateState() *CertificateOriginalState { + return &CertificateOriginalState{ + DisableReissue: false, // Default: wasn't disabled + } +} + +func getDefaultIngressState() *IngressOriginalState { + return &IngressOriginalState{ + IngressClass: IngressClassNginx, // Default: restore to nginx + } +} + +func encodeDevboxState(state *DevboxOriginalState) (string, error) { + data, err := json.Marshal(state) + if err != nil { + return "", err + } + return string(data), nil +} + +func decodeDevboxState(data string) (*DevboxOriginalState, error) { + var state DevboxOriginalState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, err + } + return &state, nil +} + +func getDefaultDevboxState() *DevboxOriginalState { + return &DevboxOriginalState{ + WasRunning: true, // Default: was running, restore to running + } +} diff --git a/controllers/account/controllers/suspend_state_test.go b/controllers/account/controllers/suspend_state_test.go new file mode 100644 index 000000000000..ce192199c104 --- /dev/null +++ b/controllers/account/controllers/suspend_state_test.go @@ -0,0 +1,173 @@ +package controllers + +import ( + "testing" +) + +func TestEncodeDecodeDeploymentState(t *testing.T) { + tests := []struct { + name string + replicas int32 + }{ + {"zero replicas", 0}, + {"one replica", 1}, + {"multiple replicas", 5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &DeploymentOriginalState{ + Replicas: tt.replicas, + } + + encoded, err := encodeDeploymentState(state) + if err != nil { + t.Fatalf("failed to encode state: %v", err) + } + + decoded, err := decodeDeploymentState(encoded) + if err != nil { + t.Fatalf("failed to decode state: %v", err) + } + + if decoded.Replicas != tt.replicas { + t.Errorf("expected replicas %d, got %d", tt.replicas, decoded.Replicas) + } + }) + } +} + +func TestEncodeDecodeCronJobState(t *testing.T) { + tests := []struct { + name string + suspend bool + }{ + {"suspended", true}, + {"not suspended", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &CronJobOriginalState{ + Suspend: tt.suspend, + } + + encoded, err := encodeCronJobState(state) + if err != nil { + t.Fatalf("failed to encode state: %v", err) + } + + decoded, err := decodeCronJobState(encoded) + if err != nil { + t.Fatalf("failed to decode state: %v", err) + } + + if decoded.Suspend != tt.suspend { + t.Errorf("expected suspend %v, got %v", tt.suspend, decoded.Suspend) + } + }) + } +} + +func TestEncodeDecodeKBClusterState(t *testing.T) { + tests := []struct { + name string + wasRunning bool + backupEnabled bool + }{ + {"running with backup", true, true}, + {"running without backup", true, false}, + {"stopped with backup", false, true}, + {"stopped without backup", false, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &KBClusterOriginalState{ + WasRunning: tt.wasRunning, + BackupEnabled: tt.backupEnabled, + } + + encoded, err := encodeKBClusterState(state) + if err != nil { + t.Fatalf("failed to encode state: %v", err) + } + + decoded, err := decodeKBClusterState(encoded) + if err != nil { + t.Fatalf("failed to decode state: %v", err) + } + + if decoded.WasRunning != tt.wasRunning { + t.Errorf("expected wasRunning %v, got %v", tt.wasRunning, decoded.WasRunning) + } + + if decoded.BackupEnabled != tt.backupEnabled { + t.Errorf( + "expected backupEnabled %v, got %v", + tt.backupEnabled, + decoded.BackupEnabled, + ) + } + }) + } +} + +func TestEncodeDecodeCertificateState(t *testing.T) { + tests := []struct { + name string + disableReissue bool + }{ + {"reissue was disabled", true}, + {"reissue was not disabled", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := &CertificateOriginalState{ + DisableReissue: tt.disableReissue, + } + + encoded, err := encodeCertificateState(state) + if err != nil { + t.Fatalf("failed to encode state: %v", err) + } + + decoded, err := decodeCertificateState(encoded) + if err != nil { + t.Fatalf("failed to decode state: %v", err) + } + + if decoded.DisableReissue != tt.disableReissue { + t.Errorf( + "expected disableReissue %v, got %v", + tt.disableReissue, + decoded.DisableReissue, + ) + } + }) + } +} + +func TestDecodeInvalidJSON(t *testing.T) { + tests := []struct { + name string + decodeFunc func(string) error + }{ + {"deployment", func(s string) error { _, err := decodeDeploymentState(s); return err }}, + {"cronjob", func(s string) error { _, err := decodeCronJobState(s); return err }}, + {"kbcluster", func(s string) error { _, err := decodeKBClusterState(s); return err }}, + {"certificate", func(s string) error { _, err := decodeCertificateState(s); return err }}, + } + + invalidJSON := "not a valid json" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.decodeFunc(invalidJSON) + if err == nil { + t.Error("expected error when decoding invalid JSON, got nil") + } + }) + } +} diff --git a/controllers/account/controllers/user_traffic_controller.go b/controllers/account/controllers/user_traffic_controller.go new file mode 100644 index 000000000000..af2ca4f5d984 --- /dev/null +++ b/controllers/account/controllers/user_traffic_controller.go @@ -0,0 +1,874 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/google/uuid" + utils2 "github.com/labring/sealos/controllers/account/controllers/utils" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils" + "gorm.io/gorm" +) + +type UserTrafficController struct { + TrafficDB database.Interface + GlobalDB *gorm.DB + *AccountReconciler +} + +func NewUserTrafficController( + ar *AccountReconciler, + trafficDBURI database.Interface, +) *UserTrafficController { + return &UserTrafficController{ + TrafficDB: trafficDBURI, + GlobalDB: ar.AccountV2.GetGlobalDB(), + AccountReconciler: ar, + } +} + +func (c *UserTrafficController) BatchGetUserUID() (map[string]uuid.UUID, error) { + allUser := make(map[string]uuid.UUID) + allUserCr := make([]types.RegionUserCr, 0) + err := c.AccountV2.GetLocalDB(). + Model(&types.RegionUserCr{}). + Select(`"crName"`, `"userUid"`). + Scan(&allUserCr). + Error + if err != nil { + return nil, fmt.Errorf("failed to get all user CRs: %w", err) + } + for _, userCr := range allUserCr { + if userCr.UserUID == uuid.Nil { + c.Logger.Info("user UID is nil, skipping", "crName", userCr.CrName) + continue + } + allUser[userCr.CrName] = userCr.UserUID + } + return allUser, nil +} + +const ( + batchSize = 500 // Number of records per batch +) + +// processUserTraffic processes user traffic data and commits in batches. +func (c *UserTrafficController) processUserTraffic(resultMap map[string]int64) error { + userUIDMap, err := c.BatchGetUserUID() + if err != nil { + c.Logger.Error(err, "failed to batch get user uids") + return fmt.Errorf("failed to batch get user uids: %w", err) + } + + trafficRecords := make([]*types.UserTimeRangeTraffic, 0, len(resultMap)) + var skippedNamespaces []string + now := time.Now() + for namespace, totalBytes := range resultMap { + if !strings.HasPrefix(namespace, "ns-") { + continue + } + owner := strings.TrimPrefix(namespace, "ns-") + userUID, exists := userUIDMap[owner] + if !exists { + skippedNamespaces = append(skippedNamespaces, namespace) + continue + } + trafficRecords = append(trafficRecords, &types.UserTimeRangeTraffic{ + CreatedAt: now, + UpdatedAt: now, + NextCleanTime: now.AddDate(0, 1, 0), + UserUID: userUID, + SentBytes: totalBytes, + Status: types.UserTimeRangeTrafficStatusProcessing, + }) + } + if len(skippedNamespaces) > 0 { + c.Logger.Info("skipped namespaces due to missing users", "namespaces", skippedNamespaces) + } + + userUIDs := make([]uuid.UUID, 0, len(userUIDMap)) + for _, uid := range userUIDMap { + userUIDs = append(userUIDs, uid) + } + var existingUserUIDs []uuid.UUID + if err := c.GlobalDB.Model(&types.UserTimeRangeTraffic{}). + Where("user_uid IN ?", userUIDs). + Pluck("user_uid", &existingUserUIDs). + Error; err != nil { + c.Logger.Error(err, "failed to fetch existing user uids") + return fmt.Errorf("failed to fetch existing user uids: %w", err) + } + existingUIDMap := make(map[uuid.UUID]struct{}, len(existingUserUIDs)) + for _, uid := range existingUserUIDs { + existingUIDMap[uid] = struct{}{} + } + + var toInsert, toUpdate []*types.UserTimeRangeTraffic + for _, record := range trafficRecords { + if _, exists := existingUIDMap[record.UserUID]; exists { + toUpdate = append(toUpdate, record) + } else { + toInsert = append(toInsert, record) + } + } + + // Process inserts in batches + if err := c.processBatchInserts(toInsert); err != nil { + return err + } + + // Process updates in batches + if err := c.processBatchUpdates(toUpdate, now); err != nil { + return err + } + + return nil +} + +// processBatchInserts inserts records in batches. +func (c *UserTrafficController) processBatchInserts(records []*types.UserTimeRangeTraffic) error { + if len(records) == 0 { + return nil + } + for i := 0; i < len(records); i += batchSize { + end := min(i+batchSize, len(records)) + batch := records[i:end] + tx := c.GlobalDB.Begin() + if tx.Error != nil { + c.Logger.Error( + tx.Error, + "failed to begin transaction for insert batch", + "batch_size", + len(batch), + ) + return fmt.Errorf("failed to begin transaction for insert batch: %w", tx.Error) + } + if err := tx.Create(batch).Error; err != nil { + tx.Rollback() + c.Logger.Error(err, "failed to batch insert user traffic", "batch_size", len(batch)) + return fmt.Errorf("failed to batch insert user traffic: %w", err) + } + if err := tx.Commit().Error; err != nil { + c.Logger.Error( + err, + "failed to commit transaction for insert batch", + "batch_size", + len(batch), + ) + return fmt.Errorf("failed to commit transaction for insert batch: %w", err) + } + c.Logger.Info("successfully inserted batch", "batch_size", len(batch), "start_index", i) + } + return nil +} + +// processBatchUpdates updates records in batches. +func (c *UserTrafficController) processBatchUpdates( + records []*types.UserTimeRangeTraffic, + now time.Time, +) error { + if len(records) == 0 { + return nil + } + + for i := 0; i < len(records); i += batchSize { + end := min(i+batchSize, len(records)) + batch := records[i:end] + + tx := c.GlobalDB.Begin() + if tx.Error != nil { + c.Logger.Error( + tx.Error, + "failed to begin transaction for update batch", + "batch_size", + len(batch), + ) + return fmt.Errorf("failed to begin transaction for update batch: %w", tx.Error) + } + + var caseStmt strings.Builder + caseStmt.WriteString("CASE user_uid ") + values := make([]any, 0, len(batch)*2) + batchUserUIDs := make([]uuid.UUID, 0, len(batch)) + for _, record := range batch { + caseStmt.WriteString("WHEN ? THEN sent_bytes + ? ") + values = append(values, record.UserUID, record.SentBytes) + batchUserUIDs = append(batchUserUIDs, record.UserUID) + } + caseStmt.WriteString("END") + + if err := tx.Model(&types.UserTimeRangeTraffic{}). + Where("user_uid IN ?", batchUserUIDs). + Updates(map[string]any{ + "sent_bytes": gorm.Expr(caseStmt.String(), values...), + "updated_at": now, + }).Error; err != nil { + tx.Rollback() + c.Logger.Error(err, "failed to batch update user traffic", "batch_size", len(batch)) + return fmt.Errorf("failed to batch update user traffic: %w", err) + } + + if err := tx.Commit().Error; err != nil { + c.Logger.Error( + err, + "failed to commit transaction for update batch", + "batch_size", + len(batch), + ) + return fmt.Errorf("failed to commit transaction for update batch: %w", err) + } + c.Logger.Info("successfully updated batch", "batch_size", len(batch), "start_index", i) + } + return nil +} + +func (c *UserTrafficController) ProcessTrafficWithTimeRange() { + c.Logger.Info("start user traffic controller") + startTime := time.Now().Add(-1 * time.Minute) + for range time.NewTicker(time.Minute).C { + c.Logger.Info("time to process user traffic", "startTime", startTime) + endTime := time.Now() + result, err := c.TrafficDB.GetNamespaceTraffic(context.Background(), startTime, endTime) + if err != nil { + c.Logger.Error(err, "failed to get namespace traffic") + endTime = startTime + } else if len(result) > 0 { + err = c.processUserTraffic(result) + if err != nil { + c.Logger.Error(err, "failed to process user traffic") + } else { + c.Logger.Info( + "successfully process user traffic", + "count", + len(result), + "start", + startTime, + "end", + endTime, + ) + } + } + startTime = endTime + } +} + +func (c *UserTrafficController) sendSuspendUserTrafficRequest(userUID uuid.UUID) error { + return c.sendUserTrafficRequest(userUID, "suspend") +} + +func (c *UserTrafficController) sendResumeUserTrafficRequest(userUID uuid.UUID) error { + return c.sendUserTrafficRequest(userUID, "resume") +} + +func (c *UserTrafficController) sendUserTrafficRequest(userUID uuid.UUID, operator string) error { + for _, domain := range c.allRegionDomain { + token, err := c.adminJwtManager.GenerateAdminToken(utils.JwtUser{ + Requester: AdminUserName, + }) + if err != nil { + return fmt.Errorf("failed to generate token: %w", err) + } + + url := fmt.Sprintf( + "https://account-api.%s/admin/v1alpha1/%s-user-traffic?userUID=%s", + domain, + operator, + userUID.String(), + ) + + var lastErr error + backoffTime := time.Second + + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + client := http.Client{} + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to send request: %w", err) + } else { + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + lastErr = nil + break + } + body, err := io.ReadAll(resp.Body) + if err != nil { + lastErr = fmt.Errorf( + "unexpected status code: %d, failed to read response body: %w", + resp.StatusCode, + err, + ) + } else { + lastErr = fmt.Errorf( + "unexpected status code: %d, response body: %s", + resp.StatusCode, + string(body), + ) + } + } + + // 进行重试 + if attempt < maxRetries { + fmt.Printf( + "Attempt %d failed: %v. Retrying in %v...\n", + attempt, + lastErr, + backoffTime, + ) + time.Sleep(backoffTime) + backoffTime *= 2 // 指数增长退避时间 + } + } + if lastErr != nil { + return lastErr + } + } + return nil +} + +const ( + ProcessingBatchSize = 1000 + WorkerPoolSize = 50 + CheckInterval = 1 * time.Minute + FreeTrafficLimit = 10 * 1024 * 1024 * 1024 +) + +// UserTrafficMonitor monitors user traffic and processes suspensions +type UserTrafficMonitor struct { + db *gorm.DB + userTrafficController *UserTrafficController + subscriptionCache *utils2.SubscriptionCache + processingUsers sync.Map // Ongoing processing users: map[uuid.UUID]bool + resumingUsers sync.Map + suspendQueue chan uuid.UUID + resumeQueue chan uuid.UUID // Queue for users to suspend or resume + workerPool chan struct{} + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +// ProcessingUser holds user data for processing +type ProcessingUser struct { + UserUID uuid.UUID + SentBytes int64 + UpdatedAt time.Time +} + +// NewUserTrafficMonitor creates a new user traffic monitor +func NewUserTrafficMonitor(controller *UserTrafficController) (*UserTrafficMonitor, error) { + ctx, cancel := context.WithCancel(context.Background()) + + // Initialize SubscriptionCache with 1-minute update interval + cache, err := utils2.NewSubscriptionCache(controller.GlobalDB, time.Minute) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to initialize subscription cache: %w", err) + } + + monitor := &UserTrafficMonitor{ + db: controller.GlobalDB, + userTrafficController: controller, + subscriptionCache: cache, + suspendQueue: make(chan uuid.UUID, 10000), // Large capacity queue + resumeQueue: make(chan uuid.UUID, 10000), + workerPool: make(chan struct{}, WorkerPoolSize), + ctx: ctx, + cancel: cancel, + } + + // Initialize worker pool + for range WorkerPoolSize { + monitor.workerPool <- struct{}{} + } + + return monitor, nil +} + +// Start begins the monitoring process +func (m *UserTrafficMonitor) Start() { + log.Println("Starting user traffic monitoring system...") + + // Start async suspend processor + m.wg.Add(1) + go m.asyncSuspendProcessor() + + m.wg.Add(1) + go m.asyncResumeProcessor() + + // Start periodic checker + m.wg.Add(1) + go m.periodicChecker() + + m.wg.Add(1) + go m.ResumeUsers() +} + +// Stop shuts down the monitoring system +func (m *UserTrafficMonitor) Stop() { + log.Println("Stopping user traffic monitoring system...") + m.cancel() + close(m.suspendQueue) + m.subscriptionCache.Close() + m.wg.Wait() +} + +// periodicChecker runs periodic checks for users exceeding traffic limits +func (m *UserTrafficMonitor) periodicChecker() { + defer m.wg.Done() + + ticker := time.NewTicker(CheckInterval) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + start := time.Now() + processed := m.checkAndProcessUsers() + duration := time.Since(start) + log.Printf("Processed %d users in %v", processed, duration) + } + } +} + +// checkAndProcessUsers checks and processes users in batches +func (m *UserTrafficMonitor) checkAndProcessUsers() int { + processed := 0 + lastUpdateTime := time.Now().Add(-CheckInterval) + + // Process in batches to avoid memory issues + offset := 0 + for { + users, err := m.getProcessingUsers(offset, ProcessingBatchSize, lastUpdateTime) + if err != nil { + log.Printf("Failed to get processing users: %v", err) + break + } + if len(users) == 0 { + break + } + log.Printf("Processing users len: %d", len(users)) + // Process this batch of users + processedBatch := m.processUsersBatch(users) + processed += processedBatch + + offset += ProcessingBatchSize + + // Stop if fewer than batch size, indicating no more data + if len(users) < ProcessingBatchSize { + break + } + } + return processed +} + +// getProcessingUsers fetches users with traffic exceeding the limit +func (m *UserTrafficMonitor) getProcessingUsers( + offset, limit int, + since time.Time, +) ([]ProcessingUser, error) { + var results []ProcessingUser + + // Optimized query without subscription join + query := ` + SELECT DISTINCT + user_uid, + sent_bytes, + updated_at + FROM "UserTimeRangeTraffic" + WHERE status = ? + AND updated_at > ? + AND sent_bytes > ? + ORDER BY updated_at DESC + LIMIT ? OFFSET ? +` + + err := m.db.Raw(query, types.UserTimeRangeTrafficStatusProcessing, since, FreeTrafficLimit, limit, offset). + Scan(&results). + Error + if err != nil { + return nil, fmt.Errorf("failed to query processing users: %w", err) + } + + return results, nil +} + +// processUsersBatch processes a batch of users +func (m *UserTrafficMonitor) processUsersBatch(users []ProcessingUser) int { + processed := 0 + + for _, user := range users { + // Check if user is already being processed + if _, exists := m.processingUsers.LoadOrStore(user.UserUID, true); exists { + continue + } + + // Check if user is on a Free plan using SubscriptionCache + if entry, exists := m.subscriptionCache.GetEntry(user.UserUID); !exists || + entry.PlanName != "Free" { + m.processingUsers.Delete(user.UserUID) + continue + } + + // Submit to suspend queue + select { + case m.suspendQueue <- user.UserUID: + processed++ + default: + // Queue full, skip user for next iteration + m.processingUsers.Delete(user.UserUID) + log.Printf("Suspend queue full, skipping user: %s", user.UserUID) + } + } + + return processed +} + +// asyncSuspendProcessor handles suspend requests asynchronously +func (m *UserTrafficMonitor) asyncSuspendProcessor() { + defer m.wg.Done() + for { + select { + case <-m.ctx.Done(): + return + case userUID, ok := <-m.suspendQueue: + if !ok { + return + } + <-m.workerPool + + go func(uid uuid.UUID) { + defer func() { + m.workerPool <- struct{}{} + m.processingUsers.Delete(uid) + }() + m.handleUserSuspension(uid) + }(userUID) + } + } +} + +// handleUserSuspension processes a user suspension +func (m *UserTrafficMonitor) handleUserSuspension(userUID uuid.UUID) { + log.Printf("Starting user suspension processing: %s", userUID) + + // Call suspend API + err := m.userTrafficController.sendSuspendUserTrafficRequest(userUID) + if err != nil { + log.Printf("Failed to suspend user traffic %s: %v", userUID, err) + return + } + + // Update status to UsedUp + err = m.updateUserTrafficStatusUsedUp(userUID) + if err != nil { + log.Printf("Failed to update user status %s: %v", userUID, err) + return + } + + log.Printf("Successfully suspended user traffic: %s", userUID) +} + +func (m *UserTrafficMonitor) updateUserTrafficStatusUsedUp(userUID uuid.UUID) error { + result := m.db.Model(&types.UserTimeRangeTraffic{}). + Where("user_uid = ?", userUID). + Update("status", types.UserTimeRangeTrafficStatusUsedUp) + if result.Error != nil { + return fmt.Errorf("failed to update status: %w", result.Error) + } + return nil +} + +func (m *UserTrafficMonitor) updateUserTrafficStatusClean(userUID uuid.UUID) error { + result := m.db.Model(&types.UserTimeRangeTraffic{}). + Where("user_uid = ?", userUID). + Update("status", types.UserTimeRangeTrafficStatusProcessing). + Update("sent_bytes", 0). + Update("next_clean_time", time.Now().UTC().AddDate(0, 1, 0)) + + if result.Error != nil { + return fmt.Errorf("failed to update status: %w", result.Error) + } + if result.RowsAffected == 0 { + return errors.New("no records found to update") + } + return nil +} + +func (m *UserTrafficMonitor) ResumeUsers() { + defer m.wg.Done() + log.Println("Starting user traffic resume process...") + + ticker := time.NewTicker(CheckInterval) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + log.Println("Stopping user traffic resume process...") + return + case <-ticker.C: + start := time.Now() + processed := m.processResumeUsers() + duration := time.Since(start) + log.Printf("Processed %d users for resume in %v", processed, duration) + } + } +} + +// processResumeUsers handles the resumption logic for eligible users +func (m *UserTrafficMonitor) processResumeUsers() int { + processed := 0 + offset := 0 + batchSize := ProcessingBatchSize + + // Get all non-Free plan users with Normal status from cache + eligibleUsers := m.getEligibleUsersFromCache() + + for { + users, err := m.getUsedUpUsers(offset, batchSize) + if err != nil { + log.Printf("Failed to get used_up users: %v", err) + break + } + if len(users) == 0 { + break + } + + processedBatch := m.processResumeBatch(users, eligibleUsers) + processed += processedBatch + + offset += batchSize + if len(users) < batchSize { + break + } + } + + // Process users approaching NextCleanTime + processed += m.processNextCleanTimeUsers() + + return processed +} + +// getEligibleUsersFromCache retrieves users with non-Free plans and Normal status +func (m *UserTrafficMonitor) getEligibleUsersFromCache() map[uuid.UUID]struct{} { + eligibleUsers := make(map[uuid.UUID]struct{}) + entries := m.subscriptionCache.GetAllEntries() + for _, entry := range entries { + if entry.PlanName != types.FreeSubscriptionPlanName && + entry.Status == types.SubscriptionStatusNormal { + eligibleUsers[entry.UserUID] = struct{}{} + } + } + return eligibleUsers +} + +// getUsedUpUsers fetches users with used_up status +func (m *UserTrafficMonitor) getUsedUpUsers(offset, limit int) ([]ProcessingUser, error) { + var results []ProcessingUser + query := ` + SELECT + user_uid, + sent_bytes, + updated_at + FROM "UserTimeRangeTraffic" + WHERE status = ? + ORDER BY updated_at DESC + LIMIT ? OFFSET ? + ` + err := m.db.Raw(query, types.UserTimeRangeTrafficStatusUsedUp, limit, offset). + Scan(&results). + Error + if err != nil { + return nil, fmt.Errorf("failed to query used_up users: %w", err) + } + return results, nil +} + +// processResumeBatch processes a batch of users for resumption +func (m *UserTrafficMonitor) processResumeBatch( + users []ProcessingUser, + eligibleUsers map[uuid.UUID]struct{}, +) int { + processed := 0 + + for _, user := range users { + // Check if user is already being processed + if _, exists := m.resumingUsers.LoadOrStore(user.UserUID, true); exists { + continue + } + + // Check if user is eligible for resumption (non-Free plan and Normal status) + if _, exists := eligibleUsers[user.UserUID]; !exists { + m.resumingUsers.Delete(user.UserUID) + continue + } + + // Submit to suspend queue for async processing + select { + case m.resumeQueue <- user.UserUID: + processed++ + default: + m.resumingUsers.Delete(user.UserUID) + log.Printf("Resume queue full, skipping user: %s", user.UserUID) + } + } + + return processed +} + +// processNextCleanTimeUsers handles users approaching NextCleanTime +func (m *UserTrafficMonitor) processNextCleanTimeUsers() int { + processed := 0 + offset := 0 + batchSize := ProcessingBatchSize + threshold := time.Now().Add(time.Hour) + + for { + var users []struct { + UserUID uuid.UUID + NextCleanTime time.Time + } + query := ` + SELECT + user_uid, + next_clean_time + FROM "UserTimeRangeTraffic" + WHERE next_clean_time <= ? + ORDER BY next_clean_time DESC + LIMIT ? OFFSET ? + ` + err := m.db.Raw(query, threshold, batchSize, offset).Scan(&users).Error + if err != nil { + log.Printf("Failed to query NextCleanTime users: %v", err) + break + } + if len(users) == 0 { + break + } + + for _, user := range users { + if _, exists := m.resumingUsers.LoadOrStore(user.UserUID, true); exists { + continue + } + + select { + case m.resumeQueue <- user.UserUID: + processed++ + default: + m.resumingUsers.Delete(user.UserUID) + log.Printf("Resume queue full, skipping user: %s", user.UserUID) + } + } + + offset += batchSize + if len(users) < batchSize { + break + } + } + + return processed +} + +// StartResumeProcessor starts the async resume processor +func (m *UserTrafficMonitor) StartResumeProcessor() { + m.wg.Add(1) + go m.asyncResumeProcessor() +} + +// asyncResumeProcessor handles resume requests asynchronously +func (m *UserTrafficMonitor) asyncResumeProcessor() { + defer m.wg.Done() + for { + select { + case <-m.ctx.Done(): + return + case userUID, ok := <-m.resumeQueue: + if !ok { + return + } + <-m.workerPool + + go func(uid uuid.UUID) { + defer func() { + m.workerPool <- struct{}{} + m.resumingUsers.Delete(uid) + }() + m.handleUserResumption(uid) + }(userUID) + } + } +} + +// handleUserResumption processes a user resumption +func (m *UserTrafficMonitor) handleUserResumption(userUID uuid.UUID) { + log.Printf("Starting user resumption processing: %s", userUID) + + // Call resume API + err := m.userTrafficController.sendResumeUserTrafficRequest(userUID) + if err != nil { + log.Printf("Failed to resume user traffic %s: %v", userUID, err) + return + } + + // Update status to Processing + err = m.updateUserTrafficStatusClean(userUID) + if err != nil { + log.Printf("Failed to update user status %s: %v", userUID, err) + return + } + + log.Printf("Successfully resumed user traffic: %s", userUID) +} + +// MonitorStats holds monitoring statistics +type MonitorStats struct { + ProcessedUsers int64 `json:"processed_users"` + PendingUsers int64 `json:"pending_users"` + CacheHitRate float64 `json:"cache_hit_rate"` + LastProcessTime time.Time `json:"last_process_time"` + AverageProcessTime time.Duration `json:"average_process_time"` +} + +// GetStats returns monitoring statistics +func (m *UserTrafficMonitor) GetStats() MonitorStats { + pendingCount := len(m.suspendQueue) + + return MonitorStats{ + PendingUsers: int64(pendingCount), + } +} + +// Suggested database indexes +/* +CREATE INDEX CONCURRENTLY idx_user_time_range_traffic_status_updated +ON user_time_range_traffic (status, updated_at) +WHERE status = 'processing'; + +CREATE INDEX CONCURRENTLY idx_user_time_range_traffic_user_uid_status +ON user_time_range_traffic (user_uid, status); + +CREATE INDEX CONCURRENTLY idx_user_time_range_traffic_sent_bytes +ON user_time_range_traffic (sent_bytes) +WHERE sent_bytes > 10737418240; -- 10GB +*/ diff --git a/controllers/account/controllers/utils/alisms.go b/controllers/account/controllers/utils/alisms.go new file mode 100644 index 000000000000..b8e43f1b0aaa --- /dev/null +++ b/controllers/account/controllers/utils/alisms.go @@ -0,0 +1,83 @@ +// Copyright © 2023 sealos. +// +// 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 utils + +import ( + "errors" + "fmt" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" +) + +func CreateSMSClient(ak, sk, endpoint string) (*dysmsapi20170525.Client, error) { + config := &openapi.Config{ + AccessKeyId: tea.String(ak), + AccessKeySecret: tea.String(sk), + Endpoint: tea.String(endpoint), + } + client, err := dysmsapi20170525.NewClient(config) + return client, err +} + +// SendSms sendSmsRequest := &dysmsapi20170525.SendSmsRequest{ +// PhoneNumbers: tea.String("18888888888"), +// SignName: tea.String("环界云"), +// TemplateCode: tea.String("SMS_xxx"), +// TemplateParam: tea.String("{\"code\":\"1234\"}"), +// }/* +func SendSms(client *dysmsapi20170525.Client, req *dysmsapi20170525.SendSmsRequest) (err error) { + runtime := &util.RuntimeOptions{} + resp, err := client.SendSmsWithOptions(req, runtime) + if err != nil { + return err + } + if *resp.Body.Code != "OK" { + return fmt.Errorf("send sms err code %s: %s", *resp.Body.Code, *resp.Body.Message) + } + return err +} + +// SendSmsMultiple sends SMS to multiple phone numbers with the same content +func SendSmsMultiple( + client *dysmsapi20170525.Client, + phoneNumbers []string, + signName, templateCode, templateParam string, +) error { + if len(phoneNumbers) == 0 { + return errors.New("phone numbers cannot be empty") + } + + for _, phoneNumber := range phoneNumbers { + if phoneNumber == "" { + continue + } + sendSmsRequest := &dysmsapi20170525.SendSmsRequest{ + PhoneNumbers: tea.String(phoneNumber), + SignName: tea.String(signName), + TemplateCode: tea.String(templateCode), + TemplateParam: tea.String(templateParam), + } + + err := SendSms(client, sendSmsRequest) + if err != nil { + return fmt.Errorf("failed to send SMS to %s: %w", phoneNumber, err) + } + } + + return nil +} diff --git a/controllers/account/controllers/utils/alisms_test.go b/controllers/account/controllers/utils/alisms_test.go new file mode 100644 index 000000000000..4cf8cb41f4c5 --- /dev/null +++ b/controllers/account/controllers/utils/alisms_test.go @@ -0,0 +1,57 @@ +// Copyright © 2023 sealos. +// +// 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 utils + +import ( + "fmt" + "os" + "testing" + + "github.com/alibabacloud-go/dysmsapi-20170525/v3/client" + "github.com/alibabacloud-go/tea/tea" +) + +func TestSendSms(t *testing.T) { + requireMessagingTest(t, "ak", "sk", "phone", "sign_name", "template_code") + clt, err := CreateSMSClient(os.Getenv("ak"), os.Getenv("sk"), "dysmsapi.aliyuncs.com") + if err != nil { + t.Fatal(err) + } + userID, oweAmount := "uid", "1234" + err = SendSms(clt, &client.SendSmsRequest{ + PhoneNumbers: tea.String(os.Getenv("phone")), + SignName: tea.String(os.Getenv("sign_name")), + TemplateCode: tea.String(os.Getenv("template_code")), + // user_id:, oweAmount + TemplateParam: tea.String( + "{\"user_id\":\"" + userID + "\",\"oweamount\":\"" + oweAmount + "\"}", + ), + }) + if err != nil { + t.Fatal(fmt.Errorf("send sms failed: %w", err)) + } +} + +func requireMessagingTest(t *testing.T, envNames ...string) { + t.Helper() + if os.Getenv("RUN_MESSAGING_TESTS") != "true" { + t.Skip("set RUN_MESSAGING_TESTS=true to run messaging provider tests") + } + for _, name := range envNames { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } +} diff --git a/controllers/account/controllers/utils/email.go b/controllers/account/controllers/utils/email.go new file mode 100644 index 000000000000..84ea0578309d --- /dev/null +++ b/controllers/account/controllers/utils/email.go @@ -0,0 +1,80 @@ +// Copyright © 2024 sealos. +// +// 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 utils + +import ( + "errors" + + "github.com/go-gomail/gomail" +) + +type SMTPConfig struct { + ServerHost string + ServerPort int + FromEmail string + Username string + Passwd string + EmailTitle string +} + +func (c *SMTPConfig) SendEmail(emailBody, to string) error { + m := gomail.NewMessage() + m.SetHeader("To", to) + m.SetAddressHeader("From", c.FromEmail, c.EmailTitle) + m.SetHeader("Subject", c.EmailTitle) + m.SetBody("text/html", emailBody) + d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd) + return d.DialAndSend(m) +} + +func (c *SMTPConfig) SendEmailWithTitle(subject, emailBody, to string) error { + m := gomail.NewMessage() + m.SetHeader("To", to) + m.SetAddressHeader("From", c.FromEmail, c.EmailTitle) + m.SetHeader("Subject", subject) + m.SetBody("text/html", emailBody) + d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd) + return d.DialAndSend(m) +} + +// SendEmailWithTitleMultiple sends email with title to multiple email addresses with the same content +func (c *SMTPConfig) SendEmailWithTitleMultiple( + subject, emailBody string, + toEmails []string, +) error { + if len(toEmails) == 0 { + return errors.New("email addresses cannot be empty") + } + + m := gomail.NewMessage() + + // Set multiple recipients + var validEmails []string + for _, email := range toEmails { + if email != "" { + validEmails = append(validEmails, email) + } + } + if len(validEmails) == 0 { + return errors.New("no valid email addresses") + } + m.SetHeader("To", validEmails...) + m.SetAddressHeader("From", c.FromEmail, c.EmailTitle) + m.SetHeader("Subject", subject) + m.SetBody("text/html", emailBody) + + d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd) + return d.DialAndSend(m) +} diff --git a/controllers/account/controllers/utils/rate_limiter.go b/controllers/account/controllers/utils/rate_limiter.go new file mode 100644 index 000000000000..4c39f43830ed --- /dev/null +++ b/controllers/account/controllers/utils/rate_limiter.go @@ -0,0 +1,63 @@ +package utils + +import ( + "flag" + "time" + + "golang.org/x/time/rate" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + defaultMinRetryDelay = 5 * time.Millisecond + defaultMaxRetryDelay = 1000 * time.Second + defaultQPS = float64(10.0) + defaultBurst = 100 + flagMinRetryDelay = "min-retry-delay" + flagMaxRetryDelay = "max-retry-delay" + flagQPS = "default-qps" + flagBurst = "default-burst" +) + +// LimiterOptions used on reconcilers. +type LimiterOptions struct { + MinRetryDelay time.Duration + QPS float64 + Burst int + MaxRetryDelay time.Duration +} + +func (o *LimiterOptions) BindFlags(fs *flag.FlagSet) { + fs.DurationVar( + &o.MinRetryDelay, + flagMinRetryDelay, + defaultMinRetryDelay, + "The minimum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.DurationVar( + &o.MaxRetryDelay, + flagMaxRetryDelay, + defaultMaxRetryDelay, + "The maximum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.Float64Var(&o.QPS, flagQPS, defaultQPS, "The maximum number of batches per second to allow.") + fs.IntVar( + &o.Burst, + flagBurst, + defaultBurst, + "The maximum number of batches to allow in a short period of time.", + ) +} + +func GetRateLimiter(opts *LimiterOptions) workqueue.TypedRateLimiter[reconcile.Request] { + return workqueue.NewTypedMaxOfRateLimiter[reconcile.Request]( + workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + opts.MinRetryDelay, + opts.MaxRetryDelay, + ), + &workqueue.TypedBucketRateLimiter[reconcile.Request]{ + Limiter: rate.NewLimiter(rate.Limit(opts.QPS), opts.Burst), + }, + ) +} diff --git a/controllers/account/controllers/utils/subscription_cache.go b/controllers/account/controllers/utils/subscription_cache.go new file mode 100644 index 000000000000..2fb21b1858b1 --- /dev/null +++ b/controllers/account/controllers/utils/subscription_cache.go @@ -0,0 +1,205 @@ +package utils + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" + "gorm.io/gorm" +) + +// CacheEntry holds the cached data for a user +type CacheEntry struct { + UserUID uuid.UUID + Status types.SubscriptionStatus + PlanName string +} + +// SubscriptionCache manages the in-memory cache with concurrent access safety +type SubscriptionCache struct { + db *gorm.DB + cache map[uuid.UUID]CacheEntry + mu sync.RWMutex // Protects cache for concurrent read/write + lastUpdate time.Time + updateTicker *time.Ticker +} + +// NewSubscriptionCache initializes the cache and starts the update goroutine +func NewSubscriptionCache(db *gorm.DB, updateInterval time.Duration) (*SubscriptionCache, error) { + cache := &SubscriptionCache{ + db: db, + cache: make(map[uuid.UUID]CacheEntry), + lastUpdate: time.Now(), + updateTicker: time.NewTicker(updateInterval), + } + + // Perform initial full cache load + if err := cache.loadFullCache(context.Background()); err != nil { + return nil, fmt.Errorf("failed to load initial cache: %w", err) + } + + // Start background update goroutine + go cache.runPeriodicUpdates(context.Background()) + + return cache, nil +} + +// loadFullCache loads all subscriptions into the cache +// Uses a write lock to ensure exclusive access during cache population +func (sc *SubscriptionCache) loadFullCache(ctx context.Context) error { + var subscriptions []types.Subscription + if err := sc.db.WithContext(ctx). + Select("user_uid", "status", "plan_name"). + Find(&subscriptions). + Error; err != nil { + return err + } + + sc.mu.Lock() + defer sc.mu.Unlock() + + // Clear existing cache + sc.cache = make(map[uuid.UUID]CacheEntry) + + // Populate cache + for _, sub := range subscriptions { + sc.cache[sub.UserUID] = CacheEntry{ + UserUID: sub.UserUID, + Status: sub.Status, + PlanName: sub.PlanName, + } + } + + sc.lastUpdate = time.Now() + log.Printf( + "Full cache loaded with %d entries at %s", + len(sc.cache), + sc.lastUpdate.Format(time.RFC3339), + ) + return nil +} + +// updateCacheSinceLast updates cache with subscriptions changed since last update +// Uses a write lock to ensure exclusive access during cache updates +func (sc *SubscriptionCache) updateCacheSinceLast(ctx context.Context) error { + var subscriptions []types.Subscription + currentTime := time.Now() + + // Fetch subscriptions where UpdateAt is between lastUpdate and currentTime + if err := sc.db.WithContext(ctx). + Select("user_uid", "status", "plan_name"). + Where("update_at > ? AND update_at <= ?", sc.lastUpdate, currentTime). + Find(&subscriptions).Error; err != nil { + return fmt.Errorf("failed to fetch updated subscriptions: %w", err) + } + + sc.mu.Lock() + defer sc.mu.Unlock() + + // Update cache with changed entries + for _, sub := range subscriptions { + if currentEntry, exists := sc.cache[sub.UserUID]; !exists || + currentEntry.Status != sub.Status || + currentEntry.PlanName != sub.PlanName { + sc.cache[sub.UserUID] = CacheEntry{ + UserUID: sub.UserUID, + Status: sub.Status, + PlanName: sub.PlanName, + } + log.Printf( + "Updated cache for UserUID %s with status %s and plan_name %s", + sub.UserUID, + sub.Status, + sub.PlanName, + ) + } + } + + sc.lastUpdate = currentTime + log.Printf( + "Cache updated with %d changed entries at %s", + len(subscriptions), + currentTime.Format(time.RFC3339), + ) + return nil +} + +// runPeriodicUpdates runs periodic cache updates +// Handles updates in a background goroutine +func (sc *SubscriptionCache) runPeriodicUpdates(ctx context.Context) { + for { + select { + case <-ctx.Done(): + sc.updateTicker.Stop() + log.Println("Cache update goroutine stopped") + return + case <-sc.updateTicker.C: + if err := sc.updateCacheSinceLast(ctx); err != nil { + log.Printf("Error updating cache: %v", err) + } + } + } +} + +// GetEntry retrieves the cache entry for a given UserUID +// Uses a read lock for concurrent-safe access +func (sc *SubscriptionCache) GetEntry(userUID uuid.UUID) (CacheEntry, bool) { + sc.mu.RLock() + defer sc.mu.RUnlock() + + entry, exists := sc.cache[userUID] + return entry, exists +} + +// GetAllEntries returns a copy of all cache entries +// Uses a read lock to ensure concurrent-safe access +func (sc *SubscriptionCache) GetAllEntries() []CacheEntry { + sc.mu.RLock() + defer sc.mu.RUnlock() + + entries := make([]CacheEntry, 0, len(sc.cache)) + for _, entry := range sc.cache { + entries = append(entries, entry) + } + return entries +} + +// Close stops the cache update ticker +func (sc *SubscriptionCache) Close() { + sc.updateTicker.Stop() + log.Println("Cache ticker stopped") +} + +// Example usage +// func main() { +// // Assuming db is a configured *gorm.DB instance +// var db *gorm.DB // Initialize your GORM DB here +// +// // Create cache with 1-minute update interval +// cache, err := NewSubscriptionCache(db, time.Minute) +// if err != nil { +// log.Fatalf("Failed to initialize cache: %v", err) +// } +// defer cache.Close() +// +// // Example: Retrieve entry for a user +// userUID, _ := uuid.Parse("123e4567-e89b-12d3-a456-426614174000") +// if entry, exists := cache.GetEntry(userUID); exists { +// fmt.Printf("User %s has status %s and plan name %s\n", entry.UserUID, entry.Status, entry.PlanName) +// } else { +// fmt.Printf("User %s not found in cache\n", userUID) +// } +// +// // Example: Retrieve all entries +// entries := cache.GetAllEntries() +// for _, entry := range entries { +// fmt.Printf("User %s: Status=%s, PlanName=%s\n", entry.UserUID, entry.Status, entry.PlanName) +// } +// +// // Keep the program running to allow periodic updates +// select {} +//} diff --git a/controllers/account/controllers/utils/vms.go b/controllers/account/controllers/utils/vms.go new file mode 100644 index 000000000000..4aee7179018f --- /dev/null +++ b/controllers/account/controllers/utils/vms.go @@ -0,0 +1,120 @@ +// Copyright © 2024 sealos. +// +// 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 utils + +import ( + "errors" + "fmt" + "time" + + "github.com/astaxie/beego/logs" + "github.com/volcengine/volc-sdk-golang/service/vms" +) + +func SendVms(phone, template, numberPollNo string, sendTime time.Time, forbidTimes []string) error { + paramList := make([]*vms.SingleParam, 0, 1) + paramList = append(paramList, &vms.SingleParam{ + Phone: phone, + Type: 1, + // RingAgainTimes: 1, + // RingAgainInterval: 5, + TriggerTime: &vms.JsonTime{Time: sendTime}, + Resource: template, + NumberPoolNo: numberPollNo, + SingleOpenId: phone + "-" + sendTime.Format(time.DateOnly), + }) + if len(forbidTimes) != 0 { + paramList[0].ForbidTimeList = []*vms.ForbidTimeItem{ + { + Times: forbidTimes, + }, + } + } + req := &vms.SingleAppendRequest{ + List: paramList, + } + result, statusCode, err := vms.DefaultInstance.SingleBatchAppend(req) + if err != nil { + return fmt.Errorf("failed to SingleBatchAppend: %w", err) + } + if result.ResponseMetadata.Error != nil { + return fmt.Errorf("failed to send vms: %v", result.ResponseMetadata.Error) + } + logs.Info("send vms status code: %d, result: %#+v", statusCode, result.Result) + if statusCode != 200 { + return fmt.Errorf("failed to send vms, status code: %d, err : %v", statusCode, result) + } + return nil +} + +// SendVmsMultiple sends VMS to multiple phone numbers with the same template and settings +func SendVmsMultiple( + phones []string, + template, numberPollNo string, + sendTime time.Time, + forbidTimes []string, +) error { + if len(phones) == 0 { + return errors.New("phone numbers cannot be empty") + } + + paramList := make([]*vms.SingleParam, 0, len(phones)) + for _, phone := range phones { + if phone == "" { + continue + } + singleParam := &vms.SingleParam{ + Phone: phone, + Type: 1, + // RingAgainTimes: 1, + // RingAgainInterval: 5, + TriggerTime: &vms.JsonTime{Time: sendTime}, + Resource: template, + NumberPoolNo: numberPollNo, + SingleOpenId: phone + "-" + sendTime.Format(time.DateOnly), + } + + if len(forbidTimes) != 0 { + singleParam.ForbidTimeList = []*vms.ForbidTimeItem{ + { + Times: forbidTimes, + }, + } + } + + paramList = append(paramList, singleParam) + } + + if len(paramList) == 0 { + return errors.New("no valid phone numbers provided") + } + + req := &vms.SingleAppendRequest{ + List: paramList, + } + + result, statusCode, err := vms.DefaultInstance.SingleBatchAppend(req) + if err != nil { + return fmt.Errorf("failed to SingleBatchAppend: %w", err) + } + if result.ResponseMetadata.Error != nil { + return fmt.Errorf("failed to send vms: %v", result.ResponseMetadata.Error) + } + logs.Info("send vms multiple status code: %d, result: %#+v", statusCode, result.Result) + if statusCode != 200 { + return fmt.Errorf("failed to send vms, status code: %d, err : %v", statusCode, result) + } + return nil +} diff --git a/controllers/account/controllers/utils/vms_test.go b/controllers/account/controllers/utils/vms_test.go new file mode 100644 index 000000000000..1c0dd626c3f9 --- /dev/null +++ b/controllers/account/controllers/utils/vms_test.go @@ -0,0 +1,57 @@ +// Copyright © 2024 sealos. +// +// 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 utils + +import ( + "os" + "testing" + "time" + + "github.com/volcengine/volc-sdk-golang/service/vms" +) + +func TestSendVms(t *testing.T) { + requireMessagingTest(t, + "VMS_AK", + "VMS_SK", + "VMS_TEST_PHONE", + "VMS_TEST_TEMPLATE", + "VMS_TEST_NUMBER_POOL_NO", + ) + vms.DefaultInstance.SetAccessKey(os.Getenv("VMS_AK")) + vms.DefaultInstance.SetSecretKey(os.Getenv("VMS_SK")) + testData := struct { + phone string + template string + numberPollNo string + sendTime time.Time + }{ + phone: os.Getenv("VMS_TEST_PHONE"), + template: os.Getenv("VMS_TEST_TEMPLATE"), + numberPollNo: os.Getenv("VMS_TEST_NUMBER_POOL_NO"), + sendTime: time.Now(), + } + err := SendVms( + testData.phone, + testData.template, + testData.numberPollNo, + testData.sendTime, + []string{"10:00-20:00"}, + ) + if err != nil { + t.Fatal(err) + } + t.Log("SendVms success") +} diff --git a/controllers/account/controllers/workspace_subscription.go b/controllers/account/controllers/workspace_subscription.go new file mode 100644 index 000000000000..e2a1faadd286 --- /dev/null +++ b/controllers/account/controllers/workspace_subscription.go @@ -0,0 +1,672 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "log" + "sync" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "gorm.io/gorm" + v1 "k8s.io/api/core/v1" + types2 "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// WorkspaceSubscriptionProcessor 处理工作空间订阅事务的处理器 +type WorkspaceSubscriptionProcessor struct { + *WorkspaceTrafficController + db *gorm.DB + pollInterval time.Duration + wg sync.WaitGroup + stopChan chan struct{} + *AccountReconciler +} + +// TODO 需要添加用户通知 +func NewWorkspaceSubscriptionProcessor( + reconciler *AccountReconciler, + workspaceTrafficProcessor *WorkspaceTrafficController, +) (*WorkspaceSubscriptionProcessor, error) { + return &WorkspaceSubscriptionProcessor{ + WorkspaceTrafficController: workspaceTrafficProcessor, + db: reconciler.AccountV2.GetGlobalDB(), + pollInterval: 5 * time.Second, + stopChan: make(chan struct{}), + AccountReconciler: reconciler, + }, nil +} + +// Start 开始监听和处理工作空间订阅事务 +func (wsp *WorkspaceSubscriptionProcessor) Start(ctx context.Context) { + wsp.wg.Add(1) + go func() { + defer wsp.wg.Done() + ticker := time.NewTicker(wsp.pollInterval) + defer ticker.Stop() + idleCount := 0 + for { + select { + case <-ctx.Done(): + return + case <-wsp.stopChan: + return + case <-ticker.C: + count, err := wsp.processPendingTransactions(ctx) + if err != nil { + log.Printf( + "Failed to process pending workspace subscription transactions: %v", + err, + ) + } + if count == 0 { + idleCount++ + if idleCount > 5 { // increase the interval after 5 idle rounds + ticker.Reset(wsp.pollInterval + 5*time.Second) + } + } else { + idleCount = 0 + ticker.Reset(wsp.pollInterval) + } + } + } + }() +} + +// Stop 停止处理器 +func (wsp *WorkspaceSubscriptionProcessor) Stop() { + close(wsp.stopChan) + wsp.wg.Wait() +} + +// processPendingTransactions 处理待处理的事务 +func (wsp *WorkspaceSubscriptionProcessor) processPendingTransactions( + ctx context.Context, +) (int, error) { + var transactions []types.WorkspaceSubscriptionTransaction + now := time.Now() + + // 查询待处理事务 + err := wsp.db.WithContext(ctx).Model(&types.WorkspaceSubscriptionTransaction{}). + Where("pay_status IN (?, ?) AND start_at <= ? AND status NOT IN (?, ?) AND region_domain = ?", + types.SubscriptionPayStatusPaid, + types.SubscriptionPayStatusNoNeed, + now, + types.SubscriptionTransactionStatusCompleted, + types.SubscriptionTransactionStatusFailed, + wsp.localDomain). + Find(&transactions).Error + if err != nil { + return 0, fmt.Errorf("failed to query pending workspace subscription transactions: %w", err) + } + + for i := range transactions { + // 检查是否需要在当前区域处理该事务 + if transactions[i].RegionDomain != wsp.localDomain { + continue + } + + wsp.Logger.Info("Processing workspace subscription transaction", + "workspace", transactions[i].Workspace, + "region", transactions[i].RegionDomain, + "operator", transactions[i].Operator, + "status", transactions[i].Status, + "plan", transactions[i].NewPlanName) + + if err := wsp.processTransaction(ctx, &transactions[i]); err != nil { + wsp.Logger.Error( + fmt.Errorf("failed to process workspace subscription transaction: %w", err), + "", + "id", + transactions[i].ID, + ) + } + } + return len(transactions), nil +} + +// processTransaction 处理单个事务 +func (wsp *WorkspaceSubscriptionProcessor) processTransaction( + ctx context.Context, + tx *types.WorkspaceSubscriptionTransaction, +) error { + return wsp.db.Transaction(func(dbTx *gorm.DB) error { + latestTx := *tx + + // 检查是否仍需处理 + if !wsp.shouldProcessTransaction(&latestTx) { + wsp.Logger.Info( + "Workspace subscription transaction needn't processed", + "id", + latestTx.ID, + ) + return nil + } + + // 根据操作类型分发处理 + handler, exists := map[types.SubscriptionOperator]func(context.Context, *gorm.DB, *types.WorkspaceSubscriptionTransaction) error{ + types.SubscriptionTransactionTypeCreated: wsp.handleCreated, + types.SubscriptionTransactionTypeUpgraded: wsp.handleUpgrade, + types.SubscriptionTransactionTypeDowngraded: wsp.handleDowngrade, + types.SubscriptionTransactionTypeRenewed: wsp.handleRenewal, + }[latestTx.Operator] + + if !exists { + wsp.Logger.Info("Unknown operator", "operator", latestTx.Operator) + return nil // 未知操作类型,跳过 + } + + return handler(ctx, dbTx, &latestTx) + }) +} + +// shouldProcessTransaction 检查事务是否需要处理 +func (wsp *WorkspaceSubscriptionProcessor) shouldProcessTransaction( + tx *types.WorkspaceSubscriptionTransaction, +) bool { + now := time.Now() + return (tx.PayStatus == types.SubscriptionPayStatusPaid || tx.PayStatus == types.SubscriptionPayStatusNoNeed) && + !tx.StartAt.After(now) && + tx.Status != types.SubscriptionTransactionStatusCompleted && + tx.Status != types.SubscriptionTransactionStatusFailed +} + +// updateWorkspaceQuotaLimit0 初始化未使用期的工作空间的资源配额限制为0 +func (r *AccountReconciler) updateWorkspaceQuotaLimit0( + ctx context.Context, + workspace string, +) error { + nsQuota := resources.GetLimit0Quota(workspace, "quota-"+workspace) + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, nsQuota, func() error { + // if nsQuota.Spec.Hard != nil { + // for usedRs, usedQuantity := range nsQuota.Status.Used { + // if quantity, ok := nsQuota.Spec.Hard[usedRs]; ok { + // if usedQuantity.Cmp(quantity) > 0 { + // // TODO situations exceeding the quota need to be handled + // // restart the space resource deploy sts + // return fmt.Errorf("used resource %s exceeds the limit 0: used %s", usedRs, usedQuantity.String()) + // } + // } + // } + //} + if nsQuota.Annotations == nil { + nsQuota.Annotations = make(map[string]string) + } + nsQuota.Annotations[types.WorkspaceSubscriptionStatusUpdateTimeAnnoKey] = time.Now(). + UTC(). + Format(time.RFC3339) + return nil + }) + if err != nil { + return fmt.Errorf("failed to create or update resource quota: %w", err) + } + ns := &v1.Namespace{} + if err := r.Get(ctx, types2.NamespacedName{Name: workspace}, ns); err != nil { + return err + } + original := ns.DeepCopy() + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[types.DebtNamespaceAnnoStatusKey] = types.SuspendDebtNamespaceAnnoStatus + ns.Annotations[types.WorkspaceSubscriptionStatusAnnoKey] = types.SuspendDebtNamespaceAnnoStatus + if err := r.Patch(ctx, ns, client.MergeFrom(original)); err != nil { + return fmt.Errorf("patch namespace annotation failed: %w", err) + } + return nil +} + +// updateWorkspaceQuota 更新工作空间的资源配额 +func (r *AccountReconciler) updateWorkspaceQuota( + ctx context.Context, + workspace, planName string, +) error { + rs, ok := r.workspaceSubPlansResourceLimit[planName] + if !ok { + return fmt.Errorf("plan %s not found in workspace subscription plans", planName) + } + nsQuota := resources.GetDefaultResourceQuota(workspace, "quota-"+workspace) + for defaultRs, quantity := range nsQuota.Spec.Hard { + if _, ok := rs[defaultRs]; ok { + continue + } + rs[defaultRs] = quantity.DeepCopy() + } + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, nsQuota, func() error { + if nsQuota.Spec.Hard != nil { + for usedRs, usedQuantity := range nsQuota.Status.Used { + if quantity, ok := rs[usedRs]; ok { + if usedQuantity.Cmp(quantity) > 0 { + // TODO situations exceeding the quota need to be handled + // restart the space resource deploy sts + return fmt.Errorf( + "used resource %s exceeds the limit in plan %s: used %s, limit %s", + usedRs, + planName, + usedQuantity.String(), + quantity.String(), + ) + } + } + } + } + if nsQuota.Annotations == nil { + nsQuota.Annotations = make(map[string]string) + } + nsQuota.Annotations[types.WorkspaceSubscriptionStatusUpdateTimeAnnoKey] = time.Now(). + UTC(). + Format(time.RFC3339) + nsQuota.Spec.Hard = rs + return nil + }) + if err != nil { + return fmt.Errorf("failed to create or update resource quota: %w", err) + } + ns := &v1.Namespace{} + if err := r.Get(ctx, types2.NamespacedName{Name: workspace}, ns); err != nil { + return err + } + original := ns.DeepCopy() + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[types.DebtNamespaceAnnoStatusKey] = types.NormalDebtNamespaceAnnoStatus + ns.Annotations[types.WorkspaceSubscriptionStatusAnnoKey] = types.NormalDebtNamespaceAnnoStatus + if err := r.Patch(ctx, ns, client.MergeFrom(original)); err != nil { + return fmt.Errorf("patch namespace annotation failed: %w", err) + } + return nil +} + +func (wsp *WorkspaceSubscriptionProcessor) handleCreated( + ctx context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + return wsp.HandleWorkspaceSubscriptionCreated(ctx, dbTx, tx) +} + +func (r *AccountReconciler) handlerNoTrialInitialWorkspaceSubscription( + ctx context.Context, + userUID uuid.UUID, + workspace string, +) error { + return r.AccountV2.GetGlobalDB().Transaction(func(dbTx *gorm.DB) error { + // 1. create subscription transaction + // 2. create limit0 quota + var sub types.WorkspaceSubscription + now := time.Now().UTC() + sub.ID = uuid.New() + sub.UserUID = userUID + sub.PlanName = types.FreeSubscriptionPlanName + sub.Workspace = workspace + sub.RegionDomain = r.localDomain + sub.Status = types.SubscriptionStatusPaused + sub.TrafficStatus = types.WorkspaceTrafficStatusActive + sub.CreateAt = now + sub.CurrentPeriodStartAt = now + sub.CurrentPeriodEndAt = now + sub.CancelAtPeriodEnd = true + sub.PayStatus = types.SubscriptionPayStatusNoNeed + sub.UpdateAt = now + if err := dbTx.Where("workspace = ? AND region_domain = ?", workspace, r.localDomain). + First(&types.WorkspaceSubscription{}). + Error; err == nil { + return nil + } else if !errors.Is( + err, + gorm.ErrRecordNotFound, + ) { + return fmt.Errorf("failed to check existing subscription: %w", err) + } + result := dbTx.Create(&sub) + // check if err = duplicated key not allowed + if err := result.Error; err != nil && !errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("failed to create workspace subscription: %w", err) + } + if result.RowsAffected > 0 { + if err := r.updateWorkspaceQuotaLimit0(ctx, workspace); err != nil { + return fmt.Errorf("failed to update workspace quota limit0: %w", err) + } + } + return nil + }) +} + +func (r *AccountReconciler) handleProbationPeriodWorkspaceSubscription( + ctx context.Context, + userUID uuid.UUID, + nsName string, +) error { + plan, err := r.AccountV2.GetWorkspaceSubscriptionPlan(types.FreeSubscriptionPlanName) + if err != nil { + return fmt.Errorf("get workspace subscription plan failed: %w", err) + } + r.Logger.Info("get workspace subscription plan", "plan", plan) + return r.AccountV2.GetGlobalDB().Transaction(func(tx *gorm.DB) error { + return r.handleWorkspaceSubscriptionCreated( + ctx, + tx, + &types.WorkspaceSubscriptionTransaction{ + UserUID: userUID, + ID: uuid.New(), + Workspace: nsName, + RegionDomain: r.localDomain, + Operator: types.SubscriptionTransactionTypeCreated, + PayStatus: types.SubscriptionPayStatusNoNeed, + NewPlanName: plan.Name, + Period: types.DayPeriod(7), + }, + ) + }) +} + +func (r *AccountReconciler) handleWorkspaceSubscriptionCreated( + ctx context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + var sub types.WorkspaceSubscription + addPeriod, err := types.ParsePeriod(tx.Period) + if err != nil { + return fmt.Errorf("failed to parse period: %w", err) + } + now := time.Now().UTC() + sub.ID = uuid.New() + sub.UserUID = tx.UserUID + sub.PlanName = tx.NewPlanName + sub.Workspace = tx.Workspace + sub.RegionDomain = tx.RegionDomain + sub.Status = types.SubscriptionStatusNormal + sub.TrafficStatus = types.WorkspaceTrafficStatusActive + sub.CreateAt = now + sub.CurrentPeriodStartAt = now + sub.CurrentPeriodEndAt = now.Add(addPeriod) + sub.CancelAtPeriodEnd = false + sub.ExpireAt = &sub.CurrentPeriodEndAt + sub.PayStatus = tx.PayStatus + sub.UpdateAt = now + if err := dbTx.Where("workspace = ? AND region_domain = ?", tx.Workspace, r.localDomain). + First(&types.WorkspaceSubscription{}). + Error; err == nil { + return nil + } else if !errors.Is( + err, + gorm.ErrRecordNotFound, + ) { + return fmt.Errorf("failed to check existing subscription: %w", err) + } + result := dbTx.Create(&sub) + if err := result.Error; err != nil && !errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("failed to create workspace subscription: %w", err) + } + if result.RowsAffected > 0 { + if err := r.updateWorkspaceQuota(ctx, sub.Workspace, tx.NewPlanName); err != nil { + return fmt.Errorf("failed to update workspace quota: %w", err) + } + plan, err := r.AccountV2.GetWorkspaceSubscriptionPlan(sub.PlanName) + if err != nil { + return fmt.Errorf("failed to get workspace subscription plan: %w", err) + } + if err = r.NewTrafficPackage( + dbTx, + &sub, + plan, + sub.CurrentPeriodEndAt, + types.WorkspaceTrafficFromWorkspaceSubscription, + tx.ID.String(), + ); err != nil { + return fmt.Errorf("failed to add traffic package: %w", err) + } + err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage( + dbTx, + sub.ID, + plan.AIQuota, + sub.CurrentPeriodEndAt, + types.PKGFromWorkspaceSubscription, + tx.ID.String(), + ) + if err != nil { + return fmt.Errorf("failed to create AI quota package: %w", err) + } + } + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + return nil +} + +// handleCreated 处理创建工作空间订阅 +func (r *AccountReconciler) HandleWorkspaceSubscriptionCreated( + ctx context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + if err := r.handleWorkspaceSubscriptionCreated(ctx, dbTx, tx); err != nil { + return err + } + return dbTx.Save(tx).Error +} + +// handleUpgrade 处理升级 +func (wsp *WorkspaceSubscriptionProcessor) handleUpgrade( + ctx context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + var sub types.WorkspaceSubscription + if err := dbTx.Model(&types.WorkspaceSubscription{}). + Where("workspace = ? AND region_domain = ?", tx.Workspace, tx.RegionDomain). + Find(&sub).Error; err != nil { + return fmt.Errorf("failed to fetch workspace subscription: %w", err) + } + addPeriod, err := types.ParsePeriod(tx.Period) + if err != nil { + return fmt.Errorf("failed to parse period: %w", err) + } + now := time.Now() + // 更新订阅信息 + sub.PlanName = tx.NewPlanName + sub.Status = types.SubscriptionStatusNormal + // sub.StartAt = now + sub.UpdateAt = now + sub.CurrentPeriodStartAt = now + sub.CurrentPeriodEndAt = now.Add(addPeriod) + // sub.ExpireAt = now.Add(addPeriod) + plan, err := wsp.AccountV2.GetWorkspaceSubscriptionPlan(sub.PlanName) + if err != nil { + return fmt.Errorf("failed to get workspace subscription plan: %w", err) + } + if err = wsp.AddTrafficPackage( + dbTx, + &sub, + plan, + sub.CurrentPeriodEndAt, + types.WorkspaceTrafficFromWorkspaceSubscription, + tx.ID.String(), + ); err != nil { + return fmt.Errorf("failed to add traffic package: %w", err) + } + if err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage( + dbTx, + sub.ID, + plan.AIQuota, + sub.CurrentPeriodEndAt, + types.PKGFromWorkspaceSubscription, + tx.ID.String(), + ); err != nil { + return fmt.Errorf("failed to create AI quota package: %w", err) + } + + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update workspace subscription: %w", err) + } + + // 更新工作空间配额 + if err := wsp.updateWorkspaceQuota(ctx, sub.Workspace, tx.NewPlanName); err != nil { + return err + } + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = time.Now().UTC() + return dbTx.Save(tx).Error +} + +// handleDowngrade 处理降级 +func (wsp *WorkspaceSubscriptionProcessor) handleDowngrade( + ctx context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + var sub types.WorkspaceSubscription + if err := dbTx.Model(&types.WorkspaceSubscription{}). + Where("workspace = ? AND region_domain = ?", tx.Workspace, tx.RegionDomain). + Find(&sub).Error; err != nil { + return fmt.Errorf("failed to fetch workspace subscription: %w", err) + } + + // 检查降级条件 + if ok, err := wsp.checkWorkspaceDowngradeConditions(ctx, &sub, tx.NewPlanName); err != nil { + return fmt.Errorf("failed to check workspace downgrade conditions: %w", err) + } else if !ok { + tx.Status = types.SubscriptionTransactionStatusFailed + return dbTx.Save(tx).Error + } + + // 检查配额条件 + if ok, err := wsp.checkWorkspaceQuotaConditions( + ctx, + sub.Workspace, + sub.RegionDomain, + tx.NewPlanName, + ); err != nil { + return fmt.Errorf("failed to check workspace quota conditions: %w", err) + } else if !ok { + tx.Status = types.SubscriptionTransactionStatusFailed + return dbTx.Save(tx).Error + } + addPeriod, err := types.ParsePeriod(tx.Period) + if err != nil { + return fmt.Errorf("failed to parse period: %w", err) + } + now := time.Now() + sub.PlanName = tx.NewPlanName + sub.Status = types.SubscriptionStatusNormal + // sub.StartAt = now + sub.CurrentPeriodStartAt = now + sub.CurrentPeriodEndAt = now.Add(addPeriod) + sub.UpdateAt = now + // sub.ExpireAt = now.Add(addPeriod) + + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update workspace subscription: %w", err) + } + + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + if err := dbTx.Save(tx).Error; err != nil { + return fmt.Errorf("failed to update transaction: %w", err) + } + + // 更新工作空间配额 + return wsp.updateWorkspaceQuota(ctx, sub.Workspace, tx.NewPlanName) +} + +// handleRenewal 处理续订 +func (wsp *WorkspaceSubscriptionProcessor) handleRenewal( + _ context.Context, + dbTx *gorm.DB, + tx *types.WorkspaceSubscriptionTransaction, +) error { + var sub types.WorkspaceSubscription + if err := dbTx.Model(&types.WorkspaceSubscription{}). + Where("workspace = ? AND region_domain = ?", tx.Workspace, tx.RegionDomain). + Find(&sub).Error; err != nil { + return fmt.Errorf("failed to fetch workspace subscription: %w", err) + } + addPeriod, err := types.ParsePeriod(tx.Period) + if err != nil { + return fmt.Errorf("failed to parse period: %w", err) + } + now := time.Now() + if sub.CurrentPeriodEndAt.Before(now) { + sub.CurrentPeriodEndAt = now + sub.CurrentPeriodEndAt = now.Add(addPeriod) + } else { + sub.CurrentPeriodEndAt = sub.CurrentPeriodEndAt.Add(addPeriod) + } + plan, err := wsp.AccountV2.GetWorkspaceSubscriptionPlan(sub.PlanName) + if err != nil { + return fmt.Errorf("failed to get workspace subscription plan: %w", err) + } + if err = wsp.AddTrafficPackage( + dbTx, + &sub, + plan, + sub.CurrentPeriodEndAt, + types.WorkspaceTrafficFromWorkspaceSubscription, + tx.ID.String(), + ); err != nil { + return fmt.Errorf("failed to add traffic package: %w", err) + } + if err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage( + dbTx, + sub.ID, + plan.AIQuota, + sub.CurrentPeriodEndAt, + types.PKGFromWorkspaceSubscription, + tx.ID.String(), + ); err != nil { + return fmt.Errorf("failed to create AI quota package: %w", err) + } + sub.Status = types.SubscriptionStatusNormal + sub.UpdateAt = now + tx.Status = types.SubscriptionTransactionStatusCompleted + tx.UpdatedAt = now + if err := dbTx.Save(&sub).Error; err != nil { + return fmt.Errorf("failed to update workspace subscription: %w", err) + } + return dbTx.Save(tx).Error +} + +// checkWorkspaceDowngradeConditions 检查工作空间降级条件 +// TODO: 需要后续实现具体的降级条件检查逻辑 +func (wsp *WorkspaceSubscriptionProcessor) checkWorkspaceDowngradeConditions( + _ context.Context, + subscription *types.WorkspaceSubscription, + newPlanName string, +) (bool, error) { + // TODO: 实现工作空间降级条件检查逻辑 + // 例如:检查工作空间内的资源使用情况是否满足新计划的限制 + // wsp.Logger.Info("Checking workspace downgrade conditions", + // "workspace", subscription.Workspace, + // "region", subscription.RegionDomain, + // "currentPlan", subscription.PlanName, + // "targetPlan", newPlanName) + + // 临时返回 true,实际实现时需要根据具体业务逻辑进行检查 + return true, nil +} + +// checkWorkspaceQuotaConditions 检查工作空间配额条件 +// TODO: 需要后续实现具体的配额条件检查逻辑 +func (wsp *WorkspaceSubscriptionProcessor) checkWorkspaceQuotaConditions( + _ context.Context, + workspace, regionDomain, planName string, +) (bool, error) { + // TODO: 实现工作空间配额条件检查逻辑 + // 例如:检查工作空间的资源使用是否超出新计划的限制 + // wsp.Logger.Info("Checking workspace quota conditions", + // "workspace", workspace, + // "region", regionDomain, + // "planName", planName) + + // 临时返回 true,实际实现时需要根据具体业务逻辑进行检查 + return true, nil +} diff --git a/controllers/account/controllers/workspace_subscription_debt.go b/controllers/account/controllers/workspace_subscription_debt.go new file mode 100644 index 000000000000..4a189ca46cfb --- /dev/null +++ b/controllers/account/controllers/workspace_subscription_debt.go @@ -0,0 +1,586 @@ +package controllers + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + "time" + + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "github.com/labring/sealos/controllers/pkg/types" + usernotify "github.com/labring/sealos/controllers/pkg/user_notify" + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types2 "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// WorkspaceSubscriptionDebtProcessor 工作空间订阅债务处理器 +type WorkspaceSubscriptionDebtProcessor struct { + *AccountReconciler + db *gorm.DB + pollInterval time.Duration + wg sync.WaitGroup + stopChan chan struct{} +} + +// 债务状态定义 +const ( + WorkspaceSubscriptionExpireAnnoKey = "workspace.sealos.io/expire-time" + + // 通知相关常量 + workspaceDebtNoticePrefix = "workspace-debt-" + workspaceFromEn = "Workspace-Subscription-System" + workspaceFromZh = "工作空间订阅系统" +) + +// 债务处理的时间节点(单位:小时) +const ( + ExpiredGracePeriodHours = 7 * 24 // 过期后开始暂停服务 + FinalDeletionPeriodHours = 14 * 24 // 过期后7天进行最终删除 +) + +// 通知模板映射 +var workspaceNoticeTemplateENMap = map[types.SubscriptionStatus]string{ + types.SubscriptionStatusDebt: "Your workspace has been suspended due to expired subscription. Please renew immediately.", + types.SubscriptionStatusDebtPreDeletion: "Your workspace will be permanently deleted soon due to expired subscription. Please renew immediately to avoid data loss.", + types.SubscriptionStatusDebtFinalDeletion: "Your workspace will be permanently deleted soon due to expired subscription. Please renew immediately to avoid data loss.", +} + +var workspaceNoticeTemplateZHMap = map[types.SubscriptionStatus]string{ + types.SubscriptionStatusDebt: "您的工作空间订阅已过期,请及时续费以继续使用服务。", + types.SubscriptionStatusDebtPreDeletion: "由于订阅过期,您的工作空间即将被永久删除,请立即续费以避免数据丢失。", + types.SubscriptionStatusDebtFinalDeletion: "由于订阅过期,您的工作空间即将被永久删除,请立即续费以避免数据丢失。", +} + +var workspaceTitleTemplateENMap = map[types.SubscriptionStatus]string{ + types.SubscriptionStatusDebt: "Workspace Suspended", + types.SubscriptionStatusDebtPreDeletion: "Workspace Final Deletion Warning", + types.SubscriptionStatusDebtFinalDeletion: "Workspace Final Deletion Warning", +} + +var workspaceTitleTemplateZHMap = map[types.SubscriptionStatus]string{ + types.SubscriptionStatusDebt: "工作空间订阅已过期", + types.SubscriptionStatusDebtPreDeletion: "工作空间已暂停", + types.SubscriptionStatusDebtFinalDeletion: "工作空间最终删除警告", +} + +// NewWorkspaceSubscriptionDebtProcessor 创建工作空间订阅债务处理器 +func NewWorkspaceSubscriptionDebtProcessor( + reconciler *AccountReconciler, +) *WorkspaceSubscriptionDebtProcessor { + return &WorkspaceSubscriptionDebtProcessor{ + AccountReconciler: reconciler, + db: reconciler.AccountV2.GetGlobalDB(), + pollInterval: 1 * time.Minute, + stopChan: make(chan struct{}), + } +} + +// Start 启动债务处理器 +func (wdp *WorkspaceSubscriptionDebtProcessor) Start(ctx context.Context) { + wdp.wg.Add(1) + go func() { + defer wdp.wg.Done() + ticker := time.NewTicker(wdp.pollInterval) + defer ticker.Stop() + + idleCount := 0 + for { + select { + case <-ctx.Done(): + return + case <-wdp.stopChan: + return + case <-ticker.C: + count, err := wdp.processExpiredWorkspaces(ctx) + if err != nil { + log.Printf("Failed to process expired workspace subscriptions: %v", err) + } + + // 动态调整检查间隔 + if count == 0 { + idleCount++ + if idleCount > 10 { // 10次空闲后增加间隔 + ticker.Reset(wdp.pollInterval * 2) + } + } else { + idleCount = 0 + ticker.Reset(wdp.pollInterval) + } + } + } + }() +} + +// Stop 停止债务处理器 +func (wdp *WorkspaceSubscriptionDebtProcessor) Stop() { + close(wdp.stopChan) + wdp.wg.Wait() +} + +func (wdp *WorkspaceSubscriptionDebtProcessor) determineCurrentStatus( + expireTime time.Time, +) types.SubscriptionStatus { + now := time.Now().UTC() + if expireTime.After(now) { + return types.SubscriptionStatusNormal + } + expiredDuration := now.Sub(expireTime) + if expiredDuration.Hours() < ExpiredGracePeriodHours { + return types.SubscriptionStatusDebt + } else if expiredDuration.Hours() < FinalDeletionPeriodHours { + return types.SubscriptionStatusDebtPreDeletion + } + return types.SubscriptionStatusDebtFinalDeletion +} + +// processExpiredWorkspaces 处理过期的工作空间 +func (wdp *WorkspaceSubscriptionDebtProcessor) processExpiredWorkspaces( + ctx context.Context, +) (int, error) { + now := time.Now().UTC() + processedCount := 0 + + // Query 1: Subscriptions with a status of Normal and that have expired + var normalExpiredSubscriptions []types.WorkspaceSubscription + err := wdp.db.WithContext(ctx).Model(&types.WorkspaceSubscription{}). + Where("region_domain = ? AND current_period_end_at < ? AND status = ?", + wdp.localDomain, + now, + types.SubscriptionStatusNormal). + Find(&normalExpiredSubscriptions).Error + if err != nil { + return 0, fmt.Errorf("failed to query normal expired workspace subscriptions: %w", err) + } + + // Query 2: Subscriptions whose status is Debt and whose expiration time exceeds ExpiredGracePeriodHours + expiredDebtThreshold := now.Add(-time.Duration(ExpiredGracePeriodHours) * time.Hour) + var debtExpiredSubscriptions []types.WorkspaceSubscription + err = wdp.db.WithContext(ctx).Model(&types.WorkspaceSubscription{}). + Where("region_domain = ? AND current_period_end_at < ? AND status = ?", + wdp.localDomain, + expiredDebtThreshold, + types.SubscriptionStatusDebt). + Find(&debtExpiredSubscriptions).Error + if err != nil { + return 0, fmt.Errorf("failed to query debt expired workspace subscriptions: %w", err) + } + + // Query 3: Subscriptions whose status is DebtPreDeletion and whose expiration time exceeds FinalDeletionPeriodHours + finalDeletionThreshold := now.Add(-time.Duration(FinalDeletionPeriodHours) * time.Hour) + var preDeletionExpiredSubscriptions []types.WorkspaceSubscription + err = wdp.db.WithContext(ctx).Model(&types.WorkspaceSubscription{}). + Where("region_domain = ? AND current_period_end_at < ? AND status = ?", + wdp.localDomain, + finalDeletionThreshold, + types.SubscriptionStatusDebtPreDeletion). + Find(&preDeletionExpiredSubscriptions).Error + if err != nil { + return 0, fmt.Errorf( + "failed to query pre-deletion expired workspace subscriptions: %w", + err, + ) + } + + subscriptions := make( + []types.WorkspaceSubscription, + 0, + len( + normalExpiredSubscriptions, + )+len( + debtExpiredSubscriptions, + )+len( + preDeletionExpiredSubscriptions, + ), + ) + subscriptions = append(subscriptions, normalExpiredSubscriptions...) + subscriptions = append(subscriptions, debtExpiredSubscriptions...) + subscriptions = append(subscriptions, preDeletionExpiredSubscriptions...) + + for i := range subscriptions { + if subscriptions[i].Status == types.SubscriptionStatusDeleted { + continue // 已删除的订阅跳过 + } + subscription := &subscriptions[i] + + // 判断当前应该处于的状态 + currentStatus := wdp.determineCurrentStatus(subscription.CurrentPeriodEndAt) + if currentStatus == subscription.Status { + continue // 状态未变化,跳过 + } + + // 处理状态变更 + if err := wdp.processExpiredWorkspace(ctx, subscription, currentStatus); err != nil { + wdp.Logger.Error(fmt.Errorf("failed to process workspace: %w", err), + "", "workspace", subscription.Workspace) + continue + } + processedCount++ + } + + return processedCount, nil +} + +// processExpiredWorkspace 处理单个过期的工作空间 +func (wdp *WorkspaceSubscriptionDebtProcessor) processExpiredWorkspace( + ctx context.Context, + subscription *types.WorkspaceSubscription, + currentStatus types.SubscriptionStatus, +) error { + lastStatus := subscription.Status + if lastStatus == currentStatus { + return nil + } + wdp.Logger.Info("Processing workspace subscription", + "workspace", subscription.Workspace, + "region", subscription.RegionDomain, + "expiredAt", subscription.CurrentPeriodEndAt, + "status", subscription.Status, "currentStatus", currentStatus) + + wdp.Logger.Info("Workspace debt status change detected", + "workspace", subscription.Workspace, + "lastStatus", lastStatus, + "currentStatus", currentStatus) + + // 处理工作空间债务状态 + return wdp.flushWorkspaceDebtStatus(ctx, subscription, lastStatus, currentStatus) +} + +// flushWorkspaceDebtStatus 处理工作空间债务状态变更 +func (wdp *WorkspaceSubscriptionDebtProcessor) flushWorkspaceDebtStatus( + ctx context.Context, + subscription *types.WorkspaceSubscription, + lastDebtStatus, + currentDebtStatus types.SubscriptionStatus, +) error { + namespaces := []string{subscription.Workspace} + + // 获取namespace状态,如果是删除中则跳过 + ns := &corev1.Namespace{} + if err := wdp.Get(ctx, types2.NamespacedName{Name: subscription.Workspace}, ns); err != nil && + !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get namespace %s: %w", subscription.Workspace, err) + } else if apierrors.IsNotFound(err) || ns.DeletionTimestamp != nil || ns.Status.Phase == corev1.NamespaceTerminating { + if subscription.CreateAt.After(time.Now().Add(-24 * time.Hour)) { + wdp.Logger.Info( + "Namespace not found or terminating, but subscription is new, skip", + "namespace", + subscription.Workspace, + "currentStatus", + currentDebtStatus, + ) + return nil + } + // currentDebtStatus = types.SubscriptionStatusDeleted + wdp.Logger.Info( + "Namespace is terminating, set to Deleted", + "namespace", + subscription.Workspace, + "currentStatus", + currentDebtStatus, + ) + return wdp.updateSubscriptionStatus(ctx, subscription, types.SubscriptionStatusDeleted) + } + + if err := wdp.syncWorkspaceDebtStatus( + ctx, + subscription, + lastDebtStatus, + currentDebtStatus, + namespaces, + ); err != nil { + return err + } + if err := wdp.updateSubscriptionStatus(ctx, subscription, currentDebtStatus); err != nil { + return fmt.Errorf("update subscription status error: %w", err) + } + + return nil +} + +func (wdp *WorkspaceSubscriptionDebtProcessor) syncWorkspaceDebtStatus( + ctx context.Context, + subscription *types.WorkspaceSubscription, + lastDebtStatus, + currentDebtStatus types.SubscriptionStatus, + namespaces []string, +) error { + eventData := &usernotify.WorkspaceSubscriptionDebtEventData{ + Type: usernotify.EventTypeWorkspaceSubscriptionDebt, + PlanName: subscription.PlanName, + LastStatus: lastDebtStatus, + CurrentStatus: currentDebtStatus, + DebtDays: 7, + RegionDomain: subscription.RegionDomain, + WorkspaceName: subscription.Workspace, + ExpirationDate: fmt.Sprintf( + "%s-%s", + subscription.CurrentPeriodStartAt.Format(time.DateOnly), + subscription.CurrentPeriodEndAt.Format(time.DateOnly), + ), + } + + // 根据债务状态进行处理 + switch currentDebtStatus { + case types.SubscriptionStatusDebt: + // 过期状态:发送通知,且暂停服务 + if err := wdp.sendWorkspaceDesktopNotice(ctx, currentDebtStatus, namespaces); err != nil { + return fmt.Errorf("send workspace desktop notice error: %w", err) + } + + // 更新工作空间状态 + if err := wdp.updateWorkspaceDebtStatus( + ctx, + types.SuspendDebtNamespaceAnnoStatus, + namespaces, + ); err != nil { + return fmt.Errorf("update workspace debt status error: %w", err) + } + wdp.sendWorkspaceDebtEmail(subscription, eventData) + + case types.SubscriptionStatusDebtPreDeletion: + if err := wdp.sendWorkspaceDesktopNotice(ctx, currentDebtStatus, namespaces); err != nil { + return fmt.Errorf("send workspace desktop notice error: %w", err) + } + + if err := wdp.updateWorkspaceDebtStatus( + ctx, + types.SuspendDebtNamespaceAnnoStatus, + namespaces, + ); err != nil { + return fmt.Errorf("suspend workspace service error: %w", err) + } + + case types.SubscriptionStatusDebtFinalDeletion: + if err := wdp.sendWorkspaceDesktopNotice(ctx, currentDebtStatus, namespaces); err != nil { + return fmt.Errorf("send workspace desktop notice error: %w", err) + } + + if err := wdp.updateWorkspaceDebtStatus( + ctx, + types.FinalDeletionDebtNamespaceAnnoStatus, + namespaces, + ); err != nil { + return fmt.Errorf("update workspace final deletion status error: %w", err) + } + } + + if err := wdp.readWorkspaceNotices( + ctx, + namespaces, + wdp.getWorkspaceStatusesGreaterThan(currentDebtStatus)...); err != nil { + return fmt.Errorf("read workspace notices error: %w", err) + } + + return nil +} + +func (wdp *WorkspaceSubscriptionDebtProcessor) sendWorkspaceDebtEmail( + subscription *types.WorkspaceSubscription, + eventData usernotify.EventData, +) { + if wdp.AccountV2 == nil || wdp.UserContactProvider == nil || + wdp.UserNotificationService == nil { + return + } + + userUID := subscription.UserUID + nr, err := wdp.AccountV2.GetNotificationRecipient(userUID) + if err != nil { + wdp.logWorkspaceDebtErrorf( + "failed to get notification recipient for user %s: %v", + userUID, + err, + ) + return + } + wdp.UserContactProvider.SetUserContact(userUID, nr) + defer wdp.UserContactProvider.RemoveUserContact(userUID) + + if _, err = wdp.UserNotificationService.HandleWorkspaceSubscriptionEvent( + context.Background(), + userUID, + eventData, + types.SubscriptionTransactionTypeDebt, + []usernotify.NotificationMethod{usernotify.NotificationMethodEmail}, + ); err != nil { + wdp.logWorkspaceDebtErrorf( + "failed to send workspace debt notification for user %s: %v", + userUID, + err, + ) + } +} + +func (wdp *WorkspaceSubscriptionDebtProcessor) logWorkspaceDebtErrorf(format string, args ...any) { + if wdp.VLogger != nil { + wdp.VLogger.Errorf(format, args...) + } +} + +// updateSubscriptionStatus 更新订阅状态 +func (wdp *WorkspaceSubscriptionDebtProcessor) updateSubscriptionStatus( + ctx context.Context, + subscription *types.WorkspaceSubscription, + status types.SubscriptionStatus, +) error { + return wdp.db.WithContext(ctx). + Debug(). + Model(&types.WorkspaceSubscription{}). + Where("id = ?", subscription.ID). + Update("status", status). + Error +} + +// updateWorkspaceDebtStatus 更新工作空间债务状态 +func (wdp *WorkspaceSubscriptionDebtProcessor) updateWorkspaceDebtStatus( + ctx context.Context, + status string, + namespaces []string, +) error { + for _, nsName := range namespaces { + ns := &corev1.Namespace{} + if err := wdp.Get(ctx, types2.NamespacedName{Name: nsName}, ns); err != nil { + return fmt.Errorf("failed to get namespace %s: %w", nsName, err) + } + original := ns.DeepCopy() + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + + // 检查是否需要更新 + if ns.Annotations[types.DebtNamespaceAnnoStatusKey] == status || + ns.Annotations[types.DebtNamespaceAnnoStatusKey] == status+"Completed" { + if ns.Annotations[types.WorkspaceSubscriptionStatusAnnoKey] == status { + continue + } + } else { + ns.Annotations[types.DebtNamespaceAnnoStatusKey] = status + } + + ns.Annotations[types.WorkspaceSubscriptionStatusAnnoKey] = status + ns.Annotations[types.WorkspaceSubscriptionStatusUpdateTimeAnnoKey] = time.Now(). + Format(time.RFC3339) + + if err := wdp.Patch(ctx, ns, client.MergeFrom(original)); err != nil { + return fmt.Errorf("patch workspace namespace annotation failed: %w", err) + } + } + return nil +} + +func workspaceDebtNoticeName(status types.SubscriptionStatus) string { + return workspaceDebtNoticePrefix + strings.ReplaceAll(strings.ToLower(string(status)), "_", "-") +} + +// sendWorkspaceDesktopNotice 发送工作空间桌面通知 +func (wdp *WorkspaceSubscriptionDebtProcessor) sendWorkspaceDesktopNotice( + ctx context.Context, + debtStatus types.SubscriptionStatus, + namespaces []string, +) error { + now := time.Now().UTC().Unix() + ntfTmp := ¬ificationv1.Notification{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceDebtNoticeName(debtStatus), + }, + } + + ntfTmpSpc := notificationv1.NotificationSpec{ + Title: workspaceTitleTemplateENMap[debtStatus], + Message: workspaceNoticeTemplateENMap[debtStatus], + From: workspaceFromEn, + Importance: notificationv1.High, + DesktopPopup: true, + Timestamp: now, + I18n: map[string]notificationv1.I18n{ + "zh": { + Title: workspaceTitleTemplateZHMap[debtStatus], + From: workspaceFromZh, + Message: workspaceNoticeTemplateZHMap[debtStatus], + }, + }, + } + + for _, nsName := range namespaces { + ntf := ntfTmp.DeepCopy() + ntfSpec := ntfTmpSpc.DeepCopy() + ntf.Namespace = nsName + + if _, err := controllerutil.CreateOrUpdate(ctx, wdp.Client, ntf, func() error { + ntf.Spec = *ntfSpec + if ntf.Labels == nil { + ntf.Labels = make(map[string]string) + } + ntf.Labels["isRead"] = "false" + return nil + }); err != nil { + return fmt.Errorf("failed to create or update workspace notification: %w", err) + } + } + return nil +} + +// readWorkspaceNotices 标记工作空间通知为已读 +func (wdp *WorkspaceSubscriptionDebtProcessor) readWorkspaceNotices( + ctx context.Context, + namespaces []string, + noticeTypes ...types.SubscriptionStatus, +) error { + for _, nsName := range namespaces { + for _, noticeStatus := range noticeTypes { + ntf := ¬ificationv1.Notification{} + notificationName := workspaceDebtNoticeName(noticeStatus) + + if err := wdp.Get(ctx, types2.NamespacedName{ + Name: notificationName, + Namespace: nsName, + }, ntf); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to get workspace notification: %w", err) + } else if err != nil { + continue + } + + if ntf.Labels == nil { + ntf.Labels = make(map[string]string) + } else if ntf.Labels["isRead"] == trueStatus { + continue + } + + ntf.Labels["isRead"] = trueStatus + if err := wdp.Update(ctx, ntf); err != nil { + return fmt.Errorf("failed to update workspace notification read status: %w", err) + } + } + } + return nil +} + +// getWorkspaceStatusesGreaterThan 获取比当前状态级别更高的所有状态 +func (wdp *WorkspaceSubscriptionDebtProcessor) getWorkspaceStatusesGreaterThan( + currentStatus types.SubscriptionStatus, +) []types.SubscriptionStatus { + // 定义状态优先级映射 + statusPriority := map[types.SubscriptionStatus]int{ + types.SubscriptionStatusDebt: 0, + types.SubscriptionStatusDebtPreDeletion: 1, + types.SubscriptionStatusDebtFinalDeletion: 2, + } + + currentPriority := statusPriority[currentStatus] + var higherStatuses []types.SubscriptionStatus + + for status, priority := range statusPriority { + if priority > currentPriority { + higherStatuses = append(higherStatuses, status) + } + } + + return higherStatuses +} diff --git a/controllers/account/controllers/workspace_subscription_debt_test.go b/controllers/account/controllers/workspace_subscription_debt_test.go new file mode 100644 index 000000000000..fc1d19690876 --- /dev/null +++ b/controllers/account/controllers/workspace_subscription_debt_test.go @@ -0,0 +1,173 @@ +package controllers + +import ( + "context" + "testing" + "time" + + "github.com/go-logr/logr" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "github.com/labring/sealos/controllers/pkg/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestWorkspaceSubscriptionDebtStatusSyncsNamespaceAnnotations(t *testing.T) { + tests := []struct { + name string + expiredFor time.Duration + expectedStatus types.SubscriptionStatus + expectedDebtStatus string + }{ + { + name: "recent expiration suspends workspace", + expiredFor: time.Hour, + expectedStatus: types.SubscriptionStatusDebt, + expectedDebtStatus: types.SuspendDebtNamespaceAnnoStatus, + }, + { + name: "debt over grace period stays suspended before deletion", + expiredFor: time.Duration(ExpiredGracePeriodHours+1) * time.Hour, + expectedStatus: types.SubscriptionStatusDebtPreDeletion, + expectedDebtStatus: types.SuspendDebtNamespaceAnnoStatus, + }, + { + name: "debt over final deletion period marks workspace for deletion", + expiredFor: time.Duration(FinalDeletionPeriodHours+1) * time.Hour, + expectedStatus: types.SubscriptionStatusDebtFinalDeletion, + expectedDebtStatus: types.FinalDeletionDebtNamespaceAnnoStatus, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + namespace := "test-workspace" + processor := newWorkspaceSubscriptionDebtTestProcessor(t, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }) + + now := time.Now().UTC() + expireTime := now.Add(-tt.expiredFor) + subscription := &types.WorkspaceSubscription{ + PlanName: "Free", + Workspace: namespace, + RegionDomain: "test.example.com", + Status: types.SubscriptionStatusDebt, + CurrentPeriodStartAt: expireTime.Add(-30 * 24 * time.Hour), + CurrentPeriodEndAt: expireTime, + CancelAtPeriodEnd: false, + CreateAt: now.Add(-60 * 24 * time.Hour), + UpdateAt: now, + } + + currentStatus := processor.determineCurrentStatus(subscription.CurrentPeriodEndAt) + if currentStatus != tt.expectedStatus { + t.Fatalf("expected current status %s, got %s", tt.expectedStatus, currentStatus) + } + + if err := processor.syncWorkspaceDebtStatus( + ctx, + subscription, + subscription.Status, + currentStatus, + []string{namespace}, + ); err != nil { + t.Fatalf("sync workspace debt status failed: %v", err) + } + + var updatedNS corev1.Namespace + if err := processor.Get( + ctx, + client.ObjectKey{Name: namespace}, + &updatedNS, + ); err != nil { + t.Fatalf("failed to get updated namespace: %v", err) + } + + if got := updatedNS.Annotations[types.DebtNamespaceAnnoStatusKey]; got != tt.expectedDebtStatus { + t.Fatalf("expected debt annotation %s, got %s", tt.expectedDebtStatus, got) + } + if got := updatedNS.Annotations[types.WorkspaceSubscriptionStatusAnnoKey]; got != tt.expectedDebtStatus { + t.Fatalf( + "expected workspace subscription annotation %s, got %s", + tt.expectedDebtStatus, + got, + ) + } + + var notice notificationv1.Notification + if err := processor.Get(ctx, client.ObjectKey{ + Name: workspaceDebtNoticeName(tt.expectedStatus), + Namespace: namespace, + }, ¬ice); err != nil { + t.Fatalf("failed to get workspace debt notice: %v", err) + } + }) + } +} + +func TestWorkspaceDebtNoticeNameIsDNS1123Compatible(t *testing.T) { + tests := []struct { + status types.SubscriptionStatus + want string + }{ + { + status: types.SubscriptionStatusDebt, + want: "workspace-debt-debt", + }, + { + status: types.SubscriptionStatusDebtPreDeletion, + want: "workspace-debt-debt-pre-deletion", + }, + { + status: types.SubscriptionStatusDebtFinalDeletion, + want: "workspace-debt-debt-final-deletion", + }, + } + + for _, tt := range tests { + t.Run(string(tt.status), func(t *testing.T) { + got := workspaceDebtNoticeName(tt.status) + if got != tt.want { + t.Fatalf("expected notice name %s, got %s", tt.want, got) + } + if errs := validation.IsDNS1123Subdomain(got); len(errs) > 0 { + t.Fatalf("expected DNS1123-compatible notice name, got errors: %v", errs) + } + }) + } +} + +func newWorkspaceSubscriptionDebtTestProcessor( + t *testing.T, + objects ...client.Object, +) *WorkspaceSubscriptionDebtProcessor { + t.Helper() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add core scheme: %v", err) + } + if err := notificationv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add notification scheme: %v", err) + } + + fakeClient := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objects...). + Build() + + return &WorkspaceSubscriptionDebtProcessor{ + AccountReconciler: &AccountReconciler{ + Client: fakeClient, + Logger: logr.Discard(), + }, + } +} diff --git a/controllers/account/controllers/workspace_traffic_controller.go b/controllers/account/controllers/workspace_traffic_controller.go new file mode 100644 index 000000000000..e52ee22c72ca --- /dev/null +++ b/controllers/account/controllers/workspace_traffic_controller.go @@ -0,0 +1,600 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" + usernotify "github.com/labring/sealos/controllers/pkg/user_notify" + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" + types2 "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// WorkspaceTrafficController handles workspace-level traffic management +type WorkspaceTrafficController struct { + TrafficDB database.Interface + GlobalDB *gorm.DB + *AccountReconciler +} + +// NewWorkspaceTrafficController creates a new workspace traffic controller +func NewWorkspaceTrafficController( + ar *AccountReconciler, + trafficDBURI database.Interface, +) *WorkspaceTrafficController { + return &WorkspaceTrafficController{ + TrafficDB: trafficDBURI, + GlobalDB: ar.AccountV2.GetGlobalDB(), + AccountReconciler: ar, + } +} + +// BatchGetWorkspaceSubscriptions retrieves all workspace subscription mappings +func (c *WorkspaceTrafficController) BatchGetWorkspaceSubscriptions() (map[string]*types.WorkspaceSubscription, error) { + allWorkspaces := make(map[string]*types.WorkspaceSubscription) + var subscriptions []types.WorkspaceSubscription + + err := c.GlobalDB.Where("region_domain = ?", c.localDomain).Find(&subscriptions).Error + if err != nil { + return nil, fmt.Errorf("failed to get workspace subscriptions: %w", err) + } + + for i := range subscriptions { + allWorkspaces[subscriptions[i].Workspace] = &subscriptions[i] + } + + return allWorkspaces, nil +} + +// processWorkspaceTraffic processes workspace traffic consumption +func (c *WorkspaceTrafficController) processWorkspaceTraffic(resultMap map[string]int64) error { + workspaceMap, err := c.BatchGetWorkspaceSubscriptions() + if err != nil { + return fmt.Errorf("failed to batch get workspace subscriptions: %w", err) + } + + for namespace, consumedBytes := range resultMap { + if !strings.HasPrefix(namespace, "ns-") { + continue + } + + // Find matching workspace subscription + var matchedSubscription *types.WorkspaceSubscription + if sub, ok := workspaceMap[namespace]; ok { + matchedSubscription = sub + } else { + // c.Logger.Info("no workspace subscription found for namespace", "namespace", namespace) + continue + } + + // Process traffic consumption for this workspace + err = c.consumeWorkspaceTraffic(matchedSubscription, consumedBytes) + if err != nil { + /*c.Logger.Error(err, "failed to consume workspace traffic", + "workspace", matchedSubscription.Workspace, + "region", matchedSubscription.RegionDomain)*/ + c.VLogger.Errorf( + "failed to consume workspace traffic for namespace %s, err: %v", + namespace, + err, + ) + } + } + + return nil +} + +// consumeWorkspaceTraffic consumes traffic from workspace packages based on priority +func (c *WorkspaceTrafficController) consumeWorkspaceTraffic( + subscription *types.WorkspaceSubscription, + consumedBytes int64, +) error { + // Get available traffic packages ordered by expiry date (nearest expiry first) + var availablePackages []types.WorkspaceTraffic + err := c.GlobalDB.Model(&types.WorkspaceTraffic{}). + Where("workspace_subscription_id = ? AND status = ? AND expired_at > ? AND total_bytes > used_bytes", + subscription.ID, types.WorkspaceTrafficStatusActive, time.Now()). + Order("expired_at ASC"). + Find(&availablePackages). + Error + if err != nil { + return fmt.Errorf("failed to get available traffic packages: %w", err) + } + + if len(availablePackages) == 0 { + // No available traffic, check if workspace needs suspension + return c.handleNoAvailableTraffic(subscription) + } + + // Calculate total and used traffic for percentage calculation + var totalTraffic, usedTraffic int64 + for _, pkg := range availablePackages { + totalTraffic += pkg.TotalBytes + usedTraffic += pkg.UsedBytes + } + + // Consume traffic from packages in priority order + remainingToConsume := consumedBytes + var packagesToUpdate []types.WorkspaceTraffic + + for i := range availablePackages { + pkg := &availablePackages[i] + availableInPackage := pkg.TotalBytes - pkg.UsedBytes + + if remainingToConsume <= 0 { + break + } + + if availableInPackage > 0 { + consumeFromPackage := min(remainingToConsume, availableInPackage) + + pkg.UsedBytes += consumeFromPackage + pkg.UpdatedAt = time.Now() + + // Mark package as exhausted if fully consumed + if pkg.UsedBytes >= pkg.TotalBytes { + pkg.Status = types.WorkspaceTrafficStatusExhausted + } + + packagesToUpdate = append(packagesToUpdate, *pkg) + remainingToConsume -= consumeFromPackage + usedTraffic += consumeFromPackage + } + } + + // Update packages in batch + if len(packagesToUpdate) > 0 { + err = c.batchUpdateTrafficPackages(packagesToUpdate) + if err != nil { + return fmt.Errorf("failed to update traffic packages: %w", err) + } + } + + // Calculate usage percentage + usagePercentage := float64(usedTraffic) / float64(totalTraffic) * 100 + + // Determine new status + var newStatus types.WorkspaceTrafficStatus + switch { + case remainingToConsume > 0: + // Traffic is used up (even if percentage <100% due to overage) + newStatus = types.WorkspaceTrafficStatusUsedUp + case usagePercentage >= 80: + // 80% or more traffic used, but still available + newStatus = types.WorkspaceTrafficStatusExhausted + default: + newStatus = types.WorkspaceTrafficStatusActive + } + + // Update subscription status if changed + oldStatus := subscription.TrafficStatus + if oldStatus != newStatus { + err = c.updateWorkspaceTrafficStatus(subscription.ID, newStatus) + if err != nil { + return fmt.Errorf("failed to update workspace traffic status: %w", err) + } + + // Send notification for Exhausted or UsedUp + userUID := subscription.UserUID + nr, err := c.AccountV2.GetNotificationRecipient(subscription.UserUID) + if err != nil { + c.VLogger.Errorf("failed to get notification recipient for user %s: %v", userUID, err) + } else { + c.UserContactProvider.SetUserContact(userUID, nr) + defer c.UserContactProvider.RemoveUserContact(userUID) + + var usagePercent int + var totalBytes, usedBytes int64 + switch newStatus { + case types.WorkspaceTrafficStatusUsedUp: + usagePercent = 100 + totalBytes = 0 + usedBytes = 0 + case types.WorkspaceTrafficStatusExhausted: + usagePercent = int(math.Round(usagePercentage)) + totalBytes = totalTraffic + usedBytes = usedTraffic + } + plan, err := c.AccountV2.GetWorkspaceSubscriptionPlan(subscription.PlanName) + if err != nil { + return fmt.Errorf("failed to get workspace subscription plan: %w", err) + } + features, err := types.ParseMaxResource(plan.MaxResources, plan.Traffic) + if err != nil { + return fmt.Errorf("failed to parse plan features: %w", err) + } + eventData := &usernotify.WorkspaceSubscriptionTrafficEventData{ + Type: usernotify.EventTypeTrafficUsageAlert, + PlanName: subscription.PlanName, + RegionDomain: subscription.RegionDomain, + UsagePercent: usagePercent, + TotalBytes: totalBytes, + UsedBytes: usedBytes, + Workspace: subscription.Workspace, + ExpirationDate: fmt.Sprintf("%s - %s", + subscription.CurrentPeriodStartAt.Format("2006.1.2"), + subscription.CurrentPeriodEndAt.Format("2006.1.2")), + Features: features, + } + if _, err = c.UserNotificationService.HandleWorkspaceSubscriptionEvent( + context.Background(), + userUID, + eventData, + types.SubscriptionTransactionTypeOther, + []usernotify.NotificationMethod{usernotify.NotificationMethodEmail}, + ); err != nil { + c.VLogger.Errorf( + "failed to send traffic usage alert notification for user %s: %v", + userUID, + err, + ) + } + } + + // Suspend workspace only if fully used up + if newStatus == types.WorkspaceTrafficStatusUsedUp { + err = c.suspendWorkspaceTraffic(subscription.Workspace) + if err != nil { + return fmt.Errorf("failed to suspend workspace traffic: %w", err) + } + } + } + + return nil +} + +// batchUpdateTrafficPackages updates multiple traffic packages +func (c *WorkspaceTrafficController) batchUpdateTrafficPackages( + packages []types.WorkspaceTraffic, +) error { + tx := c.GlobalDB.Begin() + if tx.Error != nil { + return fmt.Errorf("failed to begin transaction: %w", tx.Error) + } + + for _, pkg := range packages { + err := tx.Model(&types.WorkspaceTraffic{}).Where("id = ?", pkg.ID). + Updates(map[string]any{ + "used_bytes": pkg.UsedBytes, + "status": pkg.Status, + "updated_at": pkg.UpdatedAt, + }).Error + if err != nil { + tx.Rollback() + return fmt.Errorf("failed to update package %s: %w", pkg.ID, err) + } + } + + return tx.Commit().Error +} + +// handleNoAvailableTraffic handles the case when workspace has no available traffic +func (c *WorkspaceTrafficController) handleNoAvailableTraffic( + subscription *types.WorkspaceSubscription, +) error { + c.Logger.Info("Handling no available traffic", "workspace", subscription.Workspace) + oldStatus := subscription.TrafficStatus + if oldStatus != types.WorkspaceTrafficStatusUsedUp { + err := c.updateWorkspaceTrafficStatus(subscription.ID, types.WorkspaceTrafficStatusUsedUp) + if err != nil { + return fmt.Errorf("failed to update workspace traffic status: %w", err) + } + + err = c.suspendWorkspaceTraffic(subscription.Workspace) + if err != nil { + return fmt.Errorf("failed to suspend workspace traffic: %w", err) + } + + userUID := subscription.UserUID + nr, err := c.AccountV2.GetNotificationRecipient(subscription.UserUID) + if err != nil { + c.VLogger.Errorf("failed to get notification recipient for user %s: %v", userUID, err) + } else { + c.UserContactProvider.SetUserContact(userUID, nr) + defer c.UserContactProvider.RemoveUserContact(userUID) + plan, err := c.AccountV2.GetWorkspaceSubscriptionPlan(subscription.PlanName) + if err != nil { + return fmt.Errorf("failed to get workspace subscription plan: %w", err) + } + features, err := types.ParseMaxResource(plan.MaxResources, plan.Traffic) + if err != nil { + return fmt.Errorf("failed to parse plan features: %w", err) + } + eventData := &usernotify.WorkspaceSubscriptionTrafficEventData{ + Type: usernotify.EventTypeTrafficUsageAlert, + PlanName: subscription.PlanName, + RegionDomain: subscription.RegionDomain, + UsagePercent: 100, + Workspace: subscription.Workspace, + Features: features, + ExpirationDate: fmt.Sprintf("%s - %s", + subscription.CurrentPeriodStartAt.Format("2006.1.2"), + subscription.CurrentPeriodEndAt.Format("2006.1.2")), + } + + if _, err = c.UserNotificationService.HandleWorkspaceSubscriptionEvent( + context.Background(), + userUID, + eventData, + types.SubscriptionTransactionTypeOther, + []usernotify.NotificationMethod{usernotify.NotificationMethodEmail}, + ); err != nil { + c.VLogger.Errorf( + "failed to send subscription success notification for user %s: %v", + userUID, + err, + ) + } + } + } + + return nil +} + +// updateWorkspaceTrafficStatus updates the traffic status of a workspace +func (c *WorkspaceTrafficController) updateWorkspaceTrafficStatus( + subscriptionID uuid.UUID, + status types.WorkspaceTrafficStatus, +) error { + result := c.GlobalDB.Model(&types.WorkspaceSubscription{}). + Where("id = ?", subscriptionID). + Update("traffic_status", status) + + if result.Error != nil { + return fmt.Errorf("failed to update traffic status: %w", result.Error) + } + + return nil +} + +// ProcessTrafficWithTimeRange processes workspace traffic within time ranges +func (c *WorkspaceTrafficController) ProcessTrafficWithTimeRange() { + c.Logger.Info("start workspace traffic controller") + startTime := time.Now().Add(-1 * time.Minute) + + for range time.NewTicker(time.Minute).C { + c.Logger.Info("time to process workspace traffic", "startTime", startTime) + endTime := time.Now() + result, err := c.TrafficDB.GetNamespaceTraffic(context.Background(), startTime, endTime) + if err != nil { + c.Logger.Error(err, "failed to get namespace traffic") + endTime = startTime + } else if len(result) > 0 { + err = c.processWorkspaceTraffic(result) + if err != nil { + c.VLogger.Errorf("failed to process workspace traffic: %v", err) + } else { + c.Logger.Info( + "successfully processed workspace traffic", + "count", + len(result), + "start", + startTime, + "end", + endTime, + ) + } + } + startTime = endTime + } +} + +func (r *AccountReconciler) addTrafficPackage( + globalDB *gorm.DB, + sub *types.WorkspaceSubscription, + plan *types.WorkspaceSubscriptionPlan, + expireAt time.Time, + from types.WorkspaceTrafficFrom, + fromID string, +) error { + if plan.Traffic <= 0 { + return nil + } + totalBytes := plan.Traffic * 1024 * 1024 // Convert MiB to Bytes + err := cockroach.AddWorkspaceSubscriptionTrafficPackage( + globalDB, + sub.ID, + plan.Traffic, + expireAt, + from, + fromID, + ) + if err != nil { + return fmt.Errorf("failed to create traffic package: %w", err) + } + // Check if workspace was previously exhausted or used up and needs to be resumed + if sub.TrafficStatus == types.WorkspaceTrafficStatusUsedUp || + sub.TrafficStatus == types.WorkspaceTrafficStatusExhausted { + // Update workspace status to active + err = globalDB.Model(&types.WorkspaceSubscription{}). + Where("id = ?", sub.ID). + Update("traffic_status", types.WorkspaceTrafficStatusActive).Error + if err != nil { + return fmt.Errorf("failed to update workspace traffic status: %w", err) + } + // Send resume request + err = r.resumeWorkspaceTraffic(sub.Workspace) + if err != nil { + r.Logger.Error(err, "failed to resume workspace traffic", + "workspace", sub.Workspace, + "region", sub.RegionDomain) + } + } + + r.Logger.Info("successfully added traffic package", + "workspace", sub.Workspace, + "region", sub.RegionDomain, + "totalBytes", totalBytes) + + return nil +} + +// AddTrafficPackage adds a new traffic package to workspace +func (r *AccountReconciler) AddTrafficPackage( + globalDB *gorm.DB, + sub *types.WorkspaceSubscription, + plan *types.WorkspaceSubscriptionPlan, + expireAt time.Time, + from types.WorkspaceTrafficFrom, + fromID string, +) error { + if sub.ID == uuid.Nil { + return errors.New("workspace subscription ID cannot be nil") + } + // Get workspace subscription + var subscription types.WorkspaceSubscription + err := globalDB.First(&subscription, "id = ?", sub.ID).Error + if err != nil { + return fmt.Errorf("failed to get workspace subscription: %w", err) + } + return r.addTrafficPackage(globalDB, &subscription, plan, expireAt, from, fromID) +} + +// NewTrafficPackage adds a new traffic package, tolerating non-existent subscription +func (r *AccountReconciler) NewTrafficPackage( + globalDB *gorm.DB, + sub *types.WorkspaceSubscription, + plan *types.WorkspaceSubscriptionPlan, + expireAt time.Time, + from types.WorkspaceTrafficFrom, + fromID string, +) error { + if sub.ID == uuid.Nil { + return errors.New("workspace subscription ID cannot be nil") + } + return r.addTrafficPackage(globalDB, sub, plan, expireAt, from, fromID) +} + +// CleanupExpiredPackages marks expired traffic packages +func (c *WorkspaceTrafficController) CleanupExpiredPackages() error { + result := c.GlobalDB.Model(&types.WorkspaceTraffic{}). + Where("expire_at <= ? AND status = ?", time.Now(), types.WorkspaceTrafficStatusActive). + Update("status", types.WorkspaceTrafficStatusExpired) + + if result.Error != nil { + return fmt.Errorf("failed to cleanup expired packages: %w", result.Error) + } + + c.Logger.Info("cleaned up expired traffic packages", "count", result.RowsAffected) + return nil +} + +func updateNamespaceStatus( + clt client.Client, + ctx context.Context, + status string, + namespaces []string, +) error { + for i := range namespaces { + ns := &corev1.Namespace{} + if err := clt.Get(ctx, types2.NamespacedName{Name: namespaces[i]}, ns); err != nil { + return err + } + if ns.Annotations[types.NetworkStatusAnnoKey] == status { + continue + } + original := ns.DeepCopy() + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[types.NetworkStatusAnnoKey] = status + if err := clt.Patch(ctx, ns, client.MergeFrom(original)); err != nil { + return err + } + } + return nil +} + +// suspendWorkspaceTraffic suspends traffic for a workspace +func (c *WorkspaceTrafficController) suspendWorkspaceTraffic(workspace string) error { + return updateNamespaceStatus( + c.Client, + context.Background(), + types.NetworkSuspend, + []string{workspace}, + ) +} + +// resumeWorkspaceTraffic resumes traffic for a workspace +func (r *AccountReconciler) resumeWorkspaceTraffic(workspace string) error { + return updateNamespaceStatus( + r.Client, + context.Background(), + types.NetworkResume, + []string{workspace}, + ) +} + +// WorkspaceTrafficManager provides high-level traffic management operations +type WorkspaceTrafficManager struct { + controller *WorkspaceTrafficController + db *gorm.DB +} + +func NewWorkspaceTrafficManager(controller *WorkspaceTrafficController) *WorkspaceTrafficManager { + return &WorkspaceTrafficManager{ + controller: controller, + db: controller.GlobalDB, + } +} + +// GetWorkspaceTrafficSummary returns traffic summary for a workspace +func (m *WorkspaceTrafficManager) GetWorkspaceTrafficSummary( + workspace, regionDomain string, +) (*WorkspaceTrafficSummary, error) { + var subscription types.WorkspaceSubscription + err := m.db.Where("workspace = ? AND region_domain = ?", workspace, regionDomain). + First(&subscription).Error + if err != nil { + return nil, fmt.Errorf("failed to get workspace subscription: %w", err) + } + + var packages []types.WorkspaceTraffic + err = m.db.Where("workspace_subscription_id = ?", subscription.ID). + Order("expire_at ASC"). + Find(&packages).Error + if err != nil { + return nil, fmt.Errorf("failed to get traffic packages: %w", err) + } + + summary := &WorkspaceTrafficSummary{ + WorkspaceID: subscription.ID, + Workspace: workspace, + RegionDomain: regionDomain, + TrafficStatus: subscription.TrafficStatus, + TotalPackages: len(packages), + } + + for _, pkg := range packages { + summary.TotalTraffic += pkg.TotalBytes + summary.UsedTraffic += pkg.UsedBytes + if pkg.Status == types.WorkspaceTrafficStatusActive { + summary.AvailableTraffic += pkg.TotalBytes - pkg.UsedBytes + summary.ActivePackages++ + } + } + + return summary, nil +} + +// WorkspaceTrafficSummary contains traffic summary information +type WorkspaceTrafficSummary struct { + WorkspaceID uuid.UUID `json:"workspace_id"` + Workspace string `json:"workspace"` + RegionDomain string `json:"region_domain"` + TrafficStatus types.WorkspaceTrafficStatus `json:"traffic_status"` + TotalPackages int `json:"total_packages"` + ActivePackages int `json:"active_packages"` + TotalTraffic int64 `json:"total_traffic"` + UsedTraffic int64 `json:"used_traffic"` + AvailableTraffic int64 `json:"available_traffic"` +} diff --git a/controllers/account/deploy/Kubefile b/controllers/account/deploy/Kubefile new file mode 100644 index 000000000000..8c1e8c4476ac --- /dev/null +++ b/controllers/account/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY account-controller-entrypoint.sh account-controller-entrypoint.sh + +CMD ["bash account-controller-entrypoint.sh"] diff --git a/controllers/account/deploy/README.md b/controllers/account/deploy/README.md new file mode 100644 index 000000000000..24ffedf69ee2 --- /dev/null +++ b/controllers/account/deploy/README.md @@ -0,0 +1,191 @@ +# Account Controller Helm Chart + +Account Controller 的 Helm Chart 部署配置。 + +## 快速开始 + +### 最精简配置 + +使用默认配置直接启动: + +```bash +sealos run account-controller:latest +``` + +### 自定义配置示例 + +```bash +# 通过 HELM_OPTS 传入自定义配置 +sealos run account-controller:latest \ + --env HELM_OPTS="--set-string accountEnv.cloudDomain=cloud.sealos.io --set-string accountEnv.mongoURI=mongodb://mongo:27017/resources" +``` + +## 配置说明 + +### 自动配置(从 ConfigMap 和 Secret 获取) + +以下配置会自动从 `sealos-system/sealos-config` ConfigMap 中读取(如果存在): + +- `cloudDomain` - 云平台域名 +- `cloudPort` - 云平台端口 +- `accountApiJwtSecret` - JWT 密钥 +- `accountAdminJwtSecret` - admin API 专用 JWT 密钥,由脚本从 `account-admin-jwt` Secret 读取,不存在时自动生成 +- `localRegion` - 本地区域 UID +- `globalCockroachURI` - 全局 CockroachDB URI +- `localCockroachURI` - 本地 CockroachDB URI +- `mongoURI` - MongoDB URI +- `trafficMongoURI` - 流量 MongoDB URI(从 `sealos-system/nm-agent-config` 获取,不存在则使用 mongoURI) + +**注意**:通过 `--env HELM_OPTS` 传入的相同参数会覆盖从 ConfigMap 获取的值。 + +### 通过 HELM_OPTS 支持的配置项 + +#### 基础配置 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `accountEnvMergeStrategy` | `overwrite` | ConfigMap 合并策略:`preserve`(保留旧值) 或 `overwrite`(覆盖旧值) | + +#### 环境变量配置 (accountEnv.*) + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `accountEnv.cloudDomain` | `cloud.sealos.io` | 云平台域名 | +| `accountEnv.cloudPort` | - | 云平台端口 | +| `accountEnv.accountApiJwtSecret` | `secret` | JWT 密钥 | +| `accountEnv.accountAdminJwtSecret` | - | admin API 专用 JWT 密钥 | +| `accountEnv.localRegion` | - | 本地区域 UID | +| `accountEnv.globalCockroachURI` | - | 全局 CockroachDB URI | +| `accountEnv.localCockroachURI` | - | 本地 CockroachDB URI | +| `accountEnv.mongoURI` | `mongodb://mongo:27017/resources` | MongoDB URI | +| `accountEnv.trafficMongoURI` | - | 流量 MongoDB URI | +| `accountEnv.whitelistKubernetesHosts` | - | Kubernetes API Server 白名单(自动根据 cloudDomain 生成) | + +#### 存储配置 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `accountEnv.osAdminSecret` | - | 对象存储管理员 Secret 名称 | +| `accountEnv.osInternalEndpoint` | `object-storage.objectstorage-system.svc` | 对象存储内部端点 | +| `accountEnv.osNamespace` | `objectstorage-system` | 对象存储命名空间 | + +#### 配额限制 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `accountEnv.quotaLimitsCpu` | `16` | CPU 限制 | +| `accountEnv.quotaLimitsMemory` | `64Gi` | 内存限制 | +| `accountEnv.quotaLimitsStorage` | `200Gi` | 存储限制 | +| `accountEnv.quotaLimitsGpu` | `8` | GPU 限制 | +| `accountEnv.quotaLimitsPods` | `20` | Pod 数量限制 | +| `accountEnv.quotaLimitsNodePorts` | `10` | NodePort 数量限制 | +| `accountEnv.quotaObjectStorageSize` | `20Gi` | 对象存储大小限制 | +| `accountEnv.quotaObjectStorageBucket` | `20` | 对象存储 Bucket 数量限制 | +| `accountEnv.limitRangeEphemeralStorage` | `0` | 临时存储限制 | + +#### 账户配置 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `accountEnv.baseBalance` | `5000000` | 基础余额 | +| `accountEnv.approachingDeletionPeriod` | `345600` | 接近删除周期(秒) | +| `accountEnv.imminentDeletionPeriod` | `259200` | 即将删除周期(秒) | +| `accountEnv.finalDeletionPeriod` | `604800` | 最终删除周期(秒) | +| `accountEnv.debtDetectionCycleSeconds` | `1800` | 债务检测周期(秒) | + +#### 镜像和资源配置 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `image` | `ghcr.io/labring/sealos-account-controller:latest` | 镜像地址 | +| `imagePullPolicy` | `Always` | 镜像拉取策略 | +| `replicaCount` | `1` | 副本数 | + +#### 资源限制 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `resources.limits.cpu` | `1000m` | CPU 限制 | +| `resources.limits.memory` | `1024Mi` | 内存限制 | +| `resources.requests.cpu` | `100m` | CPU 请求 | +| `resources.requests.memory` | `64Mi` | 内存请求 | + +## 使用示例 + +### 示例 1:修改数据库连接 + +```bash +sealos run account-controller:latest \ + --env HELM_OPTS="--set-string accountEnv.mongoURI=mongodb://custom-mongo:27017/resources --set-string accountEnv.globalCockroachURI=postgres://cockroach:26257/db" +``` + +### 示例 2:自定义配额限制 + +```bash +sealos run account-controller:latest \ + --env HELM_OPTS="--set-string accountEnv.quotaLimitsCpu=32 --set-string accountEnv.quotaLimitsMemory=128Gi --set-string accountEnv.quotaLimitsStorage=500Gi" +``` + +### 示例 3:启用奖励处理并调整对象存储配置 + +```bash +sealos run account-controller:latest \ + --env HELM_OPTS="--set-string accountEnv.rewardProcessing=true --set-string accountEnv.osAdminSecret=my-admin-secret --set-string accountEnv.osNamespace=my-objectstorage" +``` + +### 示例 4:修改资源限制 + +```bash +sealos run account-controller:latest \ + --env HELM_OPTS="--set resources.limits.cpu=2000m --set resources.limits.memory=2048Mi --set replicaCount=2" +``` + +### 示例 5:设置 ConfigMap 合并策略为保留旧值 + +```bash +sealos run account-controller:latest \ + --env ACCOUNT_ENV_MERGE_STRATEGY=preserve +``` + +## 环境变量 + +### 脚本环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `RELEASE_NAME` | `account-controller` | Helm Release 名称 | +| `RELEASE_NAMESPACE` | `account-system` | 命名空间 | +| `CHART_PATH` | `./charts/account-controller` | Chart 路径 | +| `ACCOUNT_ENV_MERGE_STRATEGY` | `overwrite` | ConfigMap 合并策略 | +| `ACCOUNT_BACKUP_ENABLED` | `true` | 是否启用升级前备份 | +| `ACCOUNT_BACKUP_DIR` | `/tmp/sealos-backup/account-controller` | 备份目录 | + +## 备份机制 + +升级前会自动备份以下资源到 `/tmp/sealos-backup/account-controller/`: + +- CRD 资源(accounts、debts、payments) +- RBAC 资源(ClusterRole、ClusterRoleBinding) +- Namespace 资源 +- 命名空间内资源(ConfigMap、Secret、Service、Deployment、ServiceAccount、Role、RoleBinding、Issuer、Certificate) + +如需禁用备份: + +```bash +sealos run account-controller:latest --env ACCOUNT_BACKUP_ENABLED=false +``` + +## 配置优先级 + +配置参数的优先级从高到低为: + +1. 用户通过 `--env HELM_OPTS` 传入的参数(最高优先级) +2. 脚本内部从 ConfigMap 自动获取的配置 +3. Chart values.yaml 中的默认值(最低优先级) + +## 注意事项 + +1. **自动配置**:脚本会自动从 `sealos-system/sealos-config` ConfigMap 读取基础配置,无需手动传入 +2. **配置覆盖**:通过 `HELM_OPTS` 传入的参数会覆盖自动获取的配置和默认值 +3. **Secret 安全**:生产环境请妥善保护 `accountEnv.accountApiJwtSecret` 和 `accountEnv.accountAdminJwtSecret`,两者用途不同 +4. **持久化存储**:确保 MongoDB 和 CockroachDB 的连接配置正确 diff --git a/controllers/account/deploy/account-controller-entrypoint.sh b/controllers/account/deploy/account-controller-entrypoint.sh new file mode 100644 index 000000000000..1de80b361d4f --- /dev/null +++ b/controllers/account/deploy/account-controller-entrypoint.sh @@ -0,0 +1,185 @@ +#!/bin/bash +set -e + +HELM_OPTS=${HELM_OPTS:-""} +RELEASE_NAME=${RELEASE_NAME:-"account-controller"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"account-system"} +CHART_PATH=${CHART_PATH:-"./charts/account-controller"} +ACCOUNT_ENV_MERGE_STRATEGY=${ACCOUNT_ENV_MERGE_STRATEGY:-"overwrite"} +ACCOUNT_BACKUP_ENABLED=${ACCOUNT_BACKUP_ENABLED:-"true"} +ACCOUNT_BACKUP_DIR=${ACCOUNT_BACKUP_DIR:-"/tmp/sealos-backup/account-controller"} + +adopt_namespaced_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl -n "${RELEASE_NAMESPACE}" annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +adopt_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +get_cm_value() { + local namespace="$1" + local name="$2" + local key="$3" + kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true +} + +get_secret_value() { + local namespace="$1" + local name="$2" + local key="$3" + kubectl get secret "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null | base64 --decode 2>/dev/null || true +} + +backup_ns_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" -o yaml >> "${ACCOUNT_BACKUP_FILE}" + printf "\n---\n" >> "${ACCOUNT_BACKUP_FILE}" + fi +} + +backup_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl get "${kind}" "${name}" -o yaml >> "${ACCOUNT_BACKUP_FILE}" + printf "\n---\n" >> "${ACCOUNT_BACKUP_FILE}" + fi +} + +backup_account_resources() { + if [ "${ACCOUNT_BACKUP_ENABLED}" != "true" ]; then + return + fi + local ts + ts=$(date +%Y%m%d%H%M%S) + mkdir -p "${ACCOUNT_BACKUP_DIR}" + ACCOUNT_BACKUP_FILE="${ACCOUNT_BACKUP_DIR}/update-${ts}.yaml" + : > "${ACCOUNT_BACKUP_FILE}" + + backup_cluster_resource customresourcedefinition debts.account.sealos.io + backup_cluster_resource customresourcedefinition payments.account.sealos.io + backup_cluster_resource clusterrole account-manager-role + backup_cluster_resource clusterrole account-metrics-reader + backup_cluster_resource clusterrole account-proxy-role + backup_cluster_resource clusterrolebinding account-manager-rolebinding + backup_cluster_resource clusterrolebinding account-proxy-rolebinding + backup_cluster_resource validatingwebhookconfiguration account-validating-webhook-configuration + + if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + kubectl get namespace "${RELEASE_NAMESPACE}" -o yaml >> "${ACCOUNT_BACKUP_FILE}" + printf "\n---\n" >> "${ACCOUNT_BACKUP_FILE}" + fi + backup_ns_resource configmap account-manager-config + backup_ns_resource configmap account-manager-env + backup_ns_resource secret account-admin-jwt + backup_ns_resource service account-controller-manager-metrics-service + backup_ns_resource service account-webhook-service + backup_ns_resource deployment account-controller-manager + backup_ns_resource serviceaccount account-controller-manager + backup_ns_resource role account-leader-election-role + backup_ns_resource rolebinding account-leader-election-rolebinding + backup_ns_resource issuer account-selfsigned-issuer + backup_ns_resource certificate account-serving-cert +} + +# 执行备份 +backup_account_resources + +HELM_SET_ARGS=() + +AUTO_CONFIG_HELM_OPTS="" + +CLOUD_DOMAIN=$(get_cm_value sealos-system sealos-config cloudDomain) +CLOUD_PORT=$(get_cm_value sealos-system sealos-config cloudPort) +JWT_INTERNAL=$(get_cm_value sealos-system sealos-config jwtInternal) +ACCOUNT_ADMIN_JWT_SECRET=$(get_secret_value "${RELEASE_NAMESPACE}" account-admin-jwt ACCOUNT_ADMIN_JWT_SECRET) +if [ -z "${ACCOUNT_ADMIN_JWT_SECRET}" ]; then + ACCOUNT_ADMIN_JWT_SECRET=$(openssl rand -hex 32) +fi +REGION_UID=$(get_cm_value sealos-system sealos-config regionUID) +GLOBAL_COCKROACH_URI=$(get_cm_value sealos-system sealos-config databaseGlobalCockroachdbURI) +LOCAL_COCKROACH_URI=$(get_cm_value sealos-system sealos-config databaseLocalCockroachdbURI) +MONGODB_URI=$(get_cm_value sealos-system sealos-config databaseMongodbURI) + +TRAFFIC_MONGO=$(get_cm_value sealos-system nm-agent-config MONGO_URI) +if [ -z "${TRAFFIC_MONGO}" ] && [ -n "${MONGODB_URI}" ]; then + TRAFFIC_MONGO="${MONGODB_URI}" +fi + +[ -n "${CLOUD_DOMAIN}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.cloudDomain=${CLOUD_DOMAIN}" +[ -n "${CLOUD_PORT}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.cloudPort=${CLOUD_PORT}" +[ -n "${JWT_INTERNAL}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.accountApiJwtSecret=${JWT_INTERNAL}" +[ -n "${ACCOUNT_ADMIN_JWT_SECRET}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.accountAdminJwtSecret=${ACCOUNT_ADMIN_JWT_SECRET}" +[ -n "${REGION_UID}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.localRegion=${REGION_UID}" +[ -n "${GLOBAL_COCKROACH_URI}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.globalCockroachURI=${GLOBAL_COCKROACH_URI}" +[ -n "${LOCAL_COCKROACH_URI}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.localCockroachURI=${LOCAL_COCKROACH_URI}" +[ -n "${MONGODB_URI}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.mongoURI=${MONGODB_URI}" +[ -n "${TRAFFIC_MONGO}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.trafficMongoURI=${TRAFFIC_MONGO}" + + +if [ -n "${CLOUD_DOMAIN}" ]; then + AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string accountEnv.whitelistKubernetesHosts=https://${CLOUD_DOMAIN}:6443" +fi + +if ! helm status "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + kubectl label namespace "${RELEASE_NAMESPACE}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate namespace "${RELEASE_NAMESPACE}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi + + adopt_namespaced_resource configmap account-manager-config + adopt_namespaced_resource configmap account-manager-env + adopt_namespaced_resource secret account-admin-jwt + adopt_namespaced_resource service account-controller-manager-metrics-service + adopt_namespaced_resource service account-webhook-service + adopt_namespaced_resource deployment account-controller-manager + adopt_namespaced_resource serviceaccount account-controller-manager + adopt_namespaced_resource role account-leader-election-role + adopt_namespaced_resource rolebinding account-leader-election-rolebinding + adopt_namespaced_resource issuer account-selfsigned-issuer + adopt_namespaced_resource certificate account-serving-cert + + adopt_cluster_resource clusterrole account-manager-role + adopt_cluster_resource clusterrole account-metrics-reader + adopt_cluster_resource clusterrole account-proxy-role + adopt_cluster_resource clusterrolebinding account-manager-rolebinding + adopt_cluster_resource clusterrolebinding account-proxy-rolebinding + adopt_cluster_resource validatingwebhookconfiguration account-validating-webhook-configuration +fi + +if [ -n "${ACCOUNT_ENV_MERGE_STRATEGY}" ]; then + HELM_SET_ARGS+=(--set-string "accountEnvMergeStrategy=${ACCOUNT_ENV_MERGE_STRATEGY}") +fi + +SERVICE_NAME="account-controller" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +# merge all helm_opts +# 1. AUTO_CONFIG_HELM_OPTS (Configuration automatically obtained from ConfigMap) +# 2. HELM_SET_ARGS (parameters set internally in the script) +# 3. HELM_OPTS (the parameter passed by the user via --env, with the highest priority, can override the previous configuration) +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + ${AUTO_CONFIG_HELM_OPTS} \ + "${HELM_SET_ARGS[@]}" \ + ${HELM_OPTS} diff --git a/controllers/account/deploy/charts/account-controller/.helmignore b/controllers/account/deploy/charts/account-controller/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/controllers/account/deploy/charts/account-controller/Chart.yaml b/controllers/account/deploy/charts/account-controller/Chart.yaml new file mode 100644 index 000000000000..a264735b79e7 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: account-controller +description: Helm chart for the sealos account controller +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/controllers/account/deploy/charts/account-controller/account-controller-values.yaml b/controllers/account/deploy/charts/account-controller/account-controller-values.yaml new file mode 100644 index 000000000000..f456cb027662 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/account-controller-values.yaml @@ -0,0 +1,33 @@ +# Custom values for account controller helm chart. +# This file contains user-customizable configurations. + +replicaCount: 1 + +resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 100m + memory: 64Mi + +accountEnv: + approachingDeletionPeriod: "345600" + imminentDeletionPeriod: "259200" + finalDeletionPeriod: "604800" + debtDetectionCycleSeconds: "1800" + billingMaxCatchupDuration: "24h" + osAdminSecret: "object-storage-user-0" + osInternalEndpoint: "object-storage.objectstorage-system.svc.cluster.local" + osNamespace: "objectstorage-system" + baseBalance: "5000000" + quotaLimitsCpu: "16" + quotaLimitsMemory: "64Gi" + quotaLimitsStorage: "200Gi" + quotaLimitsGpu: "8" + quotaLimitsPods: "20" + quotaLimitsNodePorts: "10" + quotaObjectStorageSize: "20Gi" + quotaObjectStorageBucket: "20" + limitRangeEphemeralStorage: "0" + rewardProcessing: "false" diff --git a/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_debts.yaml b/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_debts.yaml new file mode 100644 index 000000000000..7793e7a39ff7 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_debts.yaml @@ -0,0 +1,87 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: account-system/account-serving-cert + controller-gen.kubebuilder.io/version: v0.14.0 + name: debts.account.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: account-webhook-service + namespace: account-system + path: /convert + conversionReviewVersions: + - v1 + group: account.sealos.io + names: + kind: Debt + listKind: DebtList + plural: debts + singular: debt + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.status + name: status + type: string + name: v1 + schema: + openAPIV3Schema: + description: Debt is the Schema for the debts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DebtSpec defines the desired state of Debt + properties: + userID: + type: string + userName: + type: string + type: object + status: + description: DebtStatus defines the observed state of Debt + properties: + debtStatusRecords: + items: + description: DebtStatusRecord defines the observed state of Debt + properties: + currentStatus: + type: string + lastDebtStatus: + type: string + updateTime: + format: date-time + type: string + type: object + type: array + lastUpdateTimestamp: + format: int64 + type: integer + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_payments.yaml b/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_payments.yaml new file mode 100644 index 000000000000..90b899e573cb --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/crds/account.sealos.io_payments.yaml @@ -0,0 +1,73 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: payments.account.sealos.io +spec: + group: account.sealos.io + names: + kind: Payment + listKind: PaymentList + plural: payments + singular: payment + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Payment is the Schema for the payments API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PaymentSpec defines the desired state of Payment + properties: + amount: + description: Amount is the amount of recharge + format: int64 + type: integer + paymentMethod: + default: wechat + description: e.g. wechat, alipay, creditcard, etc. + type: string + userCR: + description: UserCr is the user cr name who want to recharge + type: string + userID: + description: UserID is the user id who want to recharge + type: string + type: object + status: + description: PaymentStatus defines the observed state of Payment + properties: + codeURL: + description: CodeURL is the codeURL of wechatpay + type: string + status: + description: Status is the status of wechatpay, charging, closed, timeout + type: string + tradeNO: + description: TradeNO is the tradeNO of wechatpay + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/utils/archive/testdata/aa.txt b/controllers/account/deploy/charts/account-controller/templates/NOTES.txt similarity index 100% rename from pkg/utils/archive/testdata/aa.txt rename to controllers/account/deploy/charts/account-controller/templates/NOTES.txt diff --git a/controllers/account/deploy/charts/account-controller/templates/_helpers.tpl b/controllers/account/deploy/charts/account-controller/templates/_helpers.tpl new file mode 100644 index 000000000000..14e1e40befdd --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "account-controller.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "account-controller.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "account-controller.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "account-controller.labels" -}} +helm.sh/chart: {{ include "account-controller.chart" . }} +{{ include "account-controller.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "account-controller.selectorLabels" -}} +app.kubernetes.io/name: {{ include "account-controller.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "account-controller.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "account-controller.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/account/deploy/charts/account-controller/templates/cert.yaml b/controllers/account/deploy/charts/account-controller/templates/cert.yaml new file mode 100644 index 000000000000..42d1d737ed6a --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/cert.yaml @@ -0,0 +1,53 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "account-controller.fullname" . }}-selfsigned-issuer + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "account-controller.fullname" . }}-serving-cert + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ include "account-controller.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc + - {{ include "account-controller.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ include "account-controller.fullname" . }}-selfsigned-issuer + secretName: webhook-server-cert +--- +{{- if .Values.metrics.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + {{- include "account-controller.labels" . | nindent 4 }} + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + {{- include "account-controller.labels" . | nindent 4 }} + name: metrics-certs +spec: + privateKey: + rotationPolicy: Always + dnsNames: + - {{ include "account-controller.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc + - {{ include "account-controller.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: {{ .Values.metrics.secretName }} +{{- end }} diff --git a/controllers/account/deploy/charts/account-controller/templates/configmap.yaml b/controllers/account/deploy/charts/account-controller/templates/configmap.yaml new file mode 100644 index 000000000000..ab95db325007 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/configmap.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: account-manager-env + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +data: + {{- $data := dict + "ApproachingDeletionPeriod" .Values.accountEnv.approachingDeletionPeriod + "ImminentDeletionPeriod" .Values.accountEnv.imminentDeletionPeriod + "FinalDeletionPeriod" .Values.accountEnv.finalDeletionPeriod + "DebtDetectionCycleSeconds" .Values.accountEnv.debtDetectionCycleSeconds + "OSAdminSecret" .Values.accountEnv.osAdminSecret + "OSInternalEndpoint" .Values.accountEnv.osInternalEndpoint + "OSNamespace" .Values.accountEnv.osNamespace + "MONGO_URI" .Values.accountEnv.mongoURI + "LOCAL_COCKROACH_URI" .Values.accountEnv.localCockroachURI + "GLOBAL_COCKROACH_URI" .Values.accountEnv.globalCockroachURI + "TRAFFIC_MONGO_URI" .Values.accountEnv.trafficMongoURI + "LOCAL_REGION" .Values.accountEnv.localRegion + "DOMAIN" .Values.accountEnv.cloudDomain + "PORT" .Values.accountEnv.cloudPort + "ACCOUNT_API_JWT_SECRET" .Values.accountEnv.accountApiJwtSecret + "BILLING_MAX_CATCHUP_DURATION" .Values.accountEnv.billingMaxCatchupDuration + "BASE_BALANCE" .Values.accountEnv.baseBalance + "QUOTA_LIMITS_CPU" .Values.accountEnv.quotaLimitsCpu + "QUOTA_LIMITS_MEMORY" .Values.accountEnv.quotaLimitsMemory + "QUOTA_LIMITS_STORAGE" .Values.accountEnv.quotaLimitsStorage + "QUOTA_LIMITS_GPU" .Values.accountEnv.quotaLimitsGpu + "QUOTA_LIMITS_PODS" .Values.accountEnv.quotaLimitsPods + "QUOTA_LIMITS_NODE_PORTS" .Values.accountEnv.quotaLimitsNodePorts + "QUOTA_OBJECT_STORAGE_SIZE" .Values.accountEnv.quotaObjectStorageSize + "QUOTA_OBJECT_STORAGE_BUCKET" .Values.accountEnv.quotaObjectStorageBucket + "LIMIT_RANGE_EPHEMERAL_STORAGE" .Values.accountEnv.limitRangeEphemeralStorage + "REWARD_PROCESSING" .Values.accountEnv.rewardProcessing + "WHITELIST_KUBERNETES_HOSTS" (default (printf "https://%s:6443" .Values.accountEnv.cloudDomain) .Values.accountEnv.whitelistKubernetesHosts) + -}} + {{- if eq (default "preserve" .Values.accountEnvMergeStrategy) "preserve" }} + {{- $existing := (lookup "v1" "ConfigMap" .Release.Namespace "account-manager-env") }} + {{- if $existing }} + {{- range $k, $v := $existing.data }} + {{- $_ := set $data $k $v }} + {{- end }} + {{- end }} + {{- end }} + {{- range $k, $v := $data }} + {{ $k }}: {{ $v | quote }} + {{- end }} diff --git a/controllers/account/deploy/charts/account-controller/templates/deployment.yaml b/controllers/account/deploy/charts/account-controller/templates/deployment.yaml new file mode 100644 index 000000000000..2242bd6dd220 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/deployment.yaml @@ -0,0 +1,150 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "account-controller.fullname" . }}-controller-manager + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + checksum/account-manager-env: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/account-admin-secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "account-controller.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: manager + command: + - /manager + args: + - --health-probe-bind-address=:8081 + - --leader-elect + {{- if .Values.metrics.enabled }} + - --metrics-secure=true + - --metrics-bind-address=:8443 + {{- else }} + - --metrics-bind-address=127.0.0.1:8080 + {{- end }} + env: + - name: ACCOUNT_NAMESPACE + value: {{ .Values.env.accountNamespace | quote }} + - name: NAMESPACE_NAME + value: {{ .Values.env.userNamespace | quote }} + - name: WHITELIST + value: {{ .Values.env.whitelist | quote }} + - name: ACCOUNT_SYSTEM_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + envFrom: + - secretRef: + name: {{ .Values.paymentSecretName }} + optional: true + - configMapRef: + name: account-manager-env + - secretRef: + name: {{ .Values.adminJwtSecretName }} + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + - containerPort: 9444 + name: account + protocol: TCP + {{- if .Values.metrics.enabled }} + - containerPort: 8443 + name: https + protocol: TCP + {{- end }} + image: "{{ .Values.image }}" + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics-certs + mountPath: {{ .Values.metrics.certPath }} + readOnly: true + {{- end }} + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + terminationGracePeriodSeconds: 10 + affinity: + {{- if .Values.affinity }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: control-plane + operator: In + values: + - controller-manager + topologyKey: kubernetes.io/hostname + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if .Values.metrics.enabled }} + - name: metrics-certs + secret: + secretName: {{ .Values.metrics.secretName }} + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/controllers/account/deploy/charts/account-controller/templates/rbac.yaml b/controllers/account/deploy/charts/account-controller/templates/rbac.yaml new file mode 100644 index 000000000000..970058334761 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/rbac.yaml @@ -0,0 +1,516 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "account-controller.fullname" . }}-leader-election-role + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "account-controller.fullname" . }}-manager-role +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - account.sealos.io + resources: + - accounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - account.sealos.io + resources: + - accounts/finalizers + verbs: + - update + - apiGroups: + - account.sealos.io + resources: + - accounts/status + verbs: + - get + - patch + - update + - apiGroups: + - account.sealos.io + resources: + - debts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - account.sealos.io + resources: + - debts/finalizers + verbs: + - update + - apiGroups: + - account.sealos.io + resources: + - debts/status + verbs: + - get + - patch + - update + - apiGroups: + - account.sealos.io + resources: + - payments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - account.sealos.io + resources: + - payments/finalizers + verbs: + - update + - apiGroups: + - account.sealos.io + resources: + - payments/status + verbs: + - get + - patch + - update + - apiGroups: + - apps + resources: + - daemonsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - replicasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps.kubeblocks.io + resources: + - clusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps.kubeblocks.io + resources: + - clusters/status + verbs: + - get + - patch + - update + - apiGroups: + - apps.kubeblocks.io + resources: + - opsrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps.kubeblocks.io + resources: + - opsrequests/status + verbs: + - get + - patch + - update + - apiGroups: + - batch + resources: + - cronjobs + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - limitranges + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - namespaces/finalizers + verbs: + - update + - apiGroups: + - "" + resources: + - namespaces/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods/finalizers + verbs: + - update + - apiGroups: + - "" + resources: + - pods/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - resourcequotas + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - metering.common.sealos.io + resources: + - extensionresourceprices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - notification.sealos.io + resources: + - notifications + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - update + - get + - list + - patch + - watch + - apiGroups: [""] + resources: ["persistentvolumeclaims", "services", "pods"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "replicasets", "daemonsets"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["batch"] + resources: ["cronjobs", "jobs"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["list", "update", "delete", "deletecollection"] + - apiGroups: ["autoscaling"] + resources: ["horizontalpodautoscalers"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"] + - apiGroups: ["cert-manager.io"] + resources: ["issuers"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["list", "update", "delete", "deletecollection"] + - apiGroups: ["dataprotection.kubeblocks.io"] + resources: ["backups", "backupschedules"] + verbs: ["get", "list", "delete", "deletecollection"] + - apiGroups: ["apps.kubeblocks.io"] + resources: ["clusters"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["objectstorage.sealos.io"] + resources: ["objectstorageusers"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["app.sealos.io"] + resources: ["instances", "apps"] + verbs: ["delete", "deletecollection"] + - apiGroups: ["devbox.sealos.io"] + resources: ["devboxes"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["devbox.sealos.io"] + resources: ["devboxes", "devboxreleases"] + verbs: ["delete", "deletecollection"] + - apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - get + - list + - watch + - apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "account-controller.fullname" . }}-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "account-controller.fullname" . }}-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "account-controller.fullname" . }}-leader-election-rolebinding + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "account-controller.fullname" . }}-leader-election-role +subjects: + - kind: ServiceAccount + name: {{ include "account-controller.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "account-controller.fullname" . }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "account-controller.fullname" . }}-manager-role +subjects: + - kind: ServiceAccount + name: {{ include "account-controller.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "account-controller.fullname" . }}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "account-controller.fullname" . }}-proxy-role +subjects: + - kind: ServiceAccount + name: {{ include "account-controller.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/account/deploy/charts/account-controller/templates/secret.yaml b/controllers/account/deploy/charts/account-controller/templates/secret.yaml new file mode 100644 index 000000000000..1343e8391585 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.accountEnv.accountAdminJwtSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.adminJwtSecretName }} + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +type: Opaque +stringData: + ACCOUNT_ADMIN_JWT_SECRET: {{ .Values.accountEnv.accountAdminJwtSecret | quote }} +{{- end }} diff --git a/controllers/account/deploy/charts/account-controller/templates/service.yaml b/controllers/account/deploy/charts/account-controller/templates/service.yaml new file mode 100644 index 000000000000..3ff8a12921c9 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/service.yaml @@ -0,0 +1,37 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "account-controller.fullname" . }}-controller-manager-metrics-service + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +spec: + ports: + - name: https + port: {{ .Values.service.metrics.port }} + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +{{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "account-controller.fullname" . }}-webhook-service + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} +spec: + ports: + - port: {{ .Values.service.webhook.port }} + protocol: TCP + targetPort: 9443 + name: webhook + - port: 9444 + protocol: TCP + targetPort: 9444 + name: account + selector: + control-plane: controller-manager diff --git a/controllers/account/deploy/charts/account-controller/templates/serviceaccount.yaml b/controllers/account/deploy/charts/account-controller/templates/serviceaccount.yaml new file mode 100644 index 000000000000..270639069d61 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "account-controller.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "account-controller.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/controllers/account/deploy/charts/account-controller/templates/webhook.yaml b/controllers/account/deploy/charts/account-controller/templates/webhook.yaml new file mode 100644 index 000000000000..d4da305b8a10 --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/templates/webhook.yaml @@ -0,0 +1,42 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "account-controller.fullname" . }}-serving-cert + name: {{ include "account-controller.fullname" . }}-validating-webhook-configuration +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "account-controller.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-v1-sealos-cloud + timeoutSeconds: 10 + failurePolicy: Ignore + name: debt.sealos.io + namespaceSelector: + matchExpressions: + - key: user.sealos.io/owner + operator: Exists + rules: + - apiGroups: + - '*' + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + - DELETE + resources: + - '*' + - apiGroups: + - account.sealos.io + apiVersions: + - v1 + operations: + - UPDATE + resources: + - payments/status + scope: '*' + sideEffects: None diff --git a/controllers/account/deploy/charts/account-controller/values.yaml b/controllers/account/deploy/charts/account-controller/values.yaml new file mode 100644 index 000000000000..1123b08d0adc --- /dev/null +++ b/controllers/account/deploy/charts/account-controller/values.yaml @@ -0,0 +1,117 @@ +# Default values for account controller helm chart. + +image: ghcr.io/labring/sealos-account-controller:latest +imagePullPolicy: Always + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "account" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "account-controller-manager" + +adminJwtSecretName: "account-admin-jwt" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + +service: + metrics: + port: 8443 + webhook: + port: 443 + +env: + accountNamespace: sealos-system + userNamespace: user-system + whitelist: "licenses.License.license.sealos.io/v1,notifications.Notification.notification.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,billinginfoqueries.BillingInfoQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1" + +# ============================================================================ +# Auto-configured values (from sealos-system/sealos-config ConfigMap) +# ============================================================================ +# The following accountEnv values are automatically fetched from the +# sealos-system/sealos-config ConfigMap by the entrypoint script and will +# override any values set here. These are provided as reference defaults. +# +# To override these auto-configured values, use HELM_OPTIONS or modify the +# sealos-config ConfigMap directly. +# ============================================================================ + +accountEnv: + # Basic cloud configuration (auto-configured from sealos-config) + cloudDomain: "cloud.sealos.io" # Auto-fetched from sealos-config.cloudDomain + cloudPort: "" # Auto-fetched from sealos-config.cloudPort + localRegion: "" # Auto-fetched from sealos-config.regionUID + + # Database connections (auto-configured from sealos-config) + mongoURI: "mongodb://mongo:27017/resources" # Auto-fetched from sealos-config.databaseMongodbURI + globalCockroachURI: "" # Auto-fetched from sealos-config.databaseGlobalCockroachdbURI + localCockroachURI: "" # Auto-fetched from sealos-config.databaseLocalCockroachdbURI + trafficMongoURI: "" # Auto-fetched from sealos-config.databaseMongodbURI + + # Authentication secrets (auto-configured from sealos-config) + accountApiJwtSecret: "secret" # Auto-fetched from sealos-config.jwtInternal + accountAdminJwtSecret: "" # Generated by account-controller-entrypoint.sh + + # Maximum historical billing window replayed from an existing checkpoint. + billingMaxCatchupDuration: "24h" + + # Kubernetes API whitelist (auto-generated from cloudDomain) + whitelistKubernetesHosts: "" # Auto-generated: https://${cloudDomain}:6443 + +# End of auto-configured values +# ============================================================================ + +# ConfigMap 合并策略: preserve 保留旧值, overwrite 覆盖旧值 +accountEnvMergeStrategy: "overwrite" + +paymentSecretName: payment-secret + +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +startupProbe: + httpGet: + path: /startupz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 60 + +metrics: + ## To enable this parameter, it needs to be enabled in the subsequent code; otherwise, it cannot be started + enabled: false + certPath: /tmp/k8s-metrics-server/metrics-certs + secretName: metrics-server-cert + +volumes: [] +volumeMounts: [] + +nodeSelector: {} + +tolerations: [] + +# If no affinity is provided a default podAntiAffinity is used to spread controllers. +affinity: {} diff --git a/controllers/account/go.mod b/controllers/account/go.mod new file mode 100644 index 000000000000..7ec91ce8c523 --- /dev/null +++ b/controllers/account/go.mod @@ -0,0 +1,154 @@ +module github.com/labring/sealos/controllers/account + +go 1.24.0 + +replace ( + k8s.io/api => k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.32.1 + k8s.io/client-go => k8s.io/client-go v0.32.1 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 + github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 + github.com/alibabacloud-go/tea v1.2.1 + github.com/alibabacloud-go/tea-utils/v2 v2.0.4 + github.com/astaxie/beego v1.12.3 + github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df + github.com/go-logr/logr v1.4.2 + github.com/google/uuid v1.6.0 + github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b + github.com/labring/sealos/controllers/user v0.0.0 + github.com/matoous/go-nanoid/v2 v2.0.0 + github.com/minio/madmin-go/v3 v3.0.35 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.36.1 + github.com/sirupsen/logrus v1.9.3 + github.com/volcengine/volc-sdk-golang v1.0.159 + go.mongodb.org/mongo-driver v1.12.1 + golang.org/x/time v0.7.0 + gorm.io/gorm v1.25.5 + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v12.0.0+incompatible + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect + github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/openapi-util v0.1.0 // indirect + github.com/alibabacloud-go/tea-utils v1.3.1 // indirect + github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/aliyun/credentials-go v1.3.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.5.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.64 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.6.6 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/prom2json v1.3.3 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/secure-io/sio-go v0.3.1 // indirect + github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect + github.com/shirou/gopsutil/v3 v3.23.6 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/smartwalle/alipay/v3 v3.2.25 // indirect + github.com/smartwalle/ncrypto v1.0.4 // indirect + github.com/smartwalle/ngx v1.0.9 // indirect + github.com/smartwalle/nsign v1.0.9 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stripe/stripe-go/v74 v74.30.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/tjfoc/gmsm v1.3.2 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/wechatpay-apiv3/wechatpay-go v0.2.17 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.4 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + github.com/labring/sealos/controllers/pkg => ../pkg + github.com/labring/sealos/controllers/user => ../../controllers/user +) diff --git a/controllers/account/go.sum b/controllers/account/go.sum new file mode 100644 index 000000000000..a440b25538d0 --- /dev/null +++ b/controllers/account/go.sum @@ -0,0 +1,1187 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 h1:yyolbgHfV2Tp91vMjO/CF5aOxKG+UgdVAeUoloEQI3E= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 h1:UTl97mt2qfavxveqCkaVg4tKaZUPzA9RKbFIRaIdtdg= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6/go.mod h1:UWpcGrWwTbES9QW7OQ7xDffukMJ/l7lzioixIz8+lgY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.1 h1:rFF1LnrAdhaiPmKwH5xwYOKlMh66CqRwPUTzIK74ask= +github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= +github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-utils/v2 v2.0.4 h1:SoFgjJuO7pze88j9RBJNbKb7AgTS52O+J5ITxc00lCs= +github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= +github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ= +github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= +github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0= +github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= +github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/madmin-go/v3 v3.0.35 h1:cCo5ZZpHA+rlBQbsAcwFwiuh/uHJmjVoDDx1G4+zaho= +github.com/minio/madmin-go/v3 v3.0.35/go.mod h1:4QN2NftLSV7MdlT50dkrenOMmNVHluxTvlqJou3hte8= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q= +github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= +github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= +github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= +github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= +github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= +github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08= +github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q= +github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= +github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= +github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= +github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= +github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= +github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/volcengine/volc-sdk-golang v1.0.159 h1:0EzJ49M+YJbaGiGxhmD2BvPbsZOgt6RWlgOqKP6IaoQ= +github.com/volcengine/volc-sdk-golang v1.0.159/go.mod h1:iqWIQk0pkcDKEYpIG4vkocgHpeiAabfAK9g0Ob7lSxE= +github.com/wechatpay-apiv3/wechatpay-go v0.2.17 h1:i4YJA/6BqAbi2YfyPZBjpeEeO/+oa4UbKP4gSTRhhQg= +github.com/wechatpay-apiv3/wechatpay-go v0.2.17/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/account/hack/boilerplate.go.txt b/controllers/account/hack/boilerplate.go.txt new file mode 100644 index 000000000000..65b862271836 --- /dev/null +++ b/controllers/account/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +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. +*/ \ No newline at end of file diff --git a/controllers/account/main.go b/controllers/account/main.go new file mode 100644 index 000000000000..21362db324e0 --- /dev/null +++ b/controllers/account/main.go @@ -0,0 +1,590 @@ +/* +Copyright 2023. + +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 ( + "context" + "errors" + "flag" + "net/http" + "os" + "strconv" + "time" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + "github.com/labring/sealos/controllers/account/controllers" + "github.com/labring/sealos/controllers/account/controllers/cache" + "github.com/labring/sealos/controllers/account/controllers/utils" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/database/mongo" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/pkg/utils/maps" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlconfig "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +type readinessRunnable struct { + manager.RunnableFunc +} + +func (readinessRunnable) NeedLeaderElection() bool { + return false +} + +var _ manager.LeaderElectionRunnable = readinessRunnable{} + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(accountv1.AddToScheme(scheme)) + utilruntime.Must(userv1.AddToScheme(scheme)) + utilruntime.Must(notificationv1.AddToScheme(scheme)) + // utilruntime.Must(kbv1alpha1.SchemeBuilder.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func beginStartupStage(name string) func(error) { + startedAt := time.Now() + setupLog.Info("startup stage started", "stage", name) + return func(err error) { + duration := time.Since(startedAt) + if err != nil { + setupLog.Error(err, + "startup stage failed", + "stage", + name, + "duration_ms", + duration.Milliseconds(), + ) + return + } + setupLog.Info( + "startup stage completed", + "stage", + name, + "duration_ms", + duration.Milliseconds(), + ) + } +} + +func measureStartupStage(name string, fn func() error) error { + finish := beginStartupStage(name) + err := fn() + finish(err) + return err +} + +func main() { + var ( + metricsAddr string + enableLeaderElection bool + probeAddr string + concurrent int + deleteResourceConcurrent int + deleteBackupConcurrent int + development bool + rateLimiterOptions = &utils.LimiterOptions{} + leaseDuration time.Duration + renewDeadline time.Duration + retryPeriod time.Duration + ) + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + flag.BoolVar(&development, "development", false, "Enable development mode.") + flag.BoolVar(&enableLeaderElection, "leader-elect", true, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.IntVar(&concurrent, "concurrent", 100, "The number of concurrent cluster reconciles.") + flag.IntVar( + &deleteResourceConcurrent, + "delete-resource-concurrent", + 3, + "The number of concurrent DeleteUserResource calls.", + ) + flag.IntVar( + &deleteBackupConcurrent, + "delete-backup-concurrent", + 30, + "The maximum number of concurrent backup deletions.", + ) + flag.DurationVar( + &leaseDuration, + "leader-elect-lease-duration", + 60*time.Second, + "Duration that non-leader candidates will wait to force acquire leadership.", + ) + flag.DurationVar( + &renewDeadline, + "leader-elect-renew-deadline", + 40*time.Second, + "Duration the acting master will retry refreshing leadership before giving up.", + ) + flag.DurationVar( + &retryPeriod, + "leader-elect-retry-period", + 5*time.Second, + "Duration the LeaderElector clients should wait between tries of actions.", + ) + opts := zap.Options{ + Development: development, + } + rateLimiterOptions.BindFlags(flag.CommandLine) + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + ctx := ctrl.SetupSignalHandler() + probeState := &probeState{} + if err := startProbeServer(ctx, probeAddr, probeState); err != nil { + setupLog.Error(err, "unable to start probe server") + os.Exit(1) + } + // local test env + // err := godotenv.Load() + // if err != nil { + // setupLog.Error(err, "unable to load .env file") + //} + + var mgr ctrl.Manager + err := measureStartupStage("create manager", func() error { + var stageErr error + mgr, stageErr = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Cache: cache.Options(), + Client: client.Options{ + Cache: &client.CacheOptions{DisableFor: cache.UncachedObjects()}, + }, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + // Probes are served by the standalone server started before the expensive + // dependency and controller initialization below. + HealthProbeBindAddress: "0", + LeaderElection: enableLeaderElection, + LeaderElectionID: "a63686c3.sealos.io", + LeaseDuration: &leaseDuration, + RenewDeadline: &renewDeadline, + RetryPeriod: &retryPeriod, + Controller: ctrlconfig.Controller{ + UsePriorityQueue: ptr.To(true), + }, + }) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + var watchClient client.WithWatch + err = measureStartupStage("create Kubernetes watch client", func() error { + var stageErr error + watchClient, stageErr = client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: mgr.GetScheme(), + Mapper: mgr.GetRESTMapper(), + }) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to get watch client") + os.Exit(1) + } + rateOpts := controller.Options{ + MaxConcurrentReconciles: concurrent, + RateLimiter: utils.GetRateLimiter(rateLimiterOptions), + } + dbCtx := context.Background() + var dbClient database.Interface + err = measureStartupStage("connect account MongoDB", func() error { + var stageErr error + dbClient, stageErr = mongo.NewMongoInterface(dbCtx, os.Getenv(database.MongoURI)) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to connect to mongo") + os.Exit(1) + } + defer func() { + err := dbClient.Disconnect(dbCtx) + if err != nil { + setupLog.Error(err, "unable to disconnect from mongo") + } + }() + var cvmDBClient database.Interface + cvmURI := os.Getenv(database.CVMMongoURI) + if cvmURI != "" { + err = measureStartupStage("connect CVM MongoDB", func() error { + var stageErr error + cvmDBClient, stageErr = mongo.NewMongoInterface(dbCtx, cvmURI) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to connect to mongo") + os.Exit(1) + } + } + defer func() { + if cvmDBClient != nil { + err := cvmDBClient.Disconnect(dbCtx) + if err != nil { + setupLog.Error(err, "unable to disconnect from mongo") + } + } + }() + var v2Account *cockroach.Cockroach + err = measureStartupStage("connect CockroachDB", func() error { + var stageErr error + v2Account, stageErr = cockroach.NewCockRoach( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to connect to cockroach") + os.Exit(1) + } + defer func() { + err := v2Account.Close() + if err != nil { + setupLog.Error(err, "unable to disconnect from cockroach") + } + }() + err = measureStartupStage("initialize region environment", func() error { + return database.InitRegionEnv( + v2Account.GetGlobalDB(), + v2Account.GetLocalRegion().Domain, + ) + }) + if err != nil { + setupLog.Error(err, "unable to init region env") + os.Exit(1) + } + if os.Getenv(cockroach.EnvBaseBalance) != "" { + balance, err := strconv.ParseInt(os.Getenv(cockroach.EnvBaseBalance), 10, 64) + if err == nil { + v2Account.ZeroAccount.Balance = balance + } + } + skipExpiredUserTimeDuration := time.Hour * 24 * 2 + if os.Getenv("SKIP_EXPIRED_USER_TIME") != "" { + skipExpiredUserTimeDuration, err = time.ParseDuration(os.Getenv("SKIP_EXPIRED_USER_TIME")) + if err != nil { + setupLog.Error(err, "unable to parse skip expired user time") + os.Exit(1) + } + } + setupLog.Info("skip expired user time", "duration", skipExpiredUserTimeDuration) + accountReconciler := &controllers.AccountReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + DBClient: dbClient, + AccountV2: v2Account, + CVMDBClient: cvmDBClient, + SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration, + } + finishRechargeConfig := beginStartupStage("parse recharge config") + activities, discountSteps, discountRatios, err := controllers.RawParseRechargeConfig() + finishRechargeConfig(err) + if err != nil { + setupLog.Error(err, "parse recharge config failed") + } else { + setupLog.Info( + "parse recharge config success", + "activities", + activities, + "discountSteps", + discountSteps, + "discountRatios", + discountRatios, + ) + accountReconciler.Activities = activities + accountReconciler.DefaultDiscount = types.RechargeDiscount{ + DiscountRates: discountRatios, + DiscountSteps: discountSteps, + } + } + setupManagerError := func(err error, controller string) { + setupLog.Error(err, "unable to create controller", "controller", controller) + os.Exit(1) + } + if err = measureStartupStage("setup account controller", func() error { + return accountReconciler.SetupWithManager(mgr, rateOpts) + }); err != nil { + setupManagerError(err, "Account") + } + debtUserMap := maps.NewConcurrentMap() + debtController := &controllers.DebtReconciler{ + AccountReconciler: accountReconciler, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + AccountV2: v2Account, + DebtUserMap: debtUserMap, + InitUserAccountFunc: accountReconciler.InitUserAccountFunc, + SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration, + } + finishDebtInit := beginStartupStage("initialize debt controller") + debtController.Init() + finishDebtInit(nil) + + // Setup OperationRequest monitor controller to trigger debt status refresh on owner transfers + operationRequestMonitor := &controllers.OperationRequestMonitorReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + if err = measureStartupStage("setup operation request monitor", func() error { + return operationRequestMonitor.SetupWithManager(mgr) + }); err != nil { + setupManagerError(err, "OperationRequestMonitor") + } + + // if err = (&controllers.DebtReconciler{ + // AccountReconciler: accountReconciler, + // Client: mgr.GetClient(), + // Scheme: mgr.GetScheme(), + // AccountV2: v2Account, + // DebtUserMap: debtUserMap, + // InitUserAccountFunc: accountReconciler.InitUserAccountFunc, + // SkipExpiredUserTimeDuration: skipExpiredUserTimeDuration, + // }).SetupWithManager(mgr, rateOpts); err != nil { + // setupManagerError(err, "Debt") + //} + + if err = measureStartupStage("setup cache indexes", func() error { + return cache.SetupCache(mgr) + }); err != nil { + setupLog.Error(err, "unable to cache controller") + os.Exit(1) + } + _true := "true" + if os.Getenv("DISABLE_WEBHOOKS") == _true { + setupLog.Info("disable all webhooks") + } else { + mgr.GetWebhookServer(). + Register("/validate-v1-sealos-cloud", &webhook.Admission{Handler: &accountv1.DebtValidate{Client: mgr.GetClient(), AccountV2: v2Account, TTLUserMap: maps.New[*types.UsableBalanceWithCredits](env.GetIntEnvWithDefault("DEBT_WEBHOOK_CACHE_USER_TTL", 15))}}) + // Start HTTP server for property reload handler (without TLS) + jwtSecret := os.Getenv(controllers.EnvJwtSecret) + adminJwtSecret := os.Getenv(controllers.EnvAdminJwtSecret) + reloadHandler := &controllers.PropertyReloadHandler{ + AccountReconciler: accountReconciler, + DBClient: dbClient, + JwtSecret: jwtSecret, + AdminJwtSecret: adminJwtSecret, + } + go func() { + setupLog.Info("starting property reload HTTP server", "port", 9444) + server := &http.Server{ + Addr: ":9444", + Handler: reloadHandler, + ReadHeaderTimeout: 10 * time.Second, + } + if err := server.ListenAndServe(); err != nil { + setupLog.Error(err, "failed to start property reload HTTP server") + } + }() + } + + err = measureStartupStage( + "load property types from MongoDB", + dbClient.InitDefaultPropertyTypeLS, + ) + if err != nil { + setupLog.Error(err, "unable to get property type") + os.Exit(1) + } + billingReconciler := controllers.BillingReconciler{ + DBClient: dbClient, + Properties: resources.DefaultPropertyTypeLS, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + AccountV2: v2Account, + DebtUserMap: debtUserMap, + } + if err = measureStartupStage( + "initialize billing reconciler and indexes", + billingReconciler.Init, + ); err != nil { + setupLog.Error(err, "unable to init billing reconciler") + os.Exit(1) + } + billingTaskRunner := &controllers.BillingTaskRunner{ + BillingReconciler: &billingReconciler, + } + if err := measureStartupStage("add billing task runner", func() error { + return mgr.Add(billingTaskRunner) + }); err != nil { + setupLog.Error(err, "unable to add billing task runner") + os.Exit(1) + } + if env.GetEnvWithDefault("SUPPORT_DEBT", _true) == _true { + if err := measureStartupStage("add debt controller", func() error { + return mgr.Add(debtController) + }); err != nil { + setupLog.Error(err, "unable to add debt controller") + os.Exit(1) + } + } + + if err = measureStartupStage("setup pod controller", func() error { + return (&controllers.PodReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr) + }); err != nil { + setupManagerError(err, "Pod") + } + if err = measureStartupStage("setup namespace controller", func() error { + return (&controllers.NamespaceReconciler{ + Client: watchClient, + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, rateOpts, deleteResourceConcurrent, deleteBackupConcurrent) + }); err != nil { + setupManagerError(err, "Namespace") + } + + if err = measureStartupStage("setup payment controller", func() error { + return (&controllers.PaymentReconciler{ + Account: accountReconciler, + DebtReconciler: debtController, + WatchClient: watchClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr) + }); err != nil { + setupManagerError(err, "Payment") + } + var trafficDBClient database.Interface + err = measureStartupStage("connect traffic MongoDB", func() error { + var stageErr error + trafficDBClient, stageErr = mongo.NewMongoInterface( + dbCtx, + os.Getenv(database.TrafficMongoURI), + ) + return stageErr + }) + if err != nil { + setupLog.Error(err, "unable to connect to traffic mongo") + os.Exit(1) + } + workspaceTrafficProcessor := controllers.NewWorkspaceTrafficController( + accountReconciler, + trafficDBClient, + ) + // workspaceSubscriptionProcessor, err := controllers.NewWorkspaceSubscriptionProcessor(accountReconciler, workspaceTrafficProcessor) + // if err != nil { + // setupLog.Error(err, "unable to create workspace subscription processor") + // os.Exit(1) + //} + workspaceSubDebtProcessor := controllers.NewWorkspaceSubscriptionDebtProcessor( + accountReconciler, + ) + go workspaceTrafficProcessor.ProcessTrafficWithTimeRange() + // workspaceSubscriptionProcessor.Start(ctx) + workspaceSubDebtProcessor.Start(ctx) + + //+kubebuilder:scaffold:builder + + if err := measureStartupStage("register readiness marker", func() error { + return mgr.Add(readinessRunnable{ + RunnableFunc: manager.RunnableFunc(func(ctx context.Context) error { + finishCacheSync := beginStartupStage("manager cache sync") + if !mgr.GetCache().WaitForCacheSync(ctx) { + cacheSyncErr := ctx.Err() + if cacheSyncErr == nil { + cacheSyncErr = errors.New("cache synchronization failed") + } + finishCacheSync(cacheSyncErr) + return nil + } + finishCacheSync(nil) + probeState.markReady() + <-ctx.Done() + return nil + }), + }) + }); err != nil { + setupLog.Error(err, "unable to set up readiness marker") + os.Exit(1) + } + + if cvmDBClient != nil { + cvmTaskRunner := &controllers.CVMTaskRunner{ + DBClient: cvmDBClient, + Logger: ctrl.Log.WithName("CVMTaskRunner"), + AccountReconciler: accountReconciler, + } + if err := measureStartupStage("add CVM task runner", func() error { + return mgr.Add(cvmTaskRunner) + }); err != nil { + setupLog.Error(err, "unable to add cvm task runner") + os.Exit(1) + } + } + // go func() { + // now := time.Now() + // nextHour := now.Truncate(time.Hour).Add(time.Hour) + // time.Sleep(nextHour.Sub(now)) + // + // ticker := time.NewTicker(time.Hour) + // defer ticker.Stop() + // for { + // setupLog.Info("start billing reconcile", "time", time.Now().Format(time.RFC3339)) + // if err := billingReconciler.ExecuteBillingTask(); err != nil { + // setupLog.Error(err, "failed to execute billing task") + // } + // <-ticker.C + // } + // }() + + probeState.markStartupReady() + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "fail to run manager") + os.Exit(1) + } +} diff --git a/controllers/account/probe.go b/controllers/account/probe.go new file mode 100644 index 000000000000..02ef6ed4e706 --- /dev/null +++ b/controllers/account/probe.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "errors" + "net" + "net/http" + "sync/atomic" + "time" +) + +const ( + probeHealthPath = "/healthz" + probeStartupPath = "/startupz" + probeReadyPath = "/readyz" +) + +type probeState struct { + startup atomic.Bool + ready atomic.Bool +} + +func (s *probeState) markStartupReady() { + s.startup.Store(true) +} + +func (s *probeState) markReady() { + s.ready.Store(true) +} + +func newProbeHandler(state *probeState) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc(probeHealthPath, func(w http.ResponseWriter, _ *http.Request) { + writeProbeResponse(w, true) + }) + mux.HandleFunc(probeStartupPath, func(w http.ResponseWriter, _ *http.Request) { + writeProbeResponse(w, state.startup.Load()) + }) + mux.HandleFunc(probeReadyPath, func(w http.ResponseWriter, _ *http.Request) { + writeProbeResponse(w, state.ready.Load()) + }) + return mux +} + +func writeProbeResponse(w http.ResponseWriter, ready bool) { + if !ready { + http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) + return + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok\n")) +} + +func startProbeServer(ctx context.Context, addr string, state *probeState) error { + if addr == "" || addr == "0" { + return nil + } + + listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) + if err != nil { + return err + } + + server := &http.Server{ + Handler: newProbeHandler(state), + ReadHeaderTimeout: 5 * time.Second, + } + + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + setupLog.Error(err, "unable to shut down probe server") + } + }() + + go func() { + if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + setupLog.Error(err, "probe server stopped unexpectedly") + } + }() + + return nil +} diff --git a/controllers/account/probe_test.go b/controllers/account/probe_test.go new file mode 100644 index 000000000000..150e75dfe28e --- /dev/null +++ b/controllers/account/probe_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +func TestProbeHandler(t *testing.T) { + state := &probeState{} + handler := newProbeHandler(state) + + tests := []struct { + name string + path string + want int + }{ + {name: "health before initialization", path: probeHealthPath, want: http.StatusOK}, + { + name: "startup before initialization", + path: probeStartupPath, + want: http.StatusServiceUnavailable, + }, + { + name: "ready before cache sync", + path: probeReadyPath, + want: http.StatusServiceUnavailable, + }, + } + assertProbeStatuses(t, handler, tests) + + state.markStartupReady() + assertProbeStatuses(t, handler, []struct { + name string + path string + want int + }{ + {name: "health after initialization", path: probeHealthPath, want: http.StatusOK}, + {name: "startup after initialization", path: probeStartupPath, want: http.StatusOK}, + { + name: "ready before cache sync", + path: probeReadyPath, + want: http.StatusServiceUnavailable, + }, + }) + + state.markReady() + assertProbeStatuses(t, handler, []struct { + name string + path string + want int + }{ + {name: "health when ready", path: probeHealthPath, want: http.StatusOK}, + {name: "startup when ready", path: probeStartupPath, want: http.StatusOK}, + {name: "ready after cache sync", path: probeReadyPath, want: http.StatusOK}, + }) +} + +func assertProbeStatuses(t *testing.T, handler http.Handler, tests []struct { + name string + path string + want int +}, +) { + t.Helper() + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := httptest.NewRequestWithContext( + context.Background(), + http.MethodGet, + test.path, + nil, + ) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + if response.Code != test.want { + t.Fatalf("GET %s returned status %d, want %d", test.path, response.Code, test.want) + } + }) + } +} diff --git a/controllers/app/.dockerignore b/controllers/app/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/app/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/app/.gitignore b/controllers/app/.gitignore new file mode 100644 index 000000000000..c1c593e67e17 --- /dev/null +++ b/controllers/app/.gitignore @@ -0,0 +1,27 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/controllers/app/Dockerfile b/controllers/app/Dockerfile new file mode 100644 index 000000000000..d6a0401ebb41 --- /dev/null +++ b/controllers/app/Dockerfile @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot + +ARG TARGETARCH +WORKDIR / +USER 65532:65532 + +ENV USER_NAMESPACE=user-system +ENV APP_NAMESPACE=app-system + +COPY bin/controller-app-$TARGETARCH /manager + +ENTRYPOINT ["/manager"] \ No newline at end of file diff --git a/controllers/app/Makefile b/controllers/app/Makefile new file mode 100644 index 000000000000..0220c5bf5fab --- /dev/null +++ b/controllers/app/Makefile @@ -0,0 +1,172 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-app-controller:latest +TARGETARCH ?= amd64 + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.28.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +build: ## Build manager binary. + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: test build ## Build docker image with the manager. + mv bin/manager bin/controller-app-${TARGETARCH} + chmod +x bin/controller-app-${TARGETARCH} + $(CONTAINER_TOOL) build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: pre-deploy +pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.1.1 +CONTROLLER_TOOLS_VERSION ?= v0.13.0 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/app/PROJECT b/controllers/app/PROJECT new file mode 100644 index 000000000000..758b07c48f19 --- /dev/null +++ b/controllers/app/PROJECT @@ -0,0 +1,27 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: sealos.io +layout: +- go.kubebuilder.io/v4 +projectName: app +repo: github.com/labring/sealos/controllers/app +resources: +- api: + crdVersion: v1 + namespaced: true + domain: sealos.io + group: app + kind: App + path: github.com/labring/sealos/controllers/app/api/v1 + version: v1 +- api: + crdVersion: v1 + namespaced: true + domain: sealos.io + group: app + kind: Template + path: github.com/labring/sealos/controllers/app/api/v1 + version: v1 +version: "3" diff --git a/controllers/app/README.md b/controllers/app/README.md new file mode 100644 index 000000000000..686d12dee098 --- /dev/null +++ b/controllers/app/README.md @@ -0,0 +1,94 @@ +# app +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -k config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/app:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/app:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller from the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), +which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2023. + +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. + diff --git a/controllers/app/api/v1/app_types.go b/controllers/app/api/v1/app_types.go new file mode 100644 index 000000000000..ff30ec276eb3 --- /dev/null +++ b/controllers/app/api/v1/app_types.go @@ -0,0 +1,96 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Data struct { + URL string `json:"url,omitempty"` + Desc string `json:"desc,omitempty"` +} + +type MenuData struct { + Name string `json:"name,omitempty"` + Link string `json:"link,omitempty"` +} + +type DisplayType string + +// data types +const ( + DisplayNormal DisplayType = "normal" + DisplayMore DisplayType = "more" + DisplayHidden DisplayType = "hidden" +) + +// AppMeta Base Information +type AppMeta struct { + Name string `json:"name,omitempty"` + Icon string `json:"icon,omitempty"` + Type string `json:"type,omitempty"` + + //+kubebuilder:validation:Enum={ normal, more, hidden, } + //+kubebuilder:validation:Optional + DisplayType DisplayType `json:"displayType,omitempty"` + + Data Data `json:"data,omitempty"` + MenuData []MenuData `json:"menuData,omitempty"` +} + +// AppSpec defines the desired state of App +type AppSpec struct { + AppMeta `json:",inline"` + + //+kubebuilder:validation:Optional + Position float64 `json:"position,omitempty"` + + //+kubebuilder:validation:Optional + Version string `json:"version,omitempty"` + + //+kubebuilder:validation:Optional + I18N *map[string]AppMeta `json:"i18n,omitempty"` +} + +// AppStatus defines the observed state of App +type AppStatus struct{} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// App is the Schema for the apps API +type App struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AppSpec `json:"spec,omitempty"` + Status AppStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AppList contains a list of App +type AppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []App `json:"items"` +} + +func init() { + SchemeBuilder.Register(&App{}, &AppList{}) +} diff --git a/controllers/app/api/v1/groupversion_info.go b/controllers/app/api/v1/groupversion_info.go new file mode 100644 index 000000000000..0906ca890ec7 --- /dev/null +++ b/controllers/app/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +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 v1 contains API Schema definitions for the app v1 API group +// +kubebuilder:object:generate=true +// +groupName=app.sealos.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "app.sealos.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/controllers/app/api/v1/instance_types.go b/controllers/app/api/v1/instance_types.go new file mode 100644 index 000000000000..9c592ad9ba2c --- /dev/null +++ b/controllers/app/api/v1/instance_types.go @@ -0,0 +1,55 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InstanceSpec defines the desired state of Instance +// +kubebuilder:validation:XValidation:rule="'app_name' in self.defaults",message="defaults must have app_name key" +type InstanceSpec struct { + TemplateData `json:",inline"` +} + +// InstanceStatus defines the observed state of Instance +type InstanceStatus struct{} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Instance is the Schema for the instances API +type Instance struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InstanceSpec `json:"spec,omitempty"` + Status InstanceStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// InstanceList contains a list of Instance +type InstanceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Instance `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Instance{}, &InstanceList{}) +} diff --git a/controllers/app/api/v1/template_types.go b/controllers/app/api/v1/template_types.go new file mode 100644 index 000000000000..b02b1f4685c4 --- /dev/null +++ b/controllers/app/api/v1/template_types.go @@ -0,0 +1,117 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type I18nData struct { + Title string `json:"title,omitempty"` + URL string `json:"url,omitempty"` + GitRepo string `json:"gitRepo,omitempty"` + Description string `json:"description,omitempty"` + Readme string `json:"readme,omitempty"` + Icon string `json:"icon,omitempty"` +} + +type TemplateType string + +const ( + TemplateTypeInline string = "inline" +) + +type DefaultDataType string + +const ( + DefaultDataTypeString = "string" + DefaultDataTypeNumber = "number" +) + +type DefaultData struct { + Type DefaultDataType `json:"type"` + Value string `json:"value"` +} + +type Defaults map[string]DefaultData + +type InputDataType string + +const ( + InputDataTypeString = "string" + InputDataTypeNumber = "number" +) + +type InputData struct { + Description string `json:"description,omitempty"` + Type InputDataType `json:"type"` + Default string `json:"default,omitempty"` + Required bool `json:"required,omitempty"` +} + +type Inputs map[string]InputData + +type TemplateData struct { + Title string `json:"title"` + URL string `json:"url,omitempty"` + GitRepo string `json:"gitRepo,omitempty"` + Author string `json:"author,omitempty"` + Description string `json:"description,omitempty"` + Readme string `json:"readme,omitempty"` + Icon string `json:"icon,omitempty"` + TemplateType TemplateType `json:"templateType"` + Locale string `json:"locale,omitempty"` + I18n map[string]I18nData `json:"i18n,omitempty"` + Draft bool `json:"draft,omitempty"` + Categories []string `json:"categories,omitempty"` + Defaults Defaults `json:"defaults,omitempty"` + Inputs Inputs `json:"inputs,omitempty"` +} + +// TemplateSpec defines the desired state of Template +// +kubebuilder:validation:XValidation:rule="'app_name' in self.defaults",message="defaults must have app_name key" +type TemplateSpec struct { + TemplateData `json:",inline"` +} + +// TemplateStatus defines the observed state of Template +type TemplateStatus struct{} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Template is the Schema for the templates API +type Template struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TemplateSpec `json:"spec,omitempty"` + Status TemplateStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// TemplateList contains a list of Template +type TemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Template `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Template{}, &TemplateList{}) +} diff --git a/controllers/app/api/v1/zz_generated.deepcopy.go b/controllers/app/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..57c6923da35a --- /dev/null +++ b/controllers/app/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,485 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +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 v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *App) DeepCopyInto(out *App) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new App. +func (in *App) DeepCopy() *App { + if in == nil { + return nil + } + out := new(App) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *App) 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 *AppList) DeepCopyInto(out *AppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]App, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppList. +func (in *AppList) DeepCopy() *AppList { + if in == nil { + return nil + } + out := new(AppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AppList) 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 *AppMeta) DeepCopyInto(out *AppMeta) { + *out = *in + out.Data = in.Data + if in.MenuData != nil { + in, out := &in.MenuData, &out.MenuData + *out = make([]MenuData, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppMeta. +func (in *AppMeta) DeepCopy() *AppMeta { + if in == nil { + return nil + } + out := new(AppMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppSpec) DeepCopyInto(out *AppSpec) { + *out = *in + in.AppMeta.DeepCopyInto(&out.AppMeta) + if in.I18N != nil { + in, out := &in.I18N, &out.I18N + *out = new(map[string]AppMeta) + if **in != nil { + in, out := *in, *out + *out = make(map[string]AppMeta, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppSpec. +func (in *AppSpec) DeepCopy() *AppSpec { + if in == nil { + return nil + } + out := new(AppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppStatus) DeepCopyInto(out *AppStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppStatus. +func (in *AppStatus) DeepCopy() *AppStatus { + if in == nil { + return nil + } + out := new(AppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Data) DeepCopyInto(out *Data) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data. +func (in *Data) DeepCopy() *Data { + if in == nil { + return nil + } + out := new(Data) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultData) DeepCopyInto(out *DefaultData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultData. +func (in *DefaultData) DeepCopy() *DefaultData { + if in == nil { + return nil + } + out := new(DefaultData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Defaults) DeepCopyInto(out *Defaults) { + { + in := &in + *out = make(Defaults, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Defaults. +func (in Defaults) DeepCopy() Defaults { + if in == nil { + return nil + } + out := new(Defaults) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *I18nData) DeepCopyInto(out *I18nData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new I18nData. +func (in *I18nData) DeepCopy() *I18nData { + if in == nil { + return nil + } + out := new(I18nData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InputData) DeepCopyInto(out *InputData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InputData. +func (in *InputData) DeepCopy() *InputData { + if in == nil { + return nil + } + out := new(InputData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Inputs) DeepCopyInto(out *Inputs) { + { + in := &in + *out = make(Inputs, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Inputs. +func (in Inputs) DeepCopy() Inputs { + if in == nil { + return nil + } + out := new(Inputs) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Instance) DeepCopyInto(out *Instance) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instance. +func (in *Instance) DeepCopy() *Instance { + if in == nil { + return nil + } + out := new(Instance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Instance) 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 *InstanceList) DeepCopyInto(out *InstanceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Instance, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceList. +func (in *InstanceList) DeepCopy() *InstanceList { + if in == nil { + return nil + } + out := new(InstanceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InstanceList) 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 *InstanceSpec) DeepCopyInto(out *InstanceSpec) { + *out = *in + in.TemplateData.DeepCopyInto(&out.TemplateData) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceSpec. +func (in *InstanceSpec) DeepCopy() *InstanceSpec { + if in == nil { + return nil + } + out := new(InstanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceStatus) DeepCopyInto(out *InstanceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceStatus. +func (in *InstanceStatus) DeepCopy() *InstanceStatus { + if in == nil { + return nil + } + out := new(InstanceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MenuData) DeepCopyInto(out *MenuData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MenuData. +func (in *MenuData) DeepCopy() *MenuData { + if in == nil { + return nil + } + out := new(MenuData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Template) DeepCopyInto(out *Template) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Template. +func (in *Template) DeepCopy() *Template { + if in == nil { + return nil + } + out := new(Template) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Template) 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 *TemplateData) DeepCopyInto(out *TemplateData) { + *out = *in + if in.I18n != nil { + in, out := &in.I18n, &out.I18n + *out = make(map[string]I18nData, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Categories != nil { + in, out := &in.Categories, &out.Categories + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Defaults != nil { + in, out := &in.Defaults, &out.Defaults + *out = make(Defaults, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Inputs != nil { + in, out := &in.Inputs, &out.Inputs + *out = make(Inputs, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateData. +func (in *TemplateData) DeepCopy() *TemplateData { + if in == nil { + return nil + } + out := new(TemplateData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateList) DeepCopyInto(out *TemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Template, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateList. +func (in *TemplateList) DeepCopy() *TemplateList { + if in == nil { + return nil + } + out := new(TemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TemplateList) 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 *TemplateSpec) DeepCopyInto(out *TemplateSpec) { + *out = *in + in.TemplateData.DeepCopyInto(&out.TemplateData) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpec. +func (in *TemplateSpec) DeepCopy() *TemplateSpec { + if in == nil { + return nil + } + out := new(TemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateStatus) DeepCopyInto(out *TemplateStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateStatus. +func (in *TemplateStatus) DeepCopy() *TemplateStatus { + if in == nil { + return nil + } + out := new(TemplateStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/app/cmd/main.go b/controllers/app/cmd/main.go new file mode 100644 index 000000000000..5bfd6b5b7227 --- /dev/null +++ b/controllers/app/cmd/main.go @@ -0,0 +1,119 @@ +/* +Copyright 2023. + +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 ( + "flag" + "os" + + appv1 "github.com/labring/sealos/controllers/app/api/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(appv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +// Note: Add role here for controllers without real controller go file, with just CRDs. +// +kubebuilder:rbac:groups=app.sealos.io,resources=apps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=app.sealos.io,resources=templates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=app.sealos.io,resources=instances,verbs=get;list;watch;create;update;patch;delete + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "f3e714c5.sealos.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 start manager") + 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) + } +} diff --git a/controllers/app/config/crd/bases/app.sealos.io_apps.yaml b/controllers/app/config/crd/bases/app.sealos.io_apps.yaml new file mode 100644 index 000000000000..7f5444da4ad9 --- /dev/null +++ b/controllers/app/config/crd/bases/app.sealos.io_apps.yaml @@ -0,0 +1,126 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: apps.app.sealos.io +spec: + group: app.sealos.io + names: + kind: App + listKind: AppList + plural: apps + singular: app + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: App is the Schema for the apps API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AppSpec defines the desired state of App + properties: + data: + properties: + desc: + type: string + url: + type: string + type: object + displayType: + enum: + - normal + - more + - hidden + type: string + i18n: + additionalProperties: + description: AppMeta Base Information + properties: + data: + properties: + desc: + type: string + url: + type: string + type: object + displayType: + enum: + - normal + - more + - hidden + type: string + icon: + type: string + menuData: + items: + properties: + link: + type: string + name: + type: string + type: object + type: array + name: + type: string + type: + type: string + type: object + type: object + icon: + type: string + menuData: + items: + properties: + link: + type: string + name: + type: string + type: object + type: array + name: + type: string + position: + format: double + type: number + type: + type: string + version: + type: string + type: object + status: + description: AppStatus defines the observed state of App + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/app/config/crd/bases/app.sealos.io_instances.yaml b/controllers/app/config/crd/bases/app.sealos.io_instances.yaml new file mode 100644 index 000000000000..f680b25f3ba1 --- /dev/null +++ b/controllers/app/config/crd/bases/app.sealos.io_instances.yaml @@ -0,0 +1,133 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: instances.app.sealos.io +spec: + group: app.sealos.io + names: + kind: Instance + listKind: InstanceList + plural: instances + singular: instance + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Instance is the Schema for the instances API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstanceSpec defines the desired state of Instance + properties: + author: + type: string + categories: + items: + type: string + type: array + defaults: + additionalProperties: + properties: + type: + type: string + value: + type: string + required: + - type + - value + type: object + type: object + description: + type: string + draft: + type: boolean + gitRepo: + type: string + i18n: + additionalProperties: + properties: + description: + type: string + gitRepo: + type: string + icon: + type: string + readme: + type: string + title: + type: string + url: + type: string + type: object + type: object + icon: + type: string + inputs: + additionalProperties: + properties: + default: + type: string + description: + type: string + required: + type: boolean + type: + type: string + required: + - type + type: object + type: object + locale: + type: string + readme: + type: string + templateType: + type: string + title: + type: string + url: + type: string + required: + - templateType + - title + type: object + x-kubernetes-validations: + - message: defaults must have app_name key + rule: '''app_name'' in self.defaults' + status: + description: InstanceStatus defines the observed state of Instance + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/app/config/crd/bases/app.sealos.io_templates.yaml b/controllers/app/config/crd/bases/app.sealos.io_templates.yaml new file mode 100644 index 000000000000..5db66f5fc563 --- /dev/null +++ b/controllers/app/config/crd/bases/app.sealos.io_templates.yaml @@ -0,0 +1,133 @@ +# Copyright © 2024 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: templates.app.sealos.io +spec: + group: app.sealos.io + names: + kind: Template + listKind: TemplateList + plural: templates + singular: template + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Template is the Schema for the templates API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TemplateSpec defines the desired state of Template + properties: + author: + type: string + categories: + items: + type: string + type: array + defaults: + additionalProperties: + properties: + type: + type: string + value: + type: string + required: + - type + - value + type: object + type: object + description: + type: string + draft: + type: boolean + gitRepo: + type: string + i18n: + additionalProperties: + properties: + description: + type: string + gitRepo: + type: string + icon: + type: string + readme: + type: string + title: + type: string + url: + type: string + type: object + type: object + icon: + type: string + inputs: + additionalProperties: + properties: + default: + type: string + description: + type: string + required: + type: boolean + type: + type: string + required: + - type + type: object + type: object + locale: + type: string + readme: + type: string + templateType: + type: string + title: + type: string + url: + type: string + required: + - templateType + - title + type: object + x-kubernetes-validations: + - message: defaults must have app_name key + rule: '''app_name'' in self.defaults' + status: + description: TemplateStatus defines the observed state of Template + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/app/config/crd/kustomization.yaml b/controllers/app/config/crd/kustomization.yaml new file mode 100644 index 000000000000..05bd1e178109 --- /dev/null +++ b/controllers/app/config/crd/kustomization.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/app.sealos.io_apps.yaml +- bases/app.sealos.io_templates.yaml +- bases/app.sealos.io_instances.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- path: patches/webhook_in_apps.yaml +#- path: patches/webhook_in_templates.yaml +#- path: patches/webhook_in_instances.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_apps.yaml +#- path: patches/cainjection_in_templates.yaml +#- path: patches/cainjection_in_instances.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controllers/app/config/crd/kustomizeconfig.yaml b/controllers/app/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..9c99fc52fb20 --- /dev/null +++ b/controllers/app/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controllers/app/config/crd/patches/cainjection_in_apps.yaml b/controllers/app/config/crd/patches/cainjection_in_apps.yaml new file mode 100644 index 000000000000..cc0fc86752ab --- /dev/null +++ b/controllers/app/config/crd/patches/cainjection_in_apps.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: apps.app.sealos.io diff --git a/controllers/app/config/crd/patches/cainjection_in_instances.yaml b/controllers/app/config/crd/patches/cainjection_in_instances.yaml new file mode 100644 index 000000000000..046f1170be6c --- /dev/null +++ b/controllers/app/config/crd/patches/cainjection_in_instances.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: instances.app.sealos.io diff --git a/controllers/app/config/crd/patches/cainjection_in_templates.yaml b/controllers/app/config/crd/patches/cainjection_in_templates.yaml new file mode 100644 index 000000000000..2129e6c60aa4 --- /dev/null +++ b/controllers/app/config/crd/patches/cainjection_in_templates.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: templates.app.sealos.io diff --git a/controllers/app/config/crd/patches/webhook_in_apps.yaml b/controllers/app/config/crd/patches/webhook_in_apps.yaml new file mode 100644 index 000000000000..abf50cdac7a2 --- /dev/null +++ b/controllers/app/config/crd/patches/webhook_in_apps.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: apps.app.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/app/config/crd/patches/webhook_in_instances.yaml b/controllers/app/config/crd/patches/webhook_in_instances.yaml new file mode 100644 index 000000000000..f9042e8cf410 --- /dev/null +++ b/controllers/app/config/crd/patches/webhook_in_instances.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: instances.app.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/app/config/crd/patches/webhook_in_templates.yaml b/controllers/app/config/crd/patches/webhook_in_templates.yaml new file mode 100644 index 000000000000..4669a2435ee0 --- /dev/null +++ b/controllers/app/config/crd/patches/webhook_in_templates.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: templates.app.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/app/config/default/kustomization.yaml b/controllers/app/config/default/kustomization.yaml new file mode 100644 index 000000000000..a3b746fed916 --- /dev/null +++ b/controllers/app/config/default/kustomization.yaml @@ -0,0 +1,49 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: app-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: app- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: +- path: manager_auth_proxy_patch.yaml diff --git a/controllers/app/config/default/manager_auth_proxy_patch.yaml b/controllers/app/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..52d57cd15fd9 --- /dev/null +++ b/controllers/app/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,53 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/controllers/app/config/default/manager_config_patch.yaml b/controllers/app/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..c03d201a433d --- /dev/null +++ b/controllers/app/config/default/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/controllers/app/config/manager/kustomization.yaml b/controllers/app/config/manager/kustomization.yaml new file mode 100644 index 000000000000..94b72fc4306b --- /dev/null +++ b/controllers/app/config/manager/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-app-controller + newTag: latest diff --git a/controllers/app/config/manager/manager.yaml b/controllers/app/config/manager/manager.yaml new file mode 100644 index 000000000000..67e9dfd5b707 --- /dev/null +++ b/controllers/app/config/manager/manager.yaml @@ -0,0 +1,116 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/app/config/prometheus/kustomization.yaml b/controllers/app/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/app/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/app/config/prometheus/monitor.yaml b/controllers/app/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..b3ad6c959799 --- /dev/null +++ b/controllers/app/config/prometheus/monitor.yaml @@ -0,0 +1,40 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/app/config/rbac/app_editor_role.yaml b/controllers/app/config/rbac/app_editor_role.yaml new file mode 100644 index 000000000000..333c04e59c71 --- /dev/null +++ b/controllers/app/config/rbac/app_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit apps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: app-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: app-editor-role +rules: +- apiGroups: + - app.sealos.io + resources: + - apps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - apps/status + verbs: + - get diff --git a/controllers/app/config/rbac/app_viewer_role.yaml b/controllers/app/config/rbac/app_viewer_role.yaml new file mode 100644 index 000000000000..1f718f14c1d9 --- /dev/null +++ b/controllers/app/config/rbac/app_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view apps. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: app-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: app-viewer-role +rules: +- apiGroups: + - app.sealos.io + resources: + - apps + verbs: + - get + - list + - watch +- apiGroups: + - app.sealos.io + resources: + - apps/status + verbs: + - get diff --git a/controllers/app/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/app/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..556e8202e379 --- /dev/null +++ b/controllers/app/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/app/config/rbac/auth_proxy_role.yaml b/controllers/app/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..9969bbdff1ed --- /dev/null +++ b/controllers/app/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/app/config/rbac/auth_proxy_role_binding.yaml b/controllers/app/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..83adf076e179 --- /dev/null +++ b/controllers/app/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/app/config/rbac/auth_proxy_service.yaml b/controllers/app/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..a3712c78c16e --- /dev/null +++ b/controllers/app/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/app/config/rbac/instance_editor_role.yaml b/controllers/app/config/rbac/instance_editor_role.yaml new file mode 100644 index 000000000000..4c623b6d7673 --- /dev/null +++ b/controllers/app/config/rbac/instance_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit instances. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: instance-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: instance-editor-role +rules: +- apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - instances/status + verbs: + - get diff --git a/controllers/app/config/rbac/instance_viewer_role.yaml b/controllers/app/config/rbac/instance_viewer_role.yaml new file mode 100644 index 000000000000..88ca60d28a3e --- /dev/null +++ b/controllers/app/config/rbac/instance_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view instances. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: instance-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: instance-viewer-role +rules: +- apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - get + - list + - watch +- apiGroups: + - app.sealos.io + resources: + - instances/status + verbs: + - get diff --git a/controllers/app/config/rbac/kustomization.yaml b/controllers/app/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/app/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/app/config/rbac/leader_election_role.yaml b/controllers/app/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..0acbe185a344 --- /dev/null +++ b/controllers/app/config/rbac/leader_election_role.yaml @@ -0,0 +1,58 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/app/config/rbac/leader_election_role_binding.yaml b/controllers/app/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..f5f8fea58312 --- /dev/null +++ b/controllers/app/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/app/config/rbac/role.yaml b/controllers/app/config/rbac/role.yaml new file mode 100644 index 000000000000..db62cb6bd87e --- /dev/null +++ b/controllers/app/config/rbac/role.yaml @@ -0,0 +1,56 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - app.sealos.io + resources: + - apps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - templates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/controllers/app/config/rbac/role_binding.yaml b/controllers/app/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..0847b1a02f60 --- /dev/null +++ b/controllers/app/config/rbac/role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/app/config/rbac/service_account.yaml b/controllers/app/config/rbac/service_account.yaml new file mode 100644 index 000000000000..6d70cb1b32be --- /dev/null +++ b/controllers/app/config/rbac/service_account.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/controllers/app/config/rbac/template_editor_role.yaml b/controllers/app/config/rbac/template_editor_role.yaml new file mode 100644 index 000000000000..07e3f8fcd6b4 --- /dev/null +++ b/controllers/app/config/rbac/template_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit templates. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: template-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: template-editor-role +rules: +- apiGroups: + - app.sealos.io + resources: + - templates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - templates/status + verbs: + - get diff --git a/controllers/app/config/rbac/template_viewer_role.yaml b/controllers/app/config/rbac/template_viewer_role.yaml new file mode 100644 index 000000000000..e754ee648db8 --- /dev/null +++ b/controllers/app/config/rbac/template_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view templates. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: template-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + name: template-viewer-role +rules: +- apiGroups: + - app.sealos.io + resources: + - templates + verbs: + - get + - list + - watch +- apiGroups: + - app.sealos.io + resources: + - templates/status + verbs: + - get diff --git a/controllers/app/config/samples/app_v1_app.yaml b/controllers/app/config/samples/app_v1_app.yaml new file mode 100644 index 000000000000..950cb4798a64 --- /dev/null +++ b/controllers/app/config/samples/app_v1_app.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: app.sealos.io/v1 +kind: App +metadata: + labels: + app.kubernetes.io/name: app + app.kubernetes.io/instance: app-sample + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: app + name: app-sample +spec: + name: laf + icon: + type: iframe + data: + url: https://console.lafyun.com/ + desc: + menuData: + displayType: normal \ No newline at end of file diff --git a/controllers/app/config/samples/app_v1_instance.yaml b/controllers/app/config/samples/app_v1_instance.yaml new file mode 100644 index 000000000000..a8508eef6506 --- /dev/null +++ b/controllers/app/config/samples/app_v1_instance.yaml @@ -0,0 +1,56 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: app.sealos.io/v1 +kind: Instance +metadata: + labels: + app.kubernetes.io/name: instance + app.kubernetes.io/instance: instance-sample + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: app + name: instance-sample +spec: + title: 'FastGpt' + url: 'https://fastgpt.run/' + gitRepo: 'https://github.com/labring/FastGPT' + author: 'sealos' + description: 'Fast GPT allows you to use your own openai API KEY to quickly call the openai interface, currently integrating Gpt35, Gpt4 and embedding. You can build your own knowledge base.' + readme: 'https://raw.githubusercontent.com/labring/FastGPT/main/README.md' + icon: 'https://avatars.githubusercontent.com/u/50446880?s=96&v=4' + templateType: inline + defaults: + app_name: + type: string + value: fastgpt-12345678 + app_host: + type: string + value: a1234567 + inputs: + root_password: + description: 'root用户密码,用户名为: root' + type: string + default: '' + required: true + base_url: + description: 'api 基础地址,官方 key 不需要改' + type: string + default: 'https://api.openai.com/v1' + required: true + api_key: + description: 'openai api key' + type: string + default: '' + required: true diff --git a/controllers/app/config/samples/app_v1beta1_template.yaml b/controllers/app/config/samples/app_v1beta1_template.yaml new file mode 100644 index 000000000000..bf0af110db32 --- /dev/null +++ b/controllers/app/config/samples/app_v1beta1_template.yaml @@ -0,0 +1,56 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: app.sealos.io/v1 +kind: Template +metadata: + labels: + app.kubernetes.io/name: template + app.kubernetes.io/instance: template-sample + app.kubernetes.io/part-of: app + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: app + name: template-sample +spec: + title: 'FastGpt' + url: 'https://fastgpt.run/' + gitRepo: 'https://github.com/labring/FastGPT' + author: 'sealos' + description: 'Fast GPT allows you to use your own openai API KEY to quickly call the openai interface, currently integrating Gpt35, Gpt4 and embedding. You can build your own knowledge base.' + readme: 'https://raw.githubusercontent.com/labring/FastGPT/main/README.md' + icon: 'https://avatars.githubusercontent.com/u/50446880?s=96&v=4' + templateType: inline + defaults: + app_name: + type: string + value: fastgpt-${{ random(8) }} + app_host: + type: string + value: ${{ random(8) }} + inputs: + root_password: + description: 'root用户密码,用户名为: root' + type: string + default: '' + required: true + base_url: + description: 'api 基础地址,官方 key 不需要改' + type: string + default: 'https://api.openai.com/v1' + required: true + api_key: + description: 'openai api key' + type: string + default: '' + required: true diff --git a/controllers/app/config/samples/kustomization.yaml b/controllers/app/config/samples/kustomization.yaml new file mode 100644 index 000000000000..cee8a8de5eef --- /dev/null +++ b/controllers/app/config/samples/kustomization.yaml @@ -0,0 +1,20 @@ +# Copyright © 2023 sealos. +# +# 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. + +## Append samples of your project ## +resources: +- app_v1_app.yaml +- app_v1_template.yaml +- app_v1_instance.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/app/deploy/Kubefile b/controllers/app/deploy/Kubefile new file mode 100644 index 000000000000..ec4f859235eb --- /dev/null +++ b/controllers/app/deploy/Kubefile @@ -0,0 +1,8 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY manifests manifests + +CMD ["kubectl apply -f manifests"] diff --git a/controllers/app/deploy/appexample/cr.yaml b/controllers/app/deploy/appexample/cr.yaml new file mode 100644 index 000000000000..ae9ed9029fe6 --- /dev/null +++ b/controllers/app/deploy/appexample/cr.yaml @@ -0,0 +1,89 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: kubernetes-dashboard + namespace: app-system +spec: + data: + desc: Sealos Cloud Kubernetes Dashboard + url: "" + icon: /images/kubernetes.svg + menuData: + helpDropDown: true + nameColor: text-black + name: Kubernetes-Dashboard + type: iframe + displayType: hide +--- +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: postgres + namespace: app-system +spec: + data: + desc: postgres operator + url: "" + icon: /images/pgadmin.svg + menuData: + name: Postgres + type: app + displayType: normal +--- +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: sealos-cloud-provider + namespace: app-system +spec: + data: + desc: infra operator + url: "" + icon: /images/infraicon/scp.png + menuData: + name: sealos cloud provider + type: app + displayType: more +--- +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: sealos-document + namespace: app-system +spec: + data: + desc: Sealos Documents + url: https://www.sealos.io/docs/cloud/Intro + icon: /images/sealos.svg + menuData: + name: Sealos Document + type: iframe + displayType: normal +--- +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: deploy-manager +spec: + name: Deploy Manager + icon: "/images/sealos.svg" + type: iframe + data: + url: "https://deploy.cloud.sealos.io" + desc: Deploy Manager + menuData: + displayType: normal diff --git a/controllers/app/deploy/manifests/deploy.yaml b/controllers/app/deploy/manifests/deploy.yaml new file mode 100644 index 000000000000..da40498bed11 --- /dev/null +++ b/controllers/app/deploy/manifests/deploy.yaml @@ -0,0 +1,638 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: system + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: namespace + app.kubernetes.io/part-of: app + control-plane: controller-manager + name: app-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: apps.app.sealos.io +spec: + group: app.sealos.io + names: + kind: App + listKind: AppList + plural: apps + singular: app + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: App is the Schema for the apps API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AppSpec defines the desired state of App + properties: + data: + properties: + desc: + type: string + url: + type: string + type: object + displayType: + enum: + - normal + - more + - hidden + type: string + i18n: + additionalProperties: + description: AppMeta Base Information + properties: + data: + properties: + desc: + type: string + url: + type: string + type: object + displayType: + enum: + - normal + - more + - hidden + type: string + icon: + type: string + menuData: + items: + properties: + link: + type: string + name: + type: string + type: object + type: array + name: + type: string + type: + type: string + type: object + type: object + icon: + type: string + menuData: + items: + properties: + link: + type: string + name: + type: string + type: object + type: array + name: + type: string + position: + format: double + type: number + type: + type: string + version: + type: string + type: object + status: + description: AppStatus defines the observed state of App + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: instances.app.sealos.io +spec: + group: app.sealos.io + names: + kind: Instance + listKind: InstanceList + plural: instances + singular: instance + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Instance is the Schema for the instances API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstanceSpec defines the desired state of Instance + properties: + author: + type: string + categories: + items: + type: string + type: array + defaults: + additionalProperties: + properties: + type: + type: string + value: + type: string + required: + - type + - value + type: object + type: object + description: + type: string + draft: + type: boolean + gitRepo: + type: string + icon: + type: string + inputs: + additionalProperties: + properties: + default: + type: string + description: + type: string + required: + type: boolean + type: + type: string + required: + - type + type: object + type: object + readme: + type: string + templateType: + type: string + title: + type: string + url: + type: string + required: + - templateType + - title + type: object + x-kubernetes-validations: + - message: defaults must have app_name key + rule: '''app_name'' in self.defaults' + status: + description: InstanceStatus defines the observed state of Instance + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: templates.app.sealos.io +spec: + group: app.sealos.io + names: + kind: Template + listKind: TemplateList + plural: templates + singular: template + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Template is the Schema for the templates API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TemplateSpec defines the desired state of Template + properties: + author: + type: string + categories: + items: + type: string + type: array + defaults: + additionalProperties: + properties: + type: + type: string + value: + type: string + required: + - type + - value + type: object + type: object + description: + type: string + draft: + type: boolean + gitRepo: + type: string + icon: + type: string + inputs: + additionalProperties: + properties: + default: + type: string + description: + type: string + required: + type: boolean + type: + type: string + required: + - type + type: object + type: object + readme: + type: string + templateType: + type: string + title: + type: string + url: + type: string + required: + - templateType + - title + type: object + x-kubernetes-validations: + - message: defaults must have app_name key + rule: '''app_name'' in self.defaults' + status: + description: TemplateStatus defines the observed state of Template + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: app + name: app-controller-manager + namespace: app-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: role + app.kubernetes.io/part-of: app + name: app-leader-election-role + namespace: app-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: app-manager-role +rules: +- apiGroups: + - app.sealos.io + resources: + - apps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - app.sealos.io + resources: + - templates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: app + name: app-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: app + name: app-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: app + name: app-leader-election-rolebinding + namespace: app-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: app-leader-election-role +subjects: +- kind: ServiceAccount + name: app-controller-manager + namespace: app-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: app + name: app-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: app-manager-role +subjects: +- kind: ServiceAccount + name: app-controller-manager + namespace: app-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: app + name: app-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: app-proxy-role +subjects: +- kind: ServiceAccount + name: app-controller-manager + namespace: app-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: app + control-plane: controller-manager + name: app-controller-manager-metrics-service + namespace: app-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: app + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: app + control-plane: controller-manager + name: app-controller-manager + namespace: app-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: ghcr.io/labring/sealos-app-controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: app-controller-manager + terminationGracePeriodSeconds: 10 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "control-plane" + operator: In + values: + - controller-manager + topologyKey: "kubernetes.io/hostname" diff --git a/controllers/app/deploy/manifests/rbac.yaml b/controllers/app/deploy/manifests/rbac.yaml new file mode 100644 index 000000000000..581da2640339 --- /dev/null +++ b/controllers/app/deploy/manifests/rbac.yaml @@ -0,0 +1,37 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: app-default-user + namespace: app-system +rules: + - apiGroups: ["app.sealos.io"] + resources: [ "apps" ] + verbs: [ "get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: app-default-user-rolebinding + namespace: app-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: app-default-user +subjects: + - kind: Group + name: system:serviceaccounts + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/controllers/app/go.mod b/controllers/app/go.mod new file mode 100644 index 000000000000..df2b5d16d2d2 --- /dev/null +++ b/controllers/app/go.mod @@ -0,0 +1,76 @@ +module github.com/labring/sealos/controllers/app + +go 1.24.0 + +replace ( + k8s.io/api => k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.32.1 + k8s.io/client-go => k8s.io/client-go v0.32.1 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + 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/matttproud/golang_protobuf_extensions/v2 v2.0.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/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.0 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/controllers/app/go.sum b/controllers/app/go.sum new file mode 100644 index 000000000000..9e3d9a8235f9 --- /dev/null +++ b/controllers/app/go.sum @@ -0,0 +1,200 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/app/hack/boilerplate.go.txt b/controllers/app/hack/boilerplate.go.txt new file mode 100644 index 000000000000..65b862271836 --- /dev/null +++ b/controllers/app/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +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. +*/ \ No newline at end of file diff --git a/controllers/go.work b/controllers/go.work new file mode 100644 index 000000000000..4445ea69f08b --- /dev/null +++ b/controllers/go.work @@ -0,0 +1,22 @@ +go 1.25.0 + +use ( + ./account + ./app + ./job/heartbeat + ./job/init + ./license + ./node + ./pkg + ./resources + ./user +) + +replace ( + k8s.io/api => k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.32.1 + k8s.io/client-go => k8s.io/client-go v0.32.1 + k8s.io/component-base => k8s.io/component-base v0.32.1 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4 +) diff --git a/controllers/go.work.sum b/controllers/go.work.sum new file mode 100644 index 000000000000..c4d6bf7b62c6 --- /dev/null +++ b/controllers/go.work.sum @@ -0,0 +1,5217 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= +cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= +cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0 h1:x0cEHro/JFPd7eS4BlEWNTMecIj2HdXjOVB5BtvwER0= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accessapproval v1.7.1 h1:/5YjNhR6lzCvmJZAnByYkfEgWjfAKwYP6nkuTk6nKFE= +cloud.google.com/go/accessapproval v1.7.1/go.mod h1:JYczztsHRMK7NTXb6Xw+dwbs/WnOJxbo/2mTI+Kgg68= +cloud.google.com/go/accessapproval v1.7.2/go.mod h1:/gShiq9/kK/h8T/eEn1BTzalDvk0mZxJlhfw0p+Xuc0= +cloud.google.com/go/accessapproval v1.7.3/go.mod h1:4l8+pwIxGTNqSf4T3ds8nLO94NQf0W/KnMNuQ9PbnP8= +cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= +cloud.google.com/go/accessapproval v1.7.4/go.mod h1:/aTEh45LzplQgFYdQdwPMR9YdX0UlhBmvB84uAmQKUc= +cloud.google.com/go/accessapproval v1.7.5 h1:uzmAMSgYcnlHa9X9YSQZ4Q1wlfl4NNkZyQgho1Z6p04= +cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0 h1:MG60JgnEoawHJrbWw0jGdv6HLNSf6gQvYRiXpuzqgEA= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/accesscontextmanager v1.8.0/go.mod h1:uI+AI/r1oyWK99NN8cQ3UK76AMelMzgZCvJfsi2c+ps= +cloud.google.com/go/accesscontextmanager v1.8.1 h1:WIAt9lW9AXtqw/bnvrEUaE8VG/7bAAeMzRCBGMkc4+w= +cloud.google.com/go/accesscontextmanager v1.8.1/go.mod h1:JFJHfvuaTC+++1iL1coPiG1eu5D24db2wXCDWDjIrxo= +cloud.google.com/go/accesscontextmanager v1.8.2/go.mod h1:E6/SCRM30elQJ2PKtFMs2YhfJpZSNcJyejhuzoId4Zk= +cloud.google.com/go/accesscontextmanager v1.8.3/go.mod h1:4i/JkF2JiFbhLnnpnfoTX5vRXfhf9ukhU1ANOTALTOQ= +cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= +cloud.google.com/go/accesscontextmanager v1.8.4/go.mod h1:ParU+WbMpD34s5JFEnGAnPBYAgUHozaTmDJU7aCU9+M= +cloud.google.com/go/accesscontextmanager v1.8.5 h1:2GLNaNu9KRJhJBFTIVRoPwk6xE5mUDgD47abBq4Zp/I= +cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0 h1:zTw+suCVchgZyO+k847wjzdVjWmrAuehxdvcZvJwfGg= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/aiplatform v1.45.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA= +cloud.google.com/go/aiplatform v1.48.0 h1:M5davZWCTzE043rJCn+ZLW6hSxfG1KAx4vJTtas2/ec= +cloud.google.com/go/aiplatform v1.48.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA= +cloud.google.com/go/aiplatform v1.50.0/go.mod h1:IRc2b8XAMTa9ZmfJV1BCCQbieWWvDnP1A8znyz5N7y4= +cloud.google.com/go/aiplatform v1.51.0/go.mod h1:IRc2b8XAMTa9ZmfJV1BCCQbieWWvDnP1A8znyz5N7y4= +cloud.google.com/go/aiplatform v1.51.1/go.mod h1:kY3nIMAVQOK2XDqDPHaOuD9e+FdMA6OOpfBjsvaFSOo= +cloud.google.com/go/aiplatform v1.51.2/go.mod h1:hCqVYB3mY45w99TmetEoe8eCQEwZEp9WHxeZdcv9phw= +cloud.google.com/go/aiplatform v1.52.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.54.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.58.0 h1:xyCAfpI4yUMOQ4VtHN/bdmxPQ8xoEkTwFM1nbVmuQhs= +cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= +cloud.google.com/go/aiplatform v1.58.2/go.mod h1:c3kCiVmb6UC1dHAjZjcpDj6ZS0bHQ2slL88ZjC2LtlA= +cloud.google.com/go/aiplatform v1.60.0 h1:0cSrii1ZeLr16MbBoocyy5KVnrSdiQ3KN/vtrTe7RqE= +cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0 h1:LqAo3tAh2FU9+w/r7vc3hBjU23Kv7GhO/PDIW7kIYgM= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/analytics v0.21.2/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo= +cloud.google.com/go/analytics v0.21.3 h1:TFBC1ZAqX9/jL56GEXdLrVe5vT3I22bDVWyDwZX4IEg= +cloud.google.com/go/analytics v0.21.3/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo= +cloud.google.com/go/analytics v0.21.4/go.mod h1:zZgNCxLCy8b2rKKVfC1YkC2vTrpfZmeRCySM3aUbskA= +cloud.google.com/go/analytics v0.21.5/go.mod h1:BQtOBHWTlJ96axpPPnw5CvGJ6i3Ve/qX2fTxR8qWyr8= +cloud.google.com/go/analytics v0.21.6/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= +cloud.google.com/go/analytics v0.22.0 h1:w8KIgW8NRUHFVKjpkwCpLaHsr685tJ+ckPStOaSCZz0= +cloud.google.com/go/analytics v0.22.0/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= +cloud.google.com/go/analytics v0.23.0 h1:Q+y94XH84jM8SK8O7qiY/PJRexb6n7dRbQ6PiUa4YGM= +cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0 h1:ZI9mVO7x3E9RK/BURm2p1aw9YTBSCQe3klmyP1WxWEg= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigateway v1.6.1 h1:aBSwCQPcp9rZ0zVEUeJbR623palnqtvxJlUyvzsKGQc= +cloud.google.com/go/apigateway v1.6.1/go.mod h1:ufAS3wpbRjqfZrzpvLC2oh0MFlpRJm2E/ts25yyqmXA= +cloud.google.com/go/apigateway v1.6.2/go.mod h1:CwMC90nnZElorCW63P2pAYm25AtQrHfuOkbRSHj0bT8= +cloud.google.com/go/apigateway v1.6.3/go.mod h1:k68PXWpEs6BVDTtnLQAyG606Q3mz8pshItwPXjgv44Y= +cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= +cloud.google.com/go/apigateway v1.6.4/go.mod h1:0EpJlVGH5HwAN4VF4Iec8TAzGN1aQgbxAWGJsnPCGGY= +cloud.google.com/go/apigateway v1.6.5 h1:sPXnpk+6TneKIrjCjcpX5YGsAKy3PTdpIchoj8/74OE= +cloud.google.com/go/apigateway v1.6.5/go.mod h1:6wCwvYRckRQogyDDltpANi3zsCDl6kWi0b4Je+w2UiI= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0 h1:sWOmgDyAsi1AZ48XRHcATC0tsi9SkPT7DA/+VCfkaeA= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeconnect v1.6.1 h1:6u/jj0P2c3Mcm+H9qLsXI7gYcTiG9ueyQL3n6vCmFJM= +cloud.google.com/go/apigeeconnect v1.6.1/go.mod h1:C4awq7x0JpLtrlQCr8AzVIzAaYgngRqWf9S5Uhg+wWs= +cloud.google.com/go/apigeeconnect v1.6.2/go.mod h1:s6O0CgXT9RgAxlq3DLXvG8riw8PYYbU/v25jqP3Dy18= +cloud.google.com/go/apigeeconnect v1.6.3/go.mod h1:peG0HFQ0si2bN15M6QSjEW/W7Gy3NYkWGz7pFz13cbo= +cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= +cloud.google.com/go/apigeeconnect v1.6.4/go.mod h1:CapQCWZ8TCjnU0d7PobxhpOdVz/OVJ2Hr/Zcuu1xFx0= +cloud.google.com/go/apigeeconnect v1.6.5 h1:CrfIKv9Go3fh/QfQgisU3MeP90Ww7l/sVGmr3TpECo8= +cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91FNysvd3E6SJ6Ow= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0 h1:E43RdhhCxdlV+I161gUY2rI4eOaMzHTA5kNkvRsFXvc= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apigeeregistry v0.7.1 h1:hgq0ANLDx7t2FDZDJQrCMtCtddR/pjCqVuvQWGrQbXw= +cloud.google.com/go/apigeeregistry v0.7.1/go.mod h1:1XgyjZye4Mqtw7T9TsY4NW10U7BojBvG4RMD+vRDrIw= +cloud.google.com/go/apigeeregistry v0.7.2/go.mod h1:9CA2B2+TGsPKtfi3F7/1ncCCsL62NXBRfM6iPoGSM+8= +cloud.google.com/go/apigeeregistry v0.8.1/go.mod h1:MW4ig1N4JZQsXmBSwH4rwpgDonocz7FPBSw6XPGHmYw= +cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= +cloud.google.com/go/apigeeregistry v0.8.2/go.mod h1:h4v11TDGdeXJDJvImtgK2AFVvMIgGWjSb0HRnBSjcX8= +cloud.google.com/go/apigeeregistry v0.8.3 h1:C+QU2K+DzDjk4g074ouwHQGkoff1h5OMQp6sblCVreQ= +cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1 h1:aBGDKmRIaRRoWJ2tAoN0oVSHoWLhtO9aj/NvUyP4aYs= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/appengine v1.8.1 h1:J+aaUZ6IbTpBegXbmEsh8qZZy864ZVnOoWyfa1XSNbI= +cloud.google.com/go/appengine v1.8.1/go.mod h1:6NJXGLVhZCN9aQ/AEDvmfzKEfoYBlfB80/BHiKVputY= +cloud.google.com/go/appengine v1.8.2/go.mod h1:WMeJV9oZ51pvclqFN2PqHoGnys7rK0rz6s3Mp6yMvDo= +cloud.google.com/go/appengine v1.8.3/go.mod h1:2oUPZ1LVZ5EXi+AF1ihNAF+S8JrzQ3till5m9VQkrsk= +cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9IX/E= +cloud.google.com/go/appengine v1.8.4/go.mod h1:TZ24v+wXBujtkK77CXCpjZbnuTvsFNT41MUaZ28D6vg= +cloud.google.com/go/appengine v1.8.5 h1:l2SviT44zWQiOv8bPoMBzW0vOcMO22iO0s+nVtVhdts= +cloud.google.com/go/appengine v1.8.5/go.mod h1:uHBgNoGLTS5di7BvU25NFDuKa82v0qQLjyMJLuPQrVo= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1 h1:ugckkFh4XkHJMPhTIx0CyvdoBxmOpMe8rNs4Ok8GAag= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/area120 v0.8.1 h1:wiOq3KDpdqXmaHzvZwKdpoM+3lDcqsI2Lwhyac7stss= +cloud.google.com/go/area120 v0.8.1/go.mod h1:BVfZpGpB7KFVNxPiQBuHkX6Ed0rS51xIgmGyjrAfzsg= +cloud.google.com/go/area120 v0.8.2/go.mod h1:a5qfo+x77SRLXnCynFWPUZhnZGeSgvQ+Y0v1kSItkh4= +cloud.google.com/go/area120 v0.8.3/go.mod h1:5zj6pMzVTH+SVHljdSKC35sriR/CVvQZzG/Icdyriw0= +cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= +cloud.google.com/go/area120 v0.8.4/go.mod h1:jfawXjxf29wyBXr48+W+GyX/f8fflxp642D/bb9v68M= +cloud.google.com/go/area120 v0.8.5 h1:vTs08KPLN/iMzTbxpu5ciL06KcsrVPMjz4IwcQyZ4uY= +cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5UFu6GMyA0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0 h1:o1Q80vqEB6Qp8WLEH3b8FBLNUCrGQ4k5RFj0sn/sgO8= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/artifactregistry v1.14.1 h1:k6hNqab2CubhWlGcSzunJ7kfxC7UzpAfQ1UPb9PDCKI= +cloud.google.com/go/artifactregistry v1.14.1/go.mod h1:nxVdG19jTaSTu7yA7+VbWL346r3rIdkZ142BSQqhn5E= +cloud.google.com/go/artifactregistry v1.14.2/go.mod h1:Xk+QbsKEb0ElmyeMfdHAey41B+qBq3q5R5f5xD4XT3U= +cloud.google.com/go/artifactregistry v1.14.3/go.mod h1:A2/E9GXnsyXl7GUvQ/2CjHA+mVRoWAXC0brg2os+kNI= +cloud.google.com/go/artifactregistry v1.14.4/go.mod h1:SJJcZTMv6ce0LDMUnihCN7WSrI+kBSFV0KIKo8S8aYU= +cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= +cloud.google.com/go/artifactregistry v1.14.6/go.mod h1:np9LSFotNWHcjnOgh8UVK0RFPCTUGbO0ve3384xyHfE= +cloud.google.com/go/artifactregistry v1.14.7 h1:W9sVlyb1VRcUf83w7aM3yMsnp4HS4PoyGqYQNG0O5lI= +cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0 h1:YAsssO08BqZ6mncbb6FPlj9h6ACS7bJQUOlzciSfbNk= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/asset v1.14.1 h1:vlHdznX70eYW4V1y1PxocvF6tEwxJTTarwIGwOhFF3U= +cloud.google.com/go/asset v1.14.1/go.mod h1:4bEJ3dnHCqWCDbWJ/6Vn7GVI9LerSi7Rfdi03hd+WTQ= +cloud.google.com/go/asset v1.15.0/go.mod h1:tpKafV6mEut3+vN9ScGvCHXHj7FALFVta+okxFECHcg= +cloud.google.com/go/asset v1.15.1/go.mod h1:yX/amTvFWRpp5rcFq6XbCxzKT8RJUam1UoboE179jU4= +cloud.google.com/go/asset v1.15.2/go.mod h1:B6H5tclkXvXz7PD22qCA2TDxSVQfasa3iDlM89O2NXs= +cloud.google.com/go/asset v1.15.3/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= +cloud.google.com/go/asset v1.16.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= +cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg= +cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= +cloud.google.com/go/asset v1.17.1/go.mod h1:byvDw36UME5AzGNK7o4JnOnINkwOZ1yRrGrKIahHrng= +cloud.google.com/go/asset v1.17.2 h1:xgFnBP3luSbUcC9RWJvb3Zkt+y/wW6PKwPHr3ssnIP8= +cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0 h1:VLGnVFta+N4WM+ASHbhc14ZOItOabDLH1MSoDv+Xuag= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/assuredworkloads v1.11.1 h1:yaO0kwS+SnhVSTF7BqTyVGt3DTocI6Jqo+S3hHmCwNk= +cloud.google.com/go/assuredworkloads v1.11.1/go.mod h1:+F04I52Pgn5nmPG36CWFtxmav6+7Q+c5QyJoL18Lry0= +cloud.google.com/go/assuredworkloads v1.11.2/go.mod h1:O1dfr+oZJMlE6mw0Bp0P1KZSlj5SghMBvTpZqIcUAW4= +cloud.google.com/go/assuredworkloads v1.11.3/go.mod h1:vEjfTKYyRUaIeA0bsGJceFV2JKpVRgyG2op3jfa59Zs= +cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= +cloud.google.com/go/assuredworkloads v1.11.4/go.mod h1:4pwwGNwy1RP0m+y12ef3Q/8PaiWrIDQ6nD2E8kvWI9U= +cloud.google.com/go/assuredworkloads v1.11.5 h1:gCrN3IyvqY3cP0wh2h43d99CgH3G+WYs9CeuFVKChR8= +cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0 h1:50VugllC+U4IGl3tDNcZaWvApHBTrn/TvyHDJ0wM+Uw= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/automl v1.13.1 h1:iP9iQurb0qbz+YOOMfKSEjhONA/WcoOIjt6/m+6pIgo= +cloud.google.com/go/automl v1.13.1/go.mod h1:1aowgAHWYZU27MybSCFiukPO7xnyawv7pt3zK4bheQE= +cloud.google.com/go/automl v1.13.2/go.mod h1:gNY/fUmDEN40sP8amAX3MaXkxcqPIn7F1UIIPZpy4Mg= +cloud.google.com/go/automl v1.13.3/go.mod h1:Y8KwvyAZFOsMAPqUCfNu1AyclbC6ivCUF/MTwORymyY= +cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= +cloud.google.com/go/automl v1.13.4/go.mod h1:ULqwX/OLZ4hBVfKQaMtxMSTlPx0GqGbWN8uA/1EqCP8= +cloud.google.com/go/automl v1.13.5 h1:ijiJy9sYWh75WrqImXsfWc1e3HR3iO+ef9fvW03Ig/4= +cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0 h1:2AipdYXL0VxMboelTTw8c1UJ7gYu35LZYUbuRv9Q28s= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/baremetalsolution v1.1.1 h1:0Ge9PQAy6cZ1tRrkc44UVgYV15nw2TVnzJzYsMHXF+E= +cloud.google.com/go/baremetalsolution v1.1.1/go.mod h1:D1AV6xwOksJMV4OSlWHtWuFNZZYujJknMAP4Qa27QIA= +cloud.google.com/go/baremetalsolution v1.2.0/go.mod h1:68wi9AwPYkEWIUT4SvSGS9UJwKzNpshjHsH4lzk8iOw= +cloud.google.com/go/baremetalsolution v1.2.1/go.mod h1:3qKpKIw12RPXStwQXcbhfxVj1dqQGEvcmA+SX/mUR88= +cloud.google.com/go/baremetalsolution v1.2.2/go.mod h1:O5V6Uu1vzVelYahKfwEWRMaS3AbCkeYHy3145s1FkhM= +cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= +cloud.google.com/go/baremetalsolution v1.2.3/go.mod h1:/UAQ5xG3faDdy180rCUv47e0jvpp3BFxT+Cl0PFjw5g= +cloud.google.com/go/baremetalsolution v1.2.4 h1:LFydisRmS7hQk9P/YhekwuZGqb45TW4QavcrMToWo5A= +cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0 h1:YbMt0E6BtqeD5FvSv1d56jbVsWEzlGm55lYte+M6Mzs= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/batch v1.3.1 h1:uE0Q//W7FOGPjf7nuPiP0zoE8wOT3ngoIO2HIet0ilY= +cloud.google.com/go/batch v1.3.1/go.mod h1:VguXeQKXIYaeeIYbuozUmBR13AfL4SJP7IltNPS+A4A= +cloud.google.com/go/batch v1.4.1/go.mod h1:KdBmDD61K0ovcxoRHGrN6GmOBWeAOyCgKD0Mugx4Fkk= +cloud.google.com/go/batch v1.5.0/go.mod h1:KdBmDD61K0ovcxoRHGrN6GmOBWeAOyCgKD0Mugx4Fkk= +cloud.google.com/go/batch v1.5.1/go.mod h1:RpBuIYLkQu8+CWDk3dFD/t/jOCGuUpkpX+Y0n1Xccs8= +cloud.google.com/go/batch v1.6.1/go.mod h1:urdpD13zPe6YOK+6iZs/8/x2VBRofvblLpx0t57vM98= +cloud.google.com/go/batch v1.6.3/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= +cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= +cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= +cloud.google.com/go/batch v1.8.0 h1:2HK4JerwVaIcCh/lJiHwh6+uswPthiMMWhiSWLELayk= +cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0 h1:UkY2BTZkEUAVrgqnSdOJ4p3y9ZRBPEe1LkjgC8Bj/Pc= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/beyondcorp v0.6.1/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4= +cloud.google.com/go/beyondcorp v1.0.0 h1:VPg+fZXULQjs8LiMeWdLaB5oe8G9sEoZ0I0j6IMiG1Q= +cloud.google.com/go/beyondcorp v1.0.0/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4= +cloud.google.com/go/beyondcorp v1.0.1/go.mod h1:zl/rWWAFVeV+kx+X2Javly7o1EIQThU4WlkynffL/lk= +cloud.google.com/go/beyondcorp v1.0.2/go.mod h1:m8cpG7caD+5su+1eZr+TSvF6r21NdLJk4f9u4SP2Ntc= +cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= +cloud.google.com/go/beyondcorp v1.0.3/go.mod h1:HcBvnEd7eYr+HGDd5ZbuVmBYX019C6CEXBonXbCVwJo= +cloud.google.com/go/beyondcorp v1.0.4 h1:qs0J0O9Ol2h1yA0AU+r7l3hOCPzs2MjE1d6d/kaHIKo= +cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/bigquery v1.52.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= +cloud.google.com/go/bigquery v1.53.0 h1:K3wLbjbnSlxhuG5q4pntHv5AEbQM1QqHKGYgwFIqOTg= +cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= +cloud.google.com/go/bigquery v1.55.0/go.mod h1:9Y5I3PN9kQWuid6183JFhOGOW3GcirA5LpsKCUn+2ec= +cloud.google.com/go/bigquery v1.56.0/go.mod h1:KDcsploXTEY7XT3fDQzMUZlpQLHzE4itubHrnmhUrZA= +cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA= +cloud.google.com/go/bigquery v1.57.1/go.mod h1:iYzC0tGVWt1jqSzBHqCr3lrRn0u13E8e+AqowBsDgug= +cloud.google.com/go/bigquery v1.58.0 h1:drSd9RcPVLJP2iFMimvOB9SCSIrcl+9HD4II03Oy7A0= +cloud.google.com/go/bigquery v1.58.0/go.mod h1:0eh4mWNY0KrBTjUzLjoYImapGORq9gEPT7MWjCy9lik= +cloud.google.com/go/bigquery v1.59.1 h1:CpT+/njKuKT3CEmswm6IbhNu9u35zt5dO4yPDLW+nG4= +cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0 h1:JYj28UYF5w6VBAh0gQYlgHJ/OD1oA+JgW29YZQU+UHM= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/billing v1.16.0 h1:1iktEAIZ2uA6KpebC235zi/rCXDdDYQ0bTXTNetSL80= +cloud.google.com/go/billing v1.16.0/go.mod h1:y8vx09JSSJG02k5QxbycNRrN7FGZB6F3CAcgum7jvGA= +cloud.google.com/go/billing v1.17.0/go.mod h1:Z9+vZXEq+HwH7bhJkyI4OQcR6TSbeMrjlpEjO2vzY64= +cloud.google.com/go/billing v1.17.1/go.mod h1:Z9+vZXEq+HwH7bhJkyI4OQcR6TSbeMrjlpEjO2vzY64= +cloud.google.com/go/billing v1.17.2/go.mod h1:u/AdV/3wr3xoRBk5xvUzYMS1IawOAPwQMuHgHMdljDg= +cloud.google.com/go/billing v1.17.3/go.mod h1:z83AkoZ7mZwBGT3yTnt6rSGI1OOsHSIi6a5M3mJ8NaU= +cloud.google.com/go/billing v1.17.4/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= +cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= +cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= +cloud.google.com/go/billing v1.18.2 h1:oWUEQvuC4JvtnqLZ35zgzdbuHt4Itbftvzbe6aEyFdE= +cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0 h1:d3pMDBCCNivxt5a4eaV7FwL7cSH0H7RrEnFrTb1QKWs= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/binaryauthorization v1.6.1 h1:cAkOhf1ic92zEN4U1zRoSupTmwmxHfklcp1X7CCBKvE= +cloud.google.com/go/binaryauthorization v1.6.1/go.mod h1:TKt4pa8xhowwffiBmbrbcxijJRZED4zrqnwZ1lKH51U= +cloud.google.com/go/binaryauthorization v1.7.0/go.mod h1:Zn+S6QqTMn6odcMU1zDZCJxPjU2tZPV1oDl45lWY154= +cloud.google.com/go/binaryauthorization v1.7.1/go.mod h1:GTAyfRWYgcbsP3NJogpV3yeunbUIjx2T9xVeYovtURE= +cloud.google.com/go/binaryauthorization v1.7.2/go.mod h1:kFK5fQtxEp97m92ziy+hbu+uKocka1qRRL8MVJIgjv0= +cloud.google.com/go/binaryauthorization v1.7.3/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= +cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= +cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= +cloud.google.com/go/binaryauthorization v1.8.1 h1:1jcyh2uIUwSZkJ/JmL8kd5SUkL/Krbv8zmYLEbAz6kY= +cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0 h1:5C5UWeSt8Jkgp7OWn2rCkLmYurar/vIWIoSQ2+LaTOc= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/certificatemanager v1.7.1 h1:uKsohpE0hiobx1Eak9jNcPCznwfB6gvyQCcS28Ah9E8= +cloud.google.com/go/certificatemanager v1.7.1/go.mod h1:iW8J3nG6SaRYImIa+wXQ0g8IgoofDFRp5UMzaNk1UqI= +cloud.google.com/go/certificatemanager v1.7.2/go.mod h1:15SYTDQMd00kdoW0+XY5d9e+JbOPjp24AvF48D8BbcQ= +cloud.google.com/go/certificatemanager v1.7.3/go.mod h1:T/sZYuC30PTag0TLo28VedIRIj1KPGcOQzjWAptHa00= +cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= +cloud.google.com/go/certificatemanager v1.7.4/go.mod h1:FHAylPe/6IIKuaRmHbjbdLhGhVQ+CWHSD5Jq0k4+cCE= +cloud.google.com/go/certificatemanager v1.7.5 h1:UMBr/twXvH3jcT5J5/YjRxf2tvwTYIfrpemTebe0txc= +cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0 h1:GpcQY5UJKeOekYgsX3QXbzzAc/kRGtBq43fTmyKe6Uw= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/channel v1.16.0 h1:dqRkK2k7Ll/HHeYGxv18RrfhozNxuTJRkspW0iaFZoY= +cloud.google.com/go/channel v1.16.0/go.mod h1:eN/q1PFSl5gyu0dYdmxNXscY/4Fi7ABmeHCJNf/oHmc= +cloud.google.com/go/channel v1.17.0/go.mod h1:RpbhJsGi/lXWAUM1eF4IbQGbsfVlg2o8Iiy2/YLfVT0= +cloud.google.com/go/channel v1.17.1/go.mod h1:xqfzcOZAcP4b/hUDH0GkGg1Sd5to6di1HOJn/pi5uBQ= +cloud.google.com/go/channel v1.17.2/go.mod h1:aT2LhnftnyfQceFql5I/mP8mIbiiJS4lWqgXA815zMk= +cloud.google.com/go/channel v1.17.3/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= +cloud.google.com/go/channel v1.17.4 h1:yYHOORIM+wkBy3EdwArg/WL7Lg+SoGzlKH9o3Bw2/jE= +cloud.google.com/go/channel v1.17.4/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= +cloud.google.com/go/channel v1.17.5 h1:/omiBnyFjm4S1ETHoOmJbL7LH7Ljcei4rYG6Sj3hc80= +cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0 h1:GHQCjV4WlPPVU/j3Rlpc8vNIDwThhd1U9qSY/NPZdko= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/cloudbuild v1.10.1/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/cloudbuild v1.13.0 h1:YBbAWcvE4x6xPWTyS+OU4eiUpz5rCS3VCM/aqmfddPA= +cloud.google.com/go/cloudbuild v1.13.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/cloudbuild v1.14.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/cloudbuild v1.14.1/go.mod h1:K7wGc/3zfvmYWOWwYTgF/d/UVJhS4pu+HAy7PL7mCsU= +cloud.google.com/go/cloudbuild v1.14.2/go.mod h1:Bn6RO0mBYk8Vlrt+8NLrru7WXlQ9/RDWz2uo5KG1/sg= +cloud.google.com/go/cloudbuild v1.14.3/go.mod h1:eIXYWmRt3UtggLnFGx4JvXcMj4kShhVzGndL1LwleEM= +cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= +cloud.google.com/go/cloudbuild v1.15.0/go.mod h1:eIXYWmRt3UtggLnFGx4JvXcMj4kShhVzGndL1LwleEM= +cloud.google.com/go/cloudbuild v1.15.1 h1:ZB6oOmJo+MTov9n629fiCrO9YZPOg25FZvQ7gIHu5ng= +cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28u/k8f99qKals= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0 h1:E7v4TpDGUyEm1C/4KIrpVSOCTm0P6vWdHT0I4mostRA= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/clouddms v1.6.1 h1:rjR1nV6oVf2aNNB7B5uz1PDIlBjlOiBgR+q5n7bbB7M= +cloud.google.com/go/clouddms v1.6.1/go.mod h1:Ygo1vL52Ov4TBZQquhz5fiw2CQ58gvu+PlS6PVXCpZI= +cloud.google.com/go/clouddms v1.7.0/go.mod h1:MW1dC6SOtI/tPNCciTsXtsGNEM0i0OccykPvv3hiYeM= +cloud.google.com/go/clouddms v1.7.1/go.mod h1:o4SR8U95+P7gZ/TX+YbJxehOCsM+fe6/brlrFquiszk= +cloud.google.com/go/clouddms v1.7.2/go.mod h1:Rk32TmWmHo64XqDvW7jgkFQet1tUKNVzs7oajtJT3jU= +cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= +cloud.google.com/go/clouddms v1.7.3/go.mod h1:fkN2HQQNUYInAU3NQ3vRLkV2iWs8lIdmBKOx4nrL6Hc= +cloud.google.com/go/clouddms v1.7.4 h1:Sr0Zo5EAcPQiCBgHWICg3VGkcdS/LLP1d9SR7qQBM/s= +cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFotwdE79DieY= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0 h1:uK5k6abf4yligFgYFnG0ni8msai/dSv6mDmiBulU0hU= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/cloudtasks v1.11.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= +cloud.google.com/go/cloudtasks v1.12.1 h1:cMh9Q6dkvh+Ry5LAPbD/U2aw6KAqdiU6FttwhbTo69w= +cloud.google.com/go/cloudtasks v1.12.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= +cloud.google.com/go/cloudtasks v1.12.2/go.mod h1:A7nYkjNlW2gUoROg1kvJrQGhJP/38UaWwsnuBDOBVUk= +cloud.google.com/go/cloudtasks v1.12.3/go.mod h1:GPVXhIOSGEaR+3xT4Fp72ScI+HjHffSS4B8+BaBB5Ys= +cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= +cloud.google.com/go/cloudtasks v1.12.4/go.mod h1:BEPu0Gtt2dU6FxZHNqqNdGqIG86qyWKBPGnsb7udGY0= +cloud.google.com/go/cloudtasks v1.12.6 h1:EUt1hIZ9bLv8Iz9yWaCrqgMnIU+Tdh0yXM1MMVGhjfE= +cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= +cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= +cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0 h1:jXIpfcH/VYSE1SYcPzO0n1VVb+sAamiLOgCw45JbOQk= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/contactcenterinsights v1.9.1/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= +cloud.google.com/go/contactcenterinsights v1.10.0 h1:YR2aPedGVQPpFBZXJnPkqRj8M//8veIZZH5ZvICoXnI= +cloud.google.com/go/contactcenterinsights v1.10.0/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= +cloud.google.com/go/contactcenterinsights v1.11.0/go.mod h1:hutBdImE4XNZ1NV4vbPJKSFOnQruhC5Lj9bZqWMTKiU= +cloud.google.com/go/contactcenterinsights v1.11.1/go.mod h1:FeNP3Kg8iteKM80lMwSk3zZZKVxr+PGnAId6soKuXwE= +cloud.google.com/go/contactcenterinsights v1.11.2/go.mod h1:A9PIR5ov5cRcd28KlDbmmXE8Aay+Gccer2h4wzkYFso= +cloud.google.com/go/contactcenterinsights v1.11.3/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/contactcenterinsights v1.12.0/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= +cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= +cloud.google.com/go/contactcenterinsights v1.13.0 h1:6Vs/YnDG5STGjlWMEjN/xtmft7MrOTOnOZYUZtGTx0w= +cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0 h1:NKlY/wCDapfVZlbVVaeuu2UZZED5Dy1z4Zx1KhEzm8c= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/container v1.22.1/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= +cloud.google.com/go/container v1.24.0 h1:N51t/cgQJFqDD/W7Mb+IvmAPHrf8AbPx7Bb7aF4lROE= +cloud.google.com/go/container v1.24.0/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= +cloud.google.com/go/container v1.26.0/go.mod h1:YJCmRet6+6jnYYRS000T6k0D0xUXQgBSaJ7VwI8FBj4= +cloud.google.com/go/container v1.26.1/go.mod h1:5smONjPRUxeEpDG7bMKWfDL4sauswqEtnBK1/KKpR04= +cloud.google.com/go/container v1.26.2/go.mod h1:YlO84xCt5xupVbLaMY4s3XNE79MUJ+49VmkInr6HvF4= +cloud.google.com/go/container v1.27.1/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/container v1.28.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= +cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/container v1.30.1/go.mod h1:vkbfX0EnAKL/vgVECs5BZn24e1cJROzgszJirRKQ4Bg= +cloud.google.com/go/container v1.31.0 h1:MAaNH7VRNPWEhvqOypq2j+7ONJKrKzon4v9nS3nLZe0= +cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0 h1:EQ4FFxNaEAg8PqQCO7bVQfWz9NVwZCUKaM1b3ycfx3U= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/containeranalysis v0.10.1 h1:SM/ibWHWp4TYyJMwrILtcBtYKObyupwOVeceI9pNblw= +cloud.google.com/go/containeranalysis v0.10.1/go.mod h1:Ya2jiILITMY68ZLPaogjmOMNkwsDrWBSTyBubGXO7j0= +cloud.google.com/go/containeranalysis v0.11.0/go.mod h1:4n2e99ZwpGxpNcz+YsFT1dfOHPQFGcAC8FN2M2/ne/U= +cloud.google.com/go/containeranalysis v0.11.1/go.mod h1:rYlUOM7nem1OJMKwE1SadufX0JP3wnXj844EtZAwWLY= +cloud.google.com/go/containeranalysis v0.11.2/go.mod h1:xibioGBC1MD2j4reTyV1xY1/MvKaz+fyM9ENWhmIeP8= +cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= +cloud.google.com/go/containeranalysis v0.11.3/go.mod h1:kMeST7yWFQMGjiG9K7Eov+fPNQcGhb8mXj/UcTiWw9U= +cloud.google.com/go/containeranalysis v0.11.4 h1:doJ0M1ljS4hS0D2UbHywlHGwB7sQLNrt9vFk9Zyi7vY= +cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0 h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP4c52gw= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/datacatalog v1.14.0/go.mod h1:h0PrGtlihoutNMp/uvwhawLQ9+c63Kz65UFqh49Yo+E= +cloud.google.com/go/datacatalog v1.14.1/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/datacatalog v1.16.0 h1:qVeQcw1Cz93/cGu2E7TYUPh8Lz5dn5Ws2siIuQ17Vng= +cloud.google.com/go/datacatalog v1.16.0/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/datacatalog v1.17.1/go.mod h1:nCSYFHgtxh2MiEktWIz71s/X+7ds/UT9kp0PC7waCzE= +cloud.google.com/go/datacatalog v1.18.0/go.mod h1:nCSYFHgtxh2MiEktWIz71s/X+7ds/UT9kp0PC7waCzE= +cloud.google.com/go/datacatalog v1.18.1/go.mod h1:TzAWaz+ON1tkNr4MOcak8EBHX7wIRX/gZKM+yTVsv+A= +cloud.google.com/go/datacatalog v1.18.2/go.mod h1:SPVgWW2WEMuWHA+fHodYjmxPiMqcOiWfhc9OD5msigk= +cloud.google.com/go/datacatalog v1.18.3/go.mod h1:5FR6ZIF8RZrtml0VUao22FxhdjkoG+a0866rEnObryM= +cloud.google.com/go/datacatalog v1.19.0 h1:rbYNmHwvAOOwnW2FPXYkaK3Mf1MmGqRzK0mMiIEyLdo= +cloud.google.com/go/datacatalog v1.19.0/go.mod h1:5FR6ZIF8RZrtml0VUao22FxhdjkoG+a0866rEnObryM= +cloud.google.com/go/datacatalog v1.19.2 h1:BV5sB7fPc8ccv/obwtHwQtCdLMAgI4KyaQWfkh8/mWg= +cloud.google.com/go/datacatalog v1.19.2/go.mod h1:2YbODwmhpLM4lOFe3PuEhHK9EyTzQJ5AXgIy7EDKTEE= +cloud.google.com/go/datacatalog v1.19.3 h1:A0vKYCQdxQuV4Pi0LL9p39Vwvg4jH5yYveMv50gU5Tw= +cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0 h1:eYyD9o/8Nm6EttsKZaEGD84xC17bNgSKCu0ZxwqUbpg= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataflow v0.9.1 h1:VzG2tqsk/HbmOtq/XSfdF4cBvUWRK+S+oL9k4eWkENQ= +cloud.google.com/go/dataflow v0.9.1/go.mod h1:Wp7s32QjYuQDWqJPFFlnBKhkAtiFpMTdg00qGbnIHVw= +cloud.google.com/go/dataflow v0.9.2/go.mod h1:vBfdBZ/ejlTaYIGB3zB4T08UshH70vbtZeMD+urnUSo= +cloud.google.com/go/dataflow v0.9.3/go.mod h1:HI4kMVjcHGTs3jTHW/kv3501YW+eloiJSLxkJa/vqFE= +cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= +cloud.google.com/go/dataflow v0.9.4/go.mod h1:4G8vAkHYCSzU8b/kmsoR2lWyHJD85oMJPHMtan40K8w= +cloud.google.com/go/dataflow v0.9.5 h1:RYHtcPhmE664+F0Je46p+NvFbG8z//KCXp+uEqB4jZU= +cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0 h1:Dyk+fufup1FR6cbHjFpMuP4SfPiF3LI3JtoIIALoq48= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/dataform v0.8.1 h1:xcWso0hKOoxeW72AjBSIp/UfkvpqHNzzS0/oygHlcqY= +cloud.google.com/go/dataform v0.8.1/go.mod h1:3BhPSiw8xmppbgzeBbmDvmSWlwouuJkXsXsb8UBih9M= +cloud.google.com/go/dataform v0.8.2/go.mod h1:X9RIqDs6NbGPLR80tnYoPNiO1w0wenKTb8PxxlhTMKM= +cloud.google.com/go/dataform v0.8.3/go.mod h1:8nI/tvv5Fso0drO3pEjtowz58lodx8MVkdV2q0aPlqg= +cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= +cloud.google.com/go/dataform v0.9.1/go.mod h1:pWTg+zGQ7i16pyn0bS1ruqIE91SdL2FDMvEYu/8oQxs= +cloud.google.com/go/dataform v0.9.2 h1:5e4eqGrd0iDTCg4Q+VlAao5j2naKAA7xRurNtwmUknU= +cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0 h1:sZjRnS3TWkGsu1LjYPFD/fHeMLZNXDK6PDHi2s2s/bk= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datafusion v1.7.1 h1:eX9CZoyhKQW6g1Xj7+RONeDj1mV8KQDKEB9KLELX9/8= +cloud.google.com/go/datafusion v1.7.1/go.mod h1:KpoTBbFmoToDExJUso/fcCiguGDk7MEzOWXUsJo0wsI= +cloud.google.com/go/datafusion v1.7.2/go.mod h1:62K2NEC6DRlpNmI43WHMWf9Vg/YvN6QVi8EVwifElI0= +cloud.google.com/go/datafusion v1.7.3/go.mod h1:eoLt1uFXKGBq48jy9LZ+Is8EAVLnmn50lNncLzwYokE= +cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= +cloud.google.com/go/datafusion v1.7.4/go.mod h1:BBs78WTOLYkT4GVZIXQCZT3GFpkpDN4aBY4NDX/jVlM= +cloud.google.com/go/datafusion v1.7.5 h1:HQ/BUOP8OIGJxuztpYvNvlb+/U+/Bfs9SO8tQbh61fk= +cloud.google.com/go/datafusion v1.7.5/go.mod h1:bYH53Oa5UiqahfbNK9YuYKteeD4RbQSNMx7JF7peGHc= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0 h1:ch4qA2yvddGRUrlfwrNJCr79qLqhS9QBwofPHfFlDIk= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/datalabeling v0.8.1 h1:zxsCD/BLKXhNuRssen8lVXChUj8VxF3ofN06JfdWOXw= +cloud.google.com/go/datalabeling v0.8.1/go.mod h1:XS62LBSVPbYR54GfYQsPXZjTW8UxCK2fkDciSrpRFdY= +cloud.google.com/go/datalabeling v0.8.2/go.mod h1:cyDvGHuJWu9U/cLDA7d8sb9a0tWLEletStu2sTmg3BE= +cloud.google.com/go/datalabeling v0.8.3/go.mod h1:tvPhpGyS/V7lqjmb3V0TaDdGvhzgR1JoW7G2bpi2UTI= +cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= +cloud.google.com/go/datalabeling v0.8.4/go.mod h1:Z1z3E6LHtffBGrNUkKwbwbDxTiXEApLzIgmymj8A3S8= +cloud.google.com/go/datalabeling v0.8.5 h1:GpIFRdm0qIZNsxqURFJwHt0ZBJZ0nF/mUVEigR7PH/8= +cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0 h1:RvoZ5T7gySwm1CHzAw7yY1QwwqaGswunmqEssPxU/AM= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataplex v1.8.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.9.0 h1:yoBWuuUZklYp7nx26evIhzq8+i/nvKYuZr1jka9EqLs= +cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.9.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.10.1/go.mod h1:1MzmBv8FvjYfc7vDdxhnLFNskikkB+3vl475/XdCDhs= +cloud.google.com/go/dataplex v1.10.2/go.mod h1:xdC8URdTrCrZMW6keY779ZT1cTOfV8KEPNsw+LTRT1Y= +cloud.google.com/go/dataplex v1.11.1/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.11.2/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.14.0 h1:/WhVTR4v/L6ACKjlz/9CqkxkrVh2z7C44CLMUf0f60A= +cloud.google.com/go/dataplex v1.14.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= +cloud.google.com/go/dataplex v1.14.1/go.mod h1:bWxQAbg6Smg+sca2+Ex7s8D9a5qU6xfXtwmq4BVReps= +cloud.google.com/go/dataplex v1.14.2 h1:fxIfdU8fxzR3clhOoNI7XFppvAmndxDu1AMH+qX9WKQ= +cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataproc/v2 v2.0.1 h1:4OpSiPMMGV3XmtPqskBU/RwYpj3yMFjtMLj/exi425Q= +cloud.google.com/go/dataproc/v2 v2.0.1/go.mod h1:7Ez3KRHdFGcfY7GcevBbvozX+zyWGcwLJvvAMwCaoZ4= +cloud.google.com/go/dataproc/v2 v2.2.0/go.mod h1:lZR7AQtwZPvmINx5J87DSOOpTfof9LVZju6/Qo4lmcY= +cloud.google.com/go/dataproc/v2 v2.2.1/go.mod h1:QdAJLaBjh+l4PVlVZcmrmhGccosY/omC1qwfQ61Zv/o= +cloud.google.com/go/dataproc/v2 v2.2.2/go.mod h1:aocQywVmQVF4i8CL740rNI/ZRpsaaC1Wh2++BJ7HEJ4= +cloud.google.com/go/dataproc/v2 v2.2.3/go.mod h1:G5R6GBc9r36SXv/RtZIVfB8SipI+xVn0bX5SxUzVYbY= +cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= +cloud.google.com/go/dataproc/v2 v2.3.0/go.mod h1:G5R6GBc9r36SXv/RtZIVfB8SipI+xVn0bX5SxUzVYbY= +cloud.google.com/go/dataproc/v2 v2.4.0 h1:/u81Fd+BvCLp+xjctI1DiWVJn6cn9/s3Akc8xPH02yk= +cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0 h1:yFzi/YU4YAdjyo7pXkBE2FeHbgz5OQQBVDdbErEHmVQ= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/dataqna v0.8.1 h1:ITpUJep04hC9V7C+gcK390HO++xesQFSUJ7S4nSnF3U= +cloud.google.com/go/dataqna v0.8.1/go.mod h1:zxZM0Bl6liMePWsHA8RMGAfmTG34vJMapbHAxQ5+WA8= +cloud.google.com/go/dataqna v0.8.2/go.mod h1:KNEqgx8TTmUipnQsScOoDpq/VlXVptUqVMZnt30WAPs= +cloud.google.com/go/dataqna v0.8.3/go.mod h1:wXNBW2uvc9e7Gl5k8adyAMnLush1KVV6lZUhB+rqNu4= +cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= +cloud.google.com/go/dataqna v0.8.4/go.mod h1:mySRKjKg5Lz784P6sCov3p1QD+RZQONRMRjzGNcFd0c= +cloud.google.com/go/dataqna v0.8.5 h1:9ybXs3nr9BzxSGC04SsvtuXaHY0qmJSLIpIAbZo9GqQ= +cloud.google.com/go/dataqna v0.8.5/go.mod h1:vgihg1mz6n7pb5q2YJF7KlXve6tCglInd6XO0JGOlWM= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastore v1.12.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastore v1.12.1/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastore v1.13.0 h1:ktbC66bOQB3HJPQe8qNI1/aiQ77PMu7hD4mzE6uxe3w= +cloud.google.com/go/datastore v1.13.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastore v1.14.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= +cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0 h1:BBCBTnWMDwwEzQQmipUXxATa7Cm7CA/gKjKcR2w35T0= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/datastream v1.9.1/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= +cloud.google.com/go/datastream v1.10.0 h1:ra/+jMv36zTAGPfi8TRne1hXme+UsKtdcK4j6bnqQiw= +cloud.google.com/go/datastream v1.10.0/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= +cloud.google.com/go/datastream v1.10.1/go.mod h1:7ngSYwnw95YFyTd5tOGBxHlOZiL+OtpjheqU7t2/s/c= +cloud.google.com/go/datastream v1.10.2/go.mod h1:W42TFgKAs/om6x/CdXX5E4oiAsKlH+e8MTGy81zdYt0= +cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= +cloud.google.com/go/datastream v1.10.3/go.mod h1:YR0USzgjhqA/Id0Ycu1VvZe8hEWwrkjuXrGbzeDOSEA= +cloud.google.com/go/datastream v1.10.4 h1:o1QDKMo/hk0FN7vhoUQURREuA0rgKmnYapB+1M+7Qz4= +cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0 h1:otshdKEbmsi1ELYeCKNYppwV0UH5xD05drSdBm7ouTk= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/deploy v1.11.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= +cloud.google.com/go/deploy v1.13.0 h1:A+w/xpWgz99EYzB6e31gMGAI/P5jTZ2UO7veQK5jQ8o= +cloud.google.com/go/deploy v1.13.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= +cloud.google.com/go/deploy v1.13.1/go.mod h1:8jeadyLkH9qu9xgO3hVWw8jVr29N1mnW42gRJT8GY6g= +cloud.google.com/go/deploy v1.14.1/go.mod h1:N8S0b+aIHSEeSr5ORVoC0+/mOPUysVt8ae4QkZYolAw= +cloud.google.com/go/deploy v1.14.2/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/deploy v1.15.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/deploy v1.16.0 h1:5OVjzm8MPC5kP+Ywbs0mdE0O7AXvAUXksSyHAyMFyMg= +cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g= +cloud.google.com/go/deploy v1.17.0 h1:P3SgJ+4rAktC2XqaI10G0ip/vzWluNBrC5VG0abMbLw= +cloud.google.com/go/deploy v1.17.0/go.mod h1:XBr42U5jIr64t92gcpOXxNrqL2PStQCXHuKK5GRUuYo= +cloud.google.com/go/deploy v1.17.1 h1:m27Ojwj03gvpJqCbodLYiVmE9x4/LrHGGMjzc0LBfM4= +cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0 h1:uVlKKzp6G/VtSW0E7IH1Y5o0H48/UOCmqksG2riYCwQ= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dialogflow v1.38.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4= +cloud.google.com/go/dialogflow v1.40.0 h1:sCJbaXt6ogSbxWQnERKAzos57f02PP6WkGbOZvXUdwc= +cloud.google.com/go/dialogflow v1.40.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4= +cloud.google.com/go/dialogflow v1.43.0/go.mod h1:pDUJdi4elL0MFmt1REMvFkdsUTYSHq+rTCS8wg0S3+M= +cloud.google.com/go/dialogflow v1.44.0/go.mod h1:pDUJdi4elL0MFmt1REMvFkdsUTYSHq+rTCS8wg0S3+M= +cloud.google.com/go/dialogflow v1.44.1/go.mod h1:n/h+/N2ouKOO+rbe/ZnI186xImpqvCVj2DdsWS/0EAk= +cloud.google.com/go/dialogflow v1.44.2/go.mod h1:QzFYndeJhpVPElnFkUXxdlptx0wPnBWLCBT9BvtC3/c= +cloud.google.com/go/dialogflow v1.44.3/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dialogflow v1.48.0 h1:I7UsYowDdNhFI7RCix1uoThDp+8ULHByOo4n1T96y1A= +cloud.google.com/go/dialogflow v1.48.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ= +cloud.google.com/go/dialogflow v1.48.1 h1:1Uq2jDJzjJ3M4xYB608FCCFHfW3JmrTmHIxRSd7JGmY= +cloud.google.com/go/dialogflow v1.48.1/go.mod h1:C1sjs2/g9cEwjCltkKeYp3FFpz8BOzNondEaAlCpt+A= +cloud.google.com/go/dialogflow v1.48.2/go.mod h1:7A2oDf6JJ1/+hdpnFRfb/RjJUOh2X3rhIa5P8wQSEX4= +cloud.google.com/go/dialogflow v1.49.0 h1:KqG0oxGE71qo0lRVyAoeBozefCvsMfcDzDjoLYSY0F4= +cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0 h1:1JoJqezlgu6NWCroBxr4rOZnwNFILXr4cB9dMaSKO4A= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/dlp v1.10.1 h1:tF3wsJ2QulRhRLWPzWVkeDz3FkOGVoMl6cmDUHtfYxw= +cloud.google.com/go/dlp v1.10.1/go.mod h1:IM8BWz1iJd8njcNcG0+Kyd9OPnqnRNkDV8j42VT5KOI= +cloud.google.com/go/dlp v1.10.2/go.mod h1:ZbdKIhcnyhILgccwVDzkwqybthh7+MplGC3kZVZsIOQ= +cloud.google.com/go/dlp v1.10.3/go.mod h1:iUaTc/ln8I+QT6Ai5vmuwfw8fqTk2kaz0FvCwhLCom0= +cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= +cloud.google.com/go/dlp v1.11.1/go.mod h1:/PA2EnioBeXTL/0hInwgj0rfsQb3lpE3R8XUJxqUNKI= +cloud.google.com/go/dlp v1.11.2 h1:lTipOuJaSjlYnnotPMbEhKURLC6GzCMDDzVbJAEbmYM= +cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0 h1:KM3Xh0QQyyEdC8Gs2vhZfU+rt6OCPF0dwVwxKgLmWfI= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/documentai v1.20.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E= +cloud.google.com/go/documentai v1.22.0 h1:dW8ex9yb3oT9s1yD2+yLcU8Zq15AquRZ+wd0U+TkxFw= +cloud.google.com/go/documentai v1.22.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E= +cloud.google.com/go/documentai v1.22.1/go.mod h1:LKs22aDHbJv7ufXuPypzRO7rG3ALLJxzdCXDPutw4Qc= +cloud.google.com/go/documentai v1.23.0/go.mod h1:LKs22aDHbJv7ufXuPypzRO7rG3ALLJxzdCXDPutw4Qc= +cloud.google.com/go/documentai v1.23.2/go.mod h1:Q/wcRT+qnuXOpjAkvOV4A+IeQl04q2/ReT7SSbytLSo= +cloud.google.com/go/documentai v1.23.4/go.mod h1:4MYAaEMnADPN1LPN5xboDR5QVB6AgsaxgFdJhitlE2Y= +cloud.google.com/go/documentai v1.23.5/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/documentai v1.23.7 h1:hlYieOXUwiJ7HpBR/vEPfr8nfSxveLVzbqbUkSK0c/4= +cloud.google.com/go/documentai v1.23.7/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= +cloud.google.com/go/documentai v1.23.8/go.mod h1:Vd/y5PosxCpUHmwC+v9arZyeMfTqBR9VIwOwIqQYYfA= +cloud.google.com/go/documentai v1.25.0 h1:lI62GMEEPO6vXJI9hj+G9WjOvnR0hEjvjokrnex4cxA= +cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0 h1:2ti/o9tlWL4N+wIuWUNH+LbfgpwxPr8J1sv9RHA4bYQ= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/domains v0.9.1 h1:rqz6KY7mEg7Zs/69U6m6LMbB7PxFDWmT3QWNXIqhHm0= +cloud.google.com/go/domains v0.9.1/go.mod h1:aOp1c0MbejQQ2Pjf1iJvnVyT+z6R6s8pX66KaCSDYfE= +cloud.google.com/go/domains v0.9.2/go.mod h1:3YvXGYzZG1Temjbk7EyGCuGGiXHJwVNmwIf+E/cUp5I= +cloud.google.com/go/domains v0.9.3/go.mod h1:29k66YNDLDY9LCFKpGFeh6Nj9r62ZKm5EsUJxAl84KU= +cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= +cloud.google.com/go/domains v0.9.4/go.mod h1:27jmJGShuXYdUNjyDG0SodTfT5RwLi7xmH334Gvi3fY= +cloud.google.com/go/domains v0.9.5 h1:Mml/R6s3vQQvFPpi/9oX3O5dRirgjyJ8cksK8N19Y7g= +cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0 h1:O0YVE5v+O0Q/ODXYsQHmHb+sYM8KNjGZw2pjX2Ws41c= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/edgecontainer v1.1.1 h1:zhHWnLzg6AqzE+I3gzJqiIwHfjEBhWctNQEzqb+FaRo= +cloud.google.com/go/edgecontainer v1.1.1/go.mod h1:O5bYcS//7MELQZs3+7mabRqoWQhXCzenBu0R8bz2rwk= +cloud.google.com/go/edgecontainer v1.1.2/go.mod h1:wQRjIzqxEs9e9wrtle4hQPSR1Y51kqN75dgF7UllZZ4= +cloud.google.com/go/edgecontainer v1.1.3/go.mod h1:Ll2DtIABzEfaxaVSbwj3QHFaOOovlDFiWVDu349jSsA= +cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= +cloud.google.com/go/edgecontainer v1.1.4/go.mod h1:AvFdVuZuVGdgaE5YvlL1faAoa1ndRR/5XhXZvPBHbsE= +cloud.google.com/go/edgecontainer v1.1.5 h1:tBY32km78ScpK2aOP84JoW/+wtpx5WluyPUSEE3270U= +cloud.google.com/go/edgecontainer v1.1.5/go.mod h1:rgcjrba3DEDEQAidT4yuzaKWTbkTI5zAMu3yy6ZWS0M= +cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0 h1:gIzEhCoOT7bi+6QZqZIzX1Erj4SswMPIteNvYVlu+pM= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/essentialcontacts v1.6.2 h1:OEJ0MLXXCW/tX1fkxzEZOsv/wRfyFsvDVNaHWBAvoV0= +cloud.google.com/go/essentialcontacts v1.6.2/go.mod h1:T2tB6tX+TRak7i88Fb2N9Ok3PvY3UNbUsMag9/BARh4= +cloud.google.com/go/essentialcontacts v1.6.3/go.mod h1:yiPCD7f2TkP82oJEFXFTou8Jl8L6LBRPeBEkTaO0Ggo= +cloud.google.com/go/essentialcontacts v1.6.4/go.mod h1:iju5Vy3d9tJUg0PYMd1nHhjV7xoCXaOAVabrwLaPBEM= +cloud.google.com/go/essentialcontacts v1.6.5 h1:S2if6wkjR4JCEAfDtIiYtD+sTz/oXjh2NUG4cgT1y/Q= +cloud.google.com/go/essentialcontacts v1.6.5/go.mod h1:jjYbPzw0x+yglXC890l6ECJWdYeZ5dlYACTFL0U/VuM= +cloud.google.com/go/essentialcontacts v1.6.6 h1:13eHn5qBnsawxI7mIrv4jRIEmQ1xg0Ztqw5ZGqtUNfA= +cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+rpL/aU5BCZLn4DYl1Q= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0 h1:fsJmNeqvqtk74FsaVDU6cH79lyZNCYP8Rrv7EhaB/PU= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/eventarc v1.12.1/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= +cloud.google.com/go/eventarc v1.13.0 h1:xIP3XZi0Xawx8DEfh++mE2lrIi5kQmCr/KcWhJ1q0J4= +cloud.google.com/go/eventarc v1.13.0/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= +cloud.google.com/go/eventarc v1.13.1/go.mod h1:EqBxmGHFrruIara4FUQ3RHlgfCn7yo1HYsu2Hpt/C3Y= +cloud.google.com/go/eventarc v1.13.2/go.mod h1:X9A80ShVu19fb4e5sc/OLV7mpFUKZMwfJFeeWhcIObM= +cloud.google.com/go/eventarc v1.13.3 h1:+pFmO4eu4dOVipSaFBLkmqrRYG94Xl/TQZFOeohkuqU= +cloud.google.com/go/eventarc v1.13.3/go.mod h1:RWH10IAZIRcj1s/vClXkBgMHwh59ts7hSWcqD3kaclg= +cloud.google.com/go/eventarc v1.13.4 h1:ORkd6/UV5FIdA8KZQDLNZYKS7BBOrj0p01DXPmT4tE4= +cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbbuTHC6JSY8s= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0 h1:ckTEXN5towyTMu4q0uQ1Mde/JwTHur0gXs8oaIZnKfw= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/filestore v1.7.1 h1:Eiz8xZzMJc5ppBWkuaod/PUdUZGCFR8ku0uS+Ah2fRw= +cloud.google.com/go/filestore v1.7.1/go.mod h1:y10jsorq40JJnjR/lQ8AfFbbcGlw3g+Dp8oN7i7FjV4= +cloud.google.com/go/filestore v1.7.2/go.mod h1:TYOlyJs25f/omgj+vY7/tIG/E7BX369triSPzE4LdgE= +cloud.google.com/go/filestore v1.7.3/go.mod h1:Qp8WaEERR3cSkxToxFPHh/b8AACkSut+4qlCjAmKTV0= +cloud.google.com/go/filestore v1.7.4/go.mod h1:S5JCxIbFjeBhWMTfIYH2Jx24J6BqjwpkkPl+nBA5DlI= +cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= +cloud.google.com/go/filestore v1.8.0/go.mod h1:S5JCxIbFjeBhWMTfIYH2Jx24J6BqjwpkkPl+nBA5DlI= +cloud.google.com/go/filestore v1.8.1 h1:X5G4y/vrUo1B8Nsz93qSWTMAcM8LXbGUldq33OdcdCw= +cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/firestore v1.11.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4= +cloud.google.com/go/firestore v1.12.0 h1:aeEA/N7DW7+l2u5jtkO8I0qv0D95YwjggD8kUHrTHO4= +cloud.google.com/go/firestore v1.12.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4= +cloud.google.com/go/firestore v1.13.0/go.mod h1:QojqqOh8IntInDUSTAh0c8ZsPYAr68Ma8c5DWOy8xb8= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0 h1:pPDqtsXG2g9HeOQLoquLbmvmb82Y4Ezdo1GXuotFoWg= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/functions v1.15.1 h1:LtAyqvO1TFmNLcROzHZhV0agEJfBi+zfMZsF4RT/a7U= +cloud.google.com/go/functions v1.15.1/go.mod h1:P5yNWUTkyU+LvW/S9O6V+V423VZooALQlqoXdoPz5AE= +cloud.google.com/go/functions v1.15.2/go.mod h1:CHAjtcR6OU4XF2HuiVeriEdELNcnvRZSk1Q8RMqy4lE= +cloud.google.com/go/functions v1.15.3/go.mod h1:r/AMHwBheapkkySEhiZYLDBwVJCdlRwsm4ieJu35/Ug= +cloud.google.com/go/functions v1.15.4 h1:ZjdiV3MyumRM6++1Ixu6N0VV9LAGlCX4AhW6Yjr1t+U= +cloud.google.com/go/functions v1.15.4/go.mod h1:CAsTc3VlRMVvx+XqXxKqVevguqJpnVip4DdonFsX28I= +cloud.google.com/go/functions v1.16.0 h1:IWVylmK5F6hJ3R5zaRW7jI5PrWhCvtBVU4axQLmXSo4= +cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapWQVpqtM29n8k= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gaming v1.10.1 h1:5qZmZEWzMf8GEFgm9NeC3bjFRpt7x4S6U7oLbxaf7N8= +cloud.google.com/go/gaming v1.10.1/go.mod h1:XQQvtfP8Rb9Rxnxm5wFVpAp9zCQkJi2bLIb7iHGwB3s= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0 h1:za3QZvw6ujR0uyqkhomKKKNoXDyqYGPJies3voUK8DA= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkebackup v1.3.0 h1:lgyrpdhtJKV7l1GM15YFt+OCyHMxsQZuSydyNmS0Pxo= +cloud.google.com/go/gkebackup v1.3.0/go.mod h1:vUDOu++N0U5qs4IhG1pcOnD1Mac79xWy6GoBFlWCWBU= +cloud.google.com/go/gkebackup v1.3.1/go.mod h1:vUDOu++N0U5qs4IhG1pcOnD1Mac79xWy6GoBFlWCWBU= +cloud.google.com/go/gkebackup v1.3.2/go.mod h1:OMZbXzEJloyXMC7gqdSB+EOEQ1AKcpGYvO3s1ec5ixk= +cloud.google.com/go/gkebackup v1.3.3/go.mod h1:eMk7/wVV5P22KBakhQnJxWSVftL1p4VBFLpv0kIft7I= +cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BDUBg= +cloud.google.com/go/gkebackup v1.3.4/go.mod h1:gLVlbM8h/nHIs09ns1qx3q3eaXcGSELgNu1DWXYz1HI= +cloud.google.com/go/gkebackup v1.3.5 h1:iuE8KNtTsPOc79qeWoNS8zOWoXPD9SAdOmwgxtlCmh8= +cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2/5Zt1t4Tvc= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0 h1:gXYKciHS/Lgq0GJ5Kc9SzPA35NGc3yqu6SkjonpEr2Q= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkeconnect v0.8.1 h1:a1ckRvVznnuvDWESM2zZDzSVFvggeBaVY5+BVB8tbT0= +cloud.google.com/go/gkeconnect v0.8.1/go.mod h1:KWiK1g9sDLZqhxB2xEuPV8V9NYzrqTUmQR9shJHpOZw= +cloud.google.com/go/gkeconnect v0.8.2/go.mod h1:6nAVhwchBJYgQCXD2pHBFQNiJNyAd/wyxljpaa6ZPrY= +cloud.google.com/go/gkeconnect v0.8.3/go.mod h1:i9GDTrfzBSUZGCe98qSu1B8YB8qfapT57PenIb820Jo= +cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= +cloud.google.com/go/gkeconnect v0.8.4/go.mod h1:84hZz4UMlDCKl8ifVW8layK4WHlMAFeq8vbzjU0yJkw= +cloud.google.com/go/gkeconnect v0.8.5 h1:17d+ZSSXKqG/RwZCq3oFMIWLPI8Zw3b8+a9/BEVlwH0= +cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0 h1:TqCSPsEBQ6oZSJgEYZ3XT8x2gUadbvfwI32YB0kuHCs= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkehub v0.14.1 h1:2BLSb8i+Co1P05IYCKATXy5yaaIw/ZqGvVSBTLdzCQo= +cloud.google.com/go/gkehub v0.14.1/go.mod h1:VEXKIJZ2avzrbd7u+zeMtW00Y8ddk/4V9511C9CQGTY= +cloud.google.com/go/gkehub v0.14.2/go.mod h1:iyjYH23XzAxSdhrbmfoQdePnlMj2EWcvnR+tHdBQsCY= +cloud.google.com/go/gkehub v0.14.3/go.mod h1:jAl6WafkHHW18qgq7kqcrXYzN08hXeK/Va3utN8VKg8= +cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= +cloud.google.com/go/gkehub v0.14.4/go.mod h1:Xispfu2MqnnFt8rV/2/3o73SK1snL8s9dYJ9G2oQMfc= +cloud.google.com/go/gkehub v0.14.5 h1:RboLNFzf9wEMSo7DrKVBlf+YhK/A/jrLN454L5Tz99Q= +cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0 h1:8I84Q4vl02rJRsFiinBxl7WCozfdLlUVBQuSrqr9Wtk= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gkemulticloud v0.6.1/go.mod h1:kbZ3HKyTsiwqKX7Yw56+wUGwwNZViRnxWK2DVknXWfw= +cloud.google.com/go/gkemulticloud v1.0.0 h1:MluqhtPVZReoriP5+adGIw+ij/RIeRik8KApCW2WMTw= +cloud.google.com/go/gkemulticloud v1.0.0/go.mod h1:kbZ3HKyTsiwqKX7Yw56+wUGwwNZViRnxWK2DVknXWfw= +cloud.google.com/go/gkemulticloud v1.0.1/go.mod h1:AcrGoin6VLKT/fwZEYuqvVominLriQBCKmbjtnbMjG8= +cloud.google.com/go/gkemulticloud v1.0.2/go.mod h1:+ee5VXxKb3H1l4LZAcgWB/rvI16VTNTrInWxDjAGsGo= +cloud.google.com/go/gkemulticloud v1.0.3/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= +cloud.google.com/go/gkemulticloud v1.1.0 h1:C2Suwn3uPz+Yy0bxVjTlsMrUCaDovkgvfdyIa+EnUOU= +cloud.google.com/go/gkemulticloud v1.1.0/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= +cloud.google.com/go/gkemulticloud v1.1.1 h1:rsSZAGLhyjyE/bE2ToT5fqo1qSW7S+Ubsc9jFOcbhSI= +cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q= +cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/grafeas v0.3.0 h1:oyTL/KjiUeBs9eYLw/40cpSZglUC+0F7X4iu/8t7NWs= +cloud.google.com/go/grafeas v0.3.0/go.mod h1:P7hgN24EyONOTMyeJH6DxG4zD7fwiYa5Q6GUgyFSOU8= +cloud.google.com/go/grafeas v0.3.4 h1:D4x32R/cHX3MTofKwirz015uEdVk4uAxvZkZCZkOrF4= +cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0 h1:1mvhXqJzV0Vg5Fa95QwckljODJJfDFXV4pn+iL50zzA= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/gsuiteaddons v1.6.1 h1:mi9jxZpzVjLQibTS/XfPZvl+Jr6D5Bs8pGqUjllRb00= +cloud.google.com/go/gsuiteaddons v1.6.1/go.mod h1:CodrdOqRZcLp5WOwejHWYBjZvfY0kOphkAKpF/3qdZY= +cloud.google.com/go/gsuiteaddons v1.6.2/go.mod h1:K65m9XSgs8hTF3X9nNTPi8IQueljSdYo9F+Mi+s4MyU= +cloud.google.com/go/gsuiteaddons v1.6.3/go.mod h1:sCFJkZoMrLZT3JTb8uJqgKPNshH2tfXeCwTFRebTq48= +cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= +cloud.google.com/go/gsuiteaddons v1.6.4/go.mod h1:rxtstw7Fx22uLOXBpsvb9DUbC+fiXs7rF4U29KHM/pE= +cloud.google.com/go/gsuiteaddons v1.6.5 h1:CZEbaBwmbYdhFw21Fwbo+C35HMe36fTE0FBSR4KSfWg= +cloud.google.com/go/gsuiteaddons v1.6.5/go.mod h1:Lo4P2IvO8uZ9W+RaC6s1JVxo42vgy+TX5a6hfBZ0ubs= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= +cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= +cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= +cloud.google.com/go/iam v1.1.4/go.mod h1:l/rg8l1AaA+VFMho/HYx2Vv6xinPSLMF8qfhRPIZ0L8= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1 h1:PxVHFuMxmSZyfntKXHXhd8bo82WJ+LcATenq7HLdVnU= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/iap v1.8.1 h1:X1tcp+EoJ/LGX6cUPt3W2D4H2Kbqq0pLAsldnsCjLlE= +cloud.google.com/go/iap v1.8.1/go.mod h1:sJCbeqg3mvWLqjZNsI6dfAtbbV1DL2Rl7e1mTyXYREQ= +cloud.google.com/go/iap v1.9.0/go.mod h1:01OFxd1R+NFrg78S+hoPV5PxEzv22HXaNqUUlmNHFuY= +cloud.google.com/go/iap v1.9.1/go.mod h1:SIAkY7cGMLohLSdBR25BuIxO+I4fXJiL06IBL7cy/5Q= +cloud.google.com/go/iap v1.9.2/go.mod h1:GwDTOs047PPSnwRD0Us5FKf4WDRcVvHg1q9WVkKBhdI= +cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= +cloud.google.com/go/iap v1.9.3/go.mod h1:DTdutSZBqkkOm2HEOTBzhZxh2mwwxshfD/h3yofAiCw= +cloud.google.com/go/iap v1.9.4 h1:94zirc2r4t6KzhAMW0R6Dme005eTP6yf7g6vN4IhRrA= +cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0 h1:fodnCDtOXuMmS8LTC2y3h8t24U8F3eKWfhi+3LY6Qf0= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/ids v1.4.1 h1:khXYmSoDDhWGEVxHl4c4IgbwSRR+qE/L4hzP3vaU9Hc= +cloud.google.com/go/ids v1.4.1/go.mod h1:np41ed8YMU8zOgv53MMMoCntLTn2lF+SUzlM+O3u/jw= +cloud.google.com/go/ids v1.4.2/go.mod h1:3vw8DX6YddRu9BncxuzMyWn0g8+ooUjI2gslJ7FH3vk= +cloud.google.com/go/ids v1.4.3/go.mod h1:9CXPqI3GedjmkjbMWCUhMZ2P2N7TUMzAkVXYEH2orYU= +cloud.google.com/go/ids v1.4.4 h1:VuFqv2ctf/A7AyKlNxVvlHTzjrEvumWaZflUzBPz/M4= +cloud.google.com/go/ids v1.4.4/go.mod h1:z+WUc2eEl6S/1aZWzwtVNWoSZslgzPxAboS0lZX0HjI= +cloud.google.com/go/ids v1.4.5 h1:xd4U7pgl3GHV+MABnv1BF4/Vy/zBF7CYC8XngkOLzag= +cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1UuBo= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0 h1:39W5BFSarRNZfVG0eXI5LYux+OVQT8GkgpHCnrZL2vM= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/iot v1.7.1 h1:yrH0OSmicD5bqGBoMlWG8UltzdLkYzNUwNVUVz7OT54= +cloud.google.com/go/iot v1.7.1/go.mod h1:46Mgw7ev1k9KqK1ao0ayW9h0lI+3hxeanz+L1zmbbbk= +cloud.google.com/go/iot v1.7.2/go.mod h1:q+0P5zr1wRFpw7/MOgDXrG/HVA+l+cSwdObffkrpnSg= +cloud.google.com/go/iot v1.7.3/go.mod h1:t8itFchkol4VgNbHnIq9lXoOOtHNR3uAACQMYbN9N4I= +cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= +cloud.google.com/go/iot v1.7.4/go.mod h1:3TWqDVvsddYBG++nHSZmluoCAVGr1hAcabbWZNKEZLk= +cloud.google.com/go/iot v1.7.5 h1:munTeBlbqI33iuTYgXy7S8lW2TCgi5l1hA4roSIY+EE= +cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.11.0/go.mod h1:hwdiYC0xjnWsKQQCQQmIQnS9asjYVSK6jtXm+zFqXLM= +cloud.google.com/go/kms v1.12.1/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/kms v1.15.0 h1:xYl5WEaSekKYN5gGRyhjvZKM22GVBBCzegGNVPy+aIs= +cloud.google.com/go/kms v1.15.0/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/kms v1.15.2/go.mod h1:3hopT4+7ooWRCjc2DxgnpESFxhIraaI2IpAVUEhbT/w= +cloud.google.com/go/kms v1.15.3/go.mod h1:AJdXqHxS2GlPyduM99s9iGqi2nwbviBbhV/hdmt4iOQ= +cloud.google.com/go/kms v1.15.4/go.mod h1:L3Sdj6QTHK8dfwK5D1JLsAyELsNMnd3tAIwGS4ltKpc= +cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= +cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= +cloud.google.com/go/kms v1.15.6/go.mod h1:yF75jttnIdHfGBoE51AKsD/Yqf+/jICzB9v1s1acsms= +cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM= +cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0 h1:7Ulo2mDk9huBoBi8zCE3ONOoBrL6UXfAI71CLQ9GEIM= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/language v1.10.1 h1:3MXeGEv8AlX+O2LyV4pO4NGpodanc26AmXwOuipEym0= +cloud.google.com/go/language v1.10.1/go.mod h1:CPp94nsdVNiQEt1CNjF5WkTcisLiHPyIbMhvR8H2AW0= +cloud.google.com/go/language v1.11.0/go.mod h1:uDx+pFDdAKTY8ehpWbiXyQdz8tDSYLJbQcXsCkjYyvQ= +cloud.google.com/go/language v1.11.1/go.mod h1:Xyid9MG9WOX3utvDbpX7j3tXDmmDooMyMDqgUVpH17U= +cloud.google.com/go/language v1.12.1/go.mod h1:zQhalE2QlQIxbKIZt54IASBzmZpN/aDASea5zl1l+J4= +cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= +cloud.google.com/go/language v1.12.2/go.mod h1:9idWapzr/JKXBBQ4lWqVX/hcadxB194ry20m/bTrhWc= +cloud.google.com/go/language v1.12.3 h1:iaJZg6K4j/2PvZZVcjeO/btcWWIllVRBhuTFjGO4LXs= +cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0 h1:uWrMjWTsGjLZpCTWEAzYvyXj+7fhiZST45u9AgasasI= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/lifesciences v0.9.1 h1:axkANGx1wiBXHiPcJZAE+TDjjYoJRIDzbHC/WYllCBU= +cloud.google.com/go/lifesciences v0.9.1/go.mod h1:hACAOd1fFbCGLr/+weUKRAJas82Y4vrL3O5326N//Wc= +cloud.google.com/go/lifesciences v0.9.2/go.mod h1:QHEOO4tDzcSAzeJg7s2qwnLM2ji8IRpQl4p6m5Z9yTA= +cloud.google.com/go/lifesciences v0.9.3/go.mod h1:gNGBOJV80IWZdkd+xz4GQj4mbqaz737SCLHn2aRhQKM= +cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= +cloud.google.com/go/lifesciences v0.9.4/go.mod h1:bhm64duKhMi7s9jR9WYJYvjAFJwRqNj+Nia7hF0Z7JA= +cloud.google.com/go/lifesciences v0.9.5 h1:gXvN70m2p+4zgJFzaz6gMKaxTuF9WJ0USYoMLWAOm8g= +cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= +cloud.google.com/go/longrunning v0.5.0/go.mod h1:0JNuqRShmscVAhIACGtskSAWtqtOoPkwP0YF1oVEchc= +cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/longrunning v0.5.2/go.mod h1:nqo6DQbNV2pXhGDbDMoN2bWz68MjZUzqv2YttZiveCs= +cloud.google.com/go/longrunning v0.5.3/go.mod h1:y/0ga59EYu58J6SHmmQOvekvND2qODbu8ywBBW7EK7Y= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= +cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0 h1:ZRQ4k21/jAhrHBVKl/AY7SjgzeJwG1iZa+mJ82P+VNg= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/managedidentities v1.6.1 h1:2/qZuOeLgUHorSdxSQGtnOu9xQkBn37+j+oZQv/KHJY= +cloud.google.com/go/managedidentities v1.6.1/go.mod h1:h/irGhTN2SkZ64F43tfGPMbHnypMbu4RB3yl8YcuEak= +cloud.google.com/go/managedidentities v1.6.2/go.mod h1:5c2VG66eCa0WIq6IylRk3TBW83l161zkFvCj28X7jn8= +cloud.google.com/go/managedidentities v1.6.3/go.mod h1:tewiat9WLyFN0Fi7q1fDD5+0N4VUoL0SCX0OTCthZq4= +cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= +cloud.google.com/go/managedidentities v1.6.4/go.mod h1:WgyaECfHmF00t/1Uk8Oun3CQ2PGUtjc3e9Alh79wyiM= +cloud.google.com/go/managedidentities v1.6.5 h1:+bpih1piZVLxla/XBqeSUzJBp8gv9plGHIMAI7DLpDM= +cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0 h1:mv9YaczD4oZBZkM5XJl6fXQ984IkJNHPwkc8MUsdkBo= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/maps v1.3.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= +cloud.google.com/go/maps v1.4.0 h1:PdfgpBLhAoSzZrQXP+/zBc78fIPLZSJp5y8+qSMn2UU= +cloud.google.com/go/maps v1.4.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= +cloud.google.com/go/maps v1.4.1/go.mod h1:BxSa0BnW1g2U2gNdbq5zikLlHUuHW0GFWh7sgML2kIY= +cloud.google.com/go/maps v1.5.1/go.mod h1:NPMZw1LJwQZYCfz4y+EIw+SI+24A4bpdFJqdKVr0lt4= +cloud.google.com/go/maps v1.6.1/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/maps v1.6.2 h1:WxxLo//b60nNFESefLgaBQevu8QGUmRV3+noOjCfIHs= +cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= +cloud.google.com/go/maps v1.6.3 h1:Qqs6Dza+PRp5CZO5AfgPnLwU1k3pp0IMFRDtLpT+aCA= +cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= +cloud.google.com/go/maps v1.6.4 h1:EVCZAiDvog9So46460BGbCasPhi613exoaQbpilMVlk= +cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0 h1:anPxH+/WWt8Yc3EdoEJhPMBRF7EhIdz426A+tuoA0OU= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/mediatranslation v0.8.1 h1:50cF7c1l3BanfKrpnTCaTvhf+Fo6kdF21DG0byG7gYU= +cloud.google.com/go/mediatranslation v0.8.1/go.mod h1:L/7hBdEYbYHQJhX2sldtTO5SZZ1C1vkapubj0T2aGig= +cloud.google.com/go/mediatranslation v0.8.2/go.mod h1:c9pUaDRLkgHRx3irYE5ZC8tfXGrMYwNZdmDqKMSfFp8= +cloud.google.com/go/mediatranslation v0.8.3/go.mod h1:F9OnXTy336rteOEywtY7FOqCk+J43o2RF638hkOQl4Y= +cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= +cloud.google.com/go/mediatranslation v0.8.4/go.mod h1:9WstgtNVAdN53m6TQa5GjIjLqKQPXe74hwSCxUP6nj4= +cloud.google.com/go/mediatranslation v0.8.5 h1:c76KdIXljQHSCb/Cy47S8H4s05A4zbK3pAFGzwcczZo= +cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtLsU+26hZhHEEy80fSs= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0 h1:8/VEmWCpnETCrBwS3z4MhT+tIdKgR1Z4Tr2tvYH32rg= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/memcache v1.10.1 h1:7lkLsF0QF+Mre0O/NvkD9Q5utUNwtzvIYjrOLOs0HO0= +cloud.google.com/go/memcache v1.10.1/go.mod h1:47YRQIarv4I3QS5+hoETgKO40InqzLP6kpNLvyXuyaA= +cloud.google.com/go/memcache v1.10.2/go.mod h1:f9ZzJHLBrmd4BkguIAa/l/Vle6uTHzHokdnzSWOdQ6A= +cloud.google.com/go/memcache v1.10.3/go.mod h1:6z89A41MT2DVAW0P4iIRdu5cmRTsbsFn4cyiIx8gbwo= +cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= +cloud.google.com/go/memcache v1.10.4/go.mod h1:v/d8PuC8d1gD6Yn5+I3INzLR01IDn0N4Ym56RgikSI0= +cloud.google.com/go/memcache v1.10.5 h1:yeDv5qxRedFosvpMSEswrqUsJM5OdWvssPHFliNFTc4= +cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvMa1I+9QsuMA= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0 h1:QCFhZVe2289KDBQ7WxaHV2rAmPrmRAdLC6gbjUd3HPo= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/metastore v1.11.1/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= +cloud.google.com/go/metastore v1.12.0 h1:+9DsxUOHvsqvC0ylrRc/JwzbXJaaBpfIK3tX0Lx8Tcc= +cloud.google.com/go/metastore v1.12.0/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= +cloud.google.com/go/metastore v1.13.0/go.mod h1:URDhpG6XLeh5K+Glq0NOt74OfrPKTwS62gEPZzb5SOk= +cloud.google.com/go/metastore v1.13.1/go.mod h1:IbF62JLxuZmhItCppcIfzBBfUFq0DIB9HPDoLgWrVOU= +cloud.google.com/go/metastore v1.13.2/go.mod h1:KS59dD+unBji/kFebVp8XU/quNSyo8b6N6tPGspKszA= +cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= +cloud.google.com/go/metastore v1.13.3/go.mod h1:K+wdjXdtkdk7AQg4+sXS8bRrQa9gcOr+foOMF2tqINE= +cloud.google.com/go/metastore v1.13.4 h1:dR7vqWXlK6IYR8Wbu9mdFfwlVjodIBhd1JRrpZftTEg= +cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0 h1:2qsrgXGVoRXpP7otZ14eE1I568zAa92sJSDPyOJvwjM= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.15.1 h1:65JhLMd+JiYnXr6j5Z63dUYCuOg770p8a/VC+gil/58= +cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM= +cloud.google.com/go/monitoring v1.16.0/go.mod h1:Ptp15HgAyM1fNICAojDMoNc/wUmn67mLHQfyqbw+poY= +cloud.google.com/go/monitoring v1.16.1/go.mod h1:6HsxddR+3y9j+o/cMJH6q/KJ/CBTvM/38L/1m7bTRJ4= +cloud.google.com/go/monitoring v1.16.2/go.mod h1:B44KGwi4ZCF8Rk/5n+FWeispDXoKSk9oss2QNlXJBgc= +cloud.google.com/go/monitoring v1.16.3/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= +cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= +cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= +cloud.google.com/go/monitoring v1.17.1/go.mod h1:SJzPMakCF0GHOuKEH/r4hxVKF04zl+cRPQyc3d/fqII= +cloud.google.com/go/monitoring v1.18.0 h1:NfkDLQDG2UR3WYZVQE8kwSbUIEyIqJUPl+aOQdFH1T4= +cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0 h1:ZD6b4Pk1jEtp/cx9nx0ZYcL3BKqDa+KixNDZ6Bjs1B8= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkconnectivity v1.12.1 h1:LnrYM6lBEeTq+9f2lR4DjBhv31EROSAQi/P5W4Q0AEc= +cloud.google.com/go/networkconnectivity v1.12.1/go.mod h1:PelxSWYM7Sh9/guf8CFhi6vIqf19Ir/sbfZRUwXh92E= +cloud.google.com/go/networkconnectivity v1.13.0/go.mod h1:SAnGPes88pl7QRLUen2HmcBSE9AowVAcdug8c0RSBFk= +cloud.google.com/go/networkconnectivity v1.14.0/go.mod h1:SAnGPes88pl7QRLUen2HmcBSE9AowVAcdug8c0RSBFk= +cloud.google.com/go/networkconnectivity v1.14.1/go.mod h1:LyGPXR742uQcDxZ/wv4EI0Vu5N6NKJ77ZYVnDe69Zug= +cloud.google.com/go/networkconnectivity v1.14.2/go.mod h1:5UFlwIisZylSkGG1AdwK/WZUaoz12PKu6wODwIbFzJo= +cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= +cloud.google.com/go/networkconnectivity v1.14.3/go.mod h1:4aoeFdrJpYEXNvrnfyD5kIzs8YtHg945Og4koAjHQek= +cloud.google.com/go/networkconnectivity v1.14.4 h1:GBfXFhLyPspnaBE3nI/BRjdhW8vcbpT9QjE/4kDCDdc= +cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJtuCXCfwfN+C6Niyj6ji1Po= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0 h1:8KWEUNGcpSX9WwZXq7FtciuNGPdPdPN/ruDm769yAEM= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networkmanagement v1.8.0 h1:/3xP37eMxnyvkfLrsm1nv1b2FbMMSAEAOlECTvoeCq4= +cloud.google.com/go/networkmanagement v1.8.0/go.mod h1:Ho/BUGmtyEqrttTgWEe7m+8vDdK74ibQc+Be0q7Fof0= +cloud.google.com/go/networkmanagement v1.9.0/go.mod h1:UTUaEU9YwbCAhhz3jEOHr+2/K/MrBk2XxOLS89LQzFw= +cloud.google.com/go/networkmanagement v1.9.1/go.mod h1:CCSYgrQQvW73EJawO2QamemYcOb57LvrDdDU51F0mcI= +cloud.google.com/go/networkmanagement v1.9.2/go.mod h1:iDGvGzAoYRghhp4j2Cji7sF899GnfGQcQRQwgVOWnDw= +cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= +cloud.google.com/go/networkmanagement v1.9.3/go.mod h1:y7WMO1bRLaP5h3Obm4tey+NquUvB93Co1oh4wpL+XcU= +cloud.google.com/go/networkmanagement v1.9.4 h1:aLV5GcosBNmd6M8+a0ekB0XlLRexv4fvnJJrYnqeBcg= +cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0 h1:sOc42Ig1K2LiKlzG71GUVloeSJ0J3mffEBYmvu+P0eo= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/networksecurity v0.9.1 h1:TBLEkMp3AE+6IV/wbIGRNTxnqLXHCTEQWoxRVC18TzY= +cloud.google.com/go/networksecurity v0.9.1/go.mod h1:MCMdxOKQ30wsBI1eI659f9kEp4wuuAueoC9AJKSPWZQ= +cloud.google.com/go/networksecurity v0.9.2/go.mod h1:jG0SeAttWzPMUILEHDUvFYdQTl8L/E/KC8iZDj85lEI= +cloud.google.com/go/networksecurity v0.9.3/go.mod h1:l+C0ynM6P+KV9YjOnx+kk5IZqMSLccdBqW6GUoF4p/0= +cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= +cloud.google.com/go/networksecurity v0.9.4/go.mod h1:E9CeMZ2zDsNBkr8axKSYm8XyTqNhiCHf1JO/Vb8mD1w= +cloud.google.com/go/networksecurity v0.9.5 h1:+caSxBTj0E8OYVh/5wElFdjEMO1S/rZtE1152Cepchc= +cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0 h1:Kg2K3K7CbSXYJHZ1aGQpf1xi5x2GUvQWf2sFVuiZh8M= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/notebooks v1.9.1 h1:CUqMNEtv4EHFnbogV+yGHQH5iAQLmijOx191innpOcs= +cloud.google.com/go/notebooks v1.9.1/go.mod h1:zqG9/gk05JrzgBt4ghLzEepPHNwE5jgPcHZRKhlC1A8= +cloud.google.com/go/notebooks v1.10.0/go.mod h1:SOPYMZnttHxqot0SGSFSkRrwE29eqnKPBJFqgWmiK2k= +cloud.google.com/go/notebooks v1.10.1/go.mod h1:5PdJc2SgAybE76kFQCWrTfJolCOUQXF97e+gteUUA6A= +cloud.google.com/go/notebooks v1.11.1/go.mod h1:V2Zkv8wX9kDCGRJqYoI+bQAaoVeE5kSiz4yYHd2yJwQ= +cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8kllbM= +cloud.google.com/go/notebooks v1.11.2/go.mod h1:z0tlHI/lREXC8BS2mIsUeR3agM1AkgLiS+Isov3SS70= +cloud.google.com/go/notebooks v1.11.3 h1:FH48boYmrWVQ6k0Mx/WrnNafXncT5iSYxA8CNyWTgy0= +cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZIVP4Qg3AQcgo= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1 h1:dj8O4VOJRB4CUwZXdmwNViH1OtI0WtWL867/lnYH248= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/optimization v1.4.1 h1:pEwOAmO00mxdbesCRSsfj8Sd4rKY9kBrYW7Vd3Pq7cA= +cloud.google.com/go/optimization v1.4.1/go.mod h1:j64vZQP7h9bO49m2rVaTVoNM0vEBEN5eKPUPbZyXOrk= +cloud.google.com/go/optimization v1.5.0/go.mod h1:evo1OvTxeBRBu6ydPlrIRizKY/LJKo/drDMMRKqGEUU= +cloud.google.com/go/optimization v1.5.1/go.mod h1:NC0gnUD5MWVAF7XLdoYVPmYYVth93Q6BUzqAq3ZwtV8= +cloud.google.com/go/optimization v1.6.1/go.mod h1:hH2RYPTTM9e9zOiTaYPTiGPcGdNZVnBSBxjIAJzUkqo= +cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= +cloud.google.com/go/optimization v1.6.2/go.mod h1:mWNZ7B9/EyMCcwNl1frUGEuY6CPijSkz88Fz2vwKPOY= +cloud.google.com/go/optimization v1.6.3 h1:63NZaWyN+5rZEKHPX4ACpw3BjgyeuY8+rCehiCMaGPY= +cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4VMUJmhrqVKtYA= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0 h1:Vw+CEXo8M/FZ1rb4EjcLv0gJqqw89b7+g+C/EmniTb8= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orchestration v1.8.1 h1:KmN18kE/xa1n91cM5jhCh7s1/UfIguSCisw7nTMUzgE= +cloud.google.com/go/orchestration v1.8.1/go.mod h1:4sluRF3wgbYVRqz7zJ1/EUNc90TTprliq9477fGobD8= +cloud.google.com/go/orchestration v1.8.2/go.mod h1:T1cP+6WyTmh6LSZzeUhvGf0uZVmJyTx7t8z7Vg87+A0= +cloud.google.com/go/orchestration v1.8.3/go.mod h1:xhgWAYqlbYjlz2ftbFghdyqENYW+JXuhBx9KsjMoGHs= +cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= +cloud.google.com/go/orchestration v1.8.4/go.mod h1:d0lywZSVYtIoSZXb0iFjv9SaL13PGyVOKDxqGxEf/qI= +cloud.google.com/go/orchestration v1.8.5 h1:YHgWMlrPttIVGItgGfuvO2KM7x+y9ivN/Yk92pMm1a4= +cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0 h1:XDriMWug7sd0kYT1QKofRpRHzjad0bK8Q8uA9q+XrU4= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/orgpolicy v1.11.0/go.mod h1:2RK748+FtVvnfuynxBzdnyu7sygtoZa1za/0ZfpOs1M= +cloud.google.com/go/orgpolicy v1.11.1 h1:I/7dHICQkNwym9erHqmlb50LRU588NPCvkfIY0Bx9jI= +cloud.google.com/go/orgpolicy v1.11.1/go.mod h1:8+E3jQcpZJQliP+zaFfayC2Pg5bmhuLK755wKhIIUCE= +cloud.google.com/go/orgpolicy v1.11.2/go.mod h1:biRDpNwfyytYnmCRWZWxrKF22Nkz9eNVj9zyaBdpm1o= +cloud.google.com/go/orgpolicy v1.11.3/go.mod h1:oKAtJ/gkMjum5icv2aujkP4CxROxPXsBbYGCDbPO8MM= +cloud.google.com/go/orgpolicy v1.11.4/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= +cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ= +cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= +cloud.google.com/go/orgpolicy v1.12.1 h1:2JbXigqBJVp8Dx5dONUttFqewu4fP0p3pgOdIZAhpYU= +cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0 h1:PkSQx4OHit5xz2bNyr11KGcaFccL5oqglFPdTboyqwQ= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/osconfig v1.12.0/go.mod h1:8f/PaYzoS3JMVfdfTubkowZYGmAhUCjjwnjqWI7NVBc= +cloud.google.com/go/osconfig v1.12.1 h1:dgyEHdfqML6cUW6/MkihNdTVc0INQst0qSE8Ou1ub9c= +cloud.google.com/go/osconfig v1.12.1/go.mod h1:4CjBxND0gswz2gfYRCUoUzCm9zCABp91EeTtWXyz0tE= +cloud.google.com/go/osconfig v1.12.2/go.mod h1:eh9GPaMZpI6mEJEuhEjUJmaxvQ3gav+fFEJon1Y8Iw0= +cloud.google.com/go/osconfig v1.12.3/go.mod h1:L/fPS8LL6bEYUi1au832WtMnPeQNT94Zo3FwwV1/xGM= +cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= +cloud.google.com/go/osconfig v1.12.4/go.mod h1:B1qEwJ/jzqSRslvdOCI8Kdnp0gSng0xW4LOnIebQomA= +cloud.google.com/go/osconfig v1.12.5 h1:Mo5jGAxOMKH/PmDY7fgY19yFcVbvwREb5D5zMPQjFfo= +cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0 h1:whP7vhpmc+ufZa90eVpkfbgzJRK/Xomjz+XCD4aGwWw= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/oslogin v1.10.1 h1:LdSuG3xBYu2Sgr3jTUULL1XCl5QBx6xwzGqzoDUw1j0= +cloud.google.com/go/oslogin v1.10.1/go.mod h1:x692z7yAue5nE7CsSnoG0aaMbNoRJRXO4sn73R+ZqAs= +cloud.google.com/go/oslogin v1.11.0/go.mod h1:8GMTJs4X2nOAUVJiPGqIWVcDaF0eniEto3xlOxaboXE= +cloud.google.com/go/oslogin v1.11.1/go.mod h1:OhD2icArCVNUxKqtK0mcSmKL7lgr0LVlQz+v9s1ujTg= +cloud.google.com/go/oslogin v1.12.1/go.mod h1:VfwTeFJGbnakxAY236eN8fsnglLiVXndlbcNomY4iZU= +cloud.google.com/go/oslogin v1.12.2 h1:NP/KgsD9+0r9hmHC5wKye0vJXVwdciv219DtYKYjgqE= +cloud.google.com/go/oslogin v1.12.2/go.mod h1:CQ3V8Jvw4Qo4WRhNPF0o+HAM4DiLuE27Ul9CX9g2QdY= +cloud.google.com/go/oslogin v1.13.0 h1:gbA/G4p+youIR4O/Rk6DU181QlBlpwPS16kvJwqEz8o= +cloud.google.com/go/oslogin v1.13.0/go.mod h1:xPJqLwpTZ90LSE5IL1/svko+6c5avZLluiyylMb/sRA= +cloud.google.com/go/oslogin v1.13.1 h1:1K4nOT5VEZNt7XkhaTXupBYos5HjzvJMfhvyD2wWdFs= +cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0 h1:l6tDkT7qAEV49MNEJkEJTB6vOO/onbSOcNtAT09HPuA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/phishingprotection v0.8.1 h1:aK/lNmSd1vtbft/vLe2g7edXK72sIQbqr2QyrZN/iME= +cloud.google.com/go/phishingprotection v0.8.1/go.mod h1:AxonW7GovcA8qdEk13NfHq9hNx5KPtfxXNeUxTDxB6I= +cloud.google.com/go/phishingprotection v0.8.2/go.mod h1:LhJ91uyVHEYKSKcMGhOa14zMMWfbEdxG032oT6ECbC8= +cloud.google.com/go/phishingprotection v0.8.3/go.mod h1:3B01yO7T2Ra/TMojifn8EoGd4G9jts/6cIO0DgDY9J8= +cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= +cloud.google.com/go/phishingprotection v0.8.4/go.mod h1:6b3kNPAc2AQ6jZfFHioZKg9MQNybDg4ixFd4RPZZ2nE= +cloud.google.com/go/phishingprotection v0.8.5 h1:DH3WFLzEoJdW/6xgsmoDqOwT1xddFi7gKu0QGZQhpGU= +cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HDbNre8L6Z0b7XMYFmX7I= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0 h1:yKAGC4p9O61ttZUswaq9GAn1SZnEzTd0vUYXD7ZBT7Y= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/policytroubleshooter v1.7.1/go.mod h1:0NaT5v3Ag1M7U5r0GfDCpUFkWd9YqpubBWsQlhanRv0= +cloud.google.com/go/policytroubleshooter v1.8.0 h1:XTMHy31yFmXgQg57CB3w9YQX8US7irxDX0Fl0VwlZyY= +cloud.google.com/go/policytroubleshooter v1.8.0/go.mod h1:tmn5Ir5EToWe384EuboTcVQT7nTag2+DuH3uHmKd1HU= +cloud.google.com/go/policytroubleshooter v1.9.0/go.mod h1:+E2Lga7TycpeSTj2FsH4oXxTnrbHJGRlKhVZBLGgU64= +cloud.google.com/go/policytroubleshooter v1.9.1/go.mod h1:MYI8i0bCrL8cW+VHN1PoiBTyNZTstCg2WUw2eVC4c4U= +cloud.google.com/go/policytroubleshooter v1.10.1/go.mod h1:5C0rhT3TDZVxAu8813bwmTvd57Phbl8mr9F4ipOsxEs= +cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= +cloud.google.com/go/policytroubleshooter v1.10.2/go.mod h1:m4uF3f6LseVEnMV6nknlN2vYGRb+75ylQwJdnOXfnv0= +cloud.google.com/go/policytroubleshooter v1.10.3 h1:c0WOzC6hz964QWNBkyKfna8A2jOIx1zzZa43Gx/P09o= +cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0 h1:EPEJ1DpEGXLDnmc7mnCAqFmkwUJbIsaLAiLHVOkkwtc= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/privatecatalog v0.9.1 h1:B/18xGo+E0EMS9LOEQ0zXz7F2asMgmVgTYGSI89MHOA= +cloud.google.com/go/privatecatalog v0.9.1/go.mod h1:0XlDXW2unJXdf9zFz968Hp35gl/bhF4twwpXZAW50JA= +cloud.google.com/go/privatecatalog v0.9.2/go.mod h1:RMA4ATa8IXfzvjrhhK8J6H4wwcztab+oZph3c6WmtFc= +cloud.google.com/go/privatecatalog v0.9.3/go.mod h1:K5pn2GrVmOPjXz3T26mzwXLcKivfIJ9R5N79AFCF9UE= +cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= +cloud.google.com/go/privatecatalog v0.9.4/go.mod h1:SOjm93f+5hp/U3PqMZAHTtBtluqLygrDrVO8X8tYtG0= +cloud.google.com/go/privatecatalog v0.9.5 h1:UZ0assTnATXSggoxUIh61RjTQ4P9zCMk/kEMbn0nMYA= +cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0 h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsub v1.32.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= +cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsub v1.34.0 h1:ZtPbfwfi5rLaPeSvDC29fFoE20/tQvGrUS6kVJZJvkU= +cloud.google.com/go/pubsub v1.34.0/go.mod h1:alj4l4rBg+N3YTFDDC+/YyFTs6JAjam2QfYsddcAW4c= +cloud.google.com/go/pubsub v1.36.1 h1:dfEPuGCHGbWUhaMCTHUFjfroILEkx55iUmKBZTP5f+Y= +cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0 h1:cb9fsrtpINtETHiJ3ECeaVzrfIVhcGjhhJEjybHXHao= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0 h1:6iOCujSNJ0YS7oNymI64hXsjGq60T4FK1zdLugxbzvU= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.2 h1:IGkbudobsTXAwmkEYOzPCQPApUCsN4Gbq3ndGVhHQpI= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.2/go.mod h1:kR0KjsJS7Jt1YSyWFkseQ756D45kaYNTlDPPaRAvDBU= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.0/go.mod h1:QuE8EdU9dEnesG8/kG3XuJyNsjEqMlMzg3v3scCJ46c= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.1/go.mod h1:JZYZJOeZjgSSTGP4uz7NlQ4/d1w5hGmksVgM0lbEij0= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.2/go.mod h1:kpaDBOpkwD4G0GVMzG1W6Doy1tFFC97XAV3xy+Rd/pw= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.3/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.4/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.2 h1:U3Wfq12X9cVMuTpsWDSURnXF0Z9hSPTHj+xsnXDRLsw= +cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0 h1:VibRFCwWXrFebEWKHfZAt2kta6pS7Tlimsnms0fjv7k= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommendationengine v0.8.1 h1:nMr1OEVHuDambRn+/y4RmNAmnR/pXCuHtH0Y4tCgGRQ= +cloud.google.com/go/recommendationengine v0.8.1/go.mod h1:MrZihWwtFYWDzE6Hz5nKcNz3gLizXVIDI/o3G1DLcrE= +cloud.google.com/go/recommendationengine v0.8.2/go.mod h1:QIybYHPK58qir9CV2ix/re/M//Ty10OxjnnhWdaKS1Y= +cloud.google.com/go/recommendationengine v0.8.3/go.mod h1:m3b0RZV02BnODE9FeSvGv1qibFo8g0OnmB/RMwYy4V8= +cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= +cloud.google.com/go/recommendationengine v0.8.4/go.mod h1:GEteCf1PATl5v5ZsQ60sTClUE0phbWmo3rQ1Js8louU= +cloud.google.com/go/recommendationengine v0.8.5 h1:ineqLswaCSBY0csYv5/wuXJMBlxATK6Xc5jJkpiTEdM= +cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0 h1:ZnFRY5R6zOVk2IDS1Jbv5Bw+DExCI5rFumsTnMXiu/A= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/recommender v1.10.1 h1:UKp94UH5/Lv2WXSQe9+FttqV07x/2p1hFTMMYVFtilg= +cloud.google.com/go/recommender v1.10.1/go.mod h1:XFvrE4Suqn5Cq0Lf+mCP6oBHD/yRMA8XxP5sb7Q7gpA= +cloud.google.com/go/recommender v1.11.0/go.mod h1:kPiRQhPyTJ9kyXPCG6u/dlPLbYfFlkwHNRwdzPVAoII= +cloud.google.com/go/recommender v1.11.1/go.mod h1:sGwFFAyI57v2Hc5LbIj+lTwXipGu9NW015rkaEM5B18= +cloud.google.com/go/recommender v1.11.2/go.mod h1:AeoJuzOvFR/emIcXdVFkspVXVTYpliRCmKNYDnyBv6Y= +cloud.google.com/go/recommender v1.11.3/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= +cloud.google.com/go/recommender v1.12.0 h1:tC+ljmCCbuZ/ybt43odTFlay91n/HLIhflvaOeb0Dh4= +cloud.google.com/go/recommender v1.12.0/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= +cloud.google.com/go/recommender v1.12.1 h1:LVLYS3r3u0MSCxQSDUtLSkporEGi9OAE6hGvayrZNPs= +cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0 h1:JoAd3SkeDt3rLFAAxEvw6wV4t+8y4ZzfZcZmddqphQ8= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/redis v1.13.1 h1:YrjQnCC7ydk+k30op7DSjSHw1yAYhqYXFcOq1bSXRYA= +cloud.google.com/go/redis v1.13.1/go.mod h1:VP7DGLpE91M6bcsDdMuyCm2hIpB6Vp2hI090Mfd1tcg= +cloud.google.com/go/redis v1.13.2/go.mod h1:0Hg7pCMXS9uz02q+LoEVl5dNHUkIQv+C/3L76fandSA= +cloud.google.com/go/redis v1.13.3/go.mod h1:vbUpCKUAZSYzFcWKmICnYgRAhTFg9r+djWqFxDYXi4U= +cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= +cloud.google.com/go/redis v1.14.1/go.mod h1:MbmBxN8bEnQI4doZPC1BzADU4HGocHBk2de3SbgOkqs= +cloud.google.com/go/redis v1.14.2 h1:QF0maEdVv0Fj/2roU8sX3NpiDBzP9ICYTO+5F32gQNo= +cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiRuEXwyQw= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0 h1:NRM0p+RJkaQF9Ee9JMnUV9BQ2QBIOq/v8M+Pbv/wmCs= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcemanager v1.9.1 h1:QIAMfndPOHR6yTmMUB0ZN+HSeRmPjR/21Smq5/xwghI= +cloud.google.com/go/resourcemanager v1.9.1/go.mod h1:dVCuosgrh1tINZ/RwBufr8lULmWGOkPS8gL5gqyjdT8= +cloud.google.com/go/resourcemanager v1.9.2/go.mod h1:OujkBg1UZg5lX2yIyMo5Vz9O5hf7XQOSV7WxqxxMtQE= +cloud.google.com/go/resourcemanager v1.9.3/go.mod h1:IqrY+g0ZgLsihcfcmqSe+RKp1hzjXwG904B92AwBz6U= +cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= +cloud.google.com/go/resourcemanager v1.9.4/go.mod h1:N1dhP9RFvo3lUfwtfLWVxfUWq8+KUQ+XLlHLH3BoFJ0= +cloud.google.com/go/resourcemanager v1.9.5 h1:AZWr1vWVDKGwfLsVhcN+vcwOz3xqqYxtmMa0aABCMms= +cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0 h1:8Dua37kQt27CCWHm4h/Q1XqCF6ByD7Ouu49xg95qJzI= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/resourcesettings v1.6.1 h1:Fdyq418U69LhvNPFdlEO29w+DRRjwDA4/pFamm4ksAg= +cloud.google.com/go/resourcesettings v1.6.1/go.mod h1:M7mk9PIZrC5Fgsu1kZJci6mpgN8o0IUzVx3eJU3y4Jw= +cloud.google.com/go/resourcesettings v1.6.2/go.mod h1:mJIEDd9MobzunWMeniaMp6tzg4I2GvD3TTmPkc8vBXk= +cloud.google.com/go/resourcesettings v1.6.3/go.mod h1:pno5D+7oDYkMWZ5BpPsb4SO0ewg3IXcmmrUZaMJrFic= +cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= +cloud.google.com/go/resourcesettings v1.6.4/go.mod h1:pYTTkWdv2lmQcjsthbZLNBP4QW140cs7wqA3DuqErVI= +cloud.google.com/go/resourcesettings v1.6.5 h1:BTr5MVykJwClASci/7Og4Qfx70aQ4n3epsNLj94ZYgw= +cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0 h1:1Dda2OpFNzIb4qWgFZjYlpP7sxX3aLeypKG6A3H4Yys= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/retail v1.14.1 h1:gYBrb9u/Hc5s5lUTFXX1Vsbc/9BEvgtioY6ZKaK0DK8= +cloud.google.com/go/retail v1.14.1/go.mod h1:y3Wv3Vr2k54dLNIrCzenyKG8g8dhvhncT2NcNjb/6gE= +cloud.google.com/go/retail v1.14.2/go.mod h1:W7rrNRChAEChX336QF7bnMxbsjugcOCPU44i5kbLiL8= +cloud.google.com/go/retail v1.14.3/go.mod h1:Omz2akDHeSlfCq8ArPKiBxlnRpKEBjUH386JYFLUvXo= +cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= +cloud.google.com/go/retail v1.14.4/go.mod h1:l/N7cMtY78yRnJqp5JW8emy7MB1nz8E4t2yfOmklYfg= +cloud.google.com/go/retail v1.15.1/go.mod h1:In9nSBOYhLbDGa87QvWlnE1XA14xBN2FpQRiRsUs9wU= +cloud.google.com/go/retail v1.16.0 h1:Fn1GuAua1c6crCGqfJ1qMxG1Xh10Tg/x5EUODEHMqkw= +cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0 h1:ydJQo+k+MShYnBfhaRHSZYeD/SQKZzZLAROyfpeD9zw= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/run v1.2.0 h1:kHeIG8q+N6Zv0nDkBjSOYfK2eWqa5FnaiDPH/7/HirE= +cloud.google.com/go/run v1.2.0/go.mod h1:36V1IlDzQ0XxbQjUx6IYbw8H3TJnWvhii963WW3B/bo= +cloud.google.com/go/run v1.3.0/go.mod h1:S/osX/4jIPZGg+ssuqh6GNgg7syixKe3YnprwehzHKU= +cloud.google.com/go/run v1.3.1/go.mod h1:cymddtZOzdwLIAsmS6s+Asl4JoXIDm/K1cpZTxV4Q5s= +cloud.google.com/go/run v1.3.2/go.mod h1:SIhmqArbjdU/D9M6JoHaAqnAMKLFtXaVdNeq04NjnVE= +cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= +cloud.google.com/go/run v1.3.3/go.mod h1:WSM5pGyJ7cfYyYbONVQBN4buz42zFqwG67Q3ch07iK4= +cloud.google.com/go/run v1.3.4 h1:m9WDA7DzTpczhZggwYlZcBWgCRb+kgSIisWn1sbw2rQ= +cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0 h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/scheduler v1.10.1 h1:yoZbZR8880KgPGLmACOMCiY2tPk+iX4V/dkxqTirlz8= +cloud.google.com/go/scheduler v1.10.1/go.mod h1:R63Ldltd47Bs4gnhQkmNDse5w8gBRrhObZ54PxgR2Oo= +cloud.google.com/go/scheduler v1.10.2/go.mod h1:O3jX6HRH5eKCA3FutMw375XHZJudNIKVonSCHv7ropY= +cloud.google.com/go/scheduler v1.10.3/go.mod h1:8ANskEM33+sIbpJ+R4xRfw/jzOG+ZFE8WVLy7/yGvbc= +cloud.google.com/go/scheduler v1.10.4/go.mod h1:MTuXcrJC9tqOHhixdbHDFSIuh7xZF2IysiINDuiq6NI= +cloud.google.com/go/scheduler v1.10.5 h1:eMEettHlFhG5pXsoHouIM5nRT+k+zU4+GUvRtnxhuVI= +cloud.google.com/go/scheduler v1.10.5/go.mod h1:MTuXcrJC9tqOHhixdbHDFSIuh7xZF2IysiINDuiq6NI= +cloud.google.com/go/scheduler v1.10.6 h1:5U8iXLoQ03qOB+ZXlAecU7fiE33+u3QiM9nh4cd0eTE= +cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0 h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/secretmanager v1.11.1 h1:cLTCwAjFh9fKvU6F13Y4L9vPcx9yiWPyWXE4+zkuEQs= +cloud.google.com/go/secretmanager v1.11.1/go.mod h1:znq9JlXgTNdBeQk9TBW/FnR/W4uChEKGeqQWAJ8SXFw= +cloud.google.com/go/secretmanager v1.11.2/go.mod h1:MQm4t3deoSub7+WNwiC4/tRYgDBHJgJPvswqQVB1Vss= +cloud.google.com/go/secretmanager v1.11.3/go.mod h1:0bA2o6FabmShrEy328i67aV+65XoUFFSmVeLBn/51jI= +cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= +cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= +cloud.google.com/go/secretmanager v1.11.5 h1:82fpF5vBBvu9XW4qj0FU2C6qVMtj1RM/XHwKXUEAfYY= +cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0 h1:PYvDxopRQBfYAXKAuDpFCKBvDOWPWzp9k/H5nB3ud3o= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/security v1.15.1 h1:jR3itwycg/TgGA0uIgTItcVhA55hKWiNJxaNNpQJaZE= +cloud.google.com/go/security v1.15.1/go.mod h1:MvTnnbsWnehoizHi09zoiZob0iCHVcL4AUBj76h9fXA= +cloud.google.com/go/security v1.15.2/go.mod h1:2GVE/v1oixIRHDaClVbHuPcZwAqFM28mXuAKCfMgYIg= +cloud.google.com/go/security v1.15.3/go.mod h1:gQ/7Q2JYUZZgOzqKtw9McShH+MjNvtDpL40J1cT+vBs= +cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= +cloud.google.com/go/security v1.15.4/go.mod h1:oN7C2uIZKhxCLiAAijKUCuHLZbIt/ghYEo8MqwD/Ty4= +cloud.google.com/go/security v1.15.5 h1:wTKJQ10j8EYgvE8Y+KhovxDRVDk2iv/OsxZ6GrLP3kE= +cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0 h1:AF3c2s3awNTMoBtMX3oCUoOMmGlYxGOeuXSYHNBkf14= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/securitycenter v1.23.0 h1:XOGJ9OpnDtqg8izd7gYk/XUhj8ytjIalyjjsR6oyG0M= +cloud.google.com/go/securitycenter v1.23.0/go.mod h1:8pwQ4n+Y9WCWM278R8W3nF65QtY172h4S8aXyI9/hsQ= +cloud.google.com/go/securitycenter v1.23.1/go.mod h1:w2HV3Mv/yKhbXKwOCu2i8bCuLtNP1IMHuiYQn4HJq5s= +cloud.google.com/go/securitycenter v1.24.1/go.mod h1:3h9IdjjHhVMXdQnmqzVnM7b0wMn/1O/U20eWVpMpZjI= +cloud.google.com/go/securitycenter v1.24.2/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= +cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= +cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= +cloud.google.com/go/securitycenter v1.24.4 h1:/5jjkZ+uGe8hZ7pvd7pO30VW/a+pT2MrrdgOqjyucKQ= +cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0 h1:SJwk0XX2e26o25ObYUORXx6torSFiYgsGkWSkZgkoSU= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicedirectory v1.10.1/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= +cloud.google.com/go/servicedirectory v1.11.0 h1:pBWpjCFVGWkzVTkqN3TBBIqNSoSHY86/6RL0soSQ4z8= +cloud.google.com/go/servicedirectory v1.11.0/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= +cloud.google.com/go/servicedirectory v1.11.1/go.mod h1:tJywXimEWzNzw9FvtNjsQxxJ3/41jseeILgwU/QLrGI= +cloud.google.com/go/servicedirectory v1.11.2/go.mod h1:KD9hCLhncWRV5jJphwIpugKwM5bn1x0GyVVD4NO8mGg= +cloud.google.com/go/servicedirectory v1.11.3 h1:5niCMfkw+jifmFtbBrtRedbXkJm3fubSR/KHbxSJZVM= +cloud.google.com/go/servicedirectory v1.11.3/go.mod h1:LV+cHkomRLr67YoQy3Xq2tUXBGOs5z5bPofdq7qtiAw= +cloud.google.com/go/servicedirectory v1.11.4 h1:da7HFI1229kyzIyuVEzHXip0cw0d+E0s8mjQby0WN+k= +cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShYLD96NfEsWNHyitj1qM= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0 h1:wT0Uw7ib7+AgZST9eCDygwTJn4+bHMDtZo5fh7kGWDU= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/shell v1.7.1 h1:aHbwH9LSqs4r2rbay9f6fKEls61TAjT63jSyglsw7sI= +cloud.google.com/go/shell v1.7.1/go.mod h1:u1RaM+huXFaTojTbW4g9P5emOrrmLE69KrxqQahKn4g= +cloud.google.com/go/shell v1.7.2/go.mod h1:KqRPKwBV0UyLickMn0+BY1qIyE98kKyI216sH/TuHmc= +cloud.google.com/go/shell v1.7.3/go.mod h1:cTTEz/JdaBsQAeTQ3B6HHldZudFoYBOqjteev07FbIc= +cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= +cloud.google.com/go/shell v1.7.4/go.mod h1:yLeXB8eKLxw0dpEmXQ/FjriYrBijNsONpwnWsdPqlKM= +cloud.google.com/go/shell v1.7.5 h1:3Fq2hzO0ZSyaqBboJrFkwwf/qMufDtqwwA6ep8EZxEI= +cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0 h1:7VdjZ8zj4sHbDw55atp5dfY6kn1j9sam9DRNpPQhqR4= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.47.0 h1:aqiMP8dhsEXgn9K5EZBWxPG7dxIiyM2VaikqeU4iteg= +cloud.google.com/go/spanner v1.47.0/go.mod h1:IXsJwVW2j4UKs0eYDqodab6HgGuA1bViSqW4uH9lfUI= +cloud.google.com/go/spanner v1.49.0/go.mod h1:eGj9mQGK8+hkgSVbHNQ06pQ4oS+cyc4tXXd6Dif1KoM= +cloud.google.com/go/spanner v1.50.0/go.mod h1:eGj9mQGK8+hkgSVbHNQ06pQ4oS+cyc4tXXd6Dif1KoM= +cloud.google.com/go/spanner v1.51.0/go.mod h1:c5KNo5LQ1X5tJwma9rSQZsXNBDNvj4/n8BVc3LNahq0= +cloud.google.com/go/spanner v1.53.0/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws= +cloud.google.com/go/spanner v1.54.0 h1:ttU+lhARPF/iZE3OkCpmfsemCz9mLaqBhGPd3Qub2sQ= +cloud.google.com/go/spanner v1.54.0/go.mod h1:wZvSQVBgngF0Gq86fKup6KIYmN2be7uOKjtK97X+bQU= +cloud.google.com/go/spanner v1.55.0 h1:YF/A/k73EMYCjp8wcJTpkE+TcrWutHRlsCtlRSfWS64= +cloud.google.com/go/spanner v1.55.0/go.mod h1:HXEznMUVhC+PC+HDyo9YFG2Ajj5BQDkcbqB9Z2Ffxi0= +cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0= +cloud.google.com/go/spanner v1.57.0 h1:fJq+ZfQUDHE+cy1li0bJA8+sy2oiSGhuGqN5nqVaZdU= +cloud.google.com/go/spanner v1.57.0/go.mod h1:aXQ5QDdhPRIqVhYmnkAdwPYvj/DRN0FguclhEWw+jOo= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0 h1:JEVoWGNnTF128kNty7T4aG4eqv2z86yiMJPT9Zjp+iw= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/speech v1.17.1/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= +cloud.google.com/go/speech v1.19.0 h1:MCagaq8ObV2tr1kZJcJYgXYbIn8Ai5rp42tyGYw9rls= +cloud.google.com/go/speech v1.19.0/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= +cloud.google.com/go/speech v1.19.1/go.mod h1:WcuaWz/3hOlzPFOVo9DUsblMIHwxP589y6ZMtaG+iAA= +cloud.google.com/go/speech v1.19.2/go.mod h1:2OYFfj+Ch5LWjsaSINuCZsre/789zlcCI3SY4oAi2oI= +cloud.google.com/go/speech v1.20.1/go.mod h1:wwolycgONvfz2EDU8rKuHRW3+wc9ILPsAWoikBEWavY= +cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= +cloud.google.com/go/speech v1.21.0/go.mod h1:wwolycgONvfz2EDU8rKuHRW3+wc9ILPsAWoikBEWavY= +cloud.google.com/go/speech v1.21.1 h1:nuFc+Kj5B8de75nN4FdPyUbI2SiBoHZG6BLurXL56Q0= +cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8= +cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4= +cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0 h1:5T+PM+3ECU3EY2y9Brv0Sf3oka8pKmsCfpQ07+91G9o= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/storagetransfer v1.10.0 h1:+ZLkeXx0K0Pk5XdDmG0MnUVqIR18lllsihU/yq39I8Q= +cloud.google.com/go/storagetransfer v1.10.0/go.mod h1:DM4sTlSmGiNczmV6iZyceIh2dbs+7z2Ayg6YAiQlYfA= +cloud.google.com/go/storagetransfer v1.10.1/go.mod h1:rS7Sy0BtPviWYTTJVWCSV4QrbBitgPeuK4/FKa4IdLs= +cloud.google.com/go/storagetransfer v1.10.2/go.mod h1:meIhYQup5rg9juQJdyppnA/WLQCOguxtk1pr3/vBWzA= +cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= +cloud.google.com/go/storagetransfer v1.10.3/go.mod h1:Up8LY2p6X68SZ+WToswpQbQHnJpOty/ACcMafuey8gc= +cloud.google.com/go/storagetransfer v1.10.4 h1:dy4fL3wO0VABvzM05ycMUPFHxTPbJz9Em8ikAJVqSbI= +cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0 h1:nI9sVZPjMKiO2q3Uu0KhTDVov3Xrlpt63fghP9XjyEM= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/talent v1.6.2 h1:j46ZgD6N2YdpFPux9mc7OAf4YK3tiBCsbLKc8rQx+bU= +cloud.google.com/go/talent v1.6.2/go.mod h1:CbGvmKCG61mkdjcqTcLOkb2ZN1SrQI8MDyma2l7VD24= +cloud.google.com/go/talent v1.6.3/go.mod h1:xoDO97Qd4AK43rGjJvyBHMskiEf3KulgYzcH6YWOVoo= +cloud.google.com/go/talent v1.6.4/go.mod h1:QsWvi5eKeh6gG2DlBkpMaFYZYrYUnIpo34f6/V5QykY= +cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= +cloud.google.com/go/talent v1.6.5/go.mod h1:Mf5cma696HmE+P2BWJ/ZwYqeJXEeU0UqjHFXVLadEDI= +cloud.google.com/go/talent v1.6.6 h1:JssV0CE3FNujuSWn7SkosOzg7qrMxVnt6txOfGcMSa4= +cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0 h1:H4g1ULStsbVtalbZGktyzXzw6jP26RjVGYx9RaYjBzc= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/texttospeech v1.7.1 h1:S/pR/GZT9p15R7Y2dk2OXD/3AufTct/NSxT4a7nxByw= +cloud.google.com/go/texttospeech v1.7.1/go.mod h1:m7QfG5IXxeneGqTapXNxv2ItxP/FS0hCZBwXYqucgSk= +cloud.google.com/go/texttospeech v1.7.2/go.mod h1:VYPT6aTOEl3herQjFHYErTlSZJ4vB00Q2ZTmuVgluD4= +cloud.google.com/go/texttospeech v1.7.3/go.mod h1:Av/zpkcgWfXlDLRYob17lqMstGZ3GqlvJXqKMp2u8so= +cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvMhtad5Q= +cloud.google.com/go/texttospeech v1.7.4/go.mod h1:vgv0002WvR4liGuSd5BJbWy4nDn5Ozco0uJymY5+U74= +cloud.google.com/go/texttospeech v1.7.5 h1:dxY2Q5mHCbrGa3oPR2O3PCicdnvKa1JmwGQK36EFLOw= +cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0 h1:/34T6CbSi+kTv5E19Q9zbU/ix8IviInZpzwz3rsFE+A= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/tpu v1.6.1 h1:kQf1jgPY04UJBYYjNUO+3GrZtIb57MfGAW2bwgLbR3A= +cloud.google.com/go/tpu v1.6.1/go.mod h1:sOdcHVIgDEEOKuqUoi6Fq53MKHJAtOwtz0GuKsWSH3E= +cloud.google.com/go/tpu v1.6.2/go.mod h1:NXh3NDwt71TsPZdtGWgAG5ThDfGd32X1mJ2cMaRlVgU= +cloud.google.com/go/tpu v1.6.3/go.mod h1:lxiueqfVMlSToZY1151IaZqp89ELPSrk+3HIQ5HRkbY= +cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= +cloud.google.com/go/tpu v1.6.4/go.mod h1:NAm9q3Rq2wIlGnOhpYICNI7+bpBebMJbh0yyp3aNw1Y= +cloud.google.com/go/tpu v1.6.5 h1:C8YyYda8WtNdBoCgFwwBzZd+S6+EScHOxM/z1h0NNp8= +cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkMHSSs= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0 h1:olxC0QHC59zgJVALtgqfD9tGk0lfeCP5/AGXL3Px/no= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.10.1 h1:EwGdOLCNfYOOPtgqo+D2sDLZmRCEO1AagRTJCU6ztdg= +cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk= +cloud.google.com/go/trace v1.10.2/go.mod h1:NPXemMi6MToRFcSxRl2uDnu/qAlAQ3oULUphcHGh1vA= +cloud.google.com/go/trace v1.10.3/go.mod h1:Ke1bgfc73RV3wUFml+uQp7EsDw4dGaETLxB7Iq/r4CY= +cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= +cloud.google.com/go/trace v1.10.4/go.mod h1:Nso99EDIK8Mj5/zmB+iGr9dosS/bzWCJ8wGmE6TXNWY= +cloud.google.com/go/trace v1.10.5 h1:0pr4lIKJ5XZFYD9GtxXEWr0KkVeigc3wlGpZco0X1oA= +cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0 h1:GvLP4oQ4uPdChBmBaUSa/SaZxCdyWELtlAaKzpHsXdA= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.8.1/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/translate v1.8.2 h1:PQHamiOzlehqLBJMnM72lXk/OsMQewZB12BKJ8zXrU0= +cloud.google.com/go/translate v1.8.2/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/translate v1.9.0/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/translate v1.9.1/go.mod h1:TWIgDZknq2+JD4iRcojgeDtqGEp154HN/uL6hMvylS8= +cloud.google.com/go/translate v1.9.2/go.mod h1:E3Tc6rUTsQkVrXW6avbUhKJSr7ZE3j7zNmqzXKHqRrY= +cloud.google.com/go/translate v1.9.3/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= +cloud.google.com/go/translate v1.10.0 h1:tncNaKmlZnayMMRX/mMM2d5AJftecznnxVBD4w070NI= +cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= +cloud.google.com/go/translate v1.10.1 h1:upovZ0wRMdzZvXnu+RPam41B0mRJ+coRXFP2cYFJ7ew= +cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0 h1:upIbnGI0ZgACm58HPjAeBMleW3sl5cT84AbYQ8PWOgM= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.17.1/go.mod h1:9qmqPqw/Ib2tLqaeHgtakU+l5TcJxCJbhFXM7UJjVzU= +cloud.google.com/go/video v1.19.0 h1:BRyyS+wU+Do6VOXnb8WfPr42ZXti9hzmLKLUCkggeK4= +cloud.google.com/go/video v1.19.0/go.mod h1:9qmqPqw/Ib2tLqaeHgtakU+l5TcJxCJbhFXM7UJjVzU= +cloud.google.com/go/video v1.20.0/go.mod h1:U3G3FTnsvAGqglq9LxgqzOiBc/Nt8zis8S+850N2DUM= +cloud.google.com/go/video v1.20.1/go.mod h1:3gJS+iDprnj8SY6pe0SwLeC5BUW80NjhwX7INWEuWGU= +cloud.google.com/go/video v1.20.2/go.mod h1:lrixr5JeKNThsgfM9gqtwb6Okuqzfo4VrY2xynaViTA= +cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= +cloud.google.com/go/video v1.20.3/go.mod h1:TnH/mNZKVHeNtpamsSPygSR0iHtvrR/cW1/GDjN5+GU= +cloud.google.com/go/video v1.20.4 h1:TXwotxkShP1OqgKsbd+b8N5hrIHavSyLGvYnLGCZ7xc= +cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1f/+QrW0= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0 h1:Uh5BdoET8XXqXX2uXIahGb+wTKbLkGH7s4GXR58RrG8= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/videointelligence v1.11.1 h1:MBMWnkQ78GQnRz5lfdTAbBq/8QMCF3wahgtHh3s/J+k= +cloud.google.com/go/videointelligence v1.11.1/go.mod h1:76xn/8InyQHarjTWsBR058SmlPCwQjgcvoW0aZykOvo= +cloud.google.com/go/videointelligence v1.11.2/go.mod h1:ocfIGYtIVmIcWk1DsSGOoDiXca4vaZQII1C85qtoplc= +cloud.google.com/go/videointelligence v1.11.3/go.mod h1:tf0NUaGTjU1iS2KEkGWvO5hRHeCkFK3nPo0/cOZhZAo= +cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= +cloud.google.com/go/videointelligence v1.11.4/go.mod h1:kPBMAYsTPFiQxMLmmjpcZUMklJp3nC9+ipJJtprccD8= +cloud.google.com/go/videointelligence v1.11.5 h1:mYaWH8uhUCXLJCN3gdXswKzRa2+lK0zN6/KsIubm6pE= +cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0 h1:8C8RXUJoflCI4yVdqhTy9tRyygSHmp60aP363z23HKg= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vision/v2 v2.7.2 h1:ccK6/YgPfGHR/CyESz1mvIbsht5Y2xRsWCPqmTNydEw= +cloud.google.com/go/vision/v2 v2.7.2/go.mod h1:jKa8oSYBWhYiXarHPvP4USxYANYUEdEsQrloLjrSwJU= +cloud.google.com/go/vision/v2 v2.7.3/go.mod h1:V0IcLCY7W+hpMKXK1JYE0LV5llEqVmj+UJChjvA1WsM= +cloud.google.com/go/vision/v2 v2.7.4/go.mod h1:ynDKnsDN/0RtqkKxQZ2iatv3Dm9O+HfRb5djl7l4Vvw= +cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vision/v2 v2.7.5/go.mod h1:GcviprJLFfK9OLf0z8Gm6lQb6ZFUulvpZws+mm6yPLM= +cloud.google.com/go/vision/v2 v2.7.6/go.mod h1:ZkvWTVNPBU3YZYzgF9Y1jwEbD1NBOCyJn0KFdQfE6Bw= +cloud.google.com/go/vision/v2 v2.8.0 h1:W52z1b6LdGI66MVhE70g/NFty9zCYYcjdKuycqmlhtg= +cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0 h1:Azs5WKtfOC8pxvkyrDvt7J0/4DYBch0cVbuFfCCFt5k= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmmigration v1.7.1 h1:gnjIclgqbEMc+cF5IJuPxp53wjBIlqZ8h9hE8Rkwp7A= +cloud.google.com/go/vmmigration v1.7.1/go.mod h1:WD+5z7a/IpZ5bKK//YmT9E047AD+rjycCAvyMxGJbro= +cloud.google.com/go/vmmigration v1.7.2/go.mod h1:iA2hVj22sm2LLYXGPT1pB63mXHhrH1m/ruux9TwWLd8= +cloud.google.com/go/vmmigration v1.7.3/go.mod h1:ZCQC7cENwmSWlwyTrZcWivchn78YnFniEQYRWQ65tBo= +cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= +cloud.google.com/go/vmmigration v1.7.4/go.mod h1:yBXCmiLaB99hEl/G9ZooNx2GyzgsjKnw5fWcINRgD70= +cloud.google.com/go/vmmigration v1.7.5 h1:5v9RT2vWyuw3pK2ox0HQpkoftO7Q7/8591dTxxQc79g= +cloud.google.com/go/vmmigration v1.7.5/go.mod h1:pkvO6huVnVWzkFioxSghZxIGcsstDvYiVCxQ9ZH3eYI= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0 h1:b0NBu7S294l0gmtrT0nOJneMYgZapr5x9tVWvgDoVEM= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vmwareengine v0.4.1/go.mod h1:Px64x+BvjPZwWuc4HdmVhoygcXqEkGHXoa7uyfTgSI0= +cloud.google.com/go/vmwareengine v1.0.0 h1:qsJ0CPlOQu/3MFBGklu752v3AkD+Pdu091UmXJ+EjTA= +cloud.google.com/go/vmwareengine v1.0.0/go.mod h1:Px64x+BvjPZwWuc4HdmVhoygcXqEkGHXoa7uyfTgSI0= +cloud.google.com/go/vmwareengine v1.0.1/go.mod h1:aT3Xsm5sNx0QShk1Jc1B8OddrxAScYLwzVoaiXfdzzk= +cloud.google.com/go/vmwareengine v1.0.2/go.mod h1:xMSNjIk8/itYrz1JA8nV3Ajg4L4n3N+ugP8JKzk3OaA= +cloud.google.com/go/vmwareengine v1.0.3 h1:WY526PqM6QNmFHSqe2sRfK6gRpzWjmL98UFkql2+JDM= +cloud.google.com/go/vmwareengine v1.0.3/go.mod h1:QSpdZ1stlbfKtyt6Iu19M6XRxjmXO+vb5a/R6Fvy2y4= +cloud.google.com/go/vmwareengine v1.1.1 h1:EGdDi9QbqThfZq3ILcDK5g+m9jTevc34AY5tACx5v7k= +cloud.google.com/go/vmwareengine v1.1.1/go.mod h1:nMpdsIVkUrSaX8UvmnBhzVzG7PPvNYc5BszcvIVudYs= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0 h1:FOe6CuiQD3BhHJWt7E8QlbBcaIzVRddupwJlp7eqmn4= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/vpcaccess v1.7.1 h1:ram0GzjNWElmbxXMIzeOZUkQ9J8ZAahD6V8ilPGqX0Y= +cloud.google.com/go/vpcaccess v1.7.1/go.mod h1:FogoD46/ZU+JUBX9D606X21EnxiszYi2tArQwLY4SXs= +cloud.google.com/go/vpcaccess v1.7.2/go.mod h1:mmg/MnRHv+3e8FJUjeSibVFvQF1cCy2MsFaFqxeY1HU= +cloud.google.com/go/vpcaccess v1.7.3/go.mod h1:YX4skyfW3NC8vI3Fk+EegJnlYFatA+dXK4o236EUCUc= +cloud.google.com/go/vpcaccess v1.7.4 h1:zbs3V+9ux45KYq8lxxn/wgXole6SlBHHKKyZhNJoS+8= +cloud.google.com/go/vpcaccess v1.7.4/go.mod h1:lA0KTvhtEOb/VOdnH/gwPuOzGgM+CWsmGu6bb4IoMKk= +cloud.google.com/go/vpcaccess v1.7.5 h1:XyL6hTLtEM/eE4F1GEge8xUN9ZCkiVWn44K/YA7z1rQ= +cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcpIWz6P/ziig= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0 h1:IY+L2+UwxcVm2zayMAtBhZleecdIFLiC+QJMzgb0kT0= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/webrisk v1.9.1 h1:Ssy3MkOMOnyRV5H2bkMQ13Umv7CwB/kugo3qkAX83Fk= +cloud.google.com/go/webrisk v1.9.1/go.mod h1:4GCmXKcOa2BZcZPn6DCEvE7HypmEJcJkr4mtM+sqYPc= +cloud.google.com/go/webrisk v1.9.2/go.mod h1:pY9kfDgAqxUpDBOrG4w8deLfhvJmejKB0qd/5uQIPBc= +cloud.google.com/go/webrisk v1.9.3/go.mod h1:RUYXe9X/wBDXhVilss7EDLW9ZNa06aowPuinUOPCXH8= +cloud.google.com/go/webrisk v1.9.4 h1:iceR3k0BCRZgf2D/NiKviVMFfuNC9LmeNLtxUFRB/wI= +cloud.google.com/go/webrisk v1.9.4/go.mod h1:w7m4Ib4C+OseSr2GL66m0zMBywdrVNTDKsdEsfMl7X0= +cloud.google.com/go/webrisk v1.9.5 h1:251MvGuC8wisNN7+jqu9DDDZAi38KiMXxOpA/EWy4dE= +cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0 h1:AHC1xmaNMOZtNqxI9Rmm87IJEyPaRkOxeI0gpAacXGk= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/websecurityscanner v1.6.1 h1:CfEF/vZ+xXyAR3zC9iaC/QRdf1MEgS20r5UR17Q4gOg= +cloud.google.com/go/websecurityscanner v1.6.1/go.mod h1:Njgaw3rttgRHXzwCB8kgCYqv5/rGpFCsBOvPbYgszpg= +cloud.google.com/go/websecurityscanner v1.6.2/go.mod h1:7YgjuU5tun7Eg2kpKgGnDuEOXWIrh8x8lWrJT4zfmas= +cloud.google.com/go/websecurityscanner v1.6.3/go.mod h1:x9XANObUFR+83Cya3g/B9M/yoHVqzxPnFtgF8yYGAXw= +cloud.google.com/go/websecurityscanner v1.6.4 h1:5Gp7h5j7jywxLUp6NTpjNPkgZb3ngl0tUSw6ICWvtJQ= +cloud.google.com/go/websecurityscanner v1.6.4/go.mod h1:mUiyMQ+dGpPPRkHgknIZeCzSHJ45+fY4F52nZFDHm2o= +cloud.google.com/go/websecurityscanner v1.6.5 h1:YqWZrZYabG88TZt7364XWRJGhxmxhony2ZUyZEYMF2k= +cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0 h1:FfGp9w0cYnaKZJhUOMqCOJCYT/WlvYBfTQhFWV3sRKI= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/workflows v1.11.1 h1:2akeQ/PgtRhrNuD/n1WvJd5zb7YyuDZrlOanBj2ihPg= +cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= +cloud.google.com/go/workflows v1.12.0/go.mod h1:PYhSk2b6DhZ508tj8HXKaBh+OFe+xdl0dHF/tJdzPQM= +cloud.google.com/go/workflows v1.12.1/go.mod h1:5A95OhD/edtOhQd/O741NSfIMezNTbCwLM1P1tBRGHM= +cloud.google.com/go/workflows v1.12.2/go.mod h1:+OmBIgNqYJPVggnMo9nqmizW0qEXHhmnAzK/CnBqsHc= +cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= +cloud.google.com/go/workflows v1.12.3/go.mod h1:fmOUeeqEwPzIU81foMjTRQIdwQHADi/vEr1cx9R1m5g= +cloud.google.com/go/workflows v1.12.4 h1:uHNmUiatTbPQ4H1pabwfzpfEYD4BBnqDHqMm2IesOh4= +cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e h1:GwCVItFUPxwdsEYnlUcJ6PJxOjTeFFCKOh6QWg4oAzQ= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e/go.mod h1:ApHceQLLwcOkCEXM1+DyCXTHEJhNGDpJ2kmV6axsx24= +cuelang.org/go v0.8.0 h1:fO1XPe/SUGtc7dhnGnTPbpIDoQm/XxhDtoSF7jzO01c= +cuelang.org/go v0.8.0/go.mod h1:CoDbYolfMms4BhWUlhD+t5ORnihR7wvjcfgyO9lL5FI= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= +github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= +github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/KarpelesLab/reflink v1.0.1 h1:d+tdjliwOCqvub9bl0Y02GxahWkNqejNb3TZTTUcQWA= +github.com/KarpelesLab/reflink v1.0.1/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +github.com/Microsoft/cosesign1go v0.0.1/go.mod h1:fj1svfAxQeQNJ2SLaQu8mHx2rtPIsloZl065GqLF3io= +github.com/Microsoft/cosesign1go v1.4.0 h1:VdiqzsilEE6t1GQi98I/h0WpVFM7AyMEeyP8ud7V/BY= +github.com/Microsoft/cosesign1go v1.4.0/go.mod h1:1La/HcGw19rRLhPW0S6u55K6LKfti+GQSgGCtrfhVe8= +github.com/Microsoft/didx509go v0.0.2/go.mod h1:F+msvNlKCEm3RgUE3kRpi7E+6hdR6r5PtOLWQKYfGbs= +github.com/Microsoft/didx509go v0.0.3 h1:n/owuFOXVzCEzSyzivMEolKEouBm9G0NrEDgoTekM8A= +github.com/Microsoft/didx509go v0.0.3/go.mod h1:wWt+iQsLzn3011+VfESzznLIp/Owhuj7rLF7yLglYbk= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.3/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.9/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/Microsoft/hcsshim v0.12.0-rc.0 h1:wX/F5huJxH9APBkhKSEAqaiZsuBvbbDnyBROZAqsSaY= +github.com/Microsoft/hcsshim v0.12.0-rc.0/go.mod h1:rvOnw3YlfoNnEp45wReUngvsXbwRW+AFQ10GVjG1kMU= +github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= +github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/sarama v1.30.1 h1:z47lP/5PBw2UVKf1lvfS5uWXaJws6ggk9PLnKEHtZiQ= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae h1:ePgznFqEG1v3AjMklnK8H7BSc++FDSo7xfK9K7Af+0Y= +github.com/StudioSol/set v1.0.0 h1:G27J71la+Da08WidabBkoRrvPLTa4cdCn0RjvyJ5WKQ= +github.com/StudioSol/set v1.0.0/go.mod h1:hIUNZPo6rEGF43RlPXHq7Fjmf+HkVJBqAjtK7Z9LoIU= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs= +github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 h1:+XfOU14S4bGuwyvCijJwhhBIjYN+YXS18jrCY2EzJaY= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/kingpin v0.0.0-20200323085623-b6657d9477a6/go.mod h1:b6br6/pDFSfMkBgC96TbpOji05q5pa+v5rIlS0Y6XtI= +github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= +github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= +github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= +github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= +github.com/alexflint/go-filemutex v1.2.0/go.mod h1:mYyQSWvw9Tx2/H2n9qXPb52tTYfE0pZAWcBq5mK025c= +github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUymZXdOcM= +github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U= +github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +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/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= +github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= +github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/DiJbg= +github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= +github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw= +github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY= +github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q= +github.com/apecloud/kubeblocks v0.8.4/go.mod h1:xQpzfMy4V+WJI5IKBWB02qsKAlVR3nAE71CPkAs2uOs= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +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/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/authzed/controller-idioms v0.7.0 h1:HhNMUBb8hJzYqY3mhen3B2AC5nsIem3fBe0tC/AAOHo= +github.com/authzed/controller-idioms v0.7.0/go.mod h1:0B/PmqCguKv8b3azSMF+HdyKpKr2o3UAZ5eo12Ze8Fo= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI= +github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.257 h1:HwelXYZZ8c34uFFhgVw3ybu2gB5fkk8KLj2idTvzZb8= +github.com/aws/aws-sdk-go v1.44.257/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= +github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.9.1 h1:ZbovGV/qo40nrOJ4q8G33AGICzaPI45FHQWJ9650pF4= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= +github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= +github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 h1:u1KOU1S15ufyZqmH/rA3POkiRH6EcDANHj2xHRzq+zc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 h1:w/fPGB0t5rWwA43mux4e9ozFSH5zF1moQemlA131PWc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd h1:jZtX5jh5IOMu0fpOTC3ayh6QGSPJ/KWOv1lgPvbRw1M= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542 h1:nYXb+3jF6Oq/j8R/y90XrKpreCxIalBWfeyeKymgOPk= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bhmj/jsonslice v1.1.2 h1:Lzen2S9iG3HsESpiIAnTM7Obs1QiTz83ZXa5YrpTTWI= +github.com/bhmj/jsonslice v1.1.2/go.mod h1:O3ZoA0zdEefdbk1dkU5aWPOA36zQhhS/HV6RQFLTlnU= +github.com/bhmj/xpression v0.9.1 h1:N7bX/nWx9oFi/zsiMTx2ehoRApTDAWdQadq/5o2wMGk= +github.com/bhmj/xpression v0.9.1/go.mod h1:j9oYmEXJjeL9mrgW1+ZDBKJXnbupsCPGhlO9J5YhS1Q= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c h1:+0HFd5KSZ/mm3JmhmrDukiId5iR6w4+BdFtfSy4yWIc= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bshuster-repo/logrus-logstash-hook v1.0.2 h1:JYRWo+QGnQdedgshosug9hxpPYTB9oJ1ZZD3fY31alU= +github.com/bshuster-repo/logrus-logstash-hook v1.0.2/go.mod h1:HgYntJprnHSPaF9VPPPLP1L5S1vMWxRfa1J+vzDrDTw= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/bugsnag-go v2.1.2+incompatible h1:E7dor84qzwUO8KdCM68CZwq9QOSR7HXlLx3Wj5vui2s= +github.com/bugsnag/bugsnag-go v2.1.2+incompatible/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= +github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytecodealliance/wasmtime-go v0.36.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= +github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/casbin/casbin v1.7.0 h1:PuzlE8w0JBg/DhIqnkF1Dewf3z+qmUZMVN07PonvVUQ= +github.com/casbin/casbin/v2 v2.37.0 h1:/poEwPSovi4bTOcP752/CsTQiRz2xycyVKFG7GUhbDw= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI= +github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw= +github.com/checkpoint-restore/checkpointctl v1.3.0 h1:bNz5b6s+lxFdG5ZGDba3qSkBtXDDTCG2494dfAbQJ4E= +github.com/checkpoint-restore/checkpointctl v1.3.0/go.mod h1:dqZH4wDvbjnsqFGK2LdUDk21yFQ1dCAtzgRMlG44KDM= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA= +github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= +github.com/checkpoint-restore/go-criu/v7 v7.2.0 h1:qGiWA4App1gGlEfIJ68WR9jbezV9J7yZdjzglezcqKo= +github.com/checkpoint-restore/go-criu/v7 v7.2.0/go.mod h1:u0LCWLg0w4yqqu14aXhiB4YD3a1qd8EcCEg7vda5dwo= +github.com/chmduquesne/rollinghash v4.0.0+incompatible/go.mod h1:Uc2I36RRfTAf7Dge82bi3RU0OQUmXT9iweIcPqvr8A0= +github.com/chromedp/cdproto v0.0.0-20220321060548-7bc2623472b3/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.8.0/go.mod h1:odCVV9o9i7HUKwHMFz9Y7T6s4Kbcz4GOyPlwKWopI9Q= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= +github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/container-orchestrated-devices/container-device-interface v0.5.4/go.mod h1:DjE95rfPiiSmG7uVXtg0z6MnPm/Lx4wxKCIts0ZE0vg= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs/v2 v2.0.0 h1:FN4wsx7KQrYoLXN7uLP0vBV4oVWHOIKDRQ1G2Z0oL5M= +github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= +github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= +github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= +github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= +github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= +github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg= +github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= +github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8= +github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4= +github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= +github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= +github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ= +github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= +github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= +github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= +github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI= +github.com/containerd/containerd/v2 v2.1.3/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-cni v1.1.6/go.mod h1:BWtoWl5ghVymxu6MBjg79W9NZrCRyHIdUtk4cauMe34= +github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= +github.com/containerd/imgcrypt v1.1.4/go.mod h1:LorQnPtzL/T0IyCeftcsMEO7AqxUDbdO8j/tSUpgxvo= +github.com/containerd/imgcrypt v1.1.7/go.mod h1:FD8gqIcX5aTotCtOmjeCsi3A1dHmTZpnMISGKSczt4k= +github.com/containerd/imgcrypt v1.1.8 h1:ZS7TuywcRNLoHpU0g+v4/PsKynl6TYlw5xDVWWoIyFA= +github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.3.0/go.mod h1:Zw9q2lP16sdg0zYybemZ9yTDy8g7fPCIB3KXOGlggXI= +github.com/containerd/nri v0.4.0/go.mod h1:Zw9q2lP16sdg0zYybemZ9yTDy8g7fPCIB3KXOGlggXI= +github.com/containerd/nri v0.8.0 h1:n1S753B9lX8RFrHYeSgwVvS1yaUcHjxbB+f+xzEncRI= +github.com/containerd/nri v0.8.0/go.mod h1:uSkgBrCdEtAiEz4vnrq8gmAC4EnVAM5Klt0OuK5rZYQ= +github.com/containerd/otelttrpc v0.1.0 h1:UOX68eVTE8H/T45JveIg+I22Ev2aFj4qPITCmXsskjw= +github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg= +github.com/containerd/protobuild v0.3.0 h1:RIyEIu+D+iIha6E1PREBPAXspSMFaDVam81JlolZWpg= +github.com/containerd/protobuild v0.3.0/go.mod h1:5mNMFKKAwCIAkFBPiOdtRx2KiQlyEJeMXnL5R1DsWu8= +github.com/containerd/stargz-snapshotter v0.15.2-0.20240709063920-1dac5ef89319/go.mod h1:dgo5lVziOOnWX8SxxHqYuc8ShsQou54eKLdahxFlHVc= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/containerd/stargz-snapshotter/estargz v0.15.2-0.20240709063920-1dac5ef89319/go.mod h1:9WSor0wu2swhtYoFkrjy3GHt7aNgKR2A7FhnpP+CH5o= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.1.0/go.mod h1:oZF9wBnrnQjpWLaPKEinrx3TQ9a+W/RJO7Zb41d8YLE= +github.com/containerd/zfs/v2 v2.0.0-rc.0 h1:0dRlgpoaepW7HuovtcvYQMF7NlpceQVdn7+3Udeth4M= +github.com/containerd/zfs/v2 v2.0.0-rc.0/go.mod h1:g36g/XCEGDRxUXIFdM3oWAEvmTvhfz/eKWElqg4Secw= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= +github.com/containernetworking/cni v1.1.1/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= +github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= +github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8= +github.com/containernetworking/plugins v1.2.0/go.mod h1:/VjX4uHecW5vVimFa1wkG4s+r/s9qIfPdqlLF4TW8c4= +github.com/containernetworking/plugins v1.3.0/go.mod h1:Pc2wcedTQQCVuROOOaLBPPxrEXqqXBFt3cZ+/yVg6l0= +github.com/containers/common v0.55.4 h1:7IxB/G5qtDU+rp1YiVWkDpd+ZC4ZlCQ7k2jZJYkB/R8= +github.com/containers/common v0.55.4/go.mod h1:5mVCpfMBWyO+zaD7Fw+DBHFa42YFKROwle1qpEKcX3U= +github.com/containers/image/v5 v5.26.2/go.mod h1:CTS9VmzgKk8HTSPfPDkthBvRh/2GnFL8K8WBedn3L7I= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.3/go.mod h1:xpdkbVAuaH3WzbEabUd5yDsl9SwJA5pABH85425Es2g= +github.com/containers/ocicrypt v1.1.6/go.mod h1:WgjxPWdTJMqYMjf3M6cuIFFA1/MpyyhIM99YInA+Rvc= +github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw= +github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8= +github.com/containers/ocicrypt v1.2.0/go.mod h1:ZNviigQajtdlxIZGibvblVuIFBKIuUI2M0QM12SD31U= +github.com/containers/storage v1.54.0 h1:xwYAlf6n9OnIlURQLLg3FYHbO74fQ/2W2N6EtQEUM4I= +github.com/containers/storage v1.54.0/go.mod h1:PlMOoinRrBSnhYODLxt4EXl0nmJt+X0kjG0Xdt9fMTw= +github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d h1:OMrhQqj1QCyDT2sxHCDjE+k8aMdn2ngTCGG7g4wrdLo= +github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808 h1:8s2l8TVUwMXl6tZMe3+hPCRJ25nQXiA3d1x622JtOqc= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a h1:Y5XsLCEhtEI8qbD9RP3Qlv5FXdTDHxZM9UPUnMRgBp8= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 h1:Lgdd/Qp96Qj8jqLpq2cI1I1X7BJnu06efS+XkhRoLUQ= +github.com/cyberphone/json-canonicalization v0.0.0-20230514072755-504adb8a8af1/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.3.5/go.mod h1:edhVd3c6OXKjUmSrVa/tGJRS9joFTxlslFCAyaxigkE= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/data-accelerator/zdfs v0.1.5 h1:F7td8AwicTZ3t618SsEYr8CMyMcxBE1KjkoyYO4T2GI= +github.com/data-accelerator/zdfs v0.1.5/go.mod h1:/MyNTsQHHKVLznaRBz+PivhIDwglu+wuXKoZxUNmLKI= +github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= +github.com/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4= +github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= +github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.1.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= +github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= +github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5 h1:2o8D0hdBky229bNnc7a8bAZkeVMpH4qsp2Rmt4g/+Zk= +github.com/docker/go-connections v0.4.1-0.20190612165340-fd1b1942c4d5/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= +github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8= +github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= +github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fasthttp/router v1.4.20 h1:yPeNxz5WxZGojzolKqiP15DTXnxZce9Drv577GBrDgU= +github.com/fasthttp/router v1.4.20/go.mod h1:um867yNQKtERxBm+C+yzgWxjspTiQoA8z86Ec3fK/tc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flowstack/go-jsonschema v0.1.1 h1:dCrjGJRXIlbDsLAgTJZTjhwUJnnxVWl1OgNyYh5nyDc= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/freddierice/go-losetup v0.0.0-20220711213114-2a14873012db h1:StM6A9LvaVrFS2chAGcfRVDoBB6rHYPIGJ3GknpB25c= +github.com/freddierice/go-losetup v0.0.0-20220711213114-2a14873012db/go.mod h1:pwuQfHWn6j2Fpl2AWw/bPLlKfojHxIIEa5TeKIgDFW4= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/getkin/kin-openapi v0.76.0 h1:j77zg3Ec+k+r+GA3d8hBoXpAc6KX9TbBPrwQGBIy2sY= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c h1:iRTj5SRYwbvsygdwVp+y9kZT145Y1s6xOPpeOEIeGc4= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-gorp/gorp/v3 v3.0.5 h1:PUjzYdYu3HBOh8LE+UUmRG2P0IRDak9XMeGNvaeq4Ow= +github.com/go-gorp/gorp/v3 v3.0.5/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.66.6/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-zookeeper/zk v1.0.2 h1:4mx0EYENAdX/B/rbunjlt5+4RTA/a9SMHBRuSKdGxPM= +github.com/gobuffalo/flect v0.2.3 h1:f/ZukRnSNA/DUpSNDadko7Qc0PhGvsew35p/2tu+CRY= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= +github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.12.6 h1:kjeKudqV0OygrAqA9fX6J55S8gj+Jre2tckIm5RoG4M= +github.com/google/cel-go v0.12.6/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw= +github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/cel-go v0.16.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= +github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/fswalker v0.2.1-0.20200214223026-f0e929ba4126/go.mod h1:ZSEBqY0IHKqWPeAbTyvccv9bb9vCnaQfHe31cm911Ng= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/go-containerregistry v0.20.1 h1:eTgx9QNYugV4DN5mz4U8hiAGTi1ybXn0TPi4Smd8du0= +github.com/google/go-containerregistry v0.20.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= +github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9 h1:OF1IPgv+F4NmqmJ98KTjdN97Vs1JxDPB3vbmYzV2dpk= +github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= +github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= +github.com/hanwen/go-fuse/v2 v2.6.3/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= +github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= +github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.10.1 h1:MwZJp86nlnL+6+W1Zly4JUuVn9YHhMggBirMpHGD7kw= +github.com/hashicorp/consul/api v1.20.0 h1:9IHTjNVSZ7MIwjlW3N3a7iGiykCMDpxZu8jsxFJh0yc= +github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo= +github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0/go.mod h1:xvb32K2keAc+R8DSFG2IwDcydK9DBQE+fGA5fsw6hSk= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.8 h1:9Q2lu1YbbmiAgvYZ7Pr31RdlVonUpX+mmDL7Z7qTA2U= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.8/go.mod h1:qTCjxGig/kjuj3hk1z8pOUrzbse/GxB1tGfbrq8tGJg= +github.com/hashicorp/go-memdb v1.3.2 h1:RBKHOsnSszpU6vxq80LzC2BaQjuuvoyaQbkLTf7V7g8= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= +github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= +github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1 h1:XFSOubp8KWB+Jd2PDyaX5xUd5bhSP/+pTDZVDMzZJM8= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault/api v1.9.1/go.mod h1:78kktNcQYbBGSrOjQfHjXN32OhhxXnbYl3zxpd2uPUs= +github.com/hashicorp/vault/sdk v0.9.2 h1:H1kitfl1rG2SHbeGEyvhEqmIjVKE3E6c2q3ViKOs6HA= +github.com/hashicorp/vault/sdk v0.9.2/go.mod h1:gG0lA7P++KefplzvcD3vrfCmgxVAM7Z/SqX5NeOL/98= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.4.0 h1:ZDDILMbB37UlAVLlWcJ2Iz1XuahZZTDZfdCKeclfq2s= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c h1:rwmN+hgiyp8QyBqzdEX43lTjKAxaqCrYHaU5op5P9J8= +github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= +github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= +github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= +github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= +github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk= +github.com/intel/goresctrl v0.5.0/go.mod h1:mIe63ggylWYr0cU/l8n11FAkesqfvuP3oktIsxvu0T0= +github.com/intel/goresctrl v0.8.0 h1:N3shVbS3kA1Hk2AmcbHv8805Hjbv+zqsCIZCGktxx50= +github.com/intel/goresctrl v0.8.0/go.mod h1:T3ZZnuHSNouwELB5wvOoUJaB7l/4Rm23rJy/wuWJlr0= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= +github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/kopia/htmluibuild v0.0.0-20220326183613-bbc499ed4dad/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= +github.com/kopia/kopia v0.10.7 h1:6s0ZIZW3Ge2ozzefddASy7CIUadp/5tF9yCDKQfAKKI= +github.com/kopia/kopia v0.10.7/go.mod h1:0d9THPD+jwomPcXvPbCdmLyX6phQVP7AqcCcDEajfNA= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0 h1:OYDCOjVcx/5wNzlZ/At8otRibUlw0T6R0xOD31f32bw= +github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0/go.mod h1:Q7VUue/CIrKbtpBdF04a1yjGGgsMaDws1HUxtjzgnEY= +github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 h1:nHHjmvjitIiyPlUHk/ofpgvBcNcawJLtf4PYHORLjAA= +github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0/go.mod h1:YBCo4DoEeDndqvAn6eeu0vWM7QdXmHEeI9cFWplmBys= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0 h1:cMM5AB37e9aRGjErygVT6EuBPB6s5a+l95OPERmSlVM= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0/go.mod h1:VQVLCPGDX5l6V5PezjlDXLa+SpCbWSVU7B16cFWVVeE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6 h1:wxyqOzKxsRJ6vVRL9sXQ64Z45wmBuQ+OTH9sLsC5rKc= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6/go.mod h1:PUgW5vI9ANEaV6qv9a6EKu8gAySgwf0xrzG9xIB/CK0= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 h1:jUp75lepDg0phMUJBCmvaeFDldD2N3S1lBuPwUTszio= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e/go.mod h1:9z/y9glKmWEdV50tjlUPxFwi1goQfIrrsoZbnMyIZbY= +github.com/luanshaotong/nerdctl/mod/tigron v0.0.0-20250717075412-9690955bbfc2 h1:ZZSdAQJUwb+HyWle8w+B+vLZodSJaeKMojEOdWXYuUU= +github.com/luanshaotong/nerdctl/mod/tigron v0.0.0-20250717075412-9690955bbfc2/go.mod h1:gmUZh2wUVxr/msGogKUi6v9eJbP5ASO4fVYEPzHH4iI= +github.com/luanshaotong/nerdctl/v2 v2.0.0-20250717075412-9690955bbfc2 h1:Xd/Ucu301cld+j+E/+nf0aeQ5HOwmZfJ+WH2dCjCy3Y= +github.com/luanshaotong/nerdctl/v2 v2.0.0-20250717075412-9690955bbfc2/go.mod h1:un/Eqg9DrVwazXHukMsxq+PEBixrN7YC3NRXdx5J3tY= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1 h1:keaAo8hRuAT0O3DfJ/wM3rufbAjGeJ1lAtWZHDjKGB0= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0/go.mod h1:fJ0UAZc1fx3xZhU4eSHQDJ1ApFmTVhp5VTpV9tm2ogg= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b h1:Ga1nclDSe8gOw37MVLMhfu2QKWtD6gvtQ298zsKVh8g= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/vpnkit v0.6.0 h1:HEh3iQ57oigvPNbR89R14pw3difgPyFOMMD3JAoqPoY= +github.com/moby/vpnkit v0.6.0/go.mod h1:CNuEpfSK4ZY/NKFWD5M79GUZcYFydh81XQ2GZnT44cQ= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q= +github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/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/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= +github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= +github.com/nelsam/hel/v2 v2.3.3/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= +github.com/networkplumbing/go-nft v0.2.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= +github.com/networkplumbing/go-nft v0.3.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= +github.com/networkplumbing/go-nft v0.4.0 h1:kExVMwXW48DOAukkBwyI16h4uhE5lN9iMvQd52lpTyU= +github.com/networkplumbing/go-nft v0.4.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/open-policy-agent/opa v0.42.2/go.mod h1:MrmoTi/BsKWT58kXlVayBb+rYVeaMwuBm3nYAN3923s= +github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U= +github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runc v1.1.8/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/runtime-tools v0.9.1-0.20230317050512-e931285f4b69/go.mod h1:bNpfuSHA3DZRtD0TPWO8LzgtLpFPTVA/3jDkzD/OPyk= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYEJTQzU= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pashagolub/pgxmock/v2 v2.11.0 h1:ZUKqZy5Zf/5WJjAXHErjHngJBW5/3fEujGD+Cb0FuDI= +github.com/pashagolub/pgxmock/v2 v2.11.0/go.mod h1:D3YslkN/nJ4+umVqWmbwfSXugJIjPMChkGBG47OJpNw= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/performancecopilot/speed/v4 v4.0.0 h1:VxEDCmdkfbQYDlcr/GC9YoN9PQ6p8ulk9xVsepYy9ZY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233 h1:jmJndGFBPjNWW+MAYarU/Nl8QrQVzbw4B/AYE0LzETo= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13 h1:o61duiW8M9sMlkVXWlvP92sZJtGKENvW3VExs6dZukQ= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.0 h1:et+XkusxWLz+XNqZiyMom9tv9ACvNAUyLXti2LTiV7o= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.0/go.mod h1:3RiUkFmR9kmPZi9r/8a5jw0a9yg+LMmr7qa0wjqvSiI= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851/go.mod h1:JDxG6+uubnk9/BZ2yUsyAJJwlptjrnmB2MPF5d2Xe/8= +github.com/replicatedhq/troubleshoot v0.57.0 h1:m9B31Mhgiz4Lwz+W4RvFkqhfYZLCwAqRPUwiwmSAAps= +github.com/replicatedhq/troubleshoot v0.57.0/go.mod h1:R5VdixzaBXfWLbP9mcLuZKs/bDCyGGS4+vFtKGWs9xE= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rubenv/sql-migrate v1.3.1 h1:Vx+n4Du8X8VTYuXbhNxdEUoh6wiJERA0GlWocR5FrbA= +github.com/rubenv/sql-migrate v1.3.1/go.mod h1:YzG/Vh82CwyhTFXy+Mf5ahAiiEOpAlHurg+23VEzcsk= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/safchain/ethtool v0.5.10/go.mod h1:w9jh2Lx7YBR4UwzLkzCmWl85UY0W2uZdd7/DckVE5+c= +github.com/safchain/ethtool v0.6.2 h1:O3ZPFAKEUEfbtE6J/feEe2Ft7dIJ2Sy8t4SdMRiIMHY= +github.com/safchain/ethtool v0.6.2/go.mod h1:VS7cn+bP3Px3rIq55xImBiZGHVLNyBh5dqG6dDQy8+I= +github.com/sagikazarmark/crypt v0.10.0 h1:96E1qrToLBU6fGzo+PRRz7KGOc9FkYFiPnR3/zf8Smg= +github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ= +github.com/sanity-io/litter v1.5.4/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 h1:RpforrEYXWkmGwJHIGnLZ3tTWStkjVVstwzNGqxX2Ds= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= +github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= +github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0 h1:QIF48X1cihydXibm+4wfAc0r/qyPyuFiPFRNphdMpEE= +github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400 h1:091wFNQB3PXcL5+me0joH7EiyqQaI0wGMpEjVCkK04U= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d h1:NVwnfyR3rENtlz62bcrkXME3INVUa4lcdGt+opvxExs= +github.com/sigstore/fulcio v1.3.1/go.mod h1:/XfqazOec45ulJZpyL9sq+OsVQ8g2UOVoNVi7abFgqU= +github.com/sigstore/rekor v1.2.2-0.20230601122533-4c81ff246d12/go.mod h1:8c+a8Yo7r8gKuYbIaz+c3oOdw9iMXx+tMdOg2+b+2jQ= +github.com/sigstore/sigstore v1.7.1/go.mod h1:0PmMzfJP2Y9+lugD0wer4e7TihR5tM7NcIs3bQNk5xg= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/slok/kubewebhook/v2 v2.2.0 h1:pOrFBTp6hnO0zakRfR9rd5WDRGiUimerNfR/JoNg1uI= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec h1:q6XVwXmKvCRHRqesF3cSv6lNqqHi0QWOvgDlSohg8UA= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg= +github.com/sykesm/zap-logfmt v0.0.4 h1:U2WzRvmIWG1wDLCFY3sz8UeEmsdHQjHFNlIdmroVFaI= +github.com/sykesm/zap-logfmt v0.0.4/go.mod h1:AuBd9xQjAe3URrWT1BBDk2v2onAZHkZkWRMiYZXiZWA= +github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c h1:3eGShk3EQf5gJCYW+WzA0TEJQd37HLOmlYF7N0YJwv0= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= +github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM= +github.com/theupdateframework/go-tuf v0.5.2/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83 h1:9AUN7+NK4IV+A11igqjQM5i8obiOAQo4SXgjaxe+orI= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vbauerster/mpb/v8 v8.4.0/go.mod h1:vjp3hSTuCtR+x98/+2vW3eZ8XzxvGoP8CPseHMhiPyc= +github.com/vektah/gqlparser/v2 v2.4.5/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= +github.com/veraison/go-cose v1.0.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= +github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= +github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= +github.com/veraison/go-cose v1.2.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vladimirvivien/gexe v0.1.1/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +github.com/vmware-tanzu/crash-diagnostics v0.3.7/go.mod h1:gO8670rd+qdjnJVol674snT/A46GQ27u085kKhZznlM= +github.com/vmware-tanzu/velero v1.10.1 h1:6WYOolZIygHb8FOZtpp8vCqCuy5Mk3qBF1S65L5cjuo= +github.com/vmware-tanzu/velero v1.10.1/go.mod h1:N0J+j8xGSmanGpy1zCRMH2DMGPpwkUj9EZIUXfOlanY= +github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810 h1:X6ps8XHfpQjw8dUStzlMi2ybiKQ2Fmdw7UM+TinwvyM= +github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810/go.mod h1:dF0BBJ2YrV1+2eAIyEI+KeSidgA6HqoIP1u5XTlMq/o= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b h1:0Ve0/CCjiAiyKddUMUn3RwIGlq2iTW4GuVzyoKBYO/8= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= +github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973 h1:iCnkJ/qjKZGdZnlcj1N55AxPDan814kpc3s1cDpQKd8= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 h1:p7OofyZ509h8DmPLh8Hn+EIIZm/xYhdZHJ9GnXHdr6U= +github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/gorelic v0.0.7 h1:4DTF1WOM2ZZS/xMOkTFBOcb6XiHu/PKn3rVo6dbewQE= +github.com/yvasiyarov/gorelic v0.0.7/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 h1:AsFN8kXcCVkUFHyuzp1FtYbzp1nCO/H6+1uPSGEyPzM= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= +go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= +go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= +go.etcd.io/etcd/api/v3 v3.5.6/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= +go.etcd.io/etcd/api/v3 v3.5.7/go.mod h1:9qew1gCdDDLu+VwmeG+iFpL+QlpHTo7iubavdVDgCAA= +go.etcd.io/etcd/api/v3 v3.5.8/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= +go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= +go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.7/go.mod h1:o0Abi1MK86iad3YrWhgUsbGx1pmTS+hrORWc2CamuhY= +go.etcd.io/etcd/client/pkg/v3 v3.5.8/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= +go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= +go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= +go.etcd.io/etcd/client/v2 v2.305.6/go.mod h1:BHha8XJGe8vCIBfWBpbBLVZ4QjOIlfoouvOwydu63E0= +go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s= +go.etcd.io/etcd/client/v2 v2.305.8/go.mod h1:ZlAsxDK5/10I6xVHhFo9zinCMr/DDLKFetDDXlzKwqE= +go.etcd.io/etcd/client/v2 v2.305.9/go.mod h1:0NBdNx9wbxtEQLwAQtrDHwx58m02vXpDcgSYI2seohQ= +go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= +go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c= +go.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2pRKzQk= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= +go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/pkg/v3 v3.5.4 h1:V5Dvl7S39ZDwjkKqJG2BfXgxZ3QREqqKifWQgIw5IM0= +go.etcd.io/etcd/pkg/v3 v3.5.4/go.mod h1:OI+TtO+Aa3nhQSppMbwE4ld3uF1/fqqwbpfndbbrEe0= +go.etcd.io/etcd/pkg/v3 v3.5.5/go.mod h1:6ksYFxttiUGzC2uxyqiyOEvhAiD0tuIqSZkX3TyPdaE= +go.etcd.io/etcd/pkg/v3 v3.5.6/go.mod h1:qATwUzDb6MLyGWq2nUj+jwXqZJcxkCuabh0P7Cuff3k= +go.etcd.io/etcd/pkg/v3 v3.5.8/go.mod h1:C17MJkZHJIyJV+wWWx6Jz6YS6BfdkOnUkSwT9uuEO7s= +go.etcd.io/etcd/pkg/v3 v3.5.9/go.mod h1:BZl0SAShQFk0IpLWR78T/+pyt8AruMHhTNNX73hkNVY= +go.etcd.io/etcd/pkg/v3 v3.5.10 h1:WPR8K0e9kWl1gAhB5A7gEa5ZBTNkT9NdNWrR8Qpo1CM= +go.etcd.io/etcd/pkg/v3 v3.5.10/go.mod h1:TKTuCKKcF1zxmfKWDkfz5qqYaE3JncKKZPFf8c1nFUs= +go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= +go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/raft/v3 v3.5.4 h1:YGrnAgRfgXloBNuqa+oBI/aRZMcK/1GS6trJePJ/Gqc= +go.etcd.io/etcd/raft/v3 v3.5.4/go.mod h1:SCuunjYvZFC0fBX0vxMSPjuZmpcSk+XaAcMrD6Do03w= +go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= +go.etcd.io/etcd/raft/v3 v3.5.6/go.mod h1:wL8kkRGx1Hp8FmZUuHfL3K2/OaGIDaXGr1N7i2G07J0= +go.etcd.io/etcd/raft/v3 v3.5.8/go.mod h1:W6P5WxtOMfYNdLSEJX3vc8Pg6LOt+ewI9UCFKcnIexA= +go.etcd.io/etcd/raft/v3 v3.5.9/go.mod h1:WnFkqzFdZua4LVlVXQEGhmooLeyS7mqzS4Pf4BCVqXg= +go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= +go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= +go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= +go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.etcd.io/etcd/server/v3 v3.5.4 h1:CMAZd0g8Bn5NRhynW6pKhc4FRg41/0QYy3d7aNm9874= +go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c= +go.etcd.io/etcd/server/v3 v3.5.5/go.mod h1:rZ95vDw/jrvsbj9XpTqPrTAB9/kzchVdhRirySPkUBc= +go.etcd.io/etcd/server/v3 v3.5.6/go.mod h1:6/Gfe8XTGXQJgLYQ65oGKMfPivb2EASLUSMSWN9Sroo= +go.etcd.io/etcd/server/v3 v3.5.9/go.mod h1:GgI1fQClQCFIzuVjlvdbMxNbnISt90gdfYyqiAIt65g= +go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= +go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= +go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= +go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= +go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= +go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= +go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 h1:sO4WKdPAudZGKPcpZT4MJn6JaDmpyLrMPDGGyA1SttE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0/go.mod h1:E5NNboN0UqSAki0Atn9kVwaN7I+l25gGxDqBueo/74E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0/go.mod h1:h8TWwRAhQpOd0aM5nYsRD8+flnkj+526GEIVlarH7eY= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0/go.mod h1:UMklln0+MRhZC4e3PwmN3pCtq4DyIadWw4yikh6bNrw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 h1:Q3C9yzW6I9jqEc8sawxzxZmY48fs9u220KXq6d5s3XU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0 h1:0BgiNWjN7rUWO9HdjF4L12r8OW86QkVQcYmCjnayJLo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0/go.mod h1:5eCOqeGphOyz6TsY3ZDNjE33SM/TFAK3RGuCL2naTgY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.35.0/go.mod h1:9NiG9I2aHTKkcxqCILhjtyNA1QEiCjdBACv4IvrFQ+c= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.35.1/go.mod h1:9NiG9I2aHTKkcxqCILhjtyNA1QEiCjdBACv4IvrFQ+c= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.0.1/go.mod h1:OPEOD4jIT2SlZPMmwT6FqZz2C0ZNdQqiWcoK6M0SNFU= +go.opentelemetry.io/otel v1.2.0 h1:YOQDvxO1FayUcT9MIhJhgMyNO1WqoduiyvQHzGN0kUQ= +go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= +go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= +go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM= +go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ= +go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= +go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0/go.mod h1:M1hVZHNxcbkAlcvrOMlpQ4YOO3Awf+4N2dxkZL3xm04= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 h1:TaB+1rQhddO1sF71MpZOZAuSPW1klK2M8XxfrBMfK7Y= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.2/go.mod h1:rqbht/LlhVBgn5+k3M5QK96K5Xb0DvXpMJ5SFQpY6uw= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0/go.mod h1:UFG7EBMRdXyFstOwH028U0sVf+AvukSGhF0g8+dmNG8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1/go.mod h1:Kv8liBeVNFkkkbilbgWRpV+wWuu+H5xdOT6HAgd30iw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0/go.mod h1:ceUgdyfNv4h4gLxHR0WNfDiiVmZFodZhZSbOLhpxqXE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0/go.mod h1:Krqnjl22jUJ0HgMzw5eveuCvFDXY4nSYb4F8t5gdrag= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.2/go.mod h1:5Qn6qvgkMsLDX+sYK64rHb1FPhpn0UtxF+ouX1uhyJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0/go.mod h1:HrbCVv40OOLTABmOn1ZWty6CHXkU8DK/Urc43tHug70= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1/go.mod h1:xOvWoTOrQjxjW61xtOmD/WKGRYb/P4NzRo3bs65U6Rk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0/go.mod h1:E+/KKhwOSw8yoPxSSuUHG6vKppkvhN+S1Jc7Nib3k3o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0/go.mod h1:OfUCyyIiDvNXHWpcWgbF+MWvqPZiNa3YDEnivcnYsV0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2/go.mod h1:jWZUM2MWhWCJ9J9xVbRx7tzK1mXKpAlze4CeulycwVY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0/go.mod h1:5w41DY6S9gZrbjuq6Y+753e96WfPha5IcsOSZTtullM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0/go.mod h1:+N7zNjIJv4K+DeX67XXET0P+eIciESgaFDBqh+ZJFS4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.2.0 h1:OiYdrCq1Ctwnovp6EofSPwlp5aGy4LgKNbkg7PtEUw8= +go.opentelemetry.io/otel/internal/metric v0.25.0 h1:w/7RXe16WdPylaIXDgcYM6t/q0K5lXgSdZOEbIEyliE= +go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/metric v0.25.0 h1:7cXOnCADUsR3+EOqxPaSKwhEuNu0gz/56dRN1hpIdKw= +go.opentelemetry.io/otel/metric v0.30.0/go.mod h1:/ShZ7+TS4dHzDFmfi1kSXMhMVubNoP0oIaBp70J6UXU= +go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A= +go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s= +go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= +go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA= +go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0 h1:JsxtGXd06J8jrnya7fdI/U/MR6yXA5DtbZy+qoHQlr8= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.0.1/go.mod h1:HrdXne+BiwsOHYYkBE5ysIcv2bvdZstxzmCQhxTcZkI= +go.opentelemetry.io/otel/sdk v1.2.0 h1:wKN260u4DesJYhyjxDa7LRFkuhH7ncEVKU37LWcyNIo= +go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= +go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU= +go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE= +go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= +go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= +go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0 h1:7ao1wpzHRVKf0OQ7GIxiQJA6X7DLX9o14gmVon7mMK8= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.0.1/go.mod h1:5g4i4fKLaX2BQpSBsxw8YYcgKpMMSW3x7ZTuYBr3sUk= +go.opentelemetry.io/otel/trace v1.2.0 h1:Ys3iqbqZhcf28hHzrm5WAquMkDHNZTUkw7KHbuNjej0= +go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= +go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4= +go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= +go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= +go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= +go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.16.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= +go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= +go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230725012225-302865e7556b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v3 v3.0.1 h1:Te7hKxV52TKCbNYq3t84tzKav3xhThdvSsSp/W89IyI= +gomodules.xyz/orderedmap v0.1.0 h1:fM/+TGh/O1KkqGR5xjTKg6bU8OKBkg7p0Y+x/J9m8Os= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b h1:Qh4dB5D/WpoUUp3lSod7qgoyEHbDGPUWjIbnqdqqe1k= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E= +google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= +google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= +google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= +google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= +google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/api v0.161.0 h1:oYzk/bs26WN10AV7iU7MVJVXBH8oCPS2hHyBiEeFoSU= +google.golang.org/api v0.161.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps= +google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0= +google.golang.org/api v0.164.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeqUB5o= +google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= +google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto v0.0.0-20230821184602-ccc8af3d0e93/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= +google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:EMfReVxb80Dq1hhioy0sOsY9jCE46YDgHlJ7fWVUWRE= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:SUBoKXbI1Efip18FClrQVGjWcyd0QZd8KkvdP34t7ww= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= +google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= +google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= +google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= +google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= +google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc h1:g3hIDl0jRNd9PPTs2uBzYuaD5mQuwOkZY0vSc0LR32o= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405 h1:o4S3HvTUEXgRsNSUQsALDVog0O9F/U1JJlHmmUN8Uas= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240116215550-a9fa1716bcac h1:QXtV4qU5zS94SeHJhPqxJQF0XyxssnVrEZOUgp1+NuY= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:ZSvZ8l+AWJwXw91DoTjWjaVLpWU6o0eZ4YLYpH8aLeQ= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:SCz6T5xjNXM4QFPRwxHcfChp7V+9DcXR3ay2TkHR8Tg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240205150955-31a09d347014/go.mod h1:EhZbXt+eY4Yr3YVaEGLdNZF5viWowOJZ8KTPqjYMKzg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:om8Bj876Z0v9ei+RD1LnEWig7vpHQ371PUqsgjmLQEA= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78 h1:YqFWYZXim8bG9v68xU8WjTZmYKb5M5dMeSOWIp6jogI= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920183334-c177e329c48b/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240122161410-6c6643bf1457/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228201840-1f18d85a4ec2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240509183442-62759503f434/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg= +helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= +k8s.io/apiserver v0.25.6 h1:32mn8HAlsEl1tpuiVmhAl0YCVkOugjybsJ6l6kf0c8k= +k8s.io/apiserver v0.25.6/go.mod h1:IEp2B2/FvQ8GmdspscUoUS0iFF/GGc6NVrJ/cTM4OaA= +k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8= +k8s.io/apiserver v0.26.3/go.mod h1:CJe/VoQNcXdhm67EvaVjYXxR3QyfwpceKPuPaeLibTA= +k8s.io/apiserver v0.28.1/go.mod h1:d8aizlSRB6yRgJ6PKfDkdwCy2DXt/d1FDR6iJN9kY1w= +k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk= +k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA= +k8s.io/cli-runtime v0.28.3 h1:lvuJYVkwCqHEvpS6KuTZsUVwPePFjBfSGvuaLl2SxzA= +k8s.io/cli-runtime v0.28.3/go.mod h1:jeX37ZPjIcENVuXDDTskG3+FnVuZms5D9omDXS/2Jjc= +k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/code-generator v0.25.6 h1:md8jqhQiMqos8WaTY0ZxhDk8ttlmJl/ijCneJ8ahBNQ= +k8s.io/code-generator v0.25.6/go.mod h1:aDxzxJynLKQkaa117y0FFcgZ5jG8+GobxZ2JUntmvKk= +k8s.io/code-generator v0.26.3/go.mod h1:ryaiIKwfxEJEaywEzx3dhWOydpVctKYbqLajJf0O8dI= +k8s.io/code-generator v0.28.2/go.mod h1:ueeSJZJ61NHBa0ccWLey6mwawum25vX61nRZ6WOzN9A= +k8s.io/code-generator v0.28.3 h1:I847QvdpYx7xKiG2KVQeCSyNF/xU9TowaDAg601mvlw= +k8s.io/code-generator v0.28.3/go.mod h1:A2EAHTRYvCvBrb/MM2zZBNipeCk3f8NtpdNIKawC43M= +k8s.io/code-generator v0.32.1 h1:4lw1kFNDuFYXquTkB7Sl5EwPMUP2yyW9hh6BnFfRZFY= +k8s.io/code-generator v0.32.1/go.mod h1:zaILfm00CVyP/6/pJMJ3zxRepXkxyDfUV5SNG4CjZI4= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= +k8s.io/component-helpers v0.28.2 h1:r/XJ265PMirW9EcGXr/F+2yWrLPo2I69KdvcY/h9HAo= +k8s.io/component-helpers v0.28.2/go.mod h1:pF1R5YWQ+sgf0i6EbVm+MQCzkYuqutDUibdrkvAa6aI= +k8s.io/component-helpers v0.28.3 h1:te9ieTGzcztVktUs92X53P6BamAoP73MK0qQP0WmDqc= +k8s.io/component-helpers v0.28.3/go.mod h1:oJR7I9ist5UAQ3y/CTdbw6CXxdMZ1Lw2Ua/EZEwnVLs= +k8s.io/controller-manager v0.26.0/go.mod h1:GxUYtQDBE/RHh7AnZSZqwi2xBPIXlOaWsnDLflKGYrE= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= +k8s.io/cri-api v0.27.1 h1:KWO+U8MfI9drXB/P4oU9VchaWYOlwDglJZVHWMpTT3Q= +k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0= +k8s.io/cri-api v0.28.3 h1:84ifk56rAy7yYI1zYqTjLLishpFgs3q7BkCKhoLhmFA= +k8s.io/cri-api v0.28.3/go.mod h1:MTdJO2fikImnX+YzE2Ccnosj3Hw2Cinw2fXYV3ppUIE= +k8s.io/cri-api v0.31.2/go.mod h1:Po3TMAYH/+KrZabi7QiwQI4a692oZcUOUThd/rqwxrI= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20220913193501-391367153a38/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= +k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= +k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= +k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kms v0.26.3/go.mod h1:69qGnf1NsFOQP07fBYqNLZklqEHSJF024JqYCaeVxHg= +k8s.io/kms v0.28.3/go.mod h1:kSMjU2tg7vjqqoWVVCcmPmNZ/CofPsoTbSxAipCvZuE= +k8s.io/kms v0.29.0 h1:KJ1zaZt74CgvgV3NR7tnURJ/mJOKC5X3nwon/WdwgxI= +k8s.io/kms v0.29.0/go.mod h1:mB0f9HLxRXeXUfHfn1A7rpwOlzXI1gIWu86z6buNoYA= +k8s.io/kms v0.30.1 h1:gEIbEeCbFiaN2tNfp/EUhFdGr5/CSj8Eyq6Mkr7cCiY= +k8s.io/kms v0.30.1/go.mod h1:GrMurD0qk3G4yNgGcsCEmepqf9KyyIrTXYR2lyUOJC4= +k8s.io/kms v0.32.1 h1:TW6cswRI/fawoQRFGWLmEceO37rZXupdoRdmO019jCc= +k8s.io/kms v0.32.1/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= +k8s.io/kms v0.32.3 h1:HhHw5+pRCzEJp3oFFJ1q5W2N6gAI7YkUg4ay4Z0dgwM= +k8s.io/kms v0.32.3/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= +k8s.io/kube-aggregator v0.19.12/go.mod h1:K76wPd03pSHEmS1FgJOcpryac5C3va4cbCvSu+4EmE0= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/kube-openapi v0.0.0-20230918164632-68afd615200d/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.28.2 h1:fOWOtU6S0smdNjG1PB9WFbqEIMlkzU5ahyHkc7ESHgM= +k8s.io/kubectl v0.28.2/go.mod h1:6EQWTPySF1fn7yKoQZHYf9TPwIl2AygHEcJoxFekr64= +k8s.io/kubelet v0.26.1 h1:wQyCQYmLW6GN3v7gVTxnc3jAE4zMYDlzdF3FZV4rKas= +k8s.io/kubelet v0.26.1/go.mod h1:gFVZ1Ab4XdjtnYdVRATwGwku7FhTxo6LVEZwYoQaDT8= +k8s.io/kubelet v0.32.3 h1:B9HzW4yB67flx8tN2FYuDwZvxnmK3v5EjxxFvOYjmc8= +k8s.io/kubelet v0.32.3/go.mod h1:yyAQSCKC+tjSlaFw4HQG7Jein+vo+GeKBGdXdQGvL1U= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY= +k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag= +k8s.io/metrics v0.28.2/go.mod h1:QTIIdjMrq+KodO+rmp6R9Pr1LZO8kTArNtkWoQXw0sw= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= +modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= +modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= +modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/sqlite v1.18.2 h1:S2uFiaNPd/vTAP/4EmyY8Qe2Quzu26A2L1e25xRNTio= +modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0= +modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/tcl v1.13.2 h1:5PQgL/29XkQ9wsEmmNPjzKs+7iPCaYqUJAhzPvQbjDA= +modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0= +modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= +mvdan.cc/gofumpt v0.3.1/go.mod h1:w3ymliuxvzVx8DAutBnVyDqYb1Niy/yCJt/lk821YCE= +nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= +oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= +oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= +oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= +periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.35 h1:+xBL5uTc+BkPBwmMi3vYfUJjq+N3K+H6PXeETwf5cPI= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.35/go.mod h1:WxjusMwXlKzfAs4p9km6XJRndVt2FROgMVCE4cdohFo= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.36/go.mod h1:WxjusMwXlKzfAs4p9km6XJRndVt2FROgMVCE4cdohFo= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2/go.mod h1:+qG7ISXqCDVVcyO8hLn12AKVYYUjM7ftlqsqmrhMZE0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-tools v0.6.2 h1:+Y8L0UsAugDipGRw8lrkPoAi6XqlQVZuf1DQHME3PgU= +sigs.k8s.io/gateway-api v0.4.0 h1:07IJkTt21NetZTHtPKJk2I4XIgDN4BAlTIq1wK7V11o= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= +sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= +sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk= +sigs.k8s.io/kustomize/kyaml v0.14.3 h1:WpabVAKZe2YEp/irTSHwD6bfjwZnTtSDewd2BVJGMZs= +sigs.k8s.io/kustomize/kyaml v0.14.3/go.mod h1:npvh9epWysfQ689Rtt/U+dpOJDTBn8kUnF1O6VzvmZA= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.4.3/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= +tags.cncf.io/container-device-interface v0.8.1/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= +tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= diff --git a/controllers/job/heartbeat/Dockerfile b/controllers/job/heartbeat/Dockerfile new file mode 100644 index 000000000000..fbdc460a3389 --- /dev/null +++ b/controllers/job/heartbeat/Dockerfile @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-job-heartbeat-$TARGETARCH /heartbeat + +ENTRYPOINT ["/heartbeat"] \ No newline at end of file diff --git a/controllers/job/heartbeat/Makefile b/controllers/job/heartbeat/Makefile new file mode 100644 index 000000000000..3b144474a242 --- /dev/null +++ b/controllers/job/heartbeat/Makefile @@ -0,0 +1,90 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-job-heartbeat-controller:latest +TARGETARCH ?= amd64 +GOARCH ?= amd64 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +build: fmt vet ## Build manager binary. + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/heartbeat-${GOARCH} cmd/main.go && chmod +x bin/heartbeat-${GOARCH} && cp bin/heartbeat-${GOARCH} bin/manager + + +.PHONY: run +run: fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + mv bin/heartbeat-${TARGETARCH} bin/controller-job-heartbeat-${TARGETARCH} || true + chmod +x bin/controller-job-heartbeat-${TARGETARCH} + docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross diff --git a/controllers/job/heartbeat/api/v1alpha/resource.go b/controllers/job/heartbeat/api/v1alpha/resource.go new file mode 100644 index 000000000000..9ccbb998fca5 --- /dev/null +++ b/controllers/job/heartbeat/api/v1alpha/resource.go @@ -0,0 +1,26 @@ +// Copyright © 2023 sealos. +// +// 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 v1alpha + +type ClusterResource struct { + Node int64 `json:"node"` + CPU int64 `json:"cpu"` + Mem int64 `json:"mem"` +} + +type Request struct { + ClusterID string `json:"clusterID"` + ClusterResource *ClusterResource `json:"clusterResource"` +} diff --git a/controllers/job/heartbeat/cmd/main.go b/controllers/job/heartbeat/cmd/main.go new file mode 100644 index 000000000000..fc6e5f7b455c --- /dev/null +++ b/controllers/job/heartbeat/cmd/main.go @@ -0,0 +1,52 @@ +// Copyright © 2023 sealos. +// +// 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 ( + "github.com/go-resty/resty/v2" + "github.com/labring/sealos/controllers/job/heartbeat/api/v1alpha" + "github.com/labring/sealos/controllers/job/heartbeat/internal/cluster" + "github.com/labring/sealos/controllers/pkg/utils/logger" +) + +func main() { + resource, err := cluster.GetClusterResources() + if err != nil { + logger.Error(err.Error()) + return + } + logger.Info("cluster resource: %+v", resource) + + clusterID, err := cluster.GetClusterID() + if err != nil { + logger.Error(err.Error()) + return + } + logger.Info("cluster id: %s", clusterID) + + client := resty.New() + _, err = client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(v1alpha.Request{ + ClusterID: clusterID, + ClusterResource: resource, + }). + Post("https://license.sealos.io/api/v1alpha/heartbeat/cluster") + if err != nil { + logger.Error(err.Error()) + return + } + logger.Info("heartbeat success") +} diff --git a/controllers/job/heartbeat/deploy/Kubefile b/controllers/job/heartbeat/deploy/Kubefile new file mode 100644 index 000000000000..89ce24a8272f --- /dev/null +++ b/controllers/job/heartbeat/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY heartbeat-cronjob-entrypoint.sh heartbeat-cronjob-entrypoint.sh + +CMD ["bash heartbeat-cronjob-entrypoint.sh"] diff --git a/controllers/job/heartbeat/deploy/README.md b/controllers/job/heartbeat/deploy/README.md new file mode 100644 index 000000000000..2cdceeeb4a79 --- /dev/null +++ b/controllers/job/heartbeat/deploy/README.md @@ -0,0 +1,64 @@ +# job-heartbeat + +## 说明 +sealos run 镜像时会在目标节点执行 Kubefile,本镜像通过 Helm 安装/升级 heartbeat CronJob。 + +## 必填参数 + +**无** + +## 如何运行 + +```shell +# 最简配置 +sealos run ghcr.io/labring/sealos-job-heartbeat-controller:latest +``` + +## 可选参数 + +- RELEASE_NAMESPACE: Helm 安装命名空间,默认 `heartbeat-system` +- RELEASE_NAME: Helm release 名称,默认 `heartbeat` +- HELM_OPTS: 透传 Helm 参数(例如 `--set schedule="0 */6 * * *"`) +- CHART_PATH: Helm chart 路径,默认 `./charts/heartbeat` + +## 示例 + +```shell +# 1. 最简配置(每天午夜执行) +sealos run ghcr.io/labring/sealos-job-heartbeat-controller:latest + +# 2. 自定义命名空间和执行计划 +sealos run ghcr.io/labring/sealos-job-heartbeat-controller:latest \ + --env RELEASE_NAMESPACE="my-namespace" \ + --env HELM_OPTS="--set schedule=\"0 */6 * * *\"" + +# 3. 自定义资源限制 +sealos run ghcr.io/labring/sealos-job-heartbeat-controller:latest \ + --env HELM_OPTS="--set resources.limits.cpu=500m --set resources.limits.memory=256Mi" + +# 4. 组合多个参数 +sealos run ghcr.io/labring/sealos-job-heartbeat-controller:latest \ + --env RELEASE_NAMESPACE="production" \ + --env HELM_OPTS="--set schedule=\"0 2 * * *\" --set activeDeadlineSeconds=1800" +``` + +## Helm Chart 可配置参数 + +可通过 `HELM_OPTS` 传递以下参数: + +- `schedule`: Cron 执行计划,默认 `"0 0 * * *"`(每天午夜执行) +- `activeDeadlineSeconds`: Job 超时时间,默认 `600` +- `backoffLimit`: 重试次数,默认 `1` +- `image`: 容器镜像,默认 `ghcr.io/labring/sealos-job-heartbeat-controller:latest` +- `imagePullPolicy`: 镜像拉取策略,默认 `Always` +- `resources.limits.cpu/memory`: CPU/内存限制 +- `resources.requests.cpu/memory`: CPU/内存请求 +- `nodeSelector`: 节点选择器 +- `tolerations`: 容忍度配置 +- `affinity`: 亲和性配置 + +## 测试立即创建job: + +```shell +kubectl create job --from=cronjob/heartbeat-cronjob heartbeat-manual-$(date +%Y%m%d-%H%M%S) -n heartbeat-system +``` \ No newline at end of file diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/Chart.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/Chart.yaml new file mode 100644 index 000000000000..5ede882f42ac --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: heartbeat +description: Helm chart for the sealos heartbeat cronjob +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/heartbeat-values.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/heartbeat-values.yaml new file mode 100644 index 000000000000..e87c1bb43809 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/heartbeat-values.yaml @@ -0,0 +1,10 @@ +# Custom values for heartbeat cronjob helm chart. +# This file contains user-customizable configurations. + +resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/templates/NOTES.txt b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/NOTES.txt new file mode 100644 index 000000000000..b973e5f8bf76 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/NOTES.txt @@ -0,0 +1,5 @@ +Heartbeat cronjob has been installed successfully! + +CronJob name: {{ include "heartbeat.fullname" . }}-cronjob +Schedule: {{ .Values.schedule }} +Namespace: {{ .Release.Namespace }} diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/templates/_helpers.tpl b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/_helpers.tpl new file mode 100644 index 000000000000..d1496b8e6491 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "heartbeat.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "heartbeat.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "heartbeat.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "heartbeat.labels" -}} +helm.sh/chart: {{ include "heartbeat.chart" . }} +{{ include "heartbeat.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "heartbeat.selectorLabels" -}} +app.kubernetes.io/name: {{ include "heartbeat.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "heartbeat.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "heartbeat.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/templates/cronjob.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/cronjob.yaml new file mode 100644 index 000000000000..4dfc3133319c --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/cronjob.yaml @@ -0,0 +1,55 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "heartbeat.fullname" . }}-cronjob + labels: + {{- include "heartbeat.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.schedule | quote }} + jobTemplate: + metadata: + labels: + {{- include "heartbeat.labels" . | nindent 8 }} + spec: + activeDeadlineSeconds: {{ .Values.activeDeadlineSeconds }} + backoffLimit: {{ .Values.backoffLimit }} + template: + metadata: + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 12 }} + {{- end }} + labels: + {{- include "heartbeat.labels" . | nindent 12 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 12 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "heartbeat.serviceAccountName" . }} + restartPolicy: Never + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 12 }} + containers: + - name: heartbeat + securityContext: + {{- toYaml .Values.securityContext | nindent 16 }} + image: "{{ .Values.image }}" + imagePullPolicy: {{ .Values.imagePullPolicy }} + resources: + {{- toYaml .Values.resources | nindent 16 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/templates/rbac.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/rbac.yaml new file mode 100644 index 000000000000..4d46ac7c3ba5 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/rbac.yaml @@ -0,0 +1,29 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "heartbeat.fullname" . }}-cluster-role + labels: + {{- include "heartbeat.labels" . | nindent 4 }} +rules: + - apiGroups: [ "" ] + resources: [ "nodes" ] + verbs: [ "get", "list" ] + - apiGroups: [ "" ] + resources: [ "namespaces" ] + resourceNames: [ "kube-system" ] + verbs: [ "get" ] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "heartbeat.fullname" . }}-cluster-role-binding + labels: + {{- include "heartbeat.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "heartbeat.fullname" . }}-cluster-role +subjects: + - kind: ServiceAccount + name: {{ include "heartbeat.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/templates/serviceaccount.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/serviceaccount.yaml new file mode 100644 index 000000000000..2fc05d001d87 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "heartbeat.serviceAccountName" . }} + labels: + {{- include "heartbeat.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/controllers/job/heartbeat/deploy/charts/heartbeat/values.yaml b/controllers/job/heartbeat/deploy/charts/heartbeat/values.yaml new file mode 100644 index 000000000000..b91471304855 --- /dev/null +++ b/controllers/job/heartbeat/deploy/charts/heartbeat/values.yaml @@ -0,0 +1,33 @@ +# Default values for heartbeat cronjob helm chart + +schedule: "0 0 * * *" +activeDeadlineSeconds: 600 +backoffLimit: 1 + +image: ghcr.io/labring/sealos-job-heartbeat-controller:latest +imagePullPolicy: Always + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "heartbeat" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "heartbeat-cronjob" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: false + +securityContext: + allowPrivilegeEscalation: false + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/controllers/job/heartbeat/deploy/heartbeat-cronjob-entrypoint.sh b/controllers/job/heartbeat/deploy/heartbeat-cronjob-entrypoint.sh new file mode 100644 index 000000000000..3a7823e17d0e --- /dev/null +++ b/controllers/job/heartbeat/deploy/heartbeat-cronjob-entrypoint.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -e + +HELM_OPTS=${HELM_OPTS:-""} +RELEASE_NAME=${RELEASE_NAME:-"heartbeat"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"heartbeat-system"} +CHART_PATH=${CHART_PATH:-"./charts/heartbeat"} + +adopt_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +adopt_namespaced_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl -n "${RELEASE_NAMESPACE}" annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +if ! helm status "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + kubectl label namespace "${RELEASE_NAMESPACE}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate namespace "${RELEASE_NAMESPACE}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi + + adopt_namespaced_resource serviceaccount heartbeat-cronjob + adopt_cluster_resource clusterrole heartbeat-cluster-role + adopt_cluster_resource clusterrolebinding heartbeat-cluster-role-binding +fi + +# Prepare values files +SERVICE_NAME="heartbeat" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + ${HELM_OPTS} diff --git a/controllers/job/heartbeat/go.mod b/controllers/job/heartbeat/go.mod new file mode 100644 index 000000000000..29d2cc693840 --- /dev/null +++ b/controllers/job/heartbeat/go.mod @@ -0,0 +1,82 @@ +module github.com/labring/sealos/controllers/job/heartbeat + +go 1.24.0 + +require ( + github.com/go-resty/resty/v2 v2.10.0 + github.com/labring/sealos/controllers/pkg v0.0.0-20240715093553-26604f99fa5e + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + sigs.k8s.io/controller-runtime v0.15.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + 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/matttproud/golang_protobuf_extensions/v2 v2.0.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/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + k8s.io/api => k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 + k8s.io/client-go => k8s.io/client-go v0.28.3 + k8s.io/component-base => k8s.io/component-base v0.28.3 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2 +) diff --git a/controllers/job/heartbeat/go.sum b/controllers/job/heartbeat/go.sum new file mode 100644 index 000000000000..1c653c745a20 --- /dev/null +++ b/controllers/job/heartbeat/go.sum @@ -0,0 +1,235 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= +github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= +github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labring/sealos/controllers/pkg v0.0.0-20240715093553-26604f99fa5e h1:bg3rSQNTJl49nZ0TXFeLhjtxL+HWdSQvDon5EblDCYw= +github.com/labring/sealos/controllers/pkg v0.0.0-20240715093553-26604f99fa5e/go.mod h1:7WBhRcClEuLy/bVMTFICX2jzhmQDsH6UODRONWqrQHY= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/job/heartbeat/internal/cluster/cluster.go b/controllers/job/heartbeat/internal/cluster/cluster.go new file mode 100644 index 000000000000..fb8e81a7fccb --- /dev/null +++ b/controllers/job/heartbeat/internal/cluster/cluster.go @@ -0,0 +1,73 @@ +// Copyright © 2023 sealos. +// +// 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 cluster + +import ( + "context" + "errors" + + "github.com/labring/sealos/controllers/job/heartbeat/api/v1alpha" + "github.com/labring/sealos/controllers/job/heartbeat/internal/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetClusterResources() (*v1alpha.ClusterResource, error) { + c, err := util.NewKubernetesClient() + if err != nil { + return nil, err + } + nodeList := &corev1.NodeList{} + if err := c.List(context.Background(), nodeList); err != nil { + return nil, err + } + + totalCPU := resource.NewQuantity(0, resource.DecimalSI) + totalMem := resource.NewQuantity(0, resource.DecimalSI) + + for _, node := range nodeList.Items { + cpu := node.Status.Capacity["cpu"] + mem := node.Status.Capacity["memory"] + totalCPU.Add(cpu) + totalMem.Add(mem) + } + + return &v1alpha.ClusterResource{ + Node: int64(len(nodeList.Items)), + CPU: totalCPU.Value(), + Mem: totalMem.Value(), + }, err +} + +func GetClusterID() (string, error) { + c, err := util.NewKubernetesClient() + if err != nil { + return "", err + } + kubeSystemNamespace := &corev1.Namespace{} + if err := c.Get( + context.Background(), + client.ObjectKey{Namespace: "kube-system", Name: "kube-system"}, + kubeSystemNamespace, + ); err != nil { + return "", err + } + res := string(kubeSystemNamespace.UID) + if len(res) < 8 { + return "", errors.New("cluster id is invalid") + } + return res[0:8], nil +} diff --git a/controllers/job/heartbeat/internal/util/client.go b/controllers/job/heartbeat/internal/util/client.go new file mode 100644 index 000000000000..860e9c562bdf --- /dev/null +++ b/controllers/job/heartbeat/internal/util/client.go @@ -0,0 +1,35 @@ +// Copyright © 2023 sealos. +// +// 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 util + +import ( + "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/client" +) + +var scheme = runtime.NewScheme() + +const KubeSystemObjectMeta = "kube-system" + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) +} + +func NewKubernetesClient() (client.Client, error) { + return client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme}) +} diff --git a/controllers/job/init/.dockerignore b/controllers/job/init/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/job/init/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/job/init/.gitignore b/controllers/job/init/.gitignore new file mode 100644 index 000000000000..7f02333db7ab --- /dev/null +++ b/controllers/job/init/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/controllers/job/init/Dockerfile b/controllers/job/init/Dockerfile new file mode 100644 index 000000000000..c6425c372709 --- /dev/null +++ b/controllers/job/init/Dockerfile @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-job-init-$TARGETARCH /preset + +ENTRYPOINT ["/preset"] \ No newline at end of file diff --git a/controllers/job/init/Makefile b/controllers/job/init/Makefile new file mode 100644 index 000000000000..a59cbcfaf2c5 --- /dev/null +++ b/controllers/job/init/Makefile @@ -0,0 +1,93 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-job-init-controller:latest +TARGETARCH ?= amd64 +GOARCH ?= amd64 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg +CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal/controller +build: ## Build manager binary. + LD_FLAGS="-s -w"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/preset-${GOARCH} cmd/preset/main.go && chmod +x bin/preset-${GOARCH} && cp bin/preset-${GOARCH} bin/manager + + +.PHONY: run +run: fmt vet ## Run a controller from your host. + go run ./cmd/preset/main.go + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + mv bin/preset-${TARGETARCH} bin/controller-job-init-${TARGETARCH} || true + chmod +x bin/controller-job-init-${TARGETARCH} + docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross diff --git a/controllers/job/init/cmd/preset/main.go b/controllers/job/init/cmd/preset/main.go new file mode 100644 index 000000000000..b3f5661a3299 --- /dev/null +++ b/controllers/job/init/cmd/preset/main.go @@ -0,0 +1,46 @@ +// Copyright © 2023 sealos. +// +// 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 ( + "context" + "errors" + "os" + + "github.com/labring/sealos/controllers/job/init/internal/util/controller" + "github.com/labring/sealos/controllers/job/init/internal/util/database" + "github.com/labring/sealos/controllers/pkg/utils/logger" + "gorm.io/gorm" +) + +func main() { + ctx := context.Background() + + if err := controller.PresetAdminUser(ctx); err != nil { + logger.Error(err, "preset admin user in kubernetes failed") + os.Exit(1) + } + logger.Info("preset admin user in kubernetes successfully") + + if err := database.PresetAdminUser(); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + logger.Info("admin user already exists in database") + } else { + logger.Error(err, "preset admin user in database failed") + os.Exit(1) + } + } + logger.Info("preset admin user in database successfully") +} diff --git a/controllers/job/init/deploy/Kubefile b/controllers/job/init/deploy/Kubefile new file mode 100644 index 000000000000..8fb982d70d8e --- /dev/null +++ b/controllers/job/init/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY job-init-entrypoint.sh job-init-entrypoint.sh + +CMD ["bash job-init-entrypoint.sh"] diff --git a/controllers/job/init/deploy/README.md b/controllers/job/init/deploy/README.md new file mode 100644 index 000000000000..1b37ce2e5826 --- /dev/null +++ b/controllers/job/init/deploy/README.md @@ -0,0 +1,104 @@ +# job-init + +## 说明 +sealos run 镜像时会在目标节点执行 Kubefile,本镜像通过 Helm 安装/升级 job-init Job。 + +## 必填参数 + +**无** + +## 默认密码管理规则 + +当未指定 `ADMIN_PASSWORD` 环境变量时,系统将按以下规则自动处理管理员密码: + +1. **优先使用环境变量**:如果指定了 `ADMIN_PASSWORD`,直接使用该密码 +2. **从 ConfigMap 读取**:尝试从 `sealos-system` 命名空间下的 `sealos-cloud-admin` ConfigMap 读取 `PASSWORD` 字段 +3. **自动生成并保存**:如果 ConfigMap 不存在或密码为空,自动生成 16 位随机密码并保存到 ConfigMap + +> 查看保存的密码:`kubectl get cm sealos-cloud-admin -n sealos-system -o jsonpath='{.data.PASSWORD}'` + +## 如何运行 + +```shell +# 最简配置(自动生成或从 ConfigMap 读取密码) +sealos run ghcr.io/labring/sealos-job-init-controller:latest + +# 自定义密码 +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env ADMIN_PASSWORD="MyCustomPassword123" + +# 建议配置(设置密码盐值 + 自动密码) +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env PASSWORD_SALT="your-random-salt-string" +``` + +## 可选参数 + +- ADMIN_PASSWORD: 管理员密码,若不指定则自动从 ConfigMap 读取或生成随机密码 +- PASSWORD_SALT: 密码盐值(建议传入以增强安全性) +- ADMIN_USER_NAME: 管理员用户名,默认 `admin` +- WORKSPACE_PREFIX: 工作空间前缀,默认 `ns-` +- DEFAULT_NAMESPACE / RELEASE_NAMESPACE: Helm 安装命名空间,默认 `account-system` +- RELEASE_NAME: Helm release 名称,默认 `job-init` +- JOB_NAME: Job 资源名,默认 `init-job`(若改动 fullnameOverride 需同步) +- ENV_FROM_CONFIGMAP: Job envFrom 的 ConfigMap 名称,默认 `account-manager-env` +- JOB_INIT_SERVICE_ACCOUNT: Job 使用的 ServiceAccount 名称,默认 `account-controller-manager` +- HELM_OPTS: 透传 Helm 参数(例如 `--set ttlSecondsAfterFinished=3600`) +- CHART_PATH: Helm chart 路径,默认 `./charts/job-init` + +## 示例 + +```shell +# 1. 最简配置(自动生成随机密码并保存到 ConfigMap) +sealos run ghcr.io/labring/sealos-job-init-controller:latest + +# 2. 查看自动生成的密码 +kubectl get cm sealos-cloud-admin -n sealos-system -o jsonpath='{.data.PASSWORD}' + +# 3. 使用已保存的密码(从 ConfigMap 读取) +sealos run ghcr.io/labring/sealos-job-init-controller:latest + +# 4. 自定义密码(不使用随机密码) +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env ADMIN_PASSWORD="MyStrongPassword123" \ + --env PASSWORD_SALT="your-salt" + +# 5. 自定义管理员账号和密码 +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env PASSWORD_SALT="your-salt" \ + --env ADMIN_PASSWORD="MyStrongPassword123" \ + --env ADMIN_USER_NAME="admin" + +# 6. 自定义命名空间和工作空间前缀 +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env PASSWORD_SALT="your-salt" \ + --env DEFAULT_NAMESPACE="my-account-system" \ + --env WORKSPACE_PREFIX="workspace-" + +# 7. 使用 HELM_OPTS 自定义 Job 配置 +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env PASSWORD_SALT="your-salt" \ + --env HELM_OPTS="--set ttlSecondsAfterFinished=3600 --set backoffLimit=2" + +# 8. 完整配置示例 +sealos run ghcr.io/labring/sealos-job-init-controller:latest \ + --env PASSWORD_SALT="my-secure-random-salt-12345" \ + --env ADMIN_PASSWORD="Admin@2024" \ + --env ADMIN_USER_NAME="admin" \ + --env WORKSPACE_PREFIX="user-" \ + --env DEFAULT_NAMESPACE="production-account" \ + --env HELM_OPTS="--set ttlSecondsAfterFinished=7200" +``` + +## Helm Chart 可配置参数 + +可通过 `HELM_OPTS` 传递以下参数: + +- `ttlSecondsAfterFinished`: Job 完成后的保留时间(秒) +- `backoffLimit`: Job 重试次数,默认值由 Chart 定义 +- `image`: 容器镜像 +- `resources.limits.cpu/memory`: CPU/内存限制 +- `resources.requests.cpu/memory`: CPU/内存请求 +- `nodeSelector`: 节点选择器 +- `tolerations`: 容忍度配置 +- `affinity`: 亲和性配置 diff --git a/controllers/job/init/deploy/charts/job-init/Chart.yaml b/controllers/job/init/deploy/charts/job-init/Chart.yaml new file mode 100644 index 000000000000..ceb6ffc7f8de --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: job-init +description: Helm chart for the sealos job init +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/controllers/job/init/deploy/charts/job-init/job-init-values.yaml b/controllers/job/init/deploy/charts/job-init/job-init-values.yaml new file mode 100644 index 000000000000..e7132ee8b76c --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/job-init-values.yaml @@ -0,0 +1,10 @@ +# Custom values for job init helm chart. +# This file contains user-customizable configurations. + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi diff --git a/controllers/job/init/deploy/charts/job-init/templates/_helpers.tpl b/controllers/job/init/deploy/charts/job-init/templates/_helpers.tpl new file mode 100644 index 000000000000..33b6dac8a009 --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/templates/_helpers.tpl @@ -0,0 +1,42 @@ +{{- define "job-init.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "job-init.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "job-init.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "job-init.labels" -}} +helm.sh/chart: {{ include "job-init.chart" . }} +{{ include "job-init.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "job-init.selectorLabels" -}} +app.kubernetes.io/name: {{ include "job-init.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "job-init.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "job-init.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/job/init/deploy/charts/job-init/templates/job.yaml b/controllers/job/init/deploy/charts/job-init/templates/job.yaml new file mode 100644 index 000000000000..c88e9ff032c6 --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/templates/job.yaml @@ -0,0 +1,66 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "job-init.fullname" . }} + labels: + {{- include "job-init.labels" . | nindent 4 }} +spec: + ttlSecondsAfterFinished: {{ .Values.ttlSecondsAfterFinished }} + backoffLimit: {{ .Values.backoffLimit }} + template: + metadata: + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "job-init.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "job-init.serviceAccountName" . }} + restartPolicy: {{ .Values.restartPolicy }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: job-init + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image }}" + imagePullPolicy: {{ .Values.imagePullPolicy }} + env: + - name: LOCAL_REGION + value: {{.Values.env.localRegion}} + - name: DOMAIN + value: {{.Values.env.domain}} + - name: GLOBAL_COCKROACH_URI + value: {{.Values.env.globalCockroachUri}} + - name: LOCAL_COCKROACH_URI + value: {{.Values.env.localCockroachUri}} + - name: PASSWORD_SALT + value: {{ .Values.env.passwordSalt | quote }} + - name: ADMIN_PASSWORD + value: {{ .Values.env.adminPassword | quote }} + - name: ADMIN_USER_NAME + value: {{ .Values.env.adminUserName | quote }} + - name: WORKSPACE_PREFIX + value: {{ .Values.env.workspacePrefix | quote }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/controllers/job/init/deploy/charts/job-init/templates/rbac.yaml b/controllers/job/init/deploy/charts/job-init/templates/rbac.yaml new file mode 100644 index 000000000000..442c9ee8b7ff --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/templates/rbac.yaml @@ -0,0 +1,32 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "job-init.fullname" . }}-cluster-role + labels: + {{- include "job-init.labels" . | nindent 4 }} +rules: + - apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - update + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "job-init.fullname" . }}-cluster-role-binding + labels: + {{- include "job-init.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "job-init.fullname" . }}-cluster-role +subjects: + - kind: ServiceAccount + name: {{ include "job-init.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/job/init/deploy/charts/job-init/templates/serviceaccount.yaml b/controllers/job/init/deploy/charts/job-init/templates/serviceaccount.yaml new file mode 100644 index 000000000000..b17893df4177 --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "job-init.serviceAccountName" . }} + labels: + {{- include "job-init.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/controllers/job/init/deploy/charts/job-init/values.yaml b/controllers/job/init/deploy/charts/job-init/values.yaml new file mode 100644 index 000000000000..78e0484bfdc1 --- /dev/null +++ b/controllers/job/init/deploy/charts/job-init/values.yaml @@ -0,0 +1,50 @@ +image: ghcr.io/labring/sealos-job-init-controller:latest +imagePullPolicy: Always + +imagePullSecrets: [] +fullnameOverride: "init-job" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + +ttlSecondsAfterFinished: 86400 +backoffLimit: 10 +restartPolicy: OnFailure + +# Environment variables (auto-configured by entrypoint.sh) +# These values are set dynamically via --set-string flags: +# - PASSWORD_SALT: from env.passwordSalt (or env var PASSWORD_SALT) +# - ADMIN_PASSWORD: from env.adminPassword (from ConfigMap or generated) +# - ADMIN_USER_NAME: from env.adminUserName (or env var ADMIN_USER_NAME) +# - WORKSPACE_PREFIX: from env.workspacePrefix (or env var WORKSPACE_PREFIX) +# - envFromConfigMap: from env.envFromConfigMap (or env var ENV_FROM_CONFIGMAP) +env: + passwordSalt: "" + adminPassword: "sealos2023" + adminUserName: "admin" + workspacePrefix: "ns-" + domain: "" + globalCockroachUri: "" + localCockroachUri: "" + localRegion: "" + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/controllers/job/init/deploy/job-init-entrypoint.sh b/controllers/job/init/deploy/job-init-entrypoint.sh new file mode 100755 index 000000000000..b4689766840e --- /dev/null +++ b/controllers/job/init/deploy/job-init-entrypoint.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# job-init Helm 部署入口,负责安装/升级初始化 Job 资源。 +set -e + +HELM_OPTS=${HELM_OPTS:-""} +RELEASE_NAME=${RELEASE_NAME:-"job-init"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"${DEFAULT_NAMESPACE:-account-system}"} +CHART_PATH=${CHART_PATH:-"./charts/job-init"} +JOB_NAME=${JOB_NAME:-"init-job"} +ADMIN_PASSWORD_CM_NAMESPACE="sealos-system" +ADMIN_PASSWORD_CM_NAME="sealos-cloud-admin" +ADMIN_PASSWORD_CM_KEY="PASSWORD" + +add_set_string() { + local key="$1" + local value="$2" + HELM_SET_ARGS+=(--set-string "${key}=${value}") +} + +generate_random_password() { + # 生成随机密码(大小写字母+数字+特殊字符) + openssl rand -hex 24 | head -c 32 +} + +get_or_generate_admin_password() { + local password="${ADMIN_PASSWORD:-}" + + # 如果已通过环境变量指定密码,直接使用 + if [ -n "${password}" ]; then + echo "${password}" + return + fi + + # 尝试从 ConfigMap 读取已保存的密码 + if kubectl -n "${ADMIN_PASSWORD_CM_NAMESPACE}" get configmap "${ADMIN_PASSWORD_CM_NAME}" >/dev/null 2>&1; then + local saved_password + saved_password=$(kubectl -n "${ADMIN_PASSWORD_CM_NAMESPACE}" get configmap "${ADMIN_PASSWORD_CM_NAME}" -o "jsonpath={.data.${ADMIN_PASSWORD_CM_KEY}}" 2>/dev/null || true) + if [ -n "${saved_password}" ]; then + echo "${saved_password}" + return + fi + fi + + # ConfigMap 不存在或密码为空,生成新密码并保存 + password=$(generate_random_password) + + # 创建或更新 ConfigMap + if kubectl -n "${ADMIN_PASSWORD_CM_NAMESPACE}" get configmap "${ADMIN_PASSWORD_CM_NAME}" >/dev/null 2>&1; then + # ConfigMap 已存在,更新密码 + kubectl -n "${ADMIN_PASSWORD_CM_NAMESPACE}" patch configmap "${ADMIN_PASSWORD_CM_NAME}" -p "{\"data\":{\"${ADMIN_PASSWORD_CM_KEY}\":\"${password}\"}}" >/dev/null 2>&1 || true + else + # ConfigMap 不存在,创建新的 + kubectl -n "${ADMIN_PASSWORD_CM_NAMESPACE}" create configmap "${ADMIN_PASSWORD_CM_NAME}" --from-literal="${ADMIN_PASSWORD_CM_KEY}=${password}" >/dev/null 2>&1 || true + fi + + echo "${password}" +} + +HELM_SET_ARGS=() + +PASSWORD_SALT=$(kubectl get cm -n sealos-system sealos-config -o "jsonpath={.data.passwordSalt}" 2>/dev/null || true) +# 处理密码盐值 +if [ -n "${PASSWORD_SALT:-}" ]; then + add_set_string env.passwordSalt "${PASSWORD_SALT}" +fi + +# 处理管理员密码(优先使用环境变量,否则从 ConfigMap 读取或生成随机密码) +ADMIN_PASSWORD=$(get_or_generate_admin_password) +add_set_string env.adminPassword "${ADMIN_PASSWORD}" + +# 打印密码信息(如果是从 ConfigMap 读取或自动生成的) +echo "==========================================" +echo "Admin Password: ${ADMIN_PASSWORD}" +echo "Password saved to ConfigMap: ${ADMIN_PASSWORD_CM_NAME} in namespace ${ADMIN_PASSWORD_CM_NAMESPACE}" +echo "Retrieve with: kubectl get cm ${ADMIN_PASSWORD_CM_NAME} -n ${ADMIN_PASSWORD_CM_NAMESPACE} -o jsonpath='{.data.${ADMIN_PASSWORD_CM_KEY}}'" +echo "==========================================" + +if [ -n "${ADMIN_USER_NAME:-}" ]; then + add_set_string env.adminUserName "${ADMIN_USER_NAME}" +fi +if [ -n "${WORKSPACE_PREFIX:-}" ]; then + add_set_string env.workspacePrefix "${WORKSPACE_PREFIX}" +fi +if [ -n "${ENV_FROM_CONFIGMAP:-}" ]; then + add_set_string env.envFromConfigMap "${ENV_FROM_CONFIGMAP}" +fi + +if [ -n "${JOB_INIT_SERVICE_ACCOUNT:-}" ]; then + add_set_string serviceAccount.name "${JOB_INIT_SERVICE_ACCOUNT}" +fi + +SEALOS_CLOUD_DOMAIN=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.cloudDomain}') +varDatabaseGlobalCockroachdbURI=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.databaseGlobalCockroachdbURI}') +varDatabaseLocalCockroachdbURI=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.databaseLocalCockroachdbURI}') +varRegionUID=$(kubectl get configmap sealos-config -n sealos-system -o jsonpath='{.data.regionUID}') + +add_set_string env.domain "${SEALOS_CLOUD_DOMAIN}" +add_set_string env.globalCockroachUri "${varDatabaseGlobalCockroachdbURI}" +add_set_string env.localCockroachUri "${varDatabaseLocalCockroachdbURI}" +add_set_string env.localRegion "${varRegionUID}" + +if kubectl -n "${RELEASE_NAMESPACE}" get job "${JOB_NAME}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" delete job "${JOB_NAME}" --ignore-not-found --wait=true +fi + +# Prepare values files +SERVICE_NAME="job-init" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + "${HELM_SET_ARGS[@]}" \ + ${HELM_OPTS} diff --git a/controllers/job/init/go.mod b/controllers/job/init/go.mod new file mode 100644 index 000000000000..d1ea3d5bc917 --- /dev/null +++ b/controllers/job/init/go.mod @@ -0,0 +1,95 @@ +module github.com/labring/sealos/controllers/job/init + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b + github.com/labring/sealos/controllers/user v0.0.0 + gorm.io/gorm v1.25.5 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v12.0.0+incompatible + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matoous/go-nanoid/v2 v2.0.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/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.mongodb.org/mongo-driver v1.12.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.4 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + github.com/labring/sealos/controllers/account => ../../account + github.com/labring/sealos/controllers/pkg => ../../pkg + github.com/labring/sealos/controllers/user => ../../user +) + +replace ( + k8s.io/api => k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 + k8s.io/client-go => k8s.io/client-go v0.28.3 + k8s.io/component-base => k8s.io/component-base v0.28.3 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2 +) diff --git a/controllers/job/init/go.sum b/controllers/job/init/go.sum new file mode 100644 index 000000000000..34e892879304 --- /dev/null +++ b/controllers/job/init/go.sum @@ -0,0 +1,256 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +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.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/job/init/internal/util/common/uuid.go b/controllers/job/init/internal/util/common/uuid.go new file mode 100644 index 000000000000..70161ca75bdf --- /dev/null +++ b/controllers/job/init/internal/util/common/uuid.go @@ -0,0 +1,26 @@ +// Copyright © 2023 sealos. +// +// 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 common + +import "github.com/google/uuid" + +var adminUID uuid.UUID + +func AdminUID() uuid.UUID { + if adminUID == uuid.Nil { + adminUID = uuid.New() + } + return adminUID +} diff --git a/controllers/job/init/internal/util/controller/user.go b/controllers/job/init/internal/util/controller/user.go new file mode 100644 index 000000000000..214a20077dfc --- /dev/null +++ b/controllers/job/init/internal/util/controller/user.go @@ -0,0 +1,80 @@ +// Copyright © 2023 sealos. +// +// 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 controller + +import ( + "context" + + "github.com/labring/sealos/controllers/job/init/internal/util/common" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "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/client" +) + +// pre-defined user info in kubernetes + +const ( + DefaultAdminUserName = "admin" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(userv1.AddToScheme(scheme)) +} + +func newKubernetesClient() (client.Client, error) { + c, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme}) + if err != nil { + return nil, err + } + return c, nil +} + +func newAdminUser(ctx context.Context, c client.Client) (*userv1.User, error) { + u := &userv1.User{} + u.SetName(DefaultAdminUserName) + if err := c.Get(ctx, client.ObjectKeyFromObject(u), u); client.IgnoreNotFound(err) != nil { + return nil, err + } + if u.Labels == nil { + u.SetLabels( + map[string]string{"uid": common.AdminUID().String(), "updateTime": "T2301-01T00-00-00"}, + ) + } else if u.Labels["uid"] == "" { + u.Labels["uid"] = common.AdminUID().String() + u.Labels["updateTime"] = "T2301-01T00-00-00" + } + return u, nil +} + +func PresetAdminUser(ctx context.Context) error { + c, err := newKubernetesClient() + if err != nil { + return err + } + adminUser, err := newAdminUser(ctx, c) + if err != nil { + return err + } + _, err = ctrl.CreateOrUpdate(ctx, c, adminUser, func() error { + return nil + }) + return err +} diff --git a/controllers/job/init/internal/util/database/password.go b/controllers/job/init/internal/util/database/password.go new file mode 100644 index 000000000000..0cd2c2bbccc2 --- /dev/null +++ b/controllers/job/init/internal/util/database/password.go @@ -0,0 +1,29 @@ +// Copyright © 2023 sealos. +// +// 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 database + +import ( + "crypto/sha256" + "encoding/hex" + "os" +) + +var saltKey = os.Getenv("PASSWORD_SALT") + +func hashPassword(password string) string { + hash := sha256.New() + hash.Write([]byte(password + saltKey)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/controllers/job/init/internal/util/database/types.go b/controllers/job/init/internal/util/database/types.go new file mode 100644 index 000000000000..4b652843ac81 --- /dev/null +++ b/controllers/job/init/internal/util/database/types.go @@ -0,0 +1,36 @@ +// Copyright © 2023 sealos. +// +// 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 database + +import ( + "github.com/labring/sealos/controllers/pkg/utils/env" +) + +const ( + DefaultAdminUserName = "admin" + DefaultAdminPassword = "sealos2023" +) + +const ( + EnvAdminUserName = "ADMIN_USER_NAME" + EnvAdminPassword = "ADMIN_PASSWORD" + EnvWorkspacePrefix = "WORKSPACE_PREFIX" +) + +var ( + adminPassword = hashPassword(env.GetEnvWithDefault(EnvAdminPassword, DefaultAdminPassword)) + adminUserName = env.GetEnvWithDefault(EnvAdminUserName, DefaultAdminUserName) + workspacePrefix = env.GetEnvWithDefault(EnvWorkspacePrefix, "ns-") +) diff --git a/controllers/job/init/internal/util/database/user.go b/controllers/job/init/internal/util/database/user.go new file mode 100644 index 000000000000..47a1bd2f04fa --- /dev/null +++ b/controllers/job/init/internal/util/database/user.go @@ -0,0 +1,154 @@ +// Copyright © 2023 sealos. +// +// 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 database + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/job/init/internal/util/common" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/logger" + "github.com/labring/sealos/controllers/pkg/utils/retry" +) + +func PresetAdminUser() error { + v2Account, err := cockroach.NewCockRoach( + os.Getenv(database.GlobalCockroachURI), + os.Getenv(database.LocalCockroachURI), + ) + if err != nil { + return fmt.Errorf("failed to connect to cockroach: %w", err) + } + defer func() { + err := v2Account.Close() + if err != nil { + logger.Warn("failed to close cockroach connection: %v", err) + } + }() + domain := os.Getenv("DOMAIN") + if domain == "" { + return errors.New("'DOMAIN' the environment variable is not set. please check") + } + regionUID, err := uuid.Parse(os.Getenv(cockroach.EnvLocalRegion)) + if err != nil { + return fmt.Errorf( + "failed to parse region %s uid: %w", + os.Getenv(cockroach.EnvLocalRegion), + err, + ) + } + err = retry.Retry(10, 3*time.Second, func() error { + tableTypes := []any{ + types.User{}, + types.Region{}, + types.RegionUserCr{}, + types.Workspace{}, + types.UserWorkspace{}, + } + for _, tableType := range tableTypes { + if err := checkTableExists(v2Account, tableType); err != nil { + fmt.Println(err) + return err + } + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to check user table: %w", err) + } + if err = v2Account.CreateRegion(&types.Region{ + UID: regionUID, + Domain: domain, + DisplayName: domain, + Location: domain, + Description: types.RegionDescriptionJSON(types.RegionDescription{ + Provider: domain + "-local", + Serial: "A", + Description: map[string]string{ + "zh": domain + "-本地", + "en": domain + "-local", + }, + }), + }); err != nil { + return fmt.Errorf("failed to create region: %w", err) + } + userNanoID := "admin" + genUserCrUID, genWorkspaceUID := uuid.New(), uuid.New() + if err = v2Account.CreateUser(&types.OauthProvider{ + UserUID: common.AdminUID(), + ProviderType: types.OauthProviderTypePassword, + ProviderID: adminUserName, + Password: adminPassword, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, &types.RegionUserCr{ + UID: genUserCrUID, + CrName: adminUserName, + UserUID: common.AdminUID(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, &types.User{ + UID: common.AdminUID(), + ID: userNanoID, + Name: userNanoID, + Nickname: adminUserName, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Status: types.UserStatusNormal, + }, &types.Workspace{ + UID: genWorkspaceUID, + ID: workspacePrefix + adminUserName, + DisplayName: "private team", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, &types.UserWorkspace{ + WorkspaceUID: genWorkspaceUID, + UserCrUID: genUserCrUID, + Role: types.RoleOwner, + Status: types.JoinStatusInWorkspace, + IsPrivate: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + JoinAt: time.Now(), + }); err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + if err = v2Account.InitTables(); err != nil { + return fmt.Errorf("failed to init tables: %w", err) + } + if _, err = v2Account.NewAccount(&types.UserQueryOpts{Owner: adminUserName}); err != nil { + return fmt.Errorf("failed to create account: %w", err) + } + if err = v2Account.AddBalance( + &types.UserQueryOpts{Owner: adminUserName}, + 9999999_000_000, + ); err != nil { + return fmt.Errorf("failed to add balance: %w", err) + } + return nil +} + +func checkTableExists(m *cockroach.Cockroach, tableType any) error { + if !m.DB.Migrator().HasTable(tableType) && !m.Localdb.Migrator().HasTable(tableType) { + return fmt.Errorf("%T table is null, please check", tableType) + } + return nil +} diff --git a/controllers/job/init/internal/util/errors/errors.go b/controllers/job/init/internal/util/errors/errors.go new file mode 100644 index 000000000000..511878157e95 --- /dev/null +++ b/controllers/job/init/internal/util/errors/errors.go @@ -0,0 +1,15 @@ +// Copyright © 2023 sealos. +// +// 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 errors diff --git a/controllers/license/.dockerignore b/controllers/license/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/license/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/license/.gitignore b/controllers/license/.gitignore new file mode 100644 index 000000000000..7f02333db7ab --- /dev/null +++ b/controllers/license/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/controllers/license/Dockerfile b/controllers/license/Dockerfile new file mode 100644 index 000000000000..38839c30c69d --- /dev/null +++ b/controllers/license/Dockerfile @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-license-$TARGETARCH /manager + +ENTRYPOINT ["/manager"] \ No newline at end of file diff --git a/controllers/license/Makefile b/controllers/license/Makefile new file mode 100644 index 000000000000..9299c112da28 --- /dev/null +++ b/controllers/license/Makefile @@ -0,0 +1,175 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-license-controller:latest +TARGETARCH ?= amd64 +GOARCH ?= amd64 + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.26.1 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + cp -r config/crd/bases/* deploy/charts/license-controller/crds/ + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + @LD_FLAGS=""; \ + [ -n "$(LICENSE_KEY)" ] && LD_FLAGS="-X ${CONTROLLER_PKG}.encryptionKey=${LICENSE_KEY}"; \ + go test -race -ldflags "$${LD_FLAGS}" ./... -count=1 + + +##@ Build + +.PHONY: build +CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg/license +build: + LD_FLAGS="-s -w"; \ + [ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_PKG}.encryptionKey=${LICENSE_KEY}"; \ + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager cmd/manager/main.go + + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/manager/main.go + +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: build## Build docker image with the manager. + mv bin/manager bin/controller-license-${TARGETARCH} || true + chmod +x bin/controller-license-${TARGETARCH} + docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - docker buildx create --name project-v3-builder + docker buildx use project-v3-builder + - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - docker buildx rm project-v3-builder + rm Dockerfile.cross + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: pre-deploy +pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v4.2.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) --output install_kustomize.sh && bash install_kustomize.sh $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); rm install_kustomize.sh; } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/license/PROJECT b/controllers/license/PROJECT new file mode 100644 index 000000000000..1d476dd9e485 --- /dev/null +++ b/controllers/license/PROJECT @@ -0,0 +1,20 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: sealos.io +layout: +- go.kubebuilder.io/v4 +projectName: license +repo: github.com/labring/sealos/controllers/license +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: license + kind: License + path: github.com/labring/sealos/controllers/license/api/v1 + version: v1 +version: "3" diff --git a/controllers/license/README.md b/controllers/license/README.md new file mode 100644 index 000000000000..9e81c63e9d2d --- /dev/null +++ b/controllers/license/README.md @@ -0,0 +1,94 @@ +# license +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/license:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/license:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller from the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), +which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2023. + +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. + diff --git a/controllers/license/api/v1/groupversion_info.go b/controllers/license/api/v1/groupversion_info.go new file mode 100644 index 000000000000..0aa6231881d3 --- /dev/null +++ b/controllers/license/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +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 v1 contains API Schema definitions for the license v1 API group +// +kubebuilder:object:generate=true +// +groupName=license.sealos.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "license.sealos.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/controllers/license/api/v1/license_types.go b/controllers/license/api/v1/license_types.go new file mode 100644 index 000000000000..79a97bc65a47 --- /dev/null +++ b/controllers/license/api/v1/license_types.go @@ -0,0 +1,92 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LicenseType string + +const ( + ClusterLicenseType LicenseType = "Cluster" +) + +// LicenseSpec defines the desired state of License +type LicenseSpec struct { + Type LicenseType `json:"type,omitempty"` + Token string `json:"token,omitempty"` +} + +type LicenseStatusPhase string + +const ( + LicenseStatusPhasePending LicenseStatusPhase = "Pending" + LicenseStatusPhaseFailed LicenseStatusPhase = "Failed" + LicenseStatusPhaseActive LicenseStatusPhase = "Active" + LicenseStatusPhaseExpired LicenseStatusPhase = "Expired" + LicenseStatusPhaseInvalid LicenseStatusPhase = "Invalid" + LicenseStatusPhaseMismatch LicenseStatusPhase = "Mismatch" +) + +type ValidationCode int + +const ( + ValidationSuccess ValidationCode = iota + ValidationError + ValidationExpired + ValidationClusterIDMismatch + ValidationClusterInfoMismatch +) + +// LicenseStatus defines the observed state of License +type LicenseStatus struct { + //+kubebuilder:validation:Enum=Pending;Failed;Active;Expired;Invalid;Mismatch + //+kubebuilder:default=Pending + Phase LicenseStatusPhase `json:"phase,omitempty"` + Code ValidationCode `json:"code,omitempty"` + Reason string `json:"reason,omitempty"` + ActivationTime metav1.Time `json:"activationTime,omitempty"` + ExpirationTime metav1.Time `json:"expirationTime,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// License is the Schema for the licenses API +type License struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LicenseSpec `json:"spec,omitempty"` + Status LicenseStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// LicenseList contains a list of License +type LicenseList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []License `json:"items"` +} + +func init() { + SchemeBuilder.Register(&License{}, &LicenseList{}) +} diff --git a/controllers/license/api/v1/zz_generated.deepcopy.go b/controllers/license/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..52297994b8be --- /dev/null +++ b/controllers/license/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,116 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023. + +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 v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *License) DeepCopyInto(out *License) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new License. +func (in *License) DeepCopy() *License { + if in == nil { + return nil + } + out := new(License) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *License) 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 *LicenseList) DeepCopyInto(out *LicenseList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]License, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LicenseList. +func (in *LicenseList) DeepCopy() *LicenseList { + if in == nil { + return nil + } + out := new(LicenseList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LicenseList) 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 *LicenseSpec) DeepCopyInto(out *LicenseSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LicenseSpec. +func (in *LicenseSpec) DeepCopy() *LicenseSpec { + if in == nil { + return nil + } + out := new(LicenseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LicenseStatus) DeepCopyInto(out *LicenseStatus) { + *out = *in + in.ActivationTime.DeepCopyInto(&out.ActivationTime) + in.ExpirationTime.DeepCopyInto(&out.ExpirationTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LicenseStatus. +func (in *LicenseStatus) DeepCopy() *LicenseStatus { + if in == nil { + return nil + } + out := new(LicenseStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/license/cmd/manager/main.go b/controllers/license/cmd/manager/main.go new file mode 100644 index 000000000000..e1db9af4729e --- /dev/null +++ b/controllers/license/cmd/manager/main.go @@ -0,0 +1,215 @@ +/* +Copyright 2023. + +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" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + licensecache "github.com/labring/sealos/controllers/license/internal/cache" + "github.com/labring/sealos/controllers/license/internal/controller" + utilid "github.com/labring/sealos/controllers/license/internal/util/clusterid" + "github.com/labring/sealos/controllers/license/internal/util/rate" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ccontroler "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(licensev1.AddToScheme(scheme)) + utilruntime.Must(notificationv1.AddToScheme(scheme)) + utilruntime.Must(userv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + var concurrent int + rateLimiterOptions := &rate.LimiterOptions{} + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.IntVar(&concurrent, "concurrent", 100, "The number of concurrent cluster reconciles.") + + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + 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") + opts := zap.Options{ + Development: true, + } + rateLimiterOptions.BindFlags(flag.CommandLine) + 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) + } + // 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, + Cache: licensecache.Options(), + Client: client.Options{Cache: &client.CacheOptions{ + DisableFor: licensecache.UncachedObjects(), + }}, + Metrics: metricsServerOptions, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "90cbb0a1.sealos.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 start manager") + os.Exit(1) + } + rateOpts := ccontroler.Options{ + MaxConcurrentReconciles: concurrent, + RateLimiter: rate.GetRateLimiter(rateLimiterOptions), + } + + ctx := ctrl.SetupSignalHandler() + clusterID, err := utilid.GetClusterID(ctx, mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "unable to get cluster id") + os.Exit(1) + } + createTime, err := utilid.GetClusterCreateTime(ctx, mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "unable to get cluster create time") + os.Exit(1) + } + setupLog.Info( + "cluster id and create time", + "id", + clusterID, + "createTime", + createTime.String(), + ) + + reconciler := &controller.LicenseReconciler{ + ClusterID: clusterID, + CreateTimestamp: *createTime, + } + if err = licensecache.SetupInformers(mgr); err != nil { + setupLog.Error(err, "unable to set up license cache informers") + os.Exit(1) + } + if err = reconciler.SetupWithManager(mgr, rateOpts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "License") + 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(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml b/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml new file mode 100644 index 000000000000..94484c1d72c1 --- /dev/null +++ b/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: licenses.license.sealos.io +spec: + group: license.sealos.io + names: + kind: License + listKind: LicenseList + plural: licenses + singular: license + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: License is the Schema for the licenses API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: LicenseSpec defines the desired state of License + properties: + token: + type: string + type: + type: string + type: object + status: + description: LicenseStatus defines the observed state of License + properties: + activationTime: + format: date-time + type: string + code: + type: integer + expirationTime: + format: date-time + type: string + phase: + default: Pending + enum: + - Pending + - Failed + - Active + - Expired + - Invalid + - Mismatch + type: string + reason: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/license/config/crd/kustomization.yaml b/controllers/license/config/crd/kustomization.yaml new file mode 100644 index 000000000000..f01cd6fd1236 --- /dev/null +++ b/controllers/license/config/crd/kustomization.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/license.sealos.io_licenses.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- path: patches/webhook_in_licenses.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_licenses.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controllers/license/config/crd/kustomizeconfig.yaml b/controllers/license/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..9c99fc52fb20 --- /dev/null +++ b/controllers/license/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controllers/license/config/crd/patches/cainjection_in_licenses.yaml b/controllers/license/config/crd/patches/cainjection_in_licenses.yaml new file mode 100644 index 000000000000..9e784f2259b6 --- /dev/null +++ b/controllers/license/config/crd/patches/cainjection_in_licenses.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: licenses.license.sealos.io diff --git a/controllers/license/config/crd/patches/webhook_in_licenses.yaml b/controllers/license/config/crd/patches/webhook_in_licenses.yaml new file mode 100644 index 000000000000..06f60bdc066a --- /dev/null +++ b/controllers/license/config/crd/patches/webhook_in_licenses.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: licenses.license.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/license/config/default/kustomization.yaml b/controllers/license/config/default/kustomization.yaml new file mode 100644 index 000000000000..9be6f903f2a1 --- /dev/null +++ b/controllers/license/config/default/kustomization.yaml @@ -0,0 +1,158 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: account-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: license- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/controllers/license/config/default/manager_auth_proxy_patch.yaml b/controllers/license/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..52d57cd15fd9 --- /dev/null +++ b/controllers/license/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,53 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/controllers/license/config/default/manager_config_patch.yaml b/controllers/license/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..c03d201a433d --- /dev/null +++ b/controllers/license/config/default/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/controllers/license/config/manager/kustomization.yaml b/controllers/license/config/manager/kustomization.yaml new file mode 100644 index 000000000000..74ab814126c4 --- /dev/null +++ b/controllers/license/config/manager/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-license-controller + newTag: latest diff --git a/controllers/license/config/manager/manager.yaml b/controllers/license/config/manager/manager.yaml new file mode 100644 index 000000000000..5648d80633aa --- /dev/null +++ b/controllers/license/config/manager/manager.yaml @@ -0,0 +1,119 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + envFrom: + - configMapRef: + name: account-manager-env + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/license/config/prometheus/kustomization.yaml b/controllers/license/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/license/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/license/config/prometheus/monitor.yaml b/controllers/license/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..9e1527ba6bbc --- /dev/null +++ b/controllers/license/config/prometheus/monitor.yaml @@ -0,0 +1,40 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/license/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/license/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..2eb5a67bd050 --- /dev/null +++ b/controllers/license/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/license/config/rbac/auth_proxy_role.yaml b/controllers/license/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..e41d49089bc4 --- /dev/null +++ b/controllers/license/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/license/config/rbac/auth_proxy_role_binding.yaml b/controllers/license/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..4f99e6e695f6 --- /dev/null +++ b/controllers/license/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/license/config/rbac/auth_proxy_service.yaml b/controllers/license/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..0fdc0588a7c6 --- /dev/null +++ b/controllers/license/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/license/config/rbac/kustomization.yaml b/controllers/license/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/license/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/license/config/rbac/leader_election_role.yaml b/controllers/license/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..ac345e074885 --- /dev/null +++ b/controllers/license/config/rbac/leader_election_role.yaml @@ -0,0 +1,58 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/license/config/rbac/leader_election_role_binding.yaml b/controllers/license/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..9d5f16a0407a --- /dev/null +++ b/controllers/license/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/license/config/rbac/license_editor_role.yaml b/controllers/license/config/rbac/license_editor_role.yaml new file mode 100644 index 000000000000..86182c457da5 --- /dev/null +++ b/controllers/license/config/rbac/license_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit licenses. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: license-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: license-editor-role +rules: +- apiGroups: + - license.sealos.io + resources: + - licenses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses/status + verbs: + - get diff --git a/controllers/license/config/rbac/license_viewer_role.yaml b/controllers/license/config/rbac/license_viewer_role.yaml new file mode 100644 index 000000000000..f0aced53478d --- /dev/null +++ b/controllers/license/config/rbac/license_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view licenses. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: license-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: license-viewer-role +rules: +- apiGroups: + - license.sealos.io + resources: + - licenses + verbs: + - get + - list + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses/status + verbs: + - get diff --git a/controllers/license/config/rbac/role.yaml b/controllers/license/config/rbac/role.yaml new file mode 100644 index 000000000000..26ce2ca05ccc --- /dev/null +++ b/controllers/license/config/rbac/role.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses/finalizers + verbs: + - update +- apiGroups: + - license.sealos.io + resources: + - licenses/status + verbs: + - get + - patch + - update diff --git a/controllers/license/config/rbac/role_binding.yaml b/controllers/license/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..8bb92a967841 --- /dev/null +++ b/controllers/license/config/rbac/role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/license/config/rbac/service_account.yaml b/controllers/license/config/rbac/service_account.yaml new file mode 100644 index 000000000000..32fd1a1c998b --- /dev/null +++ b/controllers/license/config/rbac/service_account.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: license + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/controllers/license/config/samples/kustomization.yaml b/controllers/license/config/samples/kustomization.yaml new file mode 100644 index 000000000000..7cf1c9a58c43 --- /dev/null +++ b/controllers/license/config/samples/kustomization.yaml @@ -0,0 +1,18 @@ +# Copyright © 2023 sealos. +# +# 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. + +## Append samples of your project ## +resources: +- license_v1_license.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/license/config/samples/license_v1_license.yaml b/controllers/license/config/samples/license_v1_license.yaml new file mode 100644 index 000000000000..bf8027c56b58 --- /dev/null +++ b/controllers/license/config/samples/license_v1_license.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: license.sealos.io/v1 +kind: License +metadata: + labels: + app.kubernetes.io/name: license + app.kubernetes.io/instance: license-sample + app.kubernetes.io/part-of: license + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: license + name: license-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/license/deploy/Kubefile b/controllers/license/deploy/Kubefile new file mode 100644 index 000000000000..ccad1ecdf531 --- /dev/null +++ b/controllers/license/deploy/Kubefile @@ -0,0 +1,8 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY license-controller-entrypoint.sh license-controller-entrypoint.sh +CMD ["bash license-controller-entrypoint.sh"] diff --git a/controllers/license/deploy/charts/license-controller/.helmignore b/controllers/license/deploy/charts/license-controller/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/controllers/license/deploy/charts/license-controller/Chart.yaml b/controllers/license/deploy/charts/license-controller/Chart.yaml new file mode 100644 index 000000000000..ddcc10ed2351 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: license +description: A Helm chart for Kubernetes + +# 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.1.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. +appVersion: "1.16.0" diff --git a/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml b/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml new file mode 100644 index 000000000000..94484c1d72c1 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: licenses.license.sealos.io +spec: + group: license.sealos.io + names: + kind: License + listKind: LicenseList + plural: licenses + singular: license + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: License is the Schema for the licenses API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: LicenseSpec defines the desired state of License + properties: + token: + type: string + type: + type: string + type: object + status: + description: LicenseStatus defines the observed state of License + properties: + activationTime: + format: date-time + type: string + code: + type: integer + expirationTime: + format: date-time + type: string + phase: + default: Pending + enum: + - Pending + - Failed + - Active + - Expired + - Invalid + - Mismatch + type: string + reason: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/license/deploy/charts/license-controller/license-controller-values.yaml b/controllers/license/deploy/charts/license-controller/license-controller-values.yaml new file mode 100644 index 000000000000..79a38a70bf49 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/license-controller-values.yaml @@ -0,0 +1,23 @@ +# Custom values for license controller helm chart. +# This file contains user-customizable configurations. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi diff --git a/pkg/utils/archive/testdata/dd/bb.txt b/controllers/license/deploy/charts/license-controller/templates/NOTES.txt similarity index 100% rename from pkg/utils/archive/testdata/dd/bb.txt rename to controllers/license/deploy/charts/license-controller/templates/NOTES.txt diff --git a/controllers/license/deploy/charts/license-controller/templates/_helpers.tpl b/controllers/license/deploy/charts/license-controller/templates/_helpers.tpl new file mode 100644 index 000000000000..96c09fec2308 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "license.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "license.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "license.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "license.labels" -}} +helm.sh/chart: {{ include "license.chart" . }} +{{ include "license.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "license.selectorLabels" -}} +app.kubernetes.io/name: {{ include "license.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "license.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "license.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/license/deploy/charts/license-controller/templates/cert.yaml b/controllers/license/deploy/charts/license-controller/templates/cert.yaml new file mode 100644 index 000000000000..446df8abe8da --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/cert.yaml @@ -0,0 +1,27 @@ +{{- if .Values.metrics.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + {{- include "license.labels" . | nindent 4 }} + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + {{- include "license.labels" . | nindent 4 }} + name: metrics-certs +spec: + privateKey: + rotationPolicy: Always + dnsNames: + - {{ include "license.fullname" . }}-metrics.{{.Release.Namespace}}.svc + - {{ include "license.fullname" . }}-metrics.{{.Release.Namespace}}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: {{.Values.metrics.secretName}} +{{- end }} \ No newline at end of file diff --git a/controllers/license/deploy/charts/license-controller/templates/clusterrole.yaml b/controllers/license/deploy/charts/license-controller/templates/clusterrole.yaml new file mode 100644 index 000000000000..11fd80415a08 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/clusterrole.yaml @@ -0,0 +1,122 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: license-controller-clusterrole +rules: +- apiGroups: [ "" ] + resources: [ "namespaces" ] + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses/finalizers + verbs: + - update +- apiGroups: + - license.sealos.io + resources: + - licenses/status + verbs: + - get + - patch + - update +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + resourceNames: + - devboxes.devbox.sealos.io + verbs: + - get +- nonResourceURLs: + - /metrics + verbs: + - get +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + - subjectaccessreviews + verbs: + - create +- apiGroups: + - notification.sealos.io + resources: + - notifications + verbs: + - create + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - update + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: license-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: license-controller-clusterrole +subjects: + - kind: ServiceAccount + name: {{ include "license.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +--- +## ClusterRole for license frontend to get node info +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: license-manager-role +rules: +- apiGroups: [ "" ] + resources: [ "namespaces" ] + verbs: [ "get","watch","list" ] +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +- apiGroups: + - license.sealos.io + resources: + - licenses + verbs: + - "*" diff --git a/controllers/license/deploy/charts/license-controller/templates/deployment.yaml b/controllers/license/deploy/charts/license-controller/templates/deployment.yaml new file mode 100644 index 000000000000..337abedd10cf --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/deployment.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "license.fullname" . }} + labels: + {{- include "license.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "license.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "license.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "license.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + {{- if .Values.metrics.enabled }} + - --metrics-secure=true + - --metrics-bind-address=:8443 + {{- end }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image }}" + imagePullPolicy: Always + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics-certs + mountPath: {{.Values.metrics.certPath}} + readOnly: true + {{- end }} + + volumes: + {{- if .Values.metrics.enabled }} + - name: metrics-certs + secret: + secretName: {{.Values.metrics.secretName}} + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/controllers/license/deploy/charts/license-controller/templates/role.yaml b/controllers/license/deploy/charts/license-controller/templates/role.yaml new file mode 100644 index 000000000000..dc602384a278 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/role.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: license-controller-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: license-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: license-controller-role +subjects: + - kind: ServiceAccount + name: {{ include "license.serviceAccountName" . }} + namespace: {{.Release.Namespace}} \ No newline at end of file diff --git a/controllers/license/deploy/charts/license-controller/templates/service.yaml b/controllers/license/deploy/charts/license-controller/templates/service.yaml new file mode 100644 index 000000000000..89e062136b99 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "license.fullname" . }}-metrics + labels: + {{- include "license.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8443 + protocol: TCP + name: https + selector: + {{- include "license.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/controllers/license/deploy/charts/license-controller/templates/serviceaccount.yaml b/controllers/license/deploy/charts/license-controller/templates/serviceaccount.yaml new file mode 100644 index 000000000000..f4ee7869e6aa --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "license.serviceAccountName" . }} + labels: + {{- include "license.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +--- diff --git a/controllers/license/deploy/charts/license-controller/values.yaml b/controllers/license/deploy/charts/license-controller/values.yaml new file mode 100644 index 000000000000..1c6a651cb830 --- /dev/null +++ b/controllers/license/deploy/charts/license-controller/values.yaml @@ -0,0 +1,84 @@ +# Default values for license controller helm chart. + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: ghcr.io/labring/sealos-license-controller:latest + +# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] + +#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + # fsGroup: 2000 + +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +metrics: + # Enable metrics endpoint + enabled: false + # Path to the metrics certificate + certPath: /tmp/k8s-metrics-server/metrics-certs + secretName: metrics-server-cert diff --git a/controllers/license/deploy/license-controller-entrypoint.sh b/controllers/license/deploy/license-controller-entrypoint.sh new file mode 100644 index 000000000000..8b8a7023214c --- /dev/null +++ b/controllers/license/deploy/license-controller-entrypoint.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +HELM_OPTS=${HELM_OPTS:-""} +RELEASE_NAME=${RELEASE_NAME:-"license"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"license-system"} +CHART_PATH=${CHART_PATH:-"./charts/license-controller"} + +# Clean up old resources (for backward compatibility) +kubectl delete clusterrole kube-system-namespace-read-cluster-role --ignore-not-found +kubectl delete clusterrolebinding license-controller-role-binding --ignore-not-found +kubectl delete deployment license-controller-manager -n account-system --ignore-not-found +kubectl delete service license-controller-manager-metrics-service -n account-system --ignore-not-found +kubectl delete clusterrolebinding license-proxy-rolebinding --ignore-not-found +kubectl delete clusterrolebinding license-manager-rolebinding --ignore-not-found +kubectl delete rolebinding license-leader-election-rolebinding -n account-system --ignore-not-found +kubectl delete clusterrole license-manager-role --ignore-not-found +kubectl delete clusterrole license-metrics-reader --ignore-not-found +kubectl delete clusterrole license-proxy-role --ignore-not-found +kubectl delete clusterrole license-controller-clusterrole --ignore-not-found +kubectl delete role license-leader-election-role -n account-system --ignore-not-found +kubectl delete serviceaccount license-controller-manager -n account-system --ignore-not-found + +# Prepare values files +SERVICE_NAME="license-controller" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +# Deploy Helm chart +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + ${HELM_OPTS} + +# Apply CRDs +helm show crds ./charts/license-controller | kubectl apply -f - --server-side --force-conflicts diff --git a/controllers/license/go.mod b/controllers/license/go.mod new file mode 100644 index 000000000000..0984ff0cba10 --- /dev/null +++ b/controllers/license/go.mod @@ -0,0 +1,128 @@ +module github.com/labring/sealos/controllers/license + +go 1.24.0 + +require ( + github.com/go-logr/logr v1.4.2 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 + github.com/onsi/ginkgo/v2 v2.20.1 + github.com/onsi/gomega v1.36.1 + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v12.0.0+incompatible + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect + github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 // indirect + github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect + github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/openapi-util v0.1.0 // indirect + github.com/alibabacloud-go/tea v1.2.1 // indirect + github.com/alibabacloud-go/tea-utils v1.3.1 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.4 // indirect + github.com/alibabacloud-go/tea-xml v1.1.3 // indirect + github.com/aliyun/credentials-go v1.3.1 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/astaxie/beego v1.12.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.5.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/cel-go v0.17.7 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + 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/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/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/tjfoc/gmsm v1.3.2 // indirect + github.com/volcengine/volc-sdk-golang v1.0.159 // indirect + go.mongodb.org/mongo-driver v1.12.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/sdk v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.24.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect + google.golang.org/grpc v1.61.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/gorm v1.25.5 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + k8s.io/api => k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 + k8s.io/client-go => k8s.io/client-go v0.28.3 + k8s.io/component-base => k8s.io/component-base v0.28.3 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2 +) diff --git a/controllers/license/go.sum b/controllers/license/go.sum new file mode 100644 index 000000000000..ed97f2eb7216 --- /dev/null +++ b/controllers/license/go.sum @@ -0,0 +1,1124 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 h1:iC9YFYKDGEy3n/FtqJnOkZsene9olVspKmkX5A2YBEo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5 h1:yyolbgHfV2Tp91vMjO/CF5aOxKG+UgdVAeUoloEQI3E= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 h1:UTl97mt2qfavxveqCkaVg4tKaZUPzA9RKbFIRaIdtdg= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6/go.mod h1:UWpcGrWwTbES9QW7OQ7xDffukMJ/l7lzioixIz8+lgY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.1 h1:rFF1LnrAdhaiPmKwH5xwYOKlMh66CqRwPUTzIK74ask= +github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= +github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-utils/v2 v2.0.4 h1:SoFgjJuO7pze88j9RBJNbKb7AgTS52O+J5ITxc00lCs= +github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0= +github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1 h1:uq/0v7kWrxmoLGpqjx7vtQ/s03f0zR//0br/xWDTE28= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ= +github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= +github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0= +github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= +github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b h1:nftWTQWwbYMbteQkfp5HyKN3nT69FCKPekA8hl2z+VI= +github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b/go.mod h1:7WBhRcClEuLy/bVMTFICX2jzhmQDsH6UODRONWqrQHY= +github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= +github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= +github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= +github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/volcengine/volc-sdk-golang v1.0.159 h1:0EzJ49M+YJbaGiGxhmD2BvPbsZOgt6RWlgOqKP6IaoQ= +github.com/volcengine/volc-sdk-golang v1.0.159/go.mod h1:iqWIQk0pkcDKEYpIG4vkocgHpeiAabfAK9g0Ob7lSxE= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/apiserver v0.29.0 h1:Y1xEMjJkP+BIi0GSEv1BBrf1jLU9UPfAnnGGbbDdp7o= +k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/license/hack/boilerplate.go.txt b/controllers/license/hack/boilerplate.go.txt new file mode 100644 index 000000000000..65b862271836 --- /dev/null +++ b/controllers/license/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +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. +*/ \ No newline at end of file diff --git a/controllers/license/internal/cache/cache.go b/controllers/license/internal/cache/cache.go new file mode 100644 index 000000000000..04f06882f600 --- /dev/null +++ b/controllers/license/internal/cache/cache.go @@ -0,0 +1,119 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "context" + "fmt" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Options retains only the fields used for event delivery and node validation. +func Options() ctrlcache.Options { + return ctrlcache.Options{ + ReaderFailOnMissingInformer: true, + DefaultTransform: ctrlcache.TransformStripManagedFields(), + ByObject: map[client.Object]ctrlcache.ByObject{ + &licensev1.License{}: {Transform: transformLicense}, + &corev1.Node{}: {Transform: transformNode}, + }, + } +} + +// UncachedObjects keeps reads that require complete or low-frequency objects on the API server. +func UncachedObjects() []client.Object { + return []client.Object{ + &licensev1.License{}, + &corev1.Namespace{}, + ¬ificationv1.Notification{}, + &userv1.User{}, + } +} + +// SetupInformers registers the Node informer used by license validation. The License +// informer is registered by the controller builder. +func SetupInformers(mgr ctrl.Manager) error { + if _, err := mgr.GetCache().GetInformer(context.Background(), &corev1.Node{}); err != nil { + return fmt.Errorf("register node informer: %w", err) + } + return nil +} + +func transformLicense(obj any) (any, error) { + license, ok := obj.(*licensev1.License) + if !ok { + return obj, nil + } + + return &licensev1.License{ + TypeMeta: license.TypeMeta, + ObjectMeta: projectObjectMeta(license.ObjectMeta), + }, nil +} + +func transformNode(obj any) (any, error) { + node, ok := obj.(*corev1.Node) + if !ok { + return obj, nil + } + + return &corev1.Node{ + TypeMeta: node.TypeMeta, + ObjectMeta: projectObjectMeta(node.ObjectMeta), + Status: corev1.NodeStatus{ + Allocatable: copyNodeAllocatable(node.Status.Allocatable), + }, + }, nil +} + +func copyNodeAllocatable(source corev1.ResourceList) corev1.ResourceList { + var result corev1.ResourceList + for _, name := range []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} { + if quantity, ok := source[name]; ok { + if result == nil { + result = make(corev1.ResourceList, 2) + } + result[name] = quantity.DeepCopy() + } + } + return result +} + +func projectObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + out := metav1.ObjectMeta{ + Name: in.Name, + Namespace: in.Namespace, + UID: in.UID, + ResourceVersion: in.ResourceVersion, + Generation: in.Generation, + CreationTimestamp: in.CreationTimestamp, + } + if in.DeletionTimestamp != nil { + out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy() + } + if in.DeletionGracePeriodSeconds != nil { + gracePeriod := *in.DeletionGracePeriodSeconds + out.DeletionGracePeriodSeconds = &gracePeriod + } + return out +} diff --git a/controllers/license/internal/cache/cache_test.go b/controllers/license/internal/cache/cache_test.go new file mode 100644 index 000000000000..fc9305cb9c77 --- /dev/null +++ b/controllers/license/internal/cache/cache_test.go @@ -0,0 +1,253 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + notificationv1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type rejectingCacheReader struct { + used bool +} + +func (r *rejectingCacheReader) Get( + context.Context, + client.ObjectKey, + client.Object, + ...client.GetOption, +) error { + r.used = true + return errors.New("unexpected cache get") +} + +func (r *rejectingCacheReader) List( + context.Context, + client.ObjectList, + ...client.ListOption, +) error { + r.used = true + return errors.New("unexpected cache list") +} + +func TestOptionsRegistersProjectedInformers(t *testing.T) { + options := Options() + if !options.ReaderFailOnMissingInformer { + t.Fatal("missing informer reads are allowed") + } + if options.DefaultTransform == nil { + t.Fatal("default managed fields transform is nil") + } + + required := []client.Object{&licensev1.License{}, &corev1.Node{}} + if len(options.ByObject) != len(required) { + t.Fatalf("cache object count = %d, want %d", len(options.ByObject), len(required)) + } + for _, expected := range required { + found := false + for object, byObject := range options.ByObject { + if reflect.TypeOf(object) != reflect.TypeOf(expected) { + continue + } + found = true + if byObject.Transform == nil { + t.Fatalf("%T transform is nil", expected) + } + } + if !found { + t.Fatalf("%T cache options not found", expected) + } + } +} + +func TestUncachedObjectsKeepsCompleteAndLowFrequencyReadsDirect(t *testing.T) { + required := []client.Object{ + &licensev1.License{}, + &corev1.Namespace{}, + ¬ificationv1.Notification{}, + &userv1.User{}, + } + disabled := UncachedObjects() + if len(disabled) != len(required) { + t.Fatalf("uncached object count = %d, want %d", len(disabled), len(required)) + } + for _, expected := range required { + found := false + for _, object := range disabled { + if reflect.TypeOf(object) == reflect.TypeOf(expected) { + found = true + break + } + } + if !found { + t.Fatalf("%T is not configured to bypass the cache", expected) + } + } +} + +func TestUserMetadataListBypassesCache(t *testing.T) { + apiCalled := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + apiCalled = true + if req.URL.Path != "/apis/user.sealos.io/v1/users" { + t.Errorf("request path = %q, want User collection", req.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{ + "apiVersion":"user.sealos.io/v1", + "kind":"UserList", + "metadata":{"resourceVersion":"1"}, + "items":[{"apiVersion":"user.sealos.io/v1","kind":"User","metadata":{"name":"user-a"}}] + }`) + })) + defer server.Close() + + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("register User scheme: %v", err) + } + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{userv1.GroupVersion}) + mapper.Add(userv1.GroupVersion.WithKind("User"), meta.RESTScopeRoot) + cacheReader := &rejectingCacheReader{} + apiClient, err := client.New(&rest.Config{Host: server.URL}, client.Options{ + Scheme: scheme, + Mapper: mapper, + Cache: &client.CacheOptions{ + Reader: cacheReader, + DisableFor: []client.Object{&userv1.User{}}, + }, + }) + if err != nil { + t.Fatalf("create client: %v", err) + } + + list := &metav1.PartialObjectMetadataList{} + list.SetGroupVersionKind(userv1.GroupVersion.WithKind("UserList")) + if err := apiClient.List(context.Background(), list); err != nil { + t.Fatalf("list User metadata: %v", err) + } + if !apiCalled || cacheReader.used { + t.Fatalf("routing: api called = %t, cache used = %t", apiCalled, cacheReader.used) + } + if len(list.Items) != 1 || list.Items[0].Name != "user-a" { + t.Fatalf("User metadata list = %#v", list.Items) + } +} + +func TestTransformLicenseKeepsOnlyEventMetadata(t *testing.T) { + deletionTimestamp := metav1.Now() + license := &licensev1.License{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "ns-admin", + ResourceVersion: "42", + Generation: 3, + DeletionTimestamp: &deletionTimestamp, + Finalizers: []string{"license.sealos.io/finalizer"}, + Labels: map[string]string{"unused": "large-value"}, + Annotations: map[string]string{"unused": "large-value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: licensev1.LicenseSpec{Token: "large-license-token"}, + Status: licensev1.LicenseStatus{ + Phase: licensev1.LicenseStatusPhaseActive, + Reason: "large-status-reason", + }, + } + + transformed, err := transformLicense(license) + if err != nil { + t.Fatalf("transform license: %v", err) + } + got, ok := transformed.(*licensev1.License) + if !ok { + t.Fatalf("transformed type = %T, want *v1.License", transformed) + } + if got.Name != license.Name || got.Namespace != license.Namespace || + got.ResourceVersion != license.ResourceVersion || got.Generation != license.Generation || + got.DeletionTimestamp == nil || !got.DeletionTimestamp.Equal(license.DeletionTimestamp) { + t.Fatalf("required event metadata was not retained: %#v", got.ObjectMeta) + } + if got.Spec.Token != "" || !reflect.DeepEqual(got.Status, licensev1.LicenseStatus{}) || + len(got.Finalizers) != 0 || len(got.Labels) != 0 || len(got.Annotations) != 0 || + len(got.ManagedFields) != 0 { + t.Fatalf("unused license payload was retained: %#v", got) + } + if license.Spec.Token == "" || len(license.ManagedFields) == 0 { + t.Fatal("transform mutated the source object") + } +} + +func TestTransformNodeKeepsOnlyValidationResources(t *testing.T) { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-a", + Labels: map[string]string{"unused": "large-value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.NodeSpec{ProviderID: "unused-provider-id"}, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("8"), + corev1.ResourceMemory: resource.MustParse("32Gi"), + corev1.ResourceEphemeralStorage: resource.MustParse("100Gi"), + corev1.ResourcePods: resource.MustParse("110"), + }, + Capacity: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("10"), + }, + Conditions: []corev1.NodeCondition{{Type: corev1.NodeReady}}, + Images: []corev1.ContainerImage{{Names: []string{"unused-image"}}}, + }, + } + + transformed, err := transformNode(node) + if err != nil { + t.Fatalf("transform node: %v", err) + } + got, ok := transformed.(*corev1.Node) + if !ok { + t.Fatalf("transformed type = %T, want *corev1.Node", transformed) + } + if got.Name != node.Name || got.Status.Allocatable.Cpu().String() != "8" || + got.Status.Allocatable.Memory().String() != "32Gi" { + t.Fatalf("node validation fields were not retained: %#v", got) + } + if len(got.Status.Allocatable) != 2 || len(got.Status.Capacity) != 0 || + len(got.Status.Conditions) != 0 || len(got.Status.Images) != 0 || + got.Spec.ProviderID != "" || len(got.Labels) != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("unused node payload was retained: %#v", got) + } + if len(node.Status.Allocatable) != 4 || len(node.ManagedFields) == 0 { + t.Fatal("transform mutated the source object") + } +} diff --git a/controllers/license/internal/controller/license_activator.go b/controllers/license/internal/controller/license_activator.go new file mode 100644 index 000000000000..c523e9171c1f --- /dev/null +++ b/controllers/license/internal/controller/license_activator.go @@ -0,0 +1,60 @@ +// Copyright © 2023 sealos. +// +// 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 controller + +import ( + "context" + "time" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + licenseutil "github.com/labring/sealos/controllers/license/internal/util/license" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type LicenseActivator struct { + client.Client +} + +func (r *LicenseActivator) updateStatus( + ctx context.Context, + nn types.NamespacedName, + status *licensev1.LicenseStatus, +) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + original := &licensev1.License{} + if err := r.Get(ctx, nn, original); err != nil { + return err + } + original.Status = *status + return r.Client.Status().Update(ctx, original) + }) +} + +func (r *LicenseActivator) Active(ctx context.Context, license *licensev1.License) error { + exp, err := licenseutil.GetLicenseExpireTime(license) + if err != nil { + return err + } + updateStatus := &license.Status + updateStatus.Phase = licensev1.LicenseStatusPhaseActive + updateStatus.Reason = "License activated successfully" + updateStatus.ExpirationTime = metav1.NewTime(exp) + updateStatus.ActivationTime = metav1.NewTime(time.Now()) + + return r.updateStatus(ctx, client.ObjectKeyFromObject(license), updateStatus) +} diff --git a/controllers/license/internal/controller/license_controller.go b/controllers/license/internal/controller/license_controller.go new file mode 100644 index 000000000000..73787732c4c6 --- /dev/null +++ b/controllers/license/internal/controller/license_controller.go @@ -0,0 +1,351 @@ +/* +Copyright 2023. + +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 controller + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/go-logr/logr" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + licenseutil "github.com/labring/sealos/controllers/license/internal/util/license" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + 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/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// LicenseReconciler reconciles a License object +type LicenseReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger + // finalizer *ctrlsdk.Finalizer + + ClusterID string + CreateTimestamp metav1.Time + + validator *LicenseValidator + activator *LicenseActivator +} + +var ( + longRequeueRes = ctrl.Result{RequeueAfter: 30 * time.Minute} + shortRequeueRes = ctrl.Result{RequeueAfter: time.Minute} + immediateRequeue = ctrl.Result{Requeue: true} + dailyNotify = 24 * time.Hour +) + +const ( + licenseFinalizer = "license.sealos.io/finalizer" + licenseProtectionFinalizer = "license.sealos.io/default-license" + defaultLicenseName = "default" + defaultLicenseNamespace = "ns-admin" +) + +// +kubebuilder:rbac:groups=license.sealos.io,resources=licenses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=license.sealos.io,resources=licenses/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=license.sealos.io,resources=licenses/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch + +func (r *LicenseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Logger.V(1).Info("start reconcile for license") + license := &licensev1.License{} + if err := r.Get(ctx, req.NamespacedName, license); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if license.DeletionTimestamp.IsZero() { + // Ensure required finalizers exist; reconcile only after they are set. + addedFinalizer := false + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + latest := &licensev1.License{} + if err := r.Get(ctx, req.NamespacedName, latest); err != nil { + return err + } + changed := controllerutil.AddFinalizer(latest, licenseFinalizer) + if isDefaultLicense(latest) { + changed = controllerutil.AddFinalizer(latest, licenseProtectionFinalizer) || changed + } + if !changed { + return nil + } + addedFinalizer = true + return r.Update(ctx, latest) + }); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + if addedFinalizer { + // Requeue immediately so the reconciler can proceed with the next step. + return immediateRequeue, nil + } + } else { + if controllerutil.ContainsFinalizer(license, licenseProtectionFinalizer) && + isDefaultLicense(license) { + r.Logger.Info("deletion blocked by protection finalizer", "license", req.NamespacedName) + return ctrl.Result{}, nil + } + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + latest := &licensev1.License{} + if err := r.Get(ctx, req.NamespacedName, latest); err != nil { + return err + } + if controllerutil.RemoveFinalizer(latest, licenseFinalizer) { + return r.Update(ctx, latest) + } + return nil + }); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err) + } + if err := r.checkAndNotifyAllLicenses(ctx); err != nil { + r.Logger.Error(err, "failed to check and notify all licenses") + } + return ctrl.Result{}, nil + } + return r.reconcile(ctx, license) +} + +func (r *LicenseReconciler) reconcile( + ctx context.Context, + license *licensev1.License, +) (ctrl.Result, error) { + nsName := fmt.Sprintf("%s/%s", license.Namespace, license.Name) + r.Logger.V(1).Info("reconcile for license", "license", nsName) + + if err := r.validator.Validate(ctx, license); err != nil { + // Extract validation code if it's a ValidationError + var reason string + + var validationErr licenseutil.ValidationError + validationCode := licensev1.ValidationError + var phase licensev1.LicenseStatusPhase + if errors.As(err, &validationErr) { + reason = validationErr.Message + validationCode = validationErr.Code + switch validationErr.Code { + case licensev1.ValidationExpired: + phase = licensev1.LicenseStatusPhaseExpired + case licensev1.ValidationClusterInfoMismatch: + phase = licensev1.LicenseStatusPhaseInvalid + case licensev1.ValidationClusterIDMismatch: + phase = licensev1.LicenseStatusPhaseMismatch + default: + phase = licensev1.LicenseStatusPhaseFailed + } + } else { + // Handle non-ValidationError + reason = fmt.Sprintf("license validation failed: %v", err) + phase = licensev1.LicenseStatusPhaseFailed + } + updateStatus := &license.Status + updateStatus.Phase = phase + updateStatus.Code = validationCode + updateStatus.Reason = reason + updateStatus.ActivationTime = license.Status.ActivationTime + updateStatus.ExpirationTime = license.Status.ExpirationTime + if updateErr := r.updateStatus( + ctx, + client.ObjectKeyFromObject(license), + updateStatus, + ); updateErr != nil { + r.Logger.V(1). + Error(updateErr, "failed to update license status after validation failure", "license", nsName) + return ctrl.Result{}, updateErr + } + + // Send notification if license is expired + if phase == licensev1.LicenseStatusPhaseExpired { + if notifyErr := r.NotifyIfNeeded(ctx, license); notifyErr != nil { + r.Logger.V(1). + Error(notifyErr, "failed to send license expiration notification", "license", nsName) + } + } + + r.Logger.V(1).Error(err, "failed to validate license", "license", nsName) + return shortRequeueRes, nil + } + + if err := r.activator.Active(ctx, license); err != nil { + failStatus := &license.Status + failStatus.Phase = licensev1.LicenseStatusPhaseFailed + failStatus.Code = licensev1.ValidationError + failStatus.Reason = fmt.Sprintf("license activation failed: %v", err) + failStatus.ActivationTime = license.Status.ActivationTime + failStatus.ExpirationTime = license.Status.ExpirationTime + if updateErr := r.updateStatus( + ctx, + client.ObjectKeyFromObject(license), + failStatus, + ); updateErr != nil { + r.Logger.Error( + updateErr, + "failed to update license status after activation failure", + "license", + nsName, + ) + return ctrl.Result{}, updateErr + } + r.Logger.Error(err, "failed to activate license", "license", nsName) + return shortRequeueRes, nil + } + r.Logger.V(1). + Info("license activated successfully", "license", nsName, "phase", license.Status.Phase) + + // Send notifications for active licenses (e.g., expiring soon, user limit warnings) + if notifyErr := r.NotifyIfNeeded(ctx, license); notifyErr != nil { + r.Logger.V(1).Error(notifyErr, "failed to send license notification", "license", nsName) + } + notifier := &LicenseNotifier{ + Client: r.Client, + Logger: r.Logger, + } + if err := notifier.markMissingLicenseReadIfExists(ctx); err != nil { + r.Logger.V(1). + Error(err, "failed to mark missing license notification as read", "license", nsName) + } + + return longRequeueRes, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *LicenseReconciler) SetupWithManager(mgr ctrl.Manager, limitOps controller.Options) error { + r.Logger = mgr.GetLogger().WithName("controller").WithName("License") + r.Client = mgr.GetClient() + + r.validator = &LicenseValidator{ + Client: r.Client, + Logger: r.Logger, + ClusterID: r.ClusterID, + } + + r.activator = &LicenseActivator{ + Client: r.Client, + } + + if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + r.Logger.Info("periodic default license ensure triggered") + } + } + })); err != nil { + return err + } + + // Add periodic license notification check (daily) + if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + r.Logger.Info("initial license notification check triggered") + if err := r.checkAndNotifyAllLicenses(ctx); err != nil { + r.Logger.Error(err, "failed to check and notify all licenses") + } + ticker := time.NewTicker(dailyNotify) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + r.Logger.Info("periodic license notification check triggered") + if err := r.checkAndNotifyAllLicenses(ctx); err != nil { + r.Logger.Error(err, "failed to check and notify all licenses") + } + } + } + })); err != nil { + return err + } + // reconcile on generation change + return ctrl.NewControllerManagedBy(mgr). + WithOptions(limitOps). + For(&licensev1.License{}, builder.WithPredicates(predicate.And(predicate.GenerationChangedPredicate{}))). + Complete(r) +} + +func isDefaultLicense(license *licensev1.License) bool { + return license.Name == defaultLicenseName && license.Namespace == defaultLicenseNamespace +} + +func (r *LicenseReconciler) updateStatus( + ctx context.Context, + nn types.NamespacedName, + status *licensev1.LicenseStatus, +) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + original := &licensev1.License{} + if err := r.Get(ctx, nn, original); err != nil { + return err + } + original.Status = *status + return r.Client.Status().Update(ctx, original) + }) +} + +// checkAndNotifyAllLicenses checks all licenses and sends notifications as needed +func (r *LicenseReconciler) checkAndNotifyAllLicenses(ctx context.Context) error { + licenseList := &licensev1.LicenseList{} + if err := r.List(ctx, licenseList); err != nil { + return fmt.Errorf("failed to list licenses: %w", err) + } + + notifier := &LicenseNotifier{ + Client: r.Client, + Logger: r.Logger, + } + + activeLicenses := make([]*licensev1.License, 0, len(licenseList.Items)) + for i := range licenseList.Items { + license := &licenseList.Items[i] + if license.DeletionTimestamp.IsZero() { + activeLicenses = append(activeLicenses, license) + } + } + + if len(activeLicenses) == 0 { + if err := notifier.ensureMissingLicenseNotification(ctx); err != nil { + return fmt.Errorf("failed to ensure missing license notification: %w", err) + } + return nil + } + + if err := notifier.markMissingLicenseReadIfExists(ctx); err != nil { + return fmt.Errorf("failed to mark missing license notification as read: %w", err) + } + + for _, license := range activeLicenses { + if err := r.NotifyIfNeeded(ctx, license); err != nil { + r.Logger.V(1).Error(err, "failed to send notification for license", + "license", fmt.Sprintf("%s/%s", license.Namespace, license.Name)) + // Continue processing other licenses even if one fails + } + } + + return nil +} diff --git a/controllers/license/internal/controller/license_notifier.go b/controllers/license/internal/controller/license_notifier.go new file mode 100644 index 000000000000..e4d2e5147bb1 --- /dev/null +++ b/controllers/license/internal/controller/license_notifier.go @@ -0,0 +1,453 @@ +// Copyright © 2026 sealos. +// +// 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 controller + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "github.com/labring/sealos/controllers/user/pkg/licensegate" + "github.com/labring/sealos/controllers/user/pkg/usercount" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + // License notification name prefixes + licenseExpiredPrefix = "license-expired" + licenseUserLimitPrefix = "license-user-limit" + licenseExpiringPrefix = "license-expiring-soon" + licenseMissingPrefix = "license-missing" + + // Notification labels + readStatusLabel = "isRead" + falseStatus = "false" + trueStatus = "true" + + // Notification constants + fromEn = "License-System" + fromZh = "许可证系统" + languageZh = "zh" + + // Threshold for expiring soon warning (7 days) + expiringSoonThreshold = 7 * 24 * time.Hour + + // Target namespace for admin notifications + adminNamespace = "ns-admin" +) + +// LicenseNotifier handles license notifications +type LicenseNotifier struct { + client.Client + Logger logr.Logger +} + +// NotifyIfNeeded checks license status and sends notifications if needed +func (r *LicenseReconciler) NotifyIfNeeded(ctx context.Context, license *licensev1.License) error { + notifier := &LicenseNotifier{ + Client: r.Client, + Logger: r.Logger, + } + + // Check if we need to send notifications + if err := notifier.checkLicenseStatus(ctx, license); err != nil { + r.Logger.Error(err, "failed to check license status for notification") + return err + } + + return nil +} + +// checkLicenseStatus evaluates the license and sends appropriate notifications +func (n *LicenseNotifier) checkLicenseStatus( + ctx context.Context, + license *licensev1.License, +) error { + // Check if ns-admin namespace exists + if !n.namespaceExists(ctx, adminNamespace) { + n.Logger.V(1). + Info("admin namespace does not exist, skipping license notification", "namespace", adminNamespace) + return nil + } + + // Check license expiration + if err := n.checkLicenseExpiration(ctx, license); err != nil { + return fmt.Errorf("failed to check license expiration: %w", err) + } + + // Check user limit + if err := n.checkUserLimit(ctx, license); err != nil { + return fmt.Errorf("failed to check user limit: %w", err) + } + + return nil +} + +func (n *LicenseNotifier) ensureMissingLicenseNotification(ctx context.Context) error { + if !n.namespaceExists(ctx, adminNamespace) { + n.Logger.V(1). + Info("admin namespace does not exist, skipping license missing notification", "namespace", adminNamespace) + return nil + } + + notification := &v1.Notification{} + if err := n.Get( + ctx, + types.NamespacedName{Name: licenseMissingPrefix, Namespace: adminNamespace}, + notification, + ); err != nil { + if apierrors.IsNotFound(err) { + return n.sendMissingNotification(ctx) + } + return err + } + + if isNotificationUnread(notification) { + return nil + } + + return n.sendMissingNotification(ctx) +} + +func (n *LicenseNotifier) markMissingLicenseReadIfExists(ctx context.Context) error { + if !n.namespaceExists(ctx, adminNamespace) { + n.Logger.V(1). + Info("admin namespace does not exist, skipping license missing notification", "namespace", adminNamespace) + return nil + } + return n.markNotificationsReadIfExists(ctx, licenseMissingPrefix) +} + +// checkLicenseExpiration sends notifications for expired or expiring licenses +func (n *LicenseNotifier) checkLicenseExpiration( + ctx context.Context, + license *licensev1.License, +) error { + if license.Status.ExpirationTime.IsZero() { + return nil + } + + now := time.Now() + expirationTime := license.Status.ExpirationTime.Time + timeUntilExpiration := expirationTime.Sub(now) + + // Determine if license is expired or expiring soon + if timeUntilExpiration <= 0 { + // License has expired + if license.Status.Phase == licensev1.LicenseStatusPhaseExpired { + titleEn := "License Expired" + titleZh := "许可证已过期" + messageEn := fmt.Sprintf( + "Your license expired on %s. Please renew to continue using the service.", + expirationTime.Format(time.DateOnly), + ) + messageZh := fmt.Sprintf("您的许可证已于 %s 过期,请续费以继续使用服务。", + expirationTime.Format(time.DateOnly)) + if err := n.sendOrUpdateNotification( + ctx, + licenseExpiredPrefix, + titleEn, + titleZh, + messageEn, + messageZh, + ); err != nil { + return fmt.Errorf("failed to send expiration notification: %w", err) + } + } + return nil + } + + if timeUntilExpiration <= expiringSoonThreshold { + // License is expiring soon + notificationType := licenseExpiringPrefix + days := int(timeUntilExpiration.Hours() / 24) + titleEn := "License Expiring Soon" + titleZh := "许可证即将过期" + messageEn := fmt.Sprintf( + "Your license will expire in %d days (on %s). Please renew in time.", + days, + expirationTime.Format(time.DateOnly), + ) + messageZh := fmt.Sprintf("您的许可证将在 %d 天后过期(过期日期: %s),请及时续费。", + days, expirationTime.Format(time.DateOnly)) + if err := n.markNotificationsReadIfExists(ctx, licenseExpiredPrefix); err != nil { + return fmt.Errorf("failed to mark expired notification as read: %w", err) + } + if err := n.sendOrUpdateNotification( + ctx, + notificationType, + titleEn, + titleZh, + messageEn, + messageZh, + ); err != nil { + return fmt.Errorf("failed to send expiration notification: %w", err) + } + return nil + } + + if err := n.markNotificationsReadIfExists( + ctx, + licenseExpiredPrefix, + licenseExpiringPrefix, + ); err != nil { + return fmt.Errorf("failed to mark expiration notifications as read: %w", err) + } + + return nil +} + +// checkUserLimit sends notifications when user count approaches or reaches the limit +func (n *LicenseNotifier) checkUserLimit(ctx context.Context, license *licensev1.License) error { + // Only check if license is active + if license.Status.Phase != licensev1.LicenseStatusPhaseActive { + return nil + } + + if err := n.refreshUserLimitContext(ctx); err != nil { + return fmt.Errorf("failed to refresh user limit context: %w", err) + } + + userLimit := licensegate.UserLimit() + currentUserCount := usercount.Get() + + // Only notify if there's a positive limit (unlimited is -1) + if userLimit < 0 { + if err := n.markNotificationsReadIfExists( + ctx, + licenseUserLimitPrefix, + licenseUserLimitPrefix+"-warning", + ); err != nil { + return fmt.Errorf("failed to mark user limit notifications as read: %w", err) + } + return nil + } + + var notificationType string + var titleEn, titleZh, messageEn, messageZh string + + // Check if user limit has been reached or is close to being reached + if currentUserCount >= userLimit { + notificationType = licenseUserLimitPrefix + titleEn = "User Limit Reached" + titleZh = "用户数量已达上限" + messageEn = fmt.Sprintf( + "The current user count (%d) has reached the license limit (%d). Please upgrade your license to add more users.", + currentUserCount, + userLimit, + ) + messageZh = fmt.Sprintf("当前用户数量 (%d) 已达到许可证限制 (%d)。请升级许可证以添加更多用户。", + currentUserCount, userLimit) + } else if currentUserCount >= int(float64(userLimit)*0.9) { + // 90% threshold warning + notificationType = licenseUserLimitPrefix + "-warning" + titleEn = "User Limit Warning" + titleZh = "用户数量限制警告" + messageEn = fmt.Sprintf( + "The current user count (%d) is approaching the license limit (%d). Consider upgrading your license soon.", + currentUserCount, + userLimit, + ) + messageZh = fmt.Sprintf("当前用户数量 (%d) 已接近许可证限制 (%d)。建议尽快升级许可证。", + currentUserCount, userLimit) + } + + if notificationType != "" { + if notificationType == licenseUserLimitPrefix+"-warning" { + if err := n.markNotificationsReadIfExists(ctx, licenseUserLimitPrefix); err != nil { + return fmt.Errorf("failed to mark user limit notification as read: %w", err) + } + } + if err := n.sendOrUpdateNotification( + ctx, + notificationType, + titleEn, + titleZh, + messageEn, + messageZh, + ); err != nil { + return fmt.Errorf("failed to send user limit notification: %w", err) + } + return nil + } + + if err := n.markNotificationsReadIfExists( + ctx, + licenseUserLimitPrefix, + licenseUserLimitPrefix+"-warning", + ); err != nil { + return fmt.Errorf("failed to mark user limit notifications as read: %w", err) + } + + return nil +} + +func (n *LicenseNotifier) refreshUserLimitContext(ctx context.Context) error { + if err := licensegate.Refresh(ctx, n.Client); err != nil { + return err + } + if err := n.refreshUserCount(ctx); err != nil { + return err + } + return nil +} + +func (n *LicenseNotifier) refreshUserCount(ctx context.Context) error { + list := &metav1.PartialObjectMetadataList{} + list.SetGroupVersionKind( + schema.GroupVersion{Group: "user.sealos.io", Version: "v1"}.WithKind("UserList"), + ) + if err := n.List(ctx, list); err != nil { + return err + } + usercount.Set(len(list.Items)) + return nil +} + +// sendOrUpdateNotification creates or updates a notification, reusing the same notification resource +func (n *LicenseNotifier) sendOrUpdateNotification( + ctx context.Context, + notificationType, titleEn, titleZh, messageEn, messageZh string, +) error { + now := time.Now().UTC().Unix() + notificationName := notificationType + + notification := &v1.Notification{ + ObjectMeta: metav1.ObjectMeta{ + Name: notificationName, + Namespace: adminNamespace, + }, + } + + // Create or update the notification + opResult, err := controllerutil.CreateOrUpdate(ctx, n.Client, notification, func() error { + // Update spec + notification.Spec.Title = titleEn + notification.Spec.Message = messageEn + notification.Spec.From = fromEn + notification.Spec.Importance = v1.High + notification.Spec.DesktopPopup = true + notification.Spec.Timestamp = now + notification.Spec.I18n = map[string]v1.I18n{ + languageZh: { + Title: titleZh, + From: fromZh, + Message: messageZh, + }, + } + + // Ensure labels exist and mark as unread + if notification.Labels == nil { + notification.Labels = make(map[string]string) + } + notification.Labels[readStatusLabel] = falseStatus + + return nil + }) + if err != nil { + return fmt.Errorf("failed to create/update notification: %w", err) + } + + n.Logger.V(1).Info("license notification processed", + "operation", opResult, + "name", notificationName, + "namespace", adminNamespace, + "type", notificationType) + + return nil +} + +func (n *LicenseNotifier) sendMissingNotification(ctx context.Context) error { + titleEn := "License Missing" + titleZh := "许可证缺失" + messageEn := "No license resource found in the cluster. Please create a valid license to enable the service." + messageZh := "集群中未发现许可证资源,请创建有效许可证以启用服务。" + return n.sendOrUpdateNotification( + ctx, + licenseMissingPrefix, + titleEn, + titleZh, + messageEn, + messageZh, + ) +} + +func (n *LicenseNotifier) markNotificationsReadIfExists( + ctx context.Context, + notificationTypes ...string, +) error { + for _, notificationType := range notificationTypes { + if err := n.markNotificationReadIfExists( + ctx, + adminNamespace, + notificationType, + ); err != nil { + return err + } + } + return nil +} + +func isNotificationUnread(notification *v1.Notification) bool { + if notification.Labels == nil { + return false + } + return notification.Labels[readStatusLabel] == falseStatus +} + +func (n *LicenseNotifier) markNotificationReadIfExists( + ctx context.Context, + namespace, notificationName string, +) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + notification := &v1.Notification{} + if err := n.Get( + ctx, + types.NamespacedName{Name: notificationName, Namespace: namespace}, + notification, + ); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + if notification.Labels == nil { + notification.Labels = make(map[string]string) + } + if notification.Labels[readStatusLabel] == trueStatus { + return nil + } + notification.Labels[readStatusLabel] = trueStatus + return n.Update(ctx, notification) + }) +} + +// namespaceExists checks if a namespace exists +func (n *LicenseNotifier) namespaceExists(ctx context.Context, namespace string) bool { + ns := &corev1.Namespace{} + err := n.Get(ctx, types.NamespacedName{Name: namespace}, ns) + return err == nil +} diff --git a/controllers/license/internal/controller/license_validator.go b/controllers/license/internal/controller/license_validator.go new file mode 100644 index 000000000000..ed481ffdecc2 --- /dev/null +++ b/controllers/license/internal/controller/license_validator.go @@ -0,0 +1,76 @@ +// Copyright © 2023 sealos. +// +// 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 controller + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + "github.com/labring/sealos/controllers/license/internal/util/claims" + "github.com/labring/sealos/controllers/license/internal/util/cluster" + licenseutil "github.com/labring/sealos/controllers/license/internal/util/license" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type LicenseValidator struct { + client.Client + Logger logr.Logger + ClusterID string +} + +func (v *LicenseValidator) Validate(ctx context.Context, license *licensev1.License) error { + // Check if token is empty + if license.Spec.Token == "" { + return licenseutil.NewValidationError( + licensev1.ValidationError, + "license token is empty", + ) + } + + nodeList := &v1.NodeList{} + if err := v.List(ctx, nodeList); err != nil { + return fmt.Errorf("failed to list cluster nodes: %w", err) + } + nodeCount := len(nodeList.Items) + totalCPU := resource.NewQuantity(0, resource.DecimalSI) + totalMemory := resource.NewQuantity(0, resource.BinarySI) + + for _, node := range nodeList.Items { + allocatable := node.Status.Allocatable + totalCPU.Add(*allocatable.Cpu()) + totalMemory.Add(*allocatable.Memory()) + } + + clusterInfo := &cluster.Info{ + ClusterID: v.ClusterID, + ClusterClaimData: claims.ClusterClaimData{ + NodeCount: nodeCount, + TotalCPU: int(totalCPU.MilliValue() / 1000), + TotalMemory: int(totalMemory.Value() / (1024 * 1024 * 1024)), + }, + } + v.Logger.Info( + "Validating license", + "cluster info", + clusterInfo, + "license token", + license.Spec.Token, + ) + return licenseutil.IsLicenseValid(license, clusterInfo, v.ClusterID) +} diff --git a/controllers/license/internal/util/claims/claims.go b/controllers/license/internal/util/claims/claims.go new file mode 100644 index 000000000000..3f93a8abba00 --- /dev/null +++ b/controllers/license/internal/util/claims/claims.go @@ -0,0 +1,23 @@ +// Copyright © 2023 sealos. +// +// 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 claims + +import "github.com/labring/sealos/controllers/pkg/license" + +type ( + Claims = license.Claims + ClaimData = license.ClaimData + ClusterClaimData = license.ClusterClaimData +) diff --git a/controllers/license/internal/util/claims/claims_test.go b/controllers/license/internal/util/claims/claims_test.go new file mode 100644 index 000000000000..821c54db2295 --- /dev/null +++ b/controllers/license/internal/util/claims/claims_test.go @@ -0,0 +1,62 @@ +// Copyright © 2023 sealos. +// +// 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 claims + +import ( + "testing" +) + +func TestClaimData_SwitchToClusterData(t *testing.T) { + type args struct { + data *ClusterClaimData + } + tests := []struct { + name string + c ClaimData + args args + wantErr bool + want ClusterClaimData + }{ + { + name: "test", + c: ClaimData{ + "nodeCount": 3, + "totalCPU": 6, + "totalMemory": 12, + "userCount": 5, + }, + args: args{ + data: &ClusterClaimData{}, + }, + wantErr: false, + want: ClusterClaimData{ + NodeCount: 3, + TotalCPU: 6, + TotalMemory: 12, + UserCount: 5, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.c.SwitchToClusterData(tt.args.data); (err != nil) != tt.wantErr { + t.Errorf("SwitchToClusterData() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && *tt.args.data != tt.want { + t.Fatalf("SwitchToClusterData() got = %+v, want = %+v", *tt.args.data, tt.want) + } + }) + } +} diff --git a/controllers/license/internal/util/cluster/cluster.go b/controllers/license/internal/util/cluster/cluster.go new file mode 100644 index 000000000000..fcb594619dd5 --- /dev/null +++ b/controllers/license/internal/util/cluster/cluster.go @@ -0,0 +1,35 @@ +// Copyright © 2023 sealos. +// +// 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 cluster + +import "github.com/labring/sealos/controllers/license/internal/util/claims" + +type Info struct { + ClusterID string + claims.ClusterClaimData +} + +func (i *Info) GetClusterID() string { + return i.ClusterID +} + +func (i *Info) CompareWithClaimData(data *claims.ClaimData) bool { + cdata := &claims.ClusterClaimData{} + err := data.SwitchToClusterData(cdata) + if err != nil { + return false + } + return i.Compare(cdata) +} diff --git a/controllers/license/internal/util/clusterid/cluster_id.go b/controllers/license/internal/util/clusterid/cluster_id.go new file mode 100644 index 000000000000..ec401b45810a --- /dev/null +++ b/controllers/license/internal/util/clusterid/cluster_id.go @@ -0,0 +1,59 @@ +// Copyright © 2023 sealos. +// +// 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 clusterid + +import ( + "context" + "errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetClusterID(ctx context.Context, config *rest.Config) (string, error) { + ns := &corev1.Namespace{} + c, err := client.New(config, client.Options{}) + if err != nil { + return "", err + } + err = c.Get(ctx, client.ObjectKey{Name: "kube-system"}, ns) + if err != nil { + return "", err + } + res := string(ns.UID) + if res == "" || len(res) < 8 { + return "", errors.New("failed to get cluster id") + } + return res[0:8], nil +} + +func GetClusterCreateTime(ctx context.Context, config *rest.Config) (*metav1.Time, error) { + ns := &corev1.Namespace{} + c, err := client.New(config, client.Options{}) + if err != nil { + return nil, err + } + err = c.Get(ctx, client.ObjectKey{Name: "kube-system"}, ns) + if err != nil { + return nil, err + } + res := ns.GetCreationTimestamp() + if res.IsZero() { + return nil, errors.New("failed to get create time") + } + return &res, nil +} diff --git a/controllers/license/internal/util/errors/errors.go b/controllers/license/internal/util/errors/errors.go new file mode 100644 index 000000000000..0f1a8c05f706 --- /dev/null +++ b/controllers/license/internal/util/errors/errors.go @@ -0,0 +1,21 @@ +// Copyright © 2023 sealos. +// +// 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 errors + +import ( + "errors" +) + +var ErrClaimsConvent = errors.New("the claims data provided appears to be invalid") diff --git a/controllers/license/internal/util/key/key.go b/controllers/license/internal/util/key/key.go new file mode 100644 index 000000000000..f43416aabb88 --- /dev/null +++ b/controllers/license/internal/util/key/key.go @@ -0,0 +1,21 @@ +// Copyright © 2023 sealos. +// +// 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 key + +import "github.com/labring/sealos/controllers/pkg/license" + +func GetEncryptionKey() string { + return license.GetEncryptionKey() +} diff --git a/controllers/license/internal/util/license/validate.go b/controllers/license/internal/util/license/validate.go new file mode 100644 index 000000000000..e6deb1c9137e --- /dev/null +++ b/controllers/license/internal/util/license/validate.go @@ -0,0 +1,127 @@ +// Copyright © 2023 sealos. +// +// 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 license + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v4" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + utilclaims "github.com/labring/sealos/controllers/license/internal/util/claims" + "github.com/labring/sealos/controllers/license/internal/util/cluster" + licensepkg "github.com/labring/sealos/controllers/pkg/license" +) + +// ValidationError represents a license validation error with structured information +type ValidationError struct { + Code licensev1.ValidationCode + Message string +} + +func (e ValidationError) Error() string { + return e.Message +} + +// NewValidationError creates a new ValidationError with the given code and message +func NewValidationError(code licensev1.ValidationCode, message string) error { + return ValidationError{ + Code: code, + Message: message, + } +} + +func ParseLicenseToken(license *licensev1.License) (*jwt.Token, error) { + return licensepkg.ParseToken(license.Spec.Token) +} + +func GetClaims(license *licensev1.License) (*utilclaims.Claims, error) { + claims, err := licensepkg.GetClaimsFromLicense(license) + if err != nil { + return nil, err + } + return claims, nil +} + +func IsLicenseValid( + license *licensev1.License, + clusterInfo *cluster.Info, + clusterID string, +) error { + token, err := ParseLicenseToken(license) + if err != nil { + return NewValidationError( + licensev1.ValidationError, + fmt.Sprintf("failed to parse license token: %v", err), + ) + } + if !token.Valid { + // Get the expiration time from claims to provide more detailed error message + claims, ok := token.Claims.(*utilclaims.Claims) + if ok && claims.ExpiresAt != nil { + return NewValidationError( + licensev1.ValidationExpired, + "license has expired on "+claims.ExpiresAt.Format(time.DateTime), + ) + } + return NewValidationError( + licensev1.ValidationExpired, + "license has expired and is no longer valid", + ) + } + + claims, err := GetClaims(license) + if err != nil { + return NewValidationError( + licensev1.ValidationError, + fmt.Sprintf("failed to get license claims: %v", err), + ) + } + + // if clusterID is empty, it means this license is a super license. + if claims.ClusterID != "" && claims.ClusterID != clusterID { + return NewValidationError( + licensev1.ValidationClusterIDMismatch, + fmt.Sprintf( + "license cluster ID mismatch: license cluster ID is '%s' but current cluster ID is '%s'", + claims.ClusterID, + clusterID, + ), + ) + } + + if claims.Type == licensev1.ClusterLicenseType { + if !clusterInfo.CompareWithClaimData(&claims.Data) { + return NewValidationError( + licensev1.ValidationClusterInfoMismatch, + fmt.Sprintf( + "license cluster constraints not met: license requires %v but current cluster does not satisfy these constraints", + claims.Data, + ), + ) + } + } + + // License is valid + return nil +} + +func GetLicenseExpireTime(license *licensev1.License) (time.Time, error) { + claims, err := GetClaims(license) + if err != nil { + return time.Time{}, err + } + return claims.ExpiresAt.UTC(), nil +} diff --git a/controllers/license/internal/util/license/validate_test.go b/controllers/license/internal/util/license/validate_test.go new file mode 100644 index 000000000000..a226c075d76a --- /dev/null +++ b/controllers/license/internal/util/license/validate_test.go @@ -0,0 +1,179 @@ +// Copyright © 2023 sealos. +// +// 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 license + +import ( + "encoding/base64" + "os" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + utilclaims "github.com/labring/sealos/controllers/license/internal/util/claims" + "github.com/labring/sealos/controllers/license/internal/util/cluster" + "github.com/labring/sealos/controllers/license/internal/util/key" +) + +func newTestLicense(t *testing.T) (*licensev1.License, *utilclaims.Claims) { + t.Helper() + if os.Getenv("LICENSE_KEY") == "" { + t.Skip("requires LICENSE_KEY to sign test licenses") + } + decodeKey, err := base64.StdEncoding.DecodeString(key.GetEncryptionKey()) + if err != nil { + t.Fatalf("failed to decode encryption key: %v", err) + } + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodeKey) + if err != nil { + t.Fatalf("failed to parse private key: %v", err) + } + claimData := utilclaims.ClaimData{ + "nodeCount": 3, + "totalCPU": 16, + "totalMemory": 32, + } + claims := &utilclaims.Claims{ + Type: licensev1.ClusterLicenseType, + ClusterID: "test-cluster", + Data: claimData, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + signed, err := token.SignedString(privateKey) + if err != nil { + t.Fatalf("failed to sign test token: %v", err) + } + return &licensev1.License{ + Spec: licensev1.LicenseSpec{ + Token: signed, + Type: claims.Type, + }, + }, claims +} + +func TestIsLicenseValid(t *testing.T) { + type args struct { + license *licensev1.License + data *cluster.Info + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid license", + args: func() args { + license, claims := newTestLicense(t) + clusterData := &utilclaims.ClusterClaimData{} + if err := claims.Data.SwitchToClusterData(clusterData); err != nil { + t.Fatalf("failed to convert cluster data: %v", err) + } + return args{ + license: license, + data: &cluster.Info{ + ClusterID: claims.ClusterID, + ClusterClaimData: *clusterData, + }, + } + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := IsLicenseValid(tt.args.license, tt.args.data, tt.args.data.ClusterID) + if (err != nil) != tt.wantErr { + t.Errorf("IsLicenseValid() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + // If there's an error, the test is expecting it, so we're done + return + } + // If no error, validation succeeded, which is what we want + }) + } +} + +func TestParseLicenseToken(t *testing.T) { + type args struct { + license *licensev1.License + } + tests := []struct { + name string + args args + want *jwt.Token + wantErr bool + }{ + { + name: "parse valid token", + args: func() args { + license, _ := newTestLicense(t) + return args{license: license} + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseLicenseToken(tt.args.license) + if (err != nil) != tt.wantErr { + t.Errorf("ParseLicenseToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil || !got.Valid { + t.Fatalf("ParseLicenseToken() returned invalid token: %+v", got) + } + if _, ok := got.Claims.(*utilclaims.Claims); !ok { + t.Fatalf("ParseLicenseToken() claims type mismatch") + } + }) + } +} + +func TestGetClaims(t *testing.T) { + type args struct { + license *licensev1.License + } + license, expectedClaims := newTestLicense(t) + tests := []struct { + name string + args args + want *utilclaims.Claims + wantErr bool + }{ + { + name: "get claims from valid token", + args: args{license: license}, + want: expectedClaims, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetClaims(tt.args.license) + if (err != nil) != tt.wantErr { + t.Errorf("GetClaims() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got.Type != tt.want.Type || got.ClusterID != tt.want.ClusterID { + t.Errorf("GetClaims() got = %+v, want = %+v", got, tt.want) + } + }) + } +} diff --git a/controllers/license/internal/util/rate/rate_limiter.go b/controllers/license/internal/util/rate/rate_limiter.go new file mode 100644 index 000000000000..6bcdf2d39a1f --- /dev/null +++ b/controllers/license/internal/util/rate/rate_limiter.go @@ -0,0 +1,63 @@ +package rate + +import ( + "flag" + "time" + + "golang.org/x/time/rate" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + defaultMinRetryDelay = 5 * time.Millisecond + defaultMaxRetryDelay = 1000 * time.Second + defaultQPS = float64(10.0) + defaultBurst = 100 + flagMinRetryDelay = "min-retry-delay" + flagMaxRetryDelay = "max-retry-delay" + flagQPS = "default-qps" + flagBurst = "default-burst" +) + +// LimiterOptions used on reconcilers. +type LimiterOptions struct { + MinRetryDelay time.Duration + QPS float64 + Burst int + MaxRetryDelay time.Duration +} + +func (o *LimiterOptions) BindFlags(fs *flag.FlagSet) { + fs.DurationVar( + &o.MinRetryDelay, + flagMinRetryDelay, + defaultMinRetryDelay, + "The minimum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.DurationVar( + &o.MaxRetryDelay, + flagMaxRetryDelay, + defaultMaxRetryDelay, + "The maximum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.Float64Var(&o.QPS, flagQPS, defaultQPS, "The maximum number of batches per second to allow.") + fs.IntVar( + &o.Burst, + flagBurst, + defaultBurst, + "The maximum number of batches to allow in a short period of time.", + ) +} + +func GetRateLimiter(opts *LimiterOptions) workqueue.TypedRateLimiter[reconcile.Request] { + return workqueue.NewTypedMaxOfRateLimiter[reconcile.Request]( + workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + opts.MinRetryDelay, + opts.MaxRetryDelay, + ), + &workqueue.TypedBucketRateLimiter[reconcile.Request]{ + Limiter: rate.NewLimiter(rate.Limit(opts.QPS), opts.Burst), + }, + ) +} diff --git a/controllers/node/.dockerignore b/controllers/node/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/node/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/node/.gitignore b/controllers/node/.gitignore new file mode 100644 index 000000000000..e917e5cefe5a --- /dev/null +++ b/controllers/node/.gitignore @@ -0,0 +1,26 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/controllers/node/Dockerfile b/controllers/node/Dockerfile new file mode 100644 index 000000000000..6d2f9dfa1df1 --- /dev/null +++ b/controllers/node/Dockerfile @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-node-$TARGETARCH /manager +ENTRYPOINT ["/manager"] \ No newline at end of file diff --git a/controllers/node/Makefile b/controllers/node/Makefile new file mode 100644 index 000000000000..41f7a0cff5a9 --- /dev/null +++ b/controllers/node/Makefile @@ -0,0 +1,139 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-node-controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.23 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +build: ## Build manager binary. + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: pre-deploy +pre-deploy: ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v4.2.0 +CONTROLLER_TOOLS_VERSION ?= v0.8.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/node/PROJECT b/controllers/node/PROJECT new file mode 100644 index 000000000000..4cc185419cfd --- /dev/null +++ b/controllers/node/PROJECT @@ -0,0 +1,13 @@ +domain: sealos.io +layout: +- go.kubebuilder.io/v4-alpha +projectName: node +repo: github.com/labring/sealos/controllers/node +resources: +- controller: true + domain: k8s.io + group: node + kind: Gpu + path: k8s.io/api/node/v1 + version: v1 +version: "3" diff --git a/controllers/node/README.md b/controllers/node/README.md new file mode 100644 index 000000000000..4194f534740d --- /dev/null +++ b/controllers/node/README.md @@ -0,0 +1,94 @@ +# node +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/node:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/node:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller to the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) +which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2023. + +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. + diff --git a/controllers/node/config/configmap/configmap.yaml b/controllers/node/config/configmap/configmap.yaml new file mode 100644 index 000000000000..3700e686b3b3 --- /dev/null +++ b/controllers/node/config/configmap/configmap.yaml @@ -0,0 +1,24 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: node-gpu-info + namespace: node-system +data: + gpu: "" + alias: '{"NVIDIA-GeForce-RTX-4090":"GeForce-RTX-4090"}' + nodeLabel: '{"nvidia":"gpu=on","kunlunxin":"xpu=on"}' diff --git a/controllers/node/config/configmap/kustomization.yaml b/controllers/node/config/configmap/kustomization.yaml new file mode 100644 index 000000000000..e933a95ccffa --- /dev/null +++ b/controllers/node/config/configmap/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: + - configmap.yaml \ No newline at end of file diff --git a/controllers/node/config/default/kustomization.yaml b/controllers/node/config/default/kustomization.yaml new file mode 100644 index 000000000000..ecda75b2e0b3 --- /dev/null +++ b/controllers/node/config/default/kustomization.yaml @@ -0,0 +1,159 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: node-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: node- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +#- ../crd +- ../rbac +- ../manager +- ../configmap +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + + + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/controllers/node/config/default/manager_auth_proxy_patch.yaml b/controllers/node/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..08e002227076 --- /dev/null +++ b/controllers/node/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,69 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/controllers/node/config/default/manager_config_patch.yaml b/controllers/node/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..c03d201a433d --- /dev/null +++ b/controllers/node/config/default/manager_config_patch.yaml @@ -0,0 +1,24 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/controllers/node/config/manager/kustomization.yaml b/controllers/node/config/manager/kustomization.yaml new file mode 100644 index 000000000000..b0ccddc780b3 --- /dev/null +++ b/controllers/node/config/manager/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-node-controller + newTag: latest diff --git a/controllers/node/config/manager/manager.yaml b/controllers/node/config/manager/manager.yaml new file mode 100644 index 000000000000..abe9e0480522 --- /dev/null +++ b/controllers/node/config/manager/manager.yaml @@ -0,0 +1,116 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/node/config/prometheus/kustomization.yaml b/controllers/node/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/node/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/node/config/prometheus/monitor.yaml b/controllers/node/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..c89862ae4299 --- /dev/null +++ b/controllers/node/config/prometheus/monitor.yaml @@ -0,0 +1,40 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernets.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/node/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/node/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..e850ec4e63b3 --- /dev/null +++ b/controllers/node/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/node/config/rbac/auth_proxy_role.yaml b/controllers/node/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..f54befcebd7b --- /dev/null +++ b/controllers/node/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/node/config/rbac/auth_proxy_role_binding.yaml b/controllers/node/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..11e8437da724 --- /dev/null +++ b/controllers/node/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/node/config/rbac/auth_proxy_service.yaml b/controllers/node/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..a6854fee75dc --- /dev/null +++ b/controllers/node/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/node/config/rbac/kustomization.yaml b/controllers/node/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/node/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/node/config/rbac/leader_election_role.yaml b/controllers/node/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..5a8094abc180 --- /dev/null +++ b/controllers/node/config/rbac/leader_election_role.yaml @@ -0,0 +1,58 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernets.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/node/config/rbac/leader_election_role_binding.yaml b/controllers/node/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..f16db9778370 --- /dev/null +++ b/controllers/node/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/node/config/rbac/role.yaml b/controllers/node/config/rbac/role.yaml new file mode 100644 index 000000000000..ef0254c020ca --- /dev/null +++ b/controllers/node/config/rbac/role.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - "" + resources: + - configmaps + - nodes + - nodes/status + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - node.k8s.io + resources: + - gpus + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - node.k8s.io + resources: + - gpus/finalizers + verbs: + - update +- apiGroups: + - node.k8s.io + resources: + - gpus/status + verbs: + - get + - patch + - update diff --git a/controllers/node/config/rbac/role_binding.yaml b/controllers/node/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..ce0fded7c4c2 --- /dev/null +++ b/controllers/node/config/rbac/role_binding.yaml @@ -0,0 +1,47 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: gpu-info-cm-reader-rolebinding + namespace: node-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-gpu-info-cm-reader +subjects: + - kind: Group + name: system:serviceaccounts + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/controllers/node/config/rbac/service_account.yaml b/controllers/node/config/rbac/service_account.yaml new file mode 100644 index 000000000000..e465c6e2b756 --- /dev/null +++ b/controllers/node/config/rbac/service_account.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/part-of: node + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/controllers/node/controllers/gpu_controller.go b/controllers/node/controllers/gpu_controller.go new file mode 100644 index 000000000000..394591c82dfc --- /dev/null +++ b/controllers/node/controllers/gpu_controller.go @@ -0,0 +1,778 @@ +/* +Copyright 2023. + +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 controllers + +import ( + "context" + "encoding/json" + "os" + "reflect" + "strconv" + "strings" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +type GpuReconciler struct { + client.Client + APIReader client.Reader + Scheme *runtime.Scheme + Logger logr.Logger + aliasNamespace string + aliasName string +} + +const ( + GPU = "gpu" + GPUInfo = "node-gpu-info" + GPUInfoNameSpace = "node-system" + NvidiaGPUProduct = "nvidia.com/gpu.product" + NvidiaGPUMemory = "nvidia.com/gpu.memory" + NvidiaGPU corev1.ResourceName = "nvidia.com/gpu" + KunlunxinVXPU corev1.ResourceName = "kunlunxin.com/vxpu" + NvidiaUseGPUTypeAnnotation = "nvidia.com/use-gputype" + GPUProduct = "gpu.product" + GPURef = "gpu.ref" + GPUCount = "gpu.count" + GPUMemory = "gpu.memory" + GPUUse = "gpu.used" + GPUDevbox = "gpu.devbox" + GPUAvailable = "gpu.available" + GPUAlias = "alias" + GPUNodeLabel = "nodeLabel" + legacyGPULabelMapping = "labelMapping" + podNodeNameField = "spec.nodeName" + devboxLabel = "devbox.sealos.io/node" +) + +//+kubebuilder:rbac:groups=node.k8s.io,resources=gpus,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=node.k8s.io,resources=gpus/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=node.k8s.io,resources=gpus/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=nodes/status,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete + +func (r *GpuReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + return r.applyGPUInfoCM(ctx, r.Client) +} + +func (r *GpuReconciler) fetchPodsByNode( + ctx context.Context, + reader client.Reader, + nodeName string, +) []corev1.Pod { + var pods corev1.PodList + if err := reader.List( + ctx, + &pods, + client.MatchingFields{podNodeNameField: nodeName}, + ); err != nil { + r.Logger.Error(err, "list pods error from cache", podNodeNameField, nodeName) + } + + return pods.Items +} + +func resourceRequestValue(container corev1.Container, resource corev1.ResourceName) int64 { + if qty, ok := container.Resources.Requests[resource]; ok && qty.Sign() > 0 { + return qty.Value() + } + if qty, ok := container.Resources.Limits[resource]; ok && qty.Sign() > 0 { + return qty.Value() + } + return 0 +} + +func nvidiaGPURequestValue(container corev1.Container) int64 { + return resourceRequestValue(container, NvidiaGPU) +} + +func vxpuRequestValue(container corev1.Container) int64 { + return resourceRequestValue(container, KunlunxinVXPU) +} + +func normalizeNvidiaGPUProduct(s string) string { + parts := strings.Fields(strings.TrimSpace(s)) + if len(parts) == 0 { + return "" + } + out := strings.Join(parts, "-") + if strings.HasPrefix(out, "NVIDIA-Tesla-") { + return strings.TrimPrefix(out, "NVIDIA-") + } + return out +} + +func defaultAliasForNvidiaGPUProduct(s string) string { + s = normalizeNvidiaGPUProduct(s) + if s == "" { + return "" + } + if idx := strings.Index(s, "RTX-"); idx >= 0 { + return s[idx:] + } + if idx := strings.Index(s, "Tesla-"); idx >= 0 { + return s[idx:] + } + return s +} + +func matchesNvidiaGPUType(val, gpuType string) bool { + v := normalizeNvidiaGPUProduct(val) + if v == "" { + return false + } + if v == gpuType { + return true + } + return v == defaultAliasForNvidiaGPUProduct(gpuType) +} + +func (r *GpuReconciler) QueryGPUAllocation( + ctx context.Context, + reader client.Reader, + node *corev1.Node, + gpuType string, +) int64 { + if node == nil || node.Name == "" { + return 0 + } + nodeGPUType := normalizeNvidiaGPUProduct(node.Labels[NvidiaGPUProduct]) + if nodeGPUType == "" { + if reg, ok := node.Annotations["hami.io/node-nvidia-register"]; ok { + nodeGPUType, _ = parseHamiNvidiaRegister(reg) + } + } + nodeGPUType = normalizeNvidiaGPUProduct(nodeGPUType) + gpuType = normalizeNvidiaGPUProduct(gpuType) + if nodeGPUType != gpuType { + return 0 + } + + pods := r.fetchPodsByNode(ctx, reader, node.Name) + var count int64 + for _, pod := range pods { + // Count pods that are still running or terminating (GPU is occupied until exit). + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { + continue + } + if selectorVal, ok := pod.Spec.NodeSelector[NvidiaGPUProduct]; ok && + !matchesNvidiaGPUType(selectorVal, gpuType) { + continue + } + if pod.Annotations != nil { + if annVal, ok := pod.Annotations[NvidiaUseGPUTypeAnnotation]; ok && + annVal != "" && + !matchesNvidiaGPUType(annVal, gpuType) { + continue + } + } + if pod.Spec.NodeName != node.Name { + continue + } + for _, container := range pod.Spec.Containers { + count += nvidiaGPURequestValue(container) + } + } + return count +} + +func parseHamiNvidiaRegisterVirtualCount(s string) int64 { + s = strings.TrimSpace(s) + if strings.HasPrefix(s, "[") { + type hamiDevice struct { + Count int64 `json:"count"` + Health bool `json:"health"` + } + var devs []hamiDevice + if err := json.Unmarshal([]byte(s), &devs); err == nil && len(devs) > 0 { + var selected []hamiDevice + for _, d := range devs { + if d.Health { + selected = append(selected, d) + } + } + if len(selected) == 0 { + selected = devs + } + var total int64 + for _, d := range selected { + total += d.Count + } + return total + } + } + return 0 +} + +func parseHamiNvidiaRegister(s string) (prod, mem string) { + s = strings.TrimSpace(s) + if strings.HasPrefix(s, "[") { + type hamiDevice struct { + Type string `json:"type"` + DevMem int64 `json:"devmem"` + Health bool `json:"health"` + } + var devs []hamiDevice + if err := json.Unmarshal([]byte(s), &devs); err == nil && len(devs) > 0 { + selected := devs[0] + for _, d := range devs { + if d.Health { + selected = d + break + } + } + if selected.Type != "" && selected.DevMem > 0 { + return selected.Type, strconv.FormatInt(selected.DevMem, 10) + } + } + } + + devs := strings.SplitSeq(s, ":") + for d := range devs { + fields := strings.Split(d, ",") + if len(fields) < 5 { + continue + } + t := fields[4] + m := fields[2] + if len(fields) >= 7 && strings.EqualFold(fields[6], "true") { + return t, m + } + if prod == "" { + prod = t + mem = m + } + } + return prod, mem +} + +//nolint:gocyclo // GPU config reconciliation handles several node/alias formats in one pass. +func (r *GpuReconciler) applyGPUInfoCM( + ctx context.Context, + reader client.Reader, +) (ctrl.Result, error) { + /* + "nodeMap": { + "sealos-poc-gpu-master-0":{}, + "sealos-poc-gpu-node-1":{"gpu.count":"1","gpu.memory":"15360","gpu.product":"Tesla-T4"}} + } + */ + nodeList := &corev1.NodeList{} + err := reader.List(ctx, nodeList) + if err != nil { + return ctrl.Result{}, err + } + + nodeMap := make(map[string]map[string]string, len(nodeList.Items)) + aliasMap := make(map[string]string) + refIndex := map[string]string{} + existingNodeLabelStr := "" + legacyLabelMappingStr := "" + { + gpuInfoCM := &corev1.ConfigMap{} + if getErr := reader.Get( + ctx, + types.NamespacedName{Name: GPUInfo, Namespace: GPUInfoNameSpace}, + gpuInfoCM, + ); getErr == nil && gpuInfoCM.Data != nil { + aliasStr := strings.TrimSpace(gpuInfoCM.Data[GPUAlias]) + if aliasStr != "" { + type aliasRecord struct { + Default string `json:"default"` + } + structured := map[string]aliasRecord{} + if unmarshalErr := json.Unmarshal( + []byte(aliasStr), + &structured, + ); unmarshalErr == nil { + for ref, rec := range structured { + def := strings.TrimSpace(rec.Default) + if def == "" { + continue + } + refIndex[def] = ref + if nk := normalizeNvidiaGPUProduct(def); nk != "" { + refIndex[nk] = ref + } + } + } else { + flat := map[string]string{} + if unmarshalErr2 := json.Unmarshal( + []byte(aliasStr), + &flat, + ); unmarshalErr2 == nil { + for def, ref := range flat { + def = strings.TrimSpace(def) + ref = strings.TrimSpace(ref) + if def == "" || ref == "" { + continue + } + refIndex[def] = ref + if nk := normalizeNvidiaGPUProduct(def); nk != "" { + refIndex[nk] = ref + } + } + } + } + } + existingNodeLabelStr = strings.TrimSpace(gpuInfoCM.Data[GPUNodeLabel]) + legacyLabelMappingStr = strings.TrimSpace(gpuInfoCM.Data[legacyGPULabelMapping]) + } + } + // get the GPU product, GPU memory, GPU allocatable number on the node + for i := range nodeList.Items { + node := &nodeList.Items[i] + nodeName := node.Name + gpuProduct, ok1 := node.Labels[NvidiaGPUProduct] + gpuMemory, ok2 := node.Labels[NvidiaGPUMemory] + gpuCount, ok3 := node.Status.Allocatable[NvidiaGPU] + if node.Annotations != nil { + if reg := node.Annotations["hami.io/node-nvidia-register"]; reg != "" { + p, m := parseHamiNvidiaRegister(reg) + if p != "" { + ok1 = true + gpuProduct = strings.TrimSpace(p) + } + if m != "" { + ok2 = true + gpuMemory = m + } + } + } + if !ok1 || !ok2 || !ok3 { + continue + } + if _, ok := nodeMap[nodeName]; !ok { + nodeMap[nodeName] = make(map[string]string) + } + effectiveCount := gpuCount.Value() + if node.Annotations != nil { + virtualCount := parseHamiNvidiaRegisterVirtualCount( + node.Annotations["hami.io/node-nvidia-register"], + ) + if virtualCount > 0 { + effectiveCount = virtualCount + } + } + aliasMap[gpuProduct] = defaultAliasForNvidiaGPUProduct(gpuProduct) + nodeMap[nodeName][GPUProduct] = gpuProduct + nodeMap[nodeName][GPUMemory] = gpuMemory + nodeMap[nodeName][GPUCount] = strconv.FormatInt(effectiveCount, 10) + ref := "" + if len(refIndex) > 0 { + if v, ok := refIndex[strings.TrimSpace(gpuProduct)]; ok { + ref = v + } else if nk := normalizeNvidiaGPUProduct(gpuProduct); nk != "" { + ref = refIndex[nk] + } + } + nodeMap[nodeName][GPURef] = ref + used := r.QueryGPUAllocation(ctx, reader, node, gpuProduct) + nodeMap[nodeName][GPUDevbox] = "false" + if _, ok := node.Labels[devboxLabel]; ok { + nodeMap[nodeName][GPUDevbox] = "true" + } + nodeMap[nodeName][GPUUse] = strconv.FormatInt(used, 10) + available := max(effectiveCount-used, 0) + nodeMap[nodeName][GPUAvailable] = strconv.FormatInt(available, 10) + } + + aliasConfigmap := &corev1.ConfigMap{} + err = reader.Get( + ctx, + types.NamespacedName{Name: r.aliasName, Namespace: r.aliasNamespace}, + aliasConfigmap, + ) + if err == nil { + if aliasConfigmap.Data == nil { + aliasConfigmap.Data = map[string]string{} + } + aliasIndex := make(map[string]string, len(aliasConfigmap.Data)*2) + for k, v := range aliasConfigmap.Data { + aliasIndex[k] = v + if nk := normalizeNvidiaGPUProduct(k); nk != "" { + aliasIndex[nk] = v + } + } + for k, v := range aliasMap { + if override, ok := aliasIndex[k]; ok && override != "" { + if override != v { + aliasMap[k] = override + } + continue + } + if nk := normalizeNvidiaGPUProduct(k); nk != "" { + if override, ok := aliasIndex[nk]; ok && override != "" { + if override != v { + aliasMap[k] = override + } + } + } + } + } + aliasMapBytes, err := json.Marshal(aliasMap) + if err != nil { + r.Logger.Error(err, "failed to marshal alias map to JSON string") + return ctrl.Result{}, err + } + aliasMapStr := string(aliasMapBytes) + var nodeLabelStr string + switch { + case existingNodeLabelStr != "": + nodeLabelStr = existingNodeLabelStr + case legacyLabelMappingStr != "": + nodeLabelStr = legacyLabelMappingStr + default: + nodeLabelMap := map[string]string{ + "kunlunxin": "xpu=on", + "nvidia": "gpu=on", + } + nodeLabelBytes, marshalErr := json.Marshal(nodeLabelMap) + if marshalErr != nil { + r.Logger.Error(marshalErr, "failed to marshal node label map to JSON string") + return ctrl.Result{}, marshalErr + } + nodeLabelStr = string(nodeLabelBytes) + } + // marshal node map to JSON string + nodeMapBytes, err := json.Marshal(nodeMap) + if err != nil { + r.Logger.Error(err, "failed to marshal node map to JSON string") + return ctrl.Result{}, err + } + nodeMapStr := string(nodeMapBytes) + + // create or update gpu-info configmap + configmap := &corev1.ConfigMap{} + err = reader.Get( + ctx, + types.NamespacedName{Name: GPUInfo, Namespace: GPUInfoNameSpace}, + configmap, + ) + + if errors.IsNotFound(err) { + configmap = &corev1.ConfigMap{ + ObjectMeta: metaV1.ObjectMeta{ + Name: GPUInfo, + Namespace: GPUInfoNameSpace, + }, + Data: map[string]string{ + GPU: nodeMapStr, + GPUAlias: aliasMapStr, + GPUNodeLabel: nodeLabelStr, + }, + } + if createErr := r.Create(ctx, configmap); createErr != nil { + r.Logger.Error(createErr, "failed to create gpu-info configmap") + return ctrl.Result{}, createErr + } + } else if err != nil { + r.Logger.Error(err, "failed to get gpu-info configmap") + return ctrl.Result{}, err + } + + if configmap.Data == nil { + configmap.Data = map[string]string{} + } + if configmap.Data[GPU] != nodeMapStr { + configmap.Data[GPU] = nodeMapStr + if updateErr := r.Update(ctx, configmap); updateErr != nil && + !errors.IsConflict(updateErr) { + r.Logger.Error(updateErr, "failed to update gpu-info configmap") + return ctrl.Result{}, updateErr + } + } + if configmap.Data[GPUAlias] == "" && configmap.Data[GPUAlias] != aliasMapStr { + configmap.Data[GPUAlias] = aliasMapStr + if updateErr := r.Update(ctx, configmap); updateErr != nil && + !errors.IsConflict(updateErr) { + r.Logger.Error(updateErr, "failed to update gpu-info configmap") + return ctrl.Result{}, updateErr + } + } + if configmap.Data[GPUNodeLabel] == "" && configmap.Data[GPUNodeLabel] != nodeLabelStr { + configmap.Data[GPUNodeLabel] = nodeLabelStr + if updateErr := r.Update(ctx, configmap); updateErr != nil && + !errors.IsConflict(updateErr) { + r.Logger.Error(updateErr, "failed to update gpu-info configmap") + return ctrl.Result{}, updateErr + } + } + + r.Logger.V(1).Info("gpu-info configmap status", "gpu", configmap.Data[GPU]) + return ctrl.Result{}, nil +} + +func (r *GpuReconciler) initGPUInfoCM(ctx context.Context) error { + reader := r.APIReader + if reader == nil { + reader = r.Client + } + // filter for nodes that have GPU + _, err := r.applyGPUInfoCM(ctx, reader) + return err +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GpuReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Logger = ctrl.Log.WithName("gpu-controller") + r.Logger.V(1).Info("starting gpu controller") + r.APIReader = mgr.GetAPIReader() + r.Client = mgr.GetClient() + + { + pod := &corev1.Pod{} + podsFunc := func(obj client.Object) []string { + podObj, ok := obj.(*corev1.Pod) + if !ok { + return nil + } + nodeName := podObj.Spec.NodeName + if nodeName == "" { + return nil + } + return []string{nodeName} + } + + if err := mgr.GetFieldIndexer(). + IndexField(context.Background(), pod, podNodeNameField, podsFunc); err != nil { + return err + } + } + + r.aliasName = os.Getenv("ALIAS_NAME") + r.aliasNamespace = os.Getenv("ALIAS_NAMESPACE") + if r.aliasName == "" { + r.aliasName = "gpu-alias" + } + if r.aliasNamespace == "" { + r.aliasNamespace = "kube-system" + } + + // init node-gpu-info configmap + r.Logger.V(1).Info("initializing node-gpu-info configmap") + if err := r.initGPUInfoCM(context.Background()); err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Pod{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(event event.CreateEvent) bool { + return useGPU(event.Object) + }, + UpdateFunc: func(event event.UpdateEvent) bool { + podOld, ok := event.ObjectOld.(*corev1.Pod) + if !ok { + return false + } + podNew, ok := event.ObjectNew.(*corev1.Pod) + if !ok { + return false + } + if !useGPU(podNew) { + return false + } + phaseOld := podOld.Status.Phase + phaseNew := podNew.Status.Phase + ipOld := podOld.Status.PodIP + ipNew := podNew.Status.PodIP + nodeOld := podOld.Spec.NodeName + nodeNew := podNew.Spec.NodeName + return phaseOld != phaseNew || ipOld != ipNew || nodeOld != nodeNew + }, + DeleteFunc: func(event event.DeleteEvent) bool { + return useGPU(event.Object) + }, + })). + Watches(&corev1.Node{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(event event.CreateEvent) bool { + return hasGPU(event.Object) + }, + UpdateFunc: func(event event.UpdateEvent) bool { + oldNode, ok := event.ObjectOld.(*corev1.Node) + if !ok { + return false + } + newNode, ok := event.ObjectNew.(*corev1.Node) + if !ok { + return false + } + return gpuChanged(oldNode, newNode) + }, + DeleteFunc: func(event event.DeleteEvent) bool { + return hasGPU(event.Object) + }, + })). + Watches(&corev1.ConfigMap{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(event event.CreateEvent) bool { + return isGPUInfoConfigMap(event.Object) + }, + UpdateFunc: func(event event.UpdateEvent) bool { + return isGPUInfoConfigMap(event.ObjectNew) && + configMapChanged(event.ObjectOld, event.ObjectNew) + }, + DeleteFunc: func(event event.DeleteEvent) bool { + return isGPUInfoConfigMap(event.Object) + }, + })). + Complete(r) +} + +func gpuChanged(oldNode, newNode *corev1.Node) bool { + oldHasGPU := hasGPU(oldNode) + newHasGPU := hasGPU(newNode) + if oldHasGPU != newHasGPU { + return true + } + + if !oldHasGPU { + return false + } + + oldCount := oldNode.Status.Allocatable[NvidiaGPU] + newCount := newNode.Status.Allocatable[NvidiaGPU] + if oldCount.Cmp(newCount) != 0 { + return true + } + + if oldNode.Labels[devboxLabel] != newNode.Labels[devboxLabel] { + return true + } + + oldReg := "" + newReg := "" + if oldNode.Annotations != nil { + oldReg = oldNode.Annotations["hami.io/node-nvidia-register"] + } + if newNode.Annotations != nil { + newReg = newNode.Annotations["hami.io/node-nvidia-register"] + } + if oldReg != newReg { + return true + } + + oldProd := oldNode.Labels[NvidiaGPUProduct] + oldMem := oldNode.Labels[NvidiaGPUMemory] + if (oldProd == "" || oldMem == "") && oldNode.Annotations != nil { + if reg := oldNode.Annotations["hami.io/node-nvidia-register"]; reg != "" { + p, m := parseHamiNvidiaRegister(reg) + if oldProd == "" { + oldProd = p + } + if oldMem == "" { + oldMem = m + } + } + } + oldProd = normalizeNvidiaGPUProduct(oldProd) + + newProd := newNode.Labels[NvidiaGPUProduct] + newMem := newNode.Labels[NvidiaGPUMemory] + if (newProd == "" || newMem == "") && newNode.Annotations != nil { + if reg := newNode.Annotations["hami.io/node-nvidia-register"]; reg != "" { + p, m := parseHamiNvidiaRegister(reg) + if newProd == "" { + newProd = p + } + if newMem == "" { + newMem = m + } + } + } + newProd = normalizeNvidiaGPUProduct(newProd) + + return oldProd != newProd || oldMem != newMem +} + +func useGPU(obj client.Object) bool { + pod, ok := obj.(*corev1.Pod) + if !ok { + return false + } + if _, ok := pod.Spec.NodeSelector[NvidiaGPUProduct]; ok { + return true + } + if pod.Annotations != nil { + if annVal := pod.Annotations[NvidiaUseGPUTypeAnnotation]; annVal != "" { + return true + } + } + if podRequestsGPUContainers(pod.Spec.Containers) { + return true + } + return podRequestsGPUContainers(pod.Spec.InitContainers) +} + +func hasGPU(obj client.Object) bool { + node, ok := obj.(*corev1.Node) + if !ok { + return false + } + _, ok1 := node.Labels[NvidiaGPUMemory] + _, ok2 := node.Labels[NvidiaGPUProduct] + if (!ok1 || !ok2) && node.Annotations != nil { + if reg, ok := node.Annotations["hami.io/node-nvidia-register"]; ok { + p, m := parseHamiNvidiaRegister(reg) + ok1 = ok1 || m != "" + ok2 = ok2 || p != "" + } + } + _, ok3 := node.Status.Allocatable[NvidiaGPU] + return ok1 && ok2 && ok3 +} + +func podRequestsGPUContainers(containers []corev1.Container) bool { + for _, container := range containers { + if nvidiaGPURequestValue(container) > 0 || vxpuRequestValue(container) > 0 { + return true + } + } + return false +} + +func isGPUInfoConfigMap(obj client.Object) bool { + cm, ok := obj.(*corev1.ConfigMap) + if !ok || cm == nil { + return false + } + return cm.Name == GPUInfo && cm.Namespace == GPUInfoNameSpace +} + +func configMapChanged(oldObj, newObj client.Object) bool { + oldCM, ok1 := oldObj.(*corev1.ConfigMap) + newCM, ok2 := newObj.(*corev1.ConfigMap) + if !ok1 || !ok2 || oldCM == nil || newCM == nil { + return false + } + if !isGPUInfoConfigMap(newCM) { + return false + } + return !reflect.DeepEqual(oldCM.Data, newCM.Data) || + !reflect.DeepEqual(oldCM.BinaryData, newCM.BinaryData) +} diff --git a/controllers/node/controllers/gpu_controller_test.go b/controllers/node/controllers/gpu_controller_test.go new file mode 100644 index 000000000000..39ed5841e562 --- /dev/null +++ b/controllers/node/controllers/gpu_controller_test.go @@ -0,0 +1,105 @@ +package controllers + +import ( + "testing" +) + +func TestParseHamiNvidiaRegister(t *testing.T) { + tests := []struct { + name string + input string + wantProd string + wantMem string + }{ + { + name: "JSON format single healthy device", + input: `[{"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":1,"health":true}]`, + wantProd: "NVIDIA-GeForce-RTX-3090", + wantMem: "25363", + }, + { + name: "JSON format multiple devices, first healthy", + input: `[{"type":"Broken-GPU","devmem":1000,"count":1,"health":false}, {"type":"NVIDIA-Tesla-T4","devmem":15360,"count":1,"health":true}]`, + wantProd: "NVIDIA-Tesla-T4", + wantMem: "15360", + }, + { + name: "CSV format", + input: `GPU-uuid,pci,15360,NVIDIA,Tesla T4,1,true`, + wantProd: "Tesla T4", + wantMem: "15360", + }, + { + name: "CSV format multiple", + input: `GPU-1,pci,1000,NVIDIA,Bad,1,false:GPU-2,pci,15360,NVIDIA,Tesla T4,1,true`, + wantProd: "Tesla T4", + wantMem: "15360", + }, + { + name: "Empty input", + input: ``, + wantProd: "", + wantMem: "", + }, + { + name: "Invalid JSON", + input: `{not-an-array}`, + wantProd: "", + wantMem: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotProd, gotMem := parseHamiNvidiaRegister(tt.input) + if gotProd != tt.wantProd { + t.Errorf("parseHamiNvidiaRegister() gotProd = %v, want %v", gotProd, tt.wantProd) + } + if gotMem != tt.wantMem { + t.Errorf("parseHamiNvidiaRegister() gotMem = %v, want %v", gotMem, tt.wantMem) + } + }) + } +} + +func TestParseHamiNvidiaRegisterVirtualCount(t *testing.T) { + tests := []struct { + name string + input string + want int64 + }{ + { + name: "JSON format single device", + input: `[{"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":5,"health":true}]`, + want: 5, + }, + { + name: "JSON format multiple devices", + input: `[{"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":5,"health":true}, {"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":3,"health":true}]`, + want: 8, + }, + { + name: "JSON format with unhealthy device", + input: `[{"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":5,"health":true}, {"type":"NVIDIA-GeForce-RTX-3090","devmem":25363,"count":3,"health":false}]`, + want: 5, + }, + { + name: "Not JSON (CSV)", + input: `GPU-uuid,pci,15360,NVIDIA,Tesla T4,1,true`, + want: 0, + }, + { + name: "Empty input", + input: ``, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseHamiNvidiaRegisterVirtualCount(tt.input); got != tt.want { + t.Errorf("parseHamiNvidiaRegisterVirtualCount() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/controllers/node/deploy/Kubefile b/controllers/node/deploy/Kubefile new file mode 100644 index 000000000000..370536c61379 --- /dev/null +++ b/controllers/node/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY drop drop +COPY entrypoint.sh entrypoint.sh +CMD ["bash entrypoint.sh"] diff --git a/controllers/node/deploy/charts/node/.helmignore b/controllers/node/deploy/charts/node/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/controllers/node/deploy/charts/node/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/controllers/node/deploy/charts/node/Chart.yaml b/controllers/node/deploy/charts/node/Chart.yaml new file mode 100644 index 000000000000..1c6f3753648e --- /dev/null +++ b/controllers/node/deploy/charts/node/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: node +description: A Helm chart for Kubernetes + +# 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.1.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. +appVersion: "1.16.0" diff --git a/controllers/node/deploy/charts/node/templates/NOTES.txt b/controllers/node/deploy/charts/node/templates/NOTES.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/controllers/node/deploy/charts/node/templates/_helpers.tpl b/controllers/node/deploy/charts/node/templates/_helpers.tpl new file mode 100644 index 000000000000..2e3779f86b64 --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "node.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "node.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "node.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "node.labels" -}} +helm.sh/chart: {{ include "node.chart" . }} +{{ include "node.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "node.selectorLabels" -}} +app.kubernetes.io/name: {{ include "node.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "node.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "node.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/node/deploy/charts/node/templates/cert.yaml b/controllers/node/deploy/charts/node/templates/cert.yaml new file mode 100644 index 000000000000..de2cd1d5aac5 --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/cert.yaml @@ -0,0 +1,27 @@ +{{- if .Values.metrics.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + {{- include "node.labels" . | nindent 4 }} + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + {{- include "node.labels" . | nindent 4 }} + name: metrics-certs +spec: + privateKey: + rotationPolicy: Always + dnsNames: + - {{ include "node.fullname" . }}-metrics.{{.Release.Namespace}}.svc + - {{ include "node.fullname" . }}-metrics.{{.Release.Namespace}}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: {{.Values.metrics.secretName}} +{{- end }} \ No newline at end of file diff --git a/controllers/node/deploy/charts/node/templates/clusterrole.yaml b/controllers/node/deploy/charts/node/templates/clusterrole.yaml new file mode 100644 index 000000000000..8f1f6ef6878c --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/clusterrole.yaml @@ -0,0 +1,64 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: node-controller-clusterrole +rules: +- apiGroups: [ "" ] + resources: [ "namespaces" ] + resourceNames: [ "kube-system" ] + verbs: [ "get" ] +- apiGroups: + - "" + resources: + - nodes + - nodes/status + - pods + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + resourceNames: + - devboxes.devbox.sealos.io + verbs: + - get +- nonResourceURLs: + - /metrics + verbs: + - get +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: node-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: node-controller-clusterrole +subjects: + - kind: ServiceAccount + name: {{ include "node.serviceAccountName" . }} + namespace: {{.Release.Namespace}} \ No newline at end of file diff --git a/controllers/node/deploy/charts/node/templates/deployment.yaml b/controllers/node/deploy/charts/node/templates/deployment.yaml new file mode 100644 index 000000000000..9d157d4e8e63 --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/deployment.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "node.fullname" . }} + labels: + {{- include "node.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "node.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "node.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "node.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + command: + - /manager + env: + - name: ALIAS_NAME + value: gpu-alias + - name: ALIAS_NAMESPACE + value: {{.Release.Namespace}} + args: + - --leader-elect + - --health-probe-bind-address=:8081 + {{- if .Values.metrics.enabled }} + - --metrics-secure=true + - --metrics-bind-address=:8443 + {{- end }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image }}" + imagePullPolicy: Always + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics-certs + mountPath: {{.Values.metrics.certPath}} + readOnly: true + {{- end }} + + volumes: + {{- if .Values.metrics.enabled }} + - name: metrics-certs + secret: + secretName: {{.Values.metrics.secretName}} + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/controllers/node/deploy/charts/node/templates/role.yaml b/controllers/node/deploy/charts/node/templates/role.yaml new file mode 100644 index 000000000000..e9fb77d34f6a --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/role.yaml @@ -0,0 +1,55 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: node-controller-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: node-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-controller-role +subjects: + - kind: ServiceAccount + name: {{ include "node.serviceAccountName" . }} + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: node-gpu-info-cm-reader + namespace: node-system +rules: +- apiGroups: + - "" + resourceNames: + - node-gpu-info + resources: + - configmaps + verbs: + - get + - watch + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: node-gpu-info-cm-reader-rolebinding + namespace: node-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-gpu-info-cm-reader +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:serviceaccounts \ No newline at end of file diff --git a/controllers/node/deploy/charts/node/templates/service.yaml b/controllers/node/deploy/charts/node/templates/service.yaml new file mode 100644 index 000000000000..411d2192af1d --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "node.fullname" . }}-metrics + labels: + {{- include "node.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8443 + protocol: TCP + name: https + selector: + {{- include "node.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/controllers/node/deploy/charts/node/templates/serviceaccount.yaml b/controllers/node/deploy/charts/node/templates/serviceaccount.yaml new file mode 100644 index 000000000000..4c8a4bd9d0e3 --- /dev/null +++ b/controllers/node/deploy/charts/node/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "node.serviceAccountName" . }} + labels: + {{- include "node.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +--- diff --git a/controllers/node/deploy/charts/node/values.yaml b/controllers/node/deploy/charts/node/values.yaml new file mode 100644 index 000000000000..c0aa21442982 --- /dev/null +++ b/controllers/node/deploy/charts/node/values.yaml @@ -0,0 +1,109 @@ +# Default values for node. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: ghcr.io/labring/sealos-node-controller:latest + +# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + # fsGroup: 2000 + +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +metrics: + # Enable metrics endpoint + enabled: false + # Path to the metrics certificate + certPath: /tmp/k8s-metrics-server/metrics-certs + secretName: metrics-server-cert diff --git a/controllers/node/deploy/drop/deploy.yaml b/controllers/node/deploy/drop/deploy.yaml new file mode 100644 index 000000000000..4c3d01af2e0b --- /dev/null +++ b/controllers/node/deploy/drop/deploy.yaml @@ -0,0 +1,410 @@ +# Copyright © 2023 sealos. +# +# 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. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/part-of: node + name: node-controller-manager + namespace: node-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: node-gpu-info-cm-reader + namespace: node-system +rules: + - apiGroups: + - "" + resourceNames: + - node-gpu-info + resources: + - configmaps + verbs: + - get + - watch + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/name: role + app.kubernetes.io/part-of: node + app.kubernets.io/managed-by: kustomize + name: node-leader-election-role + namespace: node-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: node-manager-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - nodes/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - node.k8s.io + resources: + - gpus + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - node.k8s.io + resources: + - gpus/finalizers + verbs: + - update +- apiGroups: + - node.k8s.io + resources: + - gpus/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: node + name: node-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: node + name: node-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: node-gpu-info-cm-reader-rolebinding + namespace: node-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-gpu-info-cm-reader +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:serviceaccounts +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: rolebinding + app.kubernetes.io/part-of: node + name: node-leader-election-rolebinding + namespace: node-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: node-leader-election-role +subjects: +- kind: ServiceAccount + name: node-controller-manager + namespace: node-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: node + name: node-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: node-manager-role +subjects: +- kind: ServiceAccount + name: node-controller-manager + namespace: node-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/part-of: node + name: node-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: node-proxy-role +subjects: +- kind: ServiceAccount + name: node-controller-manager + namespace: node-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: node + control-plane: controller-manager + name: node-controller-manager-metrics-service + namespace: node-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: node + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: deployment + app.kubernetes.io/part-of: node + control-plane: controller-manager + name: node-controller-manager + namespace: node-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: ghcr.io/labring/sealos-node-controller:latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: node-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/node/deploy/entrypoint.sh b/controllers/node/deploy/entrypoint.sh new file mode 100644 index 000000000000..16219f63bd51 --- /dev/null +++ b/controllers/node/deploy/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +HELM_OPTS=${HELM_OPTS:-""} +kubectl delete -f ./drop/ --ignore-not-found +helm upgrade -i node -n node-system --create-namespace ./charts/node ${HELM_OPTS} diff --git a/controllers/node/go.mod b/controllers/node/go.mod new file mode 100644 index 000000000000..372af4f09d43 --- /dev/null +++ b/controllers/node/go.mod @@ -0,0 +1,82 @@ +module github.com/labring/sealos/controllers/node + +go 1.24.0 + +require ( + github.com/go-logr/logr v1.4.1 + github.com/onsi/ginkgo/v2 v2.14.0 + github.com/onsi/gomega v1.30.0 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000 +) + +replace ( + k8s.io/api => k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 + k8s.io/client-go => k8s.io/client-go v0.28.3 + k8s.io/component-base => k8s.io/component-base v0.28.3 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + 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/matttproud/golang_protobuf_extensions/v2 v2.0.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/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.16.1 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/controllers/node/go.sum b/controllers/node/go.sum new file mode 100644 index 000000000000..83f4fc864cba --- /dev/null +++ b/controllers/node/go.sum @@ -0,0 +1,206 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/node/hack/boilerplate.go.txt b/controllers/node/hack/boilerplate.go.txt new file mode 100644 index 000000000000..65b862271836 --- /dev/null +++ b/controllers/node/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023. + +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. +*/ \ No newline at end of file diff --git a/controllers/node/main.go b/controllers/node/main.go new file mode 100644 index 000000000000..336876791874 --- /dev/null +++ b/controllers/node/main.go @@ -0,0 +1,171 @@ +/* +Copyright 2023. + +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" + + "github.com/labring/sealos/controllers/node/controllers" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // 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" + 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/filters" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +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 secureMetrics bool + var enableHTTP2 bool + var tlsOpts []func(*tls.Config) + var concurrent int + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.IntVar(&concurrent, "concurrent", 100, "The number of concurrent cluster reconciles.") + + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + 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") + opts := zap.Options{ + Development: true, + } + 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) + } + // 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, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "0f9d0ffa.sealos.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 start manager") + os.Exit(1) + } + if err = (&controllers.GpuReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Gpu") + 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) + } +} diff --git a/controllers/pkg/account/count.go b/controllers/pkg/account/count.go new file mode 100644 index 000000000000..f83b812636ea --- /dev/null +++ b/controllers/pkg/account/count.go @@ -0,0 +1,26 @@ +/* +Copyright 2022. + +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 account + +import "math" + +// CurrencyUnit is the unit of currency for accounting. +const CurrencyUnit = 1000000 + +func GetCurrencyBalance(balance int64) float64 { + return math.Ceil(float64(balance) / CurrencyUnit) +} diff --git a/controllers/pkg/account/env.go b/controllers/pkg/account/env.go new file mode 100644 index 000000000000..f4ce6f371a3a --- /dev/null +++ b/controllers/pkg/account/env.go @@ -0,0 +1,10 @@ +package account + +const ( + PayIsProduction = "PAY_IS_PRODUCTION" + AlipayAppID = "ALIPAY_APP_ID" + AlipayRootCert = "ALIPAY_ROOT_CERT" + AlipayPrivateKey = "ALIPAY_PRIVATE_KEY" + AlipayCertPublicKey = "ALIPAY_CERT_PUBLIC_KEY" + AlipayAppCertPublicKey = "ALIPAY_APP_CERT_PUBLIC_KEY" +) diff --git a/controllers/pkg/code/common.go b/controllers/pkg/code/common.go new file mode 100644 index 000000000000..389a26e465e3 --- /dev/null +++ b/controllers/pkg/code/common.go @@ -0,0 +1,31 @@ +// Copyright © 2023 sealos. +// +// 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 code + +const ( + MessageFormat = "%d: %s" +) + +// code +const ( + // InsufficientBalance debt webhook + InsufficientBalance = 40001 + // IngressFailedCnameCheck admission webhook for ingress + IngressFailedCnameCheck = 40300 + IngressFailedOwnerCheck = 40301 + IngressFailedIcpCheck = 40302 + + IngressWebhookInternalError = 50000 +) diff --git a/controllers/pkg/common/account.go b/controllers/pkg/common/account.go new file mode 100644 index 000000000000..06a0f790d13c --- /dev/null +++ b/controllers/pkg/common/account.go @@ -0,0 +1,22 @@ +// Copyright © 2023 sealos. +// +// 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 common + +type Type int + +const ( + Consumption Type = iota + SubConsumption +) diff --git a/controllers/pkg/config/global.go b/controllers/pkg/config/global.go new file mode 100644 index 000000000000..893ba6750b8c --- /dev/null +++ b/controllers/pkg/config/global.go @@ -0,0 +1,59 @@ +// Copyright © 2024 sealos. +// +// 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 config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Global struct { + CloudDomain string `yaml:"cloudDomain"` + CloudPort string `yaml:"cloudPort"` + HTTPPort string `yaml:"httpPort"` + DisableHTTPS bool `yaml:"disableHttps"` + RegionUID string `yaml:"regionUID"` + CertSecretName string `yaml:"certSecretName"` +} + +type Kube struct { + Version string `yaml:"version"` + APIServerHost string `yaml:"apiServerHost"` + APIServerPort string `yaml:"apiServerPort"` +} + +type Common struct { + GuideEnabled string `yaml:"guideEnabled"` + APIEnabled string `yaml:"apiEnabled"` +} + +type Database struct { + MongodbURI string `yaml:"mongodbURI"` + GlobalCockroachdbURI string `yaml:"globalCockroachdbURI"` + RegionalCockroachdbURI string `yaml:"regionalCockroachdbURI"` +} + +func LoadConfig(path string, target any) error { + configData, err := os.ReadFile(path) + if err != nil { + return err + } + err = yaml.Unmarshal(configData, target) + if err != nil { + return err + } + return nil +} diff --git a/controllers/pkg/crypto/crypto.go b/controllers/pkg/crypto/crypto.go new file mode 100644 index 000000000000..13225fd5fc63 --- /dev/null +++ b/controllers/pkg/crypto/crypto.go @@ -0,0 +1,164 @@ +/* +Copyright 2023. + +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 crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io" + "strconv" +) + +const defaultEncryptionKey = "Bg1c3Dd5e9e0F84bdF0A5887cF43aB63" + +var encryptionKey = defaultEncryptionKey + +// Encrypt encrypts the given plaintext using AES-GCM. +func Encrypt(plaintext []byte) (string, error) { + return EncryptWithKey(plaintext, []byte(encryptionKey)) +} + +// EncryptWithKey encrypts the given plaintext using AES-GCM. +func EncryptWithKey(plaintext, encryptionKey []byte) (string, error) { + block, err := aes.NewCipher(encryptionKey) + if err != nil { + return "", err + } + + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...)), nil +} + +func EncryptInt64(in int64) (*string, error) { + out, err := Encrypt([]byte(strconv.FormatInt(in, 10))) + return &out, err +} + +// EncryptWithKey encrypts the given plaintext using AES-GCM. +func EncryptInt64WithKey(in int64, encryptionKey []byte) (*string, error) { + out, err := EncryptWithKey([]byte(strconv.FormatInt(in, 10)), encryptionKey) + return &out, err +} + +func DecryptInt64(in string) (int64, error) { + out, err := Decrypt(in) + if err != nil { + return 0, fmt.Errorf("failed to decrpt balance: %w", err) + } + return strconv.ParseInt(string(out), 10, 64) +} + +// Decrypt decrypts the given ciphertext using AES-GCM. +func Decrypt(ciphertextBase64 string) ([]byte, error) { + return DecryptWithKey(ciphertextBase64, []byte(encryptionKey)) +} + +// DecryptWithKey decrypts the given ciphertext using AES-GCM. +func DecryptWithKey(ciphertextBase64 string, encryptionKey []byte) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return nil, err + } + + if len(ciphertext) < 12 { + return nil, errors.New("ciphertext too short") + } + + block, err := aes.NewCipher(encryptionKey) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := ciphertext[:12] + ciphertext = ciphertext[12:] + plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func ParseRSAPublicKeyFromPEM(keyPEM string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(keyPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the public key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, errors.New("key is not of type *rsa.PublicKey") + } + return rsaPub, nil +} + +// ParseRSAPublicKeyFromAnyPEM tries to parse a public key; if the PEM contains a private key it derives the public part. +func ParseRSAPublicKeyFromAnyPEM(keyPEM string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(keyPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block") + } + switch block.Type { + case "PUBLIC KEY": + return ParseRSAPublicKeyFromPEM(keyPEM) + case "RSA PRIVATE KEY": + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return &priv.PublicKey, nil + case "PRIVATE KEY": + privAny, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + priv, ok := privAny.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("private key is not RSA") + } + return &priv.PublicKey, nil + default: + return nil, fmt.Errorf("unsupported key type %q", block.Type) + } +} diff --git a/controllers/pkg/crypto/crypto_test.go b/controllers/pkg/crypto/crypto_test.go new file mode 100644 index 000000000000..1f69138641c7 --- /dev/null +++ b/controllers/pkg/crypto/crypto_test.go @@ -0,0 +1,22 @@ +// Copyright © 2023 sealos. +// +// 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 crypto + +import "testing" + +func TestRechargeBalance(t *testing.T) { + data, _ := DecryptInt64("") + t.Log(data) +} diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go new file mode 100644 index 000000000000..36ed417318bb --- /dev/null +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -0,0 +1,2901 @@ +// Copyright © 2024 sealos. +// +// 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 cockroach + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" + gonanoid "github.com/matoous/go-nanoid/v2" + "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/logger" +) + +type Cockroach struct { + DB *gorm.DB + Localdb *gorm.DB + LocalRegion *types.Region + ZeroAccount *types.Account + accountConfig *types.AccountConfig + tasks map[uuid.UUID]types.Task + subscriptionPlans *sync.Map + workspaceSubPlans *sync.Map + // use sync.map : More suitable for writing once read multiple scenarios & Lock contention can be reduced when multiple processes operate on different keys + ownerUsrUIDMap *sync.Map + ownerUsrIDMap *sync.Map +} + +const ( + EnvLocalRegion = "LOCAL_REGION" + EnvBaseBalance = "BASE_BALANCE" +) + +func quoteIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + +func quoteTableName(tableName string) string { + return quoteIdentifier(tableName) +} + +func alterTableAddColumnSQL(tableName, columnDef string) string { + return fmt.Sprintf(`ALTER TABLE %s ADD COLUMN %s;`, quoteTableName(tableName), columnDef) +} + +func createEnumTypeSQL(typeName string, values []string) string { + quotedValues := make([]string, 0, len(values)) + for i := range values { + quotedValues = append( + quotedValues, + fmt.Sprintf("'%s'", strings.ReplaceAll(values[i], `'`, `''`)), + ) + } + return fmt.Sprintf( + `CREATE TYPE IF NOT EXISTS %s AS ENUM (%s)`, + quoteIdentifier(typeName), + strings.Join(quotedValues, ", "), + ) +} + +func alterEnumAddValueSQL(typeName, value string) string { + return fmt.Sprintf( + `ALTER TYPE %s ADD VALUE IF NOT EXISTS '%s'`, + quoteIdentifier(typeName), + strings.ReplaceAll(value, `'`, `''`), + ) +} + +func createEnumTypeSQLForPostgres(typeName string, values []string) string { + quotedValues := make([]string, 0, len(values)) + for i := range values { + quotedValues = append( + quotedValues, + fmt.Sprintf("'%s'", strings.ReplaceAll(values[i], `'`, `''`)), + ) + } + return fmt.Sprintf( + `DO $$ +BEGIN + CREATE TYPE %s AS ENUM (%s); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$`, + quoteIdentifier(typeName), + strings.Join(quotedValues, ", "), + ) +} + +func isCockroachDatabase(db *gorm.DB) bool { + if db == nil { + return false + } + + var version string + if err := db.Raw(`SELECT version()`).Scan(&version).Error; err != nil { + return false + } + + return strings.Contains(strings.ToLower(version), "cockroachdb") +} + +func buildGenRandomUUIDFallbackSQL() string { + return `DO $$ +BEGIN + CREATE OR REPLACE FUNCTION gen_random_uuid() + RETURNS uuid + AS 'SELECT uuid_generate_v4();' + LANGUAGE SQL; +EXCEPTION + WHEN undefined_function THEN NULL; +END +$$` +} + +func ensurePostgresExtensions(db *gorm.DB) error { + if db == nil || isCockroachDatabase(db) { + return nil + } + + if err := db.Exec(`CREATE EXTENSION IF NOT EXISTS pgcrypto`).Error; err != nil { + if uuidErr := db.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`).Error; uuidErr != nil { + return fmt.Errorf( + "failed to enable pgcrypto extension: %w; failed to enable uuid-ossp extension: %w", + err, + uuidErr, + ) + } + if fallbackErr := db.Exec(buildGenRandomUUIDFallbackSQL()).Error; fallbackErr != nil { + return fmt.Errorf("failed to install gen_random_uuid fallback: %w", fallbackErr) + } + } + + return nil +} + +func postgresUUIDDefaultExpr(db *gorm.DB) string { + if db == nil || isCockroachDatabase(db) { + return "gen_random_uuid()" + } + + var exists bool + if err := db.Raw(` + SELECT EXISTS ( + SELECT 1 + FROM pg_proc + WHERE proname = 'gen_random_uuid' + ) + `).Scan(&exists).Error; err == nil && exists { + return "gen_random_uuid()" + } + + return "uuid_generate_v4()" +} + +func ensureColumnUUIDDefault(db *gorm.DB, tableName, columnName string) error { + if db == nil { + return nil + } + if !db.Migrator().HasTable(tableName) || !db.Migrator().HasColumn(tableName, columnName) { + return nil + } + + defaultExpr := postgresUUIDDefaultExpr(db) + sql := fmt.Sprintf( + `ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s`, + quoteTableName(tableName), + quoteIdentifier(columnName), + defaultExpr, + ) + if err := db.Exec(sql).Error; err != nil { + return fmt.Errorf("failed to set default for %s.%s: %w", tableName, columnName, err) + } + return nil +} + +func ensureLegacyUUIDDefaults(c *Cockroach) error { + specs := []struct { + db *gorm.DB + tableName string + column string + }{ + {db: c.DB, tableName: types.User{}.TableName(), column: "uid"}, + {db: c.DB, tableName: types.Region{}.TableName(), column: "uid"}, + {db: c.DB, tableName: types.OauthProvider{}.TableName(), column: "uid"}, + {db: c.Localdb, tableName: types.RegionUserCr{}.TableName(), column: "uid"}, + {db: c.Localdb, tableName: types.Workspace{}.TableName(), column: "uid"}, + {db: c.Localdb, tableName: types.UserWorkspace{}.TableName(), column: "uid"}, + } + + for i := range specs { + if err := ensureColumnUUIDDefault(specs[i].db, specs[i].tableName, specs[i].column); err != nil { + return err + } + } + + return nil +} + +func ensureUUID(uuidPtr *uuid.UUID) { + if uuidPtr != nil && *uuidPtr == uuid.Nil { + *uuidPtr = uuid.New() + } +} + +func (c *Cockroach) CreateUser( + oAuth *types.OauthProvider, + regionUserCr *types.RegionUserCr, + user *types.User, + workspace *types.Workspace, + userWorkspace *types.UserWorkspace, +) error { + ensureUUID(&oAuth.UID) + ensureUUID(®ionUserCr.UID) + ensureUUID(&user.UID) + ensureUUID(&workspace.UID) + ensureUUID(&userWorkspace.UID) + + findUser, findRegionUserCr, findUserWorkspace := &types.User{}, &types.RegionUserCr{}, &types.UserWorkspace{} + if errors.Is( + c.DB.Where(&types.User{Nickname: user.Nickname}).First(findUser).Error, + gorm.ErrRecordNotFound, + ) { + findUser = user + if err := c.DB.Save(user).Error; err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + } + if errors.Is( + c.DB.Where(types.OauthProvider{UserUID: findUser.UID}).First(&types.OauthProvider{}).Error, + gorm.ErrRecordNotFound, + ) { + oAuth.UserUID = findUser.UID + if err := c.DB.Save(oAuth).Error; err != nil { + return fmt.Errorf("failed to create user oauth provider: %w", err) + } + } + if errors.Is( + c.Localdb.Where(&types.RegionUserCr{CrName: regionUserCr.CrName}). + First(findRegionUserCr). + Error, + gorm.ErrRecordNotFound, + ) { + regionUserCr.UserUID = findUser.UID + findRegionUserCr = regionUserCr + if err := c.Localdb.Save(regionUserCr).Error; err != nil { + return fmt.Errorf("failed to create user region cr: %w", err) + } + } + if errors.Is( + c.Localdb.Where(types.UserWorkspace{UserCrUID: findRegionUserCr.UID}). + First(findUserWorkspace). + Error, + gorm.ErrRecordNotFound, + ) { + userWorkspace.UserCrUID = findRegionUserCr.UID + findUserWorkspace = userWorkspace + if err := c.Localdb.Save(userWorkspace).Error; err != nil { + return fmt.Errorf("failed to create user workspace: %w", err) + } + } + if errors.Is( + c.Localdb.Where(types.Workspace{UID: findUserWorkspace.WorkspaceUID}). + First(&types.Workspace{}). + Error, + gorm.ErrRecordNotFound, + ) { + workspace.UID = findUserWorkspace.WorkspaceUID + if err := c.Localdb.Save(workspace).Error; err != nil { + return fmt.Errorf("failed to create workspace: %w", err) + } + } + return nil +} + +func (c *Cockroach) CreateRegion(region *types.Region) error { + if err := c.DB.Where(&types.Region{UID: region.UID}).FirstOrCreate(region).Error; err != nil { + return fmt.Errorf("failed to create region: %w", err) + } + return nil +} + +func (c *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.User, error) { + if err := checkOps(ops); err != nil { + return nil, err + } + queryUser := &types.User{} + switch { + case ops.UID != uuid.Nil: + queryUser.UID = ops.UID + case ops.ID != "": + queryUser.ID = ops.ID + case ops.Owner != "": + userUID, err := c.getUserUIDByOwnerWithCache(ops.Owner) + if err != nil { + if ops.IgnoreEmpty && errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + queryUser.UID = userUID + } + var user types.User + if err := c.DB.Where(queryUser).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func cloneMap(m map[int64]int64) map[int64]int64 { + newMap := make(map[int64]int64, len(m)) + for k, v := range m { + newMap[k] = v + } + return newMap +} + +func (c *Cockroach) GetUserRechargeDiscount( + ops *types.UserQueryOpts, +) (types.UserRechargeDiscount, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = userUID + } + cfg, err := c.GetAccountConfig() + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get account config: %w", err) + } + activeSteps, firstRechargeSteps := cloneMap( + cfg.DefaultDiscountSteps, + ), cloneMap( + cfg.FirstRechargeDiscountSteps, + ) + if firstRechargeSteps != nil { + var firstRechargeTime time.Time + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}). + Order("created_at ASC").Limit(1).Select("created_at").Scan(&firstRechargeTime).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return types.UserRechargeDiscount{}, fmt.Errorf( + "failed to get first recharge time: %w", + err, + ) + } + } + if !firstRechargeTime.IsZero() { + if firstRechargeTime.After(time.Date(2025, time.July, 18, 8, 40, 0, 0, time.UTC)) { + payments, err := c.getActivePayments(ops, types.ActivityTypeFirstRecharge) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return types.UserRechargeDiscount{}, fmt.Errorf( + "failed to get first recharge payments: %w", + err, + ) + } + if len(payments) != 0 { + for i := range payments { + delete(firstRechargeSteps, payments[i].Amount/BaseUnit) + } + } + } else { + firstRechargeSteps = map[int64]int64{} + } + } + if len(firstRechargeSteps) > 0 { + payments, err := c.getActivePayments(ops, types.ActivityTypeFirstRecharge) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return types.UserRechargeDiscount{}, fmt.Errorf( + "failed to get first recharge payments: %w", + err, + ) + } + if len(payments) != 0 { + for i := range payments { + delete(firstRechargeSteps, payments[i].Amount/BaseUnit) + } + } + } + } + if cfg.DefaultActiveType != "" { + count, err := c.getActivePaymentCount(ops, cfg.DefaultActiveType) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return types.UserRechargeDiscount{}, fmt.Errorf( + "failed to get active payment count: %w", + err, + ) + } + if count > 0 { + for i := range activeSteps { + activeSteps[i] = 0 + } + } + } + return types.UserRechargeDiscount{ + DefaultActiveType: cfg.DefaultActiveType, + DefaultSteps: activeSteps, + FirstRechargeSteps: firstRechargeSteps, + }, nil +} + +// func (c *Cockroach) determineRechargeRecord(ops *types.UserQueryOpts) (bool, error) { +// userUid, err := c.GetUserUID(ops) +// if err != nil { +// return false, fmt.Errorf("failed to get user uid: %v", err) +// } +// var paymentCount int64 +// if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUid}}). +// Count(&paymentCount).Error; err != nil { +// return false, fmt.Errorf("failed to get payment count: %v", err) +// } +// return paymentCount > 0, nil +//} + +func (c *Cockroach) GetAccountConfig() (types.AccountConfig, error) { + if c.accountConfig == nil { + config := &types.Configs{} + if err := c.DB.Where(&types.Configs{}).First(config).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.AccountConfig{}, nil + } + return types.AccountConfig{}, fmt.Errorf("failed to get account config: %w", err) + } + var accountConfig types.AccountConfig + if err := json.Unmarshal([]byte(config.Data), &accountConfig); err != nil { + return types.AccountConfig{}, fmt.Errorf("failed to unmarshal account config: %w", err) + } + c.accountConfig = &accountConfig + } + return *c.accountConfig, nil +} + +func (c *Cockroach) ReloadAccountConfig() (types.AccountConfig, error) { + config := &types.Configs{} + if err := c.DB.Where(&types.Configs{}).First(config).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.AccountConfig{}, nil + } + return types.AccountConfig{}, fmt.Errorf("failed to get account config: %w", err) + } + var accountConfig types.AccountConfig + if err := json.Unmarshal([]byte(config.Data), &accountConfig); err != nil { + return types.AccountConfig{}, fmt.Errorf("failed to unmarshal account config: %w", err) + } + c.accountConfig = &accountConfig + return *c.accountConfig, nil +} + +func (c *Cockroach) InsertAccountConfig(config *types.AccountConfig) error { + data, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal account config: %w", err) + } + return c.DB.Model(&types.Configs{}). + Create(&types.Configs{Type: types.AccountConfigType, Data: string(data)}). + Error +} + +func (c *Cockroach) IsNullRecharge(ops *types.UserQueryOpts) (bool, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return false, fmt.Errorf("failed to get user: %w", err) + } + ops.UID = userUID + } + var count int64 + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}). + Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to get payment count: %w", err) + } + return count == 0, nil +} + +func (c *Cockroach) getActivePayments( + ops *types.UserQueryOpts, + activeType types.ActivityType, +) ([]types.Payment, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = userUID + } + var payments []types.Payment + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where(`"activityType" = ?`, activeType). + Find(&payments).Error; err != nil { + return nil, fmt.Errorf("failed to get payment count: %w", err) + } + return payments, nil +} + +// get active payments count +func (c *Cockroach) getActivePaymentCount( + ops *types.UserQueryOpts, + activeType types.ActivityType, +) (int64, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return 0, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = userUID + } + var count int64 + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where(`"activityType" = ?`, activeType). + Count(&count).Error; err != nil { + return 0, fmt.Errorf("failed to get payment count: %w", err) + } + return count, nil +} + +func (c *Cockroach) ProcessPendingTaskRewards() error { + for { + var userTask types.UserTask + err := c.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Clauses(clause.Locking{ + Strength: "UPDATE", + Options: "SKIP LOCKED", + }).Where(&types.UserTask{ + Status: types.TaskStatusCompleted, + RewardStatus: types.TaskStatusNotCompleted, + }).First(&userTask).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return fmt.Errorf("failed to get pending reward user task: %w", err) + } + tasks, err := c.getTask() + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + + task := tasks[userTask.TaskID] + if task.Reward == 0 { + fmt.Printf("usertask %v reward is 0, skip\n", userTask) + return nil + } + if err = c.updateBalanceRaw(tx, &types.UserQueryOpts{UID: userTask.UserUID}, task.Reward, false, true, true); err != nil { + return fmt.Errorf("failed to update balance: %w", err) + } + msg := fmt.Sprintf("task %s reward", task.Title) + transaction := types.AccountTransaction{ + Balance: task.Reward, + Type: string(task.TaskType) + "_Reward", + UserUID: userTask.UserUID, + ID: uuid.New(), + Message: &msg, + BillingID: userTask.ID, + } + if err = tx.Create(&transaction).Error; err != nil { + return fmt.Errorf("failed to save transaction: %w", err) + } + if err = tx.Model(&userTask).Update("rewardStatus", types.TaskStatusCompleted).Error; err != nil { + return fmt.Errorf("failed to update user task status: %w", err) + } + return nil + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + break + } + if err != nil { + return err + } + } + return nil +} + +func (c *Cockroach) getTask() (map[uuid.UUID]types.Task, error) { + if len(c.tasks) != 0 { + return c.tasks, nil + } + c.tasks = make(map[uuid.UUID]types.Task) + var tasks []types.Task + if err := c.DB.Model(&types.Task{IsActive: true, IsNewUserTask: true}).Find(&tasks).Error; err != nil { + return nil, fmt.Errorf("failed to get tasks: %w", err) + } + for i := range tasks { + c.tasks[tasks[i].ID] = tasks[i] + } + return c.tasks, nil +} + +func (c *Cockroach) GetUserCr(ops *types.UserQueryOpts) (*types.RegionUserCr, error) { + if ops.UID == uuid.Nil && ops.Owner == "" { + if ops.ID == "" { + return nil, errors.New("empty query opts") + } + userUID, err := c.getUserUIDByID(ops.ID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + ops.UID = userUID + } + query := &types.RegionUserCr{ + CrName: ops.Owner, + } + if ops.UID != uuid.Nil { + query.UserUID = ops.UID + } + var userCr types.RegionUserCr + if err := c.Localdb.Where(query).First(&userCr).Error; err != nil { + return nil, err + } + return &userCr, nil +} + +func (c *Cockroach) GetAccountWithWorkspace(workspace string) (*types.Account, error) { + if workspace == "" { + return nil, errors.New("empty workspace") + } + var userUIDString string + err := c.Localdb.Table("Workspace"). + Select(`"UserCr"."userUid"`). + Joins(`JOIN "UserWorkspace" ON "Workspace".uid = "UserWorkspace"."workspaceUid"`). + Joins(`JOIN "UserCr" ON "UserWorkspace"."userCrUid" = "UserCr".uid`). + Where(`"Workspace".id = ?`, workspace). + Where(`"UserWorkspace".role = ?`, "OWNER"). + Limit(1). + Scan(&userUIDString).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("not found user uid with workspace %s", workspace) + } + return nil, fmt.Errorf("failed to get user uid with workspace %s: %w", workspace, err) + } + if userUIDString == "" { + return nil, fmt.Errorf("empty user uid with workspace %s", workspace) + } + + userUID, err := uuid.Parse(userUIDString) + if err != nil { + return nil, fmt.Errorf("failed to parse user uid %s: %w", userUIDString, err) + } + if userUID == uuid.Nil { + return nil, errors.New("empty user uid") + } + + var account types.Account + err = c.DB.Where(&types.Account{UserUID: userUID}).First(&account).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("not found account with user uid %s", userUID) + } + return nil, fmt.Errorf("failed to get account with user uid %s: %w", userUID, err) + } + return &account, nil +} + +func (c *Cockroach) GetUserUID(ops *types.UserQueryOpts) (uid uuid.UUID, err error) { + if ops.UID != uuid.Nil { + return ops.UID, nil + } + if ops.ID == "" && ops.Owner == "" { + return uuid.Nil, errors.New("empty query opts") + } + // data in the cache owner is preferred + if ops.Owner != "" { + uid, err = c.getUserUIDByOwner(ops.Owner) + } else { + uid, err = c.getUserUIDByID(ops.ID) + } + if err != nil { + if ops.IgnoreEmpty && errors.Is(err, gorm.ErrRecordNotFound) { + return uuid.Nil, nil + } + return uuid.Nil, err + } + if uid == uuid.Nil && !ops.IgnoreEmpty { + return uuid.Nil, errors.New("failed to get userUID: record not found") + } + ops.UID = uid + return uid, nil +} + +func (c *Cockroach) GetUserID(ops *types.UserQueryOpts) (id string, err error) { + if ops.ID != "" { + return ops.ID, nil + } + if ops.Owner == "" && ops.UID == uuid.Nil { + return "", errors.New("empty query opts") + } + if ops.Owner != "" { + id, err = c.getUserIDByOwner(ops.Owner, !ops.WithOutCache) + if ops.IgnoreEmpty && errors.Is(err, gorm.ErrRecordNotFound) { + return "", nil + } + return id, err + } + id, err = c.getUserIDByUID(ops.UID) + if err != nil { + return "", fmt.Errorf("failed to get userID: %w", err) + } + if !ops.IgnoreEmpty && id == "" { + return "", errors.New("user record not found") + } + return id, nil +} + +func (c *Cockroach) getUserIDByUID(uid uuid.UUID) (string, error) { + var user struct { + ID string `gorm:"id"` + } + err := c.DB.Table("User").Select("id").Where("uid = ?", uid).First(&user).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return "", err + } + return user.ID, nil +} + +func (c *Cockroach) getUserUIDByID(id string) (uuid.UUID, error) { + var user struct { + UID uuid.UUID `gorm:"uid,type:uuid"` + } + err := c.DB.Table("User").Select("uid").Where("id = ?", id).First(&user).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return uuid.Nil, fmt.Errorf("failed to get userUIDByID: %w", err) + } + return user.UID, nil +} + +// cache user owner uid mapping +func (c *Cockroach) getUserUIDByOwnerWithCache(owner string) (uuid.UUID, error) { + if v, ok := c.ownerUsrUIDMap.Load(owner); ok { + userUID, ok := v.(uuid.UUID) + if !ok { + return uuid.Nil, fmt.Errorf("failed to assert userUID type: %T", v) + } + return userUID, nil + } + userUID, err := c.getUserUIDByOwner(owner) + if err != nil { + return uuid.Nil, err + } + c.ownerUsrUIDMap.Store(owner, userUID) + return userUID, nil +} + +func (c *Cockroach) getUserUIDByOwner(owner string) (uuid.UUID, error) { + var user struct { + UID uuid.UUID `gorm:"column:userUid;type:uuid"` + } + err := c.Localdb.Table(`"UserCr"`). + Select(`"userUid"`). + Where(`"crName" = ?`, owner). + First(&user). + Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return uuid.Nil, err + } + return uuid.Nil, fmt.Errorf("failed to get userUID: %w", err) + } + return user.UID, nil +} + +func (c *Cockroach) getUserIDByOwner(owner string, withCache bool) (string, error) { + if withCache { + if v, ok := c.ownerUsrIDMap.Load(owner); ok { + userID, ok := v.(string) + if !ok { + return "", fmt.Errorf("failed to assert userID type: %T", v) + } + return userID, nil + } + } + var ( + userUID uuid.UUID + err error + ) + if withCache { + userUID, err = c.getUserUIDByOwnerWithCache(owner) + } else { + userUID, err = c.getUserUIDByOwner(owner) + } + if err != nil { + return "", err + } + userID, err := c.getUserIDByUID(userUID) + if err != nil { + return "", err + } + c.ownerUsrIDMap.Store(owner, userID) + return userID, nil +} + +func (c *Cockroach) GetWorkspace(namespaces ...string) ([]types.Workspace, error) { + if len(namespaces) == 0 { + return nil, errors.New("empty namespaces") + } + var workspaces []types.Workspace + if err := c.Localdb.Where("id IN ?", namespaces).Find(&workspaces).Error; err != nil { + return nil, fmt.Errorf("failed to get workspaces: %w", err) + } + return workspaces, nil +} + +func checkOps(ops *types.UserQueryOpts) error { + if ops.Owner == "" && ops.UID == uuid.Nil && ops.ID == "" { + return errors.New("empty query opts") + } + return nil +} + +func (c *Cockroach) GetAccount(ops *types.UserQueryOpts) (*types.Account, error) { + return c.getAccount(ops) +} + +func (c *Cockroach) GetAccountWithCredits( + userUID uuid.UUID, +) (*types.UsableBalanceWithCredits, error) { + ctx := context.Background() + result := &types.UsableBalanceWithCredits{ + UserUID: userUID, + } + err := c.DB.WithContext(ctx).Raw(` + SELECT + a.balance, + a.deduction_balance, + a.create_region_id, + COALESCE(( + SELECT SUM(c.amount - c.used_amount) + FROM "Credits" c + WHERE c.user_uid = a."userUid" + AND c.status = 'active' + AND (c.expire_at IS NULL OR c.expire_at > CURRENT_TIMESTAMP) + AND (c.start_at IS NULL OR c.start_at <= CURRENT_TIMESTAMP) + ), 0) as usable_credits + FROM "Account" a + WHERE a."userUid" = ? + LIMIT 1 + `, userUID).Scan(result).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err // Return gorm.ErrRecordNotFound values if no account found + } + return nil, fmt.Errorf("failed to query account with credits: %w", err) + } + return result, nil +} + +func (c *Cockroach) GetAvailableCredits(ops *types.UserQueryOpts) ([]types.Credits, error) { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + var credits []types.Credits + err = c.DB.Model(&types.Credits{}).Where( + `user_uid = ? + AND (expire_at IS NULL OR expire_at > CURRENT_TIMESTAMP) + AND (start_at IS NULL OR start_at <= CURRENT_TIMESTAMP) + AND status != 'expired'`, userUID).Find(&credits).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("failed to get credits: %w", err) + } + return credits, nil +} + +func (c *Cockroach) SetAccountCreateLocalRegion(account *types.Account, region string) error { + account.CreateRegionID = region + return c.DB.Save(account).Error +} + +func (c *Cockroach) GetTransfer(ops *types.GetTransfersReq) (*types.GetTransfersResp, error) { + if ops.ID == "" { + user, err := c.GetUser(ops.UserQueryOpts) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = user.UID + ops.ID = user.ID + } + page, pageSize := ops.Page, ops.PageSize + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 10 + } + + start, end := ops.StartTime, ops.EndTime + if end.IsZero() { + end = time.Now().UTC() + } + + fmt.Printf("start: %v, end: %v\n", start, end) + var ( + transfers []types.Transfer + count int64 + ) + + err := c.performTransferQuery(ops, pageSize, (page-1)*pageSize, start, end, &transfers, &count) + if err != nil { + return nil, err + } + + resp := types.GetTransfersResp{ + Transfers: transfers, + LimitResp: types.LimitResp{ + Total: count, + TotalPage: (count + int64(pageSize) - 1) / int64(pageSize), + }, + } + return &resp, nil +} + +func (c *Cockroach) performTransferQuery( + ops *types.GetTransfersReq, + limit, offset int, + start, end time.Time, + transfers *[]types.Transfer, + count *int64, +) error { + var err error + query := c.DB.Model(&types.Transfer{}).Limit(limit).Offset(offset). + Where("created_at BETWEEN ? AND ?", start, end) + countQuery := c.DB.Model(&types.Transfer{}). + Where("created_at BETWEEN ? AND ?", start, end) + + userCondition := "1 = 1" + args := []any{} + if ops.TransferID != "" { + query = c.DB.Where(types.Transfer{ID: ops.TransferID}) + } else { + switch ops.Type { + case types.TypeTransferIn: + userCondition = `"toUserUid" = ? OR "toUserId" = ?` + args = append(args, ops.UID, ops.ID) + case types.TypeTransferOut: + userCondition = `"fromUserUid" = ? OR "fromUserId" = ?` + args = append(args, ops.UID, ops.ID) + default: + userCondition = `"fromUserUid" = ? OR "fromUserId" = ? OR "toUserUid" = ? OR "toUserId" = ?` + args = append(args, ops.UID, ops.ID, ops.UID, ops.ID) + } + } + + query = query.Where(userCondition, args...) + countQuery = countQuery.Where(userCondition, args...) + + query = query.Order("created_at DESC") + err = query.Find(transfers).Error + if err != nil { + return fmt.Errorf("failed to get transfer: %w", err) + } + + if ops.TransferID == "" { + err = countQuery.Count(count).Error + if err != nil { + return fmt.Errorf("failed to get transfer count: %w", err) + } + } else { + *count = 1 + } + + return nil +} + +func (c *Cockroach) getAccount(ops *types.UserQueryOpts) (*types.Account, error) { + var err error + if ops.UID == uuid.Nil { + ops.UID, err = c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + if ops.UID == uuid.Nil { + return nil, fmt.Errorf("user record not found with query opts: %v", ops) + } + } + var account types.Account + if err := c.DB.Where(types.Account{UserUID: ops.UID}).First(&account).Error; err != nil { + if ops.IgnoreEmpty && errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &account, nil +} + +func (c *Cockroach) GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.OauthProvider, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = userUID + } + var provider []types.OauthProvider + if err := c.DB.Where(types.OauthProvider{UserUID: ops.UID}).Find(&provider).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, fmt.Errorf("failed to get user oauth provider: %w", err) + } + + // Create a map to track existing providerType+providerID combinations + existingProviders := make(map[string]bool) + for _, p := range provider { + key := string(p.ProviderType) + "_" + p.ProviderID + existingProviders[key] = true + } + + // Query UserAlertNotificationAccount records that are enabled + var alertNotificationAccounts []types.UserAlertNotificationAccount + if err := c.DB.Where(types.UserAlertNotificationAccount{UserUID: ops.UID, IsEnabled: true}).Find(&alertNotificationAccounts).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return provider, nil + } + return nil, fmt.Errorf("failed to get user alert notification accounts: %w", err) + } + + // Convert UserAlertNotificationAccount to OauthProvider and append non-duplicates + for _, account := range alertNotificationAccounts { + // Only include EMAIL and PHONE provider types + if account.ProviderType != types.OauthProviderTypeEmail && + account.ProviderType != types.OauthProviderTypePhone { + continue + } + + key := string(account.ProviderType) + "_" + account.ProviderID + if !existingProviders[key] { + // Convert UserAlertNotificationAccount to OauthProvider + newProvider := types.OauthProvider{ + UserUID: account.UserUID, + ProviderType: account.ProviderType, + ProviderID: account.ProviderID, + CreatedAt: account.CreatedAt, + UpdatedAt: account.UpdatedAt, + } + provider = append(provider, newProvider) + existingProviders[key] = true + } + } + + return provider, nil +} + +func (c *Cockroach) AddDeductionBalanceWithCredits( + ops *types.UserQueryOpts, + deductionAmount int64, + orderIDs []string, +) error { + return c.AddDeductionBalanceWithCreditsAt( + ops, deductionAmount, orderIDs, time.Now().UTC(), + ) +} + +func (c *Cockroach) AddDeductionBalanceWithCreditsAt( + ops *types.UserQueryOpts, + deductionAmount int64, + _ []string, + at time.Time, +) error { + if at.IsZero() { + at = time.Now().UTC() + } + err := RetryTransaction(3, 2*time.Second, c.DB, func(tx *gorm.DB) error { + remainingAmount := deductionAmount + userUID, dErr := c.GetUserUID(ops) + if dErr != nil { + return fmt.Errorf("failed to get user uid: %w", dErr) + } + var credits []types.Credits + if dErr = tx.Where( + "user_uid = ? AND start_at <= ? AND expire_at > ? AND status = ?", + userUID, + at, + at, + types.CreditsStatusActive, + ).Order("expire_at ASC").Find(&credits).Error; dErr != nil { + return fmt.Errorf("failed to get credits: %w", dErr) + } + now := time.Now().UTC() + var updateCredits []types.Credits + // var creditTransactions []types.CreditsTransaction + for i := range credits { + creditAmt := credits[i].Amount - credits[i].UsedAmount + if creditAmt > 0 && remainingAmount > 0 { + var usedAmount int64 + if creditAmt > remainingAmount { + credits[i].UsedAmount += remainingAmount + usedAmount = remainingAmount + } else { + credits[i].UsedAmount = credits[i].Amount + credits[i].Status = types.CreditsStatusUsedUp + usedAmount = creditAmt + } + remainingAmount -= usedAmount + // creditTransactions = append(creditTransactions, types.CreditsTransaction{ + // ID: uuid.New(), + // UserUID: userUID, + // RegionUID: c.LocalRegion.UID, + // AccountTransactionID: &accountTransactionID, + // CreditsID: credits[i].ID, + // UsedAmount: usedAmount, + // CreatedAt: now, + // Reason: types.CreditsRecordReasonResourceAccountTransaction, + // }) + credits[i].UpdatedAt = now + updateCredits = append(updateCredits, credits[i]) + } + } + if len(updateCredits) > 0 { + for _, credit := range updateCredits { + if dErr = tx.Save(&credit).Error; dErr != nil { + return fmt.Errorf("failed to update credits: %w", dErr) + } + } + } + if remainingAmount > 0 { + if dErr = c.updateBalance(tx, ops, remainingAmount, true, true); dErr != nil { + return fmt.Errorf("failed to update balance: %w", dErr) + } + } + // if len(creditTransactions) > 0 { + // if dErr = tx.Create(&creditTransactions).Error; dErr != nil { + // return fmt.Errorf("failed to create credit transactions: %v", dErr) + // } + //} + return nil + }) + return err +} + +func RetryTransaction( + retryCount int, + interval time.Duration, + db *gorm.DB, + f func(tx *gorm.DB) error, +) error { + var err error + for i := range retryCount { + err = db.Transaction(f) + if err == nil { + return nil + } + logrus.Errorf("failed to execute transaction: %v, retrying %d", err, i+1) + time.Sleep(interval) + } + return err +} + +func (c *Cockroach) CreateCredits(credits *types.Credits) error { + return c.DB.Create(credits).Error +} + +func CreateCredits(db *gorm.DB, credits *types.Credits) error { + if credits.ID == uuid.Nil { + credits.ID = uuid.New() + } + if credits.CreatedAt.IsZero() { + credits.CreatedAt = time.Now() + } + if credits.ExpireAt.IsZero() { + credits.ExpireAt = time.Now().AddDate(0, 1, 0) + } + if credits.Status == "" { + credits.Status = types.CreditsStatusActive + } + return db.Create(credits).Error +} + +func (c *Cockroach) updateBalance( + tx *gorm.DB, + ops *types.UserQueryOpts, + amount int64, + isDeduction, add bool, +) error { + return c.updateBalanceRaw(tx, ops, amount, isDeduction, add, false) +} + +func (c *Cockroach) updateBalanceRaw( + tx *gorm.DB, + ops *types.UserQueryOpts, + amount int64, + isDeduction, add, isActive bool, +) error { + if amount == 0 { + return nil + } + userUID, err := c.GetUserUID(ops) + if err != nil { + return fmt.Errorf("failed to get user uid: %w", err) + } + return c.updateWithAccount(userUID, isDeduction, add, isActive, amount, tx) +} + +func AddDeductionAccount(tx *gorm.DB, userUID uuid.UUID, amount int64) error { + return tx.Model(&types.Account{}).Where(`"userUid" = ?`, userUID).Updates(map[string]any{ + `"deduction_balance"`: gorm.Expr("deduction_balance + ?", amount), + }).Error +} + +func (c *Cockroach) updateWithAccount( + userUID uuid.UUID, + isDeduction, add, isActive bool, + amount int64, + db *gorm.DB, +) error { + exprs := map[string]any{} + control := "-" + if add { + control = "+" + } + if isDeduction { + exprs["deduction_balance"] = gorm.Expr("deduction_balance "+control+" ?", amount) + } else { + exprs["balance"] = gorm.Expr("balance "+control+" ?", amount) + } + if isActive { + exprs[`"activityBonus"`] = gorm.Expr(`"activityBonus" + ?`, amount) + } + exprs["updated_at"] = gorm.Expr("CURRENT_TIMESTAMP") + result := db.Model(&types.Account{}).Where(`"userUid" = ?`, userUID).Updates(exprs) + return HandleUpdateResult(result, types.Account{}.TableName()) +} + +func (c *Cockroach) UpdateWithAccount( + userUID uuid.UUID, + isDeduction, add, isActive bool, + amount int64, + db *gorm.DB, +) error { + return c.updateWithAccount(userUID, isDeduction, add, isActive, amount, db) +} + +func HandleUpdateResult(result *gorm.DB, entityName string) error { + if result.Error != nil { + return fmt.Errorf("failed to update %s: %w", entityName, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("no %s updated", entityName) + } + return nil +} + +func (c *Cockroach) AddBalance(ops *types.UserQueryOpts, amount int64) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, true) + }) +} + +func (c *Cockroach) AddRewardBalance(ops *types.UserQueryOpts, amount int64, db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, true) + }) +} + +func (c *Cockroach) ReduceBalance(ops *types.UserQueryOpts, amount int64) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, false) + }) +} + +func (c *Cockroach) ReduceDeductionBalance(ops *types.UserQueryOpts, amount int64) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, false) + }) +} + +func (c *Cockroach) AddDeductionBalance(ops *types.UserQueryOpts, amount int64) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, true, true) + }) +} + +func (c *Cockroach) AddDeductionBalanceWithDB( + ops *types.UserQueryOpts, + amount int64, + tx *gorm.DB, +) error { + return c.updateBalance(tx, ops, amount, true, true) +} + +func (c *Cockroach) AddDeductionBalanceWithFunc( + ops *types.UserQueryOpts, + amount int64, + preDo, postDo func() error, +) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + if err := preDo(); err != nil { + return err + } + if err := c.updateBalance(tx, ops, amount, true, true); err != nil { + return err + } + return postDo() + }) +} + +func (c *Cockroach) CreateAccount( + ops *types.UserQueryOpts, + account *types.Account, +) (*types.Account, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + ops.UID = userUID + } + account.UserUID = ops.UID + + if err := c.DB.FirstOrCreate(account).Error; err != nil { + return nil, fmt.Errorf("failed to create account: %w", err) + } + + return account, nil +} + +func (c *Cockroach) PaymentWithFunc( + payment *types.Payment, + preDo, postDo func(tx *gorm.DB) error, +) error { + return c.paymentWithFunc(payment, true, preDo, postDo) +} + +func (c *Cockroach) Payment(payment *types.Payment) error { + return c.payment(payment, true) +} + +func (c *Cockroach) SavePayment(payment *types.Payment) error { + return c.payment(payment, false) +} + +func (c *Cockroach) payment(payment *types.Payment, updateBalance bool) error { + return c.paymentWithFunc(payment, updateBalance, nil, nil) +} + +func (c *Cockroach) paymentWithFunc( + payment *types.Payment, + updateBalance bool, + preDo, postDo func(db *gorm.DB) error, +) error { + if payment.ID == "" { + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("failed to generate payment id: %w", err) + } + payment.ID = id + } + if payment.CreatedAt.IsZero() { + payment.CreatedAt = time.Now() + } + if payment.RegionUID == uuid.Nil { + payment.RegionUID = c.LocalRegion.UID + } + if payment.UserUID == uuid.Nil { + if payment.RegionUserOwner == "" { + return errors.New("empty payment owner and user") + } + userUID, err := c.getUserUIDByOwnerWithCache(payment.RegionUserOwner) + if err != nil { + return fmt.Errorf("failed to get user uid: %w", err) + } + payment.UserUID = userUID + } + + return c.DB.Transaction(func(tx *gorm.DB) error { + if preDo != nil { + if err := preDo(tx); err != nil { + return fmt.Errorf("failed to preDo: %w", err) + } + } + if err := tx.First(&types.Payment{ID: payment.ID}).Error; err == nil { + return nil + } + if err := tx.Create(payment).Error; err != nil { + return fmt.Errorf("failed to save payment: %w", err) + } + if updateBalance { + if err := c.updateBalance(tx, &types.UserQueryOpts{UID: payment.UserUID}, payment.Amount+payment.Gift, false, true); err != nil { + return fmt.Errorf("failed to add balance: %w", err) + } + } + if postDo != nil { + if err := postDo(tx); err != nil { + return fmt.Errorf("failed to postDo: %w", err) + } + } + return nil + }) +} + +func (c *Cockroach) GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error { + return GlobalTransactionHandler(c.DB, funcs...) +} + +func GlobalTransactionHandler(db *gorm.DB, funcs ...func(tx *gorm.DB) error) error { + return db.Transaction(func(tx *gorm.DB) error { + for _, f := range funcs { + if err := f(tx); err != nil { + return err + } + } + return nil + }) +} + +func (c *Cockroach) GetRegions() ([]types.Region, error) { + var regions []types.Region + if err := c.DB.Find(®ions).Error; err != nil { + return nil, fmt.Errorf("failed to get regions: %w", err) + } + return regions, nil +} + +func (c *Cockroach) GetLocalRegion() types.Region { + if c.LocalRegion.Domain == "" { + regions, err := c.GetRegions() + if err == nil { + for i := range regions { + if regions[i].UID == c.LocalRegion.UID { + c.LocalRegion = ®ions[i] + return *c.LocalRegion + } + } + } + } + return *c.LocalRegion +} + +func (c *Cockroach) GetPayment( + ops *types.UserQueryOpts, + startTime, endTime time.Time, +) ([]types.Payment, error) { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + var payment []types.Payment + if startTime != endTime { + if err := c.DB.Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUID}}).Where("created_at >= ? AND created_at <= ?", startTime, endTime).Find(&payment).Error; err != nil { + return nil, fmt.Errorf("failed to get payment: %w", err) + } + } else { + if err := c.DB.Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUID}}).Find(&payment).Error; err != nil { + return nil, fmt.Errorf("failed to get payment: %w", err) + } + } + return payment, nil +} + +func (c *Cockroach) GetPaymentWithID(paymentID string) (*types.Payment, error) { + var payment types.Payment + if err := c.DB.Where(types.Payment{ID: paymentID}).First(&payment).Error; err != nil { + return nil, fmt.Errorf("failed to get payment: %w", err) + } + return &payment, nil +} + +func (c *Cockroach) GetPaymentOrderWithID(paymentID string) (*types.PaymentOrder, error) { + var order types.PaymentOrder + if err := c.DB.Where(types.PaymentOrder{ID: paymentID}).First(&order).Error; err != nil { + return nil, fmt.Errorf("failed to get payment order: %w", err) + } + return &order, nil +} + +func (c *Cockroach) GetPaymentWithLimit( + ops *types.UserQueryOpts, + req types.LimitReq, + invoiced *bool, +) ([]types.Payment, types.LimitResp, error) { + var payment []types.Payment + var total int64 + var limitResp types.LimitResp + page, pageSize := req.Page, req.PageSize + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, limitResp, fmt.Errorf("failed to get user uid: %w", err) + } + + queryPayment := types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUID}} + query := c.DB.Model(&types.Payment{}).Where(queryPayment) + if invoiced != nil { + query = query.Where("invoiced_at = ?", *invoiced) + } + if !req.StartTime.IsZero() { + query = query.Where("created_at >= ?", req.StartTime) + } + if !req.EndTime.IsZero() { + query = query.Where("created_at <= ?", req.EndTime) + } + if err := query.Count(&total).Error; err != nil { + return nil, limitResp, fmt.Errorf("failed to get total count: %w", err) + } + totalPage := (total + int64(pageSize) - 1) / int64(pageSize) + if err := query.Order("created_at DESC"). + Limit(pageSize). + Offset((page - 1) * pageSize). + Find(&payment).Error; err != nil { + return nil, limitResp, fmt.Errorf("failed to get payment: %w", err) + } + limitResp = types.LimitResp{ + Total: total, + TotalPage: totalPage, + } + return payment, limitResp, nil +} + +func (c *Cockroach) GetUnInvoicedPaymentListWithIDs(ids []string) ([]types.Payment, error) { + var payment []types.Payment + if err := c.DB.Where("id IN ?", ids).Where("invoiced_at = ?", false).Find(&payment).Error; err != nil { + return nil, fmt.Errorf("failed to get payment: %w", err) + } + return payment, nil +} + +func (c *Cockroach) SetPaymentInvoice(ops *types.UserQueryOpts, paymentIDList []string) error { + userUID, err := c.GetUserUID(ops) + if err != nil { + return fmt.Errorf("failed to get user uid: %w", err) + } + if err := c.DB.Model(&types.Payment{}).Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUID}}).Where("id IN ?", paymentIDList).Update("invoiced_at", true).Error; err != nil { + return fmt.Errorf("failed to save payment: %w", err) + } + return nil +} + +func (c *Cockroach) CreatePaymentOrder(order *types.PaymentOrder) error { + return CreatePaymentOrder(c.DB, order) +} + +func CreatePaymentOrder(tx *gorm.DB, order *types.PaymentOrder) error { + if order.UserUID == uuid.Nil { + return errors.New("empty user uid") + } + if order.ID == "" { + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("failed to generate payment order id: %w", err) + } + order.ID = id + } + if order.Status == "" { + order.Status = types.PaymentOrderStatusPending + } + if order.CreatedAt.IsZero() { + order.CreatedAt = time.Now() + } + if err := tx.Create(order).Error; err != nil { + return fmt.Errorf("failed to save payment order: %w", err) + } + return nil +} + +func (c *Cockroach) SetPaymentOrderStatusWithTradeNo( + status types.PaymentOrderStatus, + tradeNo string, +) error { + return SetPaymentOrderStatusWithTradeNo(c.DB, status, tradeNo) +} + +func SetPaymentOrderStatusWithTradeNo( + db *gorm.DB, + status types.PaymentOrderStatus, + tradeNo string, +) error { + return db.Model(&types.PaymentOrder{}). + Where(types.PaymentOrder{PaymentRaw: types.PaymentRaw{TradeNO: tradeNo}}). + Update("status", status). + Error +} + +func (c *Cockroach) GetPaymentOrderWithTradeNo(tradeNo string) (*types.PaymentOrder, error) { + if tradeNo == "" { + return nil, errors.New("empty trade no") + } + var order types.PaymentOrder + if err := c.DB.Model(&types.PaymentOrder{}).Where(types.PaymentOrder{PaymentRaw: types.PaymentRaw{TradeNO: tradeNo}}).Find(&order).Error; err != nil { + return nil, fmt.Errorf("failed to get payment order: %w", err) + } + return &order, nil +} + +func (c *Cockroach) GetAllCardInfo(ops *types.UserQueryOpts) ([]types.CardInfo, error) { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + var cardInfos []types.CardInfo + if err := c.DB.Where(types.CardInfo{UserUID: userUID}).Find(&cardInfos).Error; err != nil { + return nil, err + } + return cardInfos, nil +} + +func (c *Cockroach) GetSubscriptionPlanList() ([]types.SubscriptionPlan, error) { + var plans []types.SubscriptionPlan + if err := c.DB.Model(types.SubscriptionPlan{}).Find(&plans).Error; err != nil { + return nil, fmt.Errorf("failed to get subscription plan: %w", err) + } + return plans, nil +} + +func (c *Cockroach) SetSubscriptionPlanList(plans []types.SubscriptionPlan) error { + return c.DB.Create(plans).Error +} + +func (c *Cockroach) GetCardList(ops *types.UserQueryOpts) ([]types.CardInfo, error) { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + var cards []types.CardInfo + if err := c.DB.Where(types.CardInfo{UserUID: userUID}).Find(&cards).Error; err != nil { + return nil, err + } + return cards, nil +} + +func (c *Cockroach) DeleteCardInfo(id, userUID uuid.UUID) error { + if userUID == uuid.Nil || id == uuid.Nil { + return errors.New("empty user uid or card id") + } + var card types.CardInfo + if err := c.DB.Where(types.CardInfo{ID: id, UserUID: userUID}).First(&card).Error; err != nil { + return fmt.Errorf("failed to get card info: %w", err) + } + if card.Default { + return errors.New("can not delete default card") + } + return c.DB.Delete(&card).Error +} + +func (c *Cockroach) SetDefaultCard(cardID, userUID uuid.UUID) error { + if userUID == uuid.Nil || cardID == uuid.Nil { + return errors.New("empty user uid or card id") + } + var card types.CardInfo + if err := c.DB.Where(types.CardInfo{ID: cardID, UserUID: userUID}).First(&card).Error; err != nil { + return fmt.Errorf("failed to get card info: %w", err) + } + if card.Default { + return nil + } + if err := c.DB.Model(&types.CardInfo{}).Where(types.CardInfo{UserUID: userUID}).Update("default", false).Error; err != nil { + return fmt.Errorf("failed to update card info: %w", err) + } + return c.DB.Model(&types.CardInfo{}). + Where(types.CardInfo{ID: cardID, UserUID: userUID}). + Update("default", true). + Error +} + +func (c *Cockroach) GetSubscription(ops *types.UserQueryOpts) (*types.Subscription, error) { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + var subscription types.Subscription + if err := c.DB.Where( + "user_uid", userUID, + ).Find(&subscription).Error; err != nil { + return nil, err + } + return &subscription, nil +} + +func (c *Cockroach) CreateSubscription(subscription *types.Subscription) error { + if subscription.PlanID == uuid.Nil || subscription.PlanName == "" || + subscription.UserUID == uuid.Nil || + subscription.Status == "" { + return errors.New("empty subscription info") + } + return c.DB.Save(subscription).Error +} + +func CreateSubscriptionTransaction(db *gorm.DB, transaction *types.SubscriptionTransaction) error { + if transaction.SubscriptionID == uuid.Nil { + return errors.New("empty subscription id") + } + if transaction.CreatedAt.IsZero() { + transaction.CreatedAt = time.Now() + } + return db.Create(transaction).Error +} + +func GetActiveSubscriptionTransactionCount(db *gorm.DB, userUID uuid.UUID) (int64, error) { + nulActiveStatus := []types.SubscriptionTransactionStatus{ + types.SubscriptionTransactionStatusCompleted, + types.SubscriptionTransactionStatusFailed, + } + var count int64 + if err := db.Model(&types.SubscriptionTransaction{}).Where("user_uid = ? AND status NOT IN ?", userUID, nulActiveStatus).Count(&count).Error; err != nil { + return 0, err + } + return count, nil +} + +func (c *Cockroach) GetCardInfo(cardID, userUID uuid.UUID) (*types.CardInfo, error) { + var cardInfo types.CardInfo + if err := c.DB.Where(types.CardInfo{ID: cardID, UserUID: userUID}).First(&cardInfo).Error; err != nil { + return nil, err + } + return &cardInfo, nil +} + +func (c *Cockroach) SetCardInfo(info *types.CardInfo) (uuid.UUID, error) { + return SetCardInfo(c.DB, info) +} + +func SetCardInfo(db *gorm.DB, info *types.CardInfo) (uuid.UUID, error) { + if info.ID == uuid.Nil { + info.ID = uuid.New() + } + if info.CardToken == "" { + return uuid.Nil, errors.New("empty card token") + } + if info.CreatedAt.IsZero() { + info.CreatedAt = time.Now() + } + var count int64 + // 如果没有设置默认卡片,设置第一张卡片为默认卡片 + if err := db.Model(&types.CardInfo{}).Where(types.CardInfo{UserUID: info.UserUID}).Count(&count).Error; err != nil { + return uuid.Nil, fmt.Errorf("failed to get card count: %w", err) + } + if count == 0 { + info.Default = true + return info.ID, db.Save(info).Error + } + cardInfo := types.CardInfo{} + if err := db.Model(&types.CardInfo{}).Where(types.CardInfo{UserUID: info.UserUID, CardNo: info.CardNo, CardBrand: info.CardBrand}).First(&cardInfo).Error; err != nil { + logrus.Errorf("failed to get card info: %v", err) + if !errors.Is(err, gorm.ErrRecordNotFound) { + return uuid.Nil, fmt.Errorf("failed to get card info: %w", err) + } + logrus.Infof("card info not found, create new card info") + return info.ID, db.Save(info).Error + } + if cardInfo.CardToken != info.CardToken && info.CardToken != "" { + err := db.Model(&types.CardInfo{}). + Where(types.CardInfo{ID: cardInfo.ID}). + Update("card_token", info.CardToken). + Error + if err != nil { + return uuid.Nil, fmt.Errorf("failed to update card token: %w", err) + } + } + logrus.Infof("card info found, update card info") + return cardInfo.ID, nil +} + +func (c *Cockroach) SetPaymentInvoiceWithDB( + ops *types.UserQueryOpts, + paymentIDList []string, + db *gorm.DB, +) error { + userUID, err := c.GetUserUID(ops) + if err != nil { + return fmt.Errorf("failed to get user uid: %w", err) + } + if err := db.Model(&types.Payment{}).Where(types.Payment{PaymentRaw: types.PaymentRaw{UserUID: userUID}}).Where("id IN ?", paymentIDList).Update("invoiced_at", true).Error; err != nil { + return fmt.Errorf("failed to save payment: %w", err) + } + return nil +} + +func (c *Cockroach) CreateInvoiceWithDB(i *types.Invoice, db *gorm.DB) error { + if i.ID == "" { + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("failed to generate invoice id: %w", err) + } + i.ID = id + } + if i.CreatedAt.IsZero() { + i.CreatedAt = time.Now() + } + if err := db.Create(i).Error; err != nil { + return fmt.Errorf("failed to save invoice: %w", err) + } + return nil +} + +// create invoicePayments +func (c *Cockroach) CreateInvoicePaymentsWithDB( + invoicePayments []types.InvoicePayment, + db *gorm.DB, +) error { + if err := db.Create(invoicePayments).Error; err != nil { + return fmt.Errorf("failed to save invoice payments: %w", err) + } + return nil +} + +// GetInvoiceWithID +func (c *Cockroach) GetInvoiceWithID(invoiceID string) (*types.Invoice, error) { + var invoice types.Invoice + if err := c.DB.Where(types.Invoice{ID: invoiceID}).First(&invoice).Error; err != nil { + return nil, fmt.Errorf("failed to get invoice: %w", err) + } + return &invoice, nil +} + +func (c *Cockroach) GetInvoice( + userID string, + req types.LimitReq, +) ([]types.Invoice, types.LimitResp, error) { + var invoices []types.Invoice + var total int64 + var limitResp types.LimitResp + + query := c.DB.Model(&types.Invoice{}).Where("user_id = ?", userID) + + if !req.StartTime.IsZero() { + query = query.Where("created_at >= ?", req.StartTime) + } + if !req.EndTime.IsZero() { + query = query.Where("created_at <= ?", req.EndTime) + } + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 { + req.PageSize = 10 + } + + if err := query.Count(&total).Error; err != nil { + return nil, limitResp, fmt.Errorf("failed to get total count: %w", err) + } + + totalPage := (total + int64(req.PageSize) - 1) / int64(req.PageSize) + + if err := query.Limit(req.PageSize). + Offset((req.Page - 1) * req.PageSize). + Find(&invoices).Error; err != nil { + return nil, limitResp, fmt.Errorf("failed to get invoices: %w", err) + } + + limitResp = types.LimitResp{ + Total: total, + TotalPage: totalPage, + } + + return invoices, limitResp, nil +} + +func (c *Cockroach) GetInvoicePayments(invoiceID string) ([]types.InvoicePayment, error) { + var invoicePayments []types.InvoicePayment + query := c.DB.Model(&types.InvoicePayment{}).Where("invoice_id = ?", invoiceID) + if err := query.Find(&invoicePayments).Error; err != nil { + return nil, fmt.Errorf("failed to get invoice payments: %w", err) + } + + return invoicePayments, nil +} + +func (c *Cockroach) GetPaymentWithInvoice(invoiceID string) ([]types.Payment, error) { + invoicePayments, err := c.GetInvoicePayments(invoiceID) + if err != nil { + return nil, fmt.Errorf("failed to get invoice payments: %w", err) + } + paymentIDs := make([]string, 0, len(invoicePayments)) + for _, invoicePayment := range invoicePayments { + paymentIDs = append(paymentIDs, invoicePayment.PaymentID) + } + var payments []types.Payment + if err := c.DB.Where("id IN ?", paymentIDs).Find(&payments).Error; err != nil { + return nil, fmt.Errorf("failed to get payments: %w", err) + } + return payments, nil +} + +func (c *Cockroach) SetInvoiceStatus(ids []string, stats string) error { + if err := c.DB.Model(&types.Invoice{}).Where("id IN ?", ids).Update("status", stats).Error; err != nil { + return fmt.Errorf("failed to update invoice status: %w", err) + } + return nil +} + +// NewAccount create a new account +func (c *Cockroach) NewAccount(ops *types.UserQueryOpts) (*types.Account, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, err + } + ops.UID = userUID + } + account := &types.Account{ + UserUID: ops.UID, + CreateRegionID: c.LocalRegion.UID.String(), + Balance: c.ZeroAccount.Balance, + DeductionBalance: c.ZeroAccount.DeductionBalance, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := c.DB.FirstOrCreate(account).Error; err != nil { + return nil, fmt.Errorf("failed to create account: %w", err) + } + + return account, nil +} + +// NewAccountWithFreeSubscriptionPlan create a new account with free plan +func (c *Cockroach) NewAccountWithFreeSubscriptionPlan( + ops *types.UserQueryOpts, +) (*types.Account, error) { + if ops.UID == uuid.Nil { + userUID, err := c.GetUserUID(ops) + if err != nil { + return nil, err + } + ops.UID = userUID + } + now := time.Now().UTC() + account := &types.Account{ + UserUID: ops.UID, + Balance: 0, + DeductionBalance: 0, + CreateRegionID: c.LocalRegion.UID.String(), + CreatedAt: now, + } + // 1. create credits + // 2. create account + // 3. create subscription + err := c.DB.Transaction(func(tx *gorm.DB) error { + result := tx.Where(&types.Account{UserUID: ops.UID}).FirstOrCreate(account) + if err := result.Error; err != nil { + return fmt.Errorf("failed to create account: %w", err) + } + if result.RowsAffected == 0 { + return nil + } + freePlan, err := c.GetSubscriptionPlan(types.FreeSubscriptionPlanName) + if err != nil { + return fmt.Errorf("failed to get free plan: %w", err) + } + userInfo := &types.UserInfo{} + err = c.DB.Model(&types.UserInfo{}).Where(`"userUid" = ?`, ops.UID).Find(userInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get user info: %w", err) + } + githubDetection := true + if userInfo.Config != nil { + if userInfo.Config.Github.CreatedAt != "" { + createdAt, err := time.Parse(time.RFC3339, userInfo.Config.Github.CreatedAt) + if err != nil { + return fmt.Errorf("failed to parse github created at: %w", err) + } + if time.Since(createdAt) < 7*24*time.Hour { + githubDetection = false + } + } + } + if freePlan.GiftAmount > 0 && githubDetection { + credits := &types.Credits{ + ID: uuid.New(), + UserUID: ops.UID, + Amount: freePlan.GiftAmount, + UsedAmount: 0, + FromID: freePlan.ID.String(), + FromType: types.CreditsFromTypeSubscription, + ExpireAt: now.AddDate(0, 1, 0), + CreatedAt: now, + StartAt: now, + Status: types.CreditsStatusActive, + } + creditsCount := int64(0) + if err := c.DB.Model(&types.Credits{}).Where(&types.Credits{UserUID: ops.UID, FromID: credits.FromID, FromType: credits.FromType}).Count(&creditsCount).Error; err != nil { + return fmt.Errorf("failed to create credits: %w", err) + } + if creditsCount == 0 { + if err := tx.Create(credits).Error; err != nil { + return fmt.Errorf("failed to create credits: %w", err) + } + } + } + userSubscription := types.Subscription{ + ID: uuid.New(), + UserUID: ops.UID, + PlanID: freePlan.ID, + PlanName: freePlan.Name, + Status: types.SubscriptionStatusNormal, + StartAt: now, + ExpireAt: now.AddDate(0, 1, 0), + NextCycleDate: now.AddDate(0, 1, 0), + } + subCount := int64(0) + if err := c.DB.Model(&types.Subscription{}).Where(&types.Subscription{UserUID: ops.UID}).Count(&subCount).Error; err != nil { + return fmt.Errorf("failed to create subscription: %w", err) + } + if subCount == 0 { + if err := tx.Create(&userSubscription).Error; err != nil { + return fmt.Errorf("failed to create subscription: %w", err) + } + } + userKYC := &types.UserKYC{ + UserUID: ops.UID, + Status: types.UserKYCStatusPending, + CreatedAt: now, + UpdatedAt: now, + NextAt: now.AddDate(0, 1, 0), + } + if !githubDetection { + userKYC.Status = types.UserKYCStatusFailed + } + err = tx.Model(&types.UserKYC{}). + Where(&types.UserKYC{UserUID: ops.UID}). + FirstOrCreate(userKYC). + Error + if err != nil { + return fmt.Errorf("failed to create user kyc: %w", err) + } + return nil + }) + return account, err +} + +func (c *Cockroach) GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error) { + if planLoad, ok := c.subscriptionPlans.Load(planName); ok { + plan, ok := planLoad.(*types.SubscriptionPlan) + if !ok { + return nil, fmt.Errorf("failed to assert subscription plan: %v", planLoad) + } + return plan, nil + } + var plan types.SubscriptionPlan + if err := c.DB.Where(types.SubscriptionPlan{Name: planName}).Find(&plan).Error; err != nil { + return nil, fmt.Errorf("failed to get subscription plan: %w", err) + } + c.subscriptionPlans.Store(planName, &plan) + return &plan, nil +} + +const ( + BaseUnit = 1_000_000 + MinBalance = 10 * BaseUnit + DefaultBaseBalance = 5 * BaseUnit +) + +var BaseBalance = int64(DefaultBaseBalance) + +func (c *Cockroach) TransferAccount(from, to *types.UserQueryOpts, amount int64) error { + return c.transferAccount(from, to, amount, false) +} + +func (c *Cockroach) TransferAccountAll(from, to *types.UserQueryOpts) error { + return c.transferAccount(from, to, 0, true) +} + +var ErrInsufficientBalance = errors.New("insufficient balance") + +func (c *Cockroach) transferAccount( + from, to *types.UserQueryOpts, + amount int64, + transferAll bool, +) (err error) { + if from.UID == uuid.Nil || from.ID == "" { + userFrom, err := c.GetUser(from) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + from.UID = userFrom.UID + from.ID = userFrom.ID + } + if to.UID == uuid.Nil || to.ID == "" { + userTo, err := c.GetUser(to) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + to.UID = userTo.UID + to.ID = userTo.ID + } + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("failed to generate transfer id: %w", err) + } + err = c.DB.Transaction(func(tx *gorm.DB) error { + sender, err := c.GetAccount(&types.UserQueryOpts{UID: from.UID}) + if err != nil { + return fmt.Errorf("failed to get sender account: %w", err) + } + if !transferAll { + if sender.Balance < sender.DeductionBalance+amount+MinBalance+sender.ActivityBonus { + return fmt.Errorf( + "insufficient balance in sender account, sender is %v, transfer amount %d, the transferable amount is: %d", + sender, + amount, + sender.Balance-sender.DeductionBalance-MinBalance-sender.ActivityBonus, + ) + } + } else { + amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance - sender.ActivityBonus + if amount <= 0 { + return ErrInsufficientBalance + } + } + + if err = c.updateBalance(tx, &types.UserQueryOpts{UID: from.UID}, -amount, false, true); err != nil { + return fmt.Errorf("failed to update sender balance: %w", err) + } + if err = c.updateBalance(tx, &types.UserQueryOpts{UID: to.UID}, amount, false, true); err != nil { + return fmt.Errorf("failed to update receiver balance: %w", err) + } + if err = c.DB.Create(&types.Transfer{ + ID: id, + FromUserUID: from.UID, + FromUserID: from.ID, + ToUserUID: to.UID, + ToUserID: to.ID, + Amount: amount, + }).Error; err != nil { + return fmt.Errorf("failed to create transfer record: %w", err) + } + return nil + }) + + return err +} + +func (c *Cockroach) createTables() error { + if err := ensurePostgresExtensions(c.DB); err != nil { + return err + } + if err := ensurePostgresExtensions(c.Localdb); err != nil { + return err + } + + err := CreateTableIfNotExist( + c.DB, + types.Account{}, + types.AccountTransaction{}, + types.Payment{}, + types.Transfer{}, + types.Region{}, + types.Invoice{}, + types.InvoicePayment{}, + types.Configs{}, + types.Credits{}, + types.CreditsTransaction{}, + types.CardInfo{}, + types.PaymentOrder{}, + types.PaymentRefund{}, + types.Corporate{}, + types.SubscriptionPlan{}, + types.Subscription{}, + types.SubscriptionTransaction{}, + types.AccountRegionUserTask{}, + types.UserKYC{}, + types.RegionConfig{}, + types.Debt{}, + types.DebtStatusRecord{}, + types.DebtResumeDeductionBalanceTransaction{}, + types.UserTimeRangeTraffic{}, + types.WorkspaceTraffic{}, + types.WorkspaceAIQuotaPackage{}, + types.WorkspaceSubscription{}, + types.WorkspaceSubscriptionTransaction{}, + types.WorkspaceSubscriptionPlan{}, + types.ProductPrice{}, + types.UserAlertNotificationAccount{}, + ) + if err != nil { + return fmt.Errorf("failed to create table: %w", err) + } + err = CreateTableIfNotExist(c.Localdb, types.WorkspaceSubscriptionPlan{}, types.ProductPrice{}) + if err != nil { + return fmt.Errorf("failed to create table in local db: %w", err) + } + if err := ensureLegacyUUIDDefaults(c); err != nil { + return fmt.Errorf("failed to repair legacy uuid defaults: %w", err) + } + return nil +} + +func (c *Cockroach) InitTables() error { + createEnumSQL := createEnumTypeSQLForPostgres + if isCockroachDatabase(c.DB) { + createEnumSQL = createEnumTypeSQL + } + + enumTypes := []string{ + createEnumSQL( + "subscription_status", + []string{ + "NORMAL", + "PAUSED", + "DEBT", + "DEBT_PRE_DELETION", + "DEBT_FINAL_DELETION", + "DELETED", + }, + ), + createEnumSQL( + "subscription_operator", + []string{"created", "upgraded", "downgraded", "canceled", "renewed", "deleted"}, + ), + createEnumSQL( + "subscription_pay_status", + []string{"pending", "unpaid", "paid", "no_need", "failed", "expired", "canceled"}, + ), + createEnumSQL( + "workspace_traffic_status", + []string{"active", "exhausted", "used_up", "expired"}, + ), + createEnumSQL( + "subscription_transaction_status", + []string{"completed", "pending", "processing", "failed", "canceled"}, + ), + } + // subscription_pay_status 如果不存在canceled 状态,则添加 + enumTypes = append( + enumTypes, + alterEnumAddValueSQL("subscription_pay_status", "canceled"), + ) + enumTypes = append( + enumTypes, + alterEnumAddValueSQL("subscription_pay_status", "unpaid"), + ) + enumTypes = append( + enumTypes, + alterEnumAddValueSQL("subscription_transaction_status", "canceled"), + alterEnumAddValueSQL("subscription_operator", "resumed"), + ) + for _, query := range enumTypes { + err := c.DB.Exec(query).Error + if err != nil { + return fmt.Errorf("failed to create exec : %w", err) + } + } + if err := c.createTables(); err != nil { + return err + } + err := c.DB.Exec(` + CREATE INDEX IF NOT EXISTS idx_pending_transactions ON "WorkspaceSubscriptionTransaction" (pay_status, start_at, status, region_domain);`).Error + if err != nil { + return fmt.Errorf("failed to create index on WorkspaceSubscriptionTransaction: %w", err) + } + if err := ensureBillingQueryIndexes(c.DB); err != nil { + return err + } + + // TODO: remove this after migration + if err := c.migrateColumns(); err != nil { + return fmt.Errorf("failed to migrate columns: %w", err) + } + return nil +} + +// ensureBillingQueryIndexes covers the billing queries that operate on +// growing historical tables. Equality columns lead each index so a billing +// request can avoid scanning rows belonging to other users or workspaces. +func ensureBillingQueryIndexes(db *gorm.DB) error { + statements := []struct { + name string + sql string + }{ + { + name: "DebtStatusRecord first status", + sql: `CREATE INDEX IF NOT EXISTS idx_debt_record_user_time + ON "DebtStatusRecord" (user_uid, create_at, id, last_status);`, + }, + { + name: "WorkspaceSubscriptionTransaction history", + sql: `CREATE INDEX IF NOT EXISTS idx_workspace_subscription_billing_history + ON "WorkspaceSubscriptionTransaction" (region_domain, workspace, status, updated_at);`, + }, + { + name: "Credits active period", + sql: `CREATE INDEX IF NOT EXISTS idx_credits_active_period + ON "Credits" (user_uid, status, expire_at, start_at);`, + }, + } + for _, statement := range statements { + if err := db.Exec(statement.sql).Error; err != nil { + return fmt.Errorf("failed to create billing history index on %s: %w", statement.name, err) + } + } + return nil +} + +func (c *Cockroach) migratorPaymentRefundTable() error { + // If the id field does not exist or exists but is not the primary key and is not not null, skip the migration + if !c.DB.Migrator().HasConstraint(&types.PaymentRefund{}, "PaymentRefund_pkey") { + fmt.Println("PaymentRefund.id is not primary key, skipping migration") + return nil + } + + // Migrate the PaymentRefund table: Rename the id field to order_id and remove the unique constraint + if c.DB.Migrator().HasColumn(&types.PaymentRefund{}, "id") { + fmt.Println("check PaymentRefund table for id -> order_id migration") + + // check if the order_id field already exists + if !c.DB.Migrator().HasColumn(&types.PaymentRefund{}, "order_id") { + fmt.Println("migrate PaymentRefund: add order_id column") + err := c.DB.Exec(` + ALTER TABLE "PaymentRefund" ADD COLUMN "order_id" TEXT; + `).Error + if err != nil { + return fmt.Errorf("failed to add order_id column: %w", err) + } + + // copy the value of the id field to order_id + fmt.Println("copy id values to order_id") + err = c.DB.Exec(` + UPDATE "PaymentRefund" SET "order_id" = "id" WHERE "id" IS NOT NULL; + `).Error + if err != nil { + return fmt.Errorf("failed to copy id to order_id: %w", err) + } + } + // add the rowid column (if the table still has a primary key) + fmt.Println("add rowid column if not exists") + err := c.DB.Exec(` + ALTER TABLE "PaymentRefund" ADD COLUMN IF NOT EXISTS "rowid" UUID DEFAULT gen_random_uuid() NOT NULL; + `).Error + if err != nil { + return fmt.Errorf("failed to add rowid column: %w", err) + } + + // Remove the unique constraint of the id field (if it exists) and add rowid as an alternative constraint + // (https://go.crdb.dev/issue-v/48026/v23.1) + fmt.Println("migrate PaymentRefund primary key from id to rowid") + tranDB := c.DB.Begin() + // To modify the primary key within the same transaction: delete the old primary key and add a new one + fmt.Println("modify primary key from id to rowid") + err = tranDB.Exec(` + ALTER TABLE "PaymentRefund" + DROP CONSTRAINT "PaymentRefund_pkey", + ADD CONSTRAINT "PaymentRefund_pkey" PRIMARY KEY ("rowid"); + `).Error + if err != nil { + tranDB.Rollback() + return fmt.Errorf("failed to modify primary key constraint: %w", err) + } + err = tranDB.Commit().Error + if err != nil { + return fmt.Errorf("failed to commit transaction for PaymentRefund migration: %w", err) + } + // remove the id not null constraint + fmt.Println("drop not null constraint on id column") + err = c.DB.Exec(` + ALTER TABLE "PaymentRefund" ALTER COLUMN "id" DROP NOT NULL; + `).Error + if err != nil { + return fmt.Errorf("failed to drop not null constraint on id: %w", err) + } + fmt.Println("PaymentRefund migration completed: id -> order_id with uniqueness removed") + } + return nil +} + +func (c *Cockroach) migrateColumns() error { + // Update the PaymentRefund table. If trade_no is of uuid type, update it to string type: + needMigratorPaymentRefundID := false + if c.DB.Migrator().HasColumn(&types.PaymentRefund{}, "trade_no") { + columnTypes, err := c.DB.Migrator().ColumnTypes(&types.PaymentRefund{}) + if err != nil { + return fmt.Errorf("failed to get column type of trade_no: %w", err) + } + for _, columnType := range columnTypes { + if columnType.Name() == "trade_no" { + if strings.ToLower(columnType.DatabaseTypeName()) == "uuid" { + // 1. add the temporary column trade_no_tmp + err := c.DB.Exec(` + ALTER TABLE "PaymentRefund" ADD COLUMN IF NOT EXISTS "trade_no_tmp" TEXT; + `).Error + if err != nil { + return fmt.Errorf("failed to add temporary column trade_no_tmp: %w", err) + } + // 2. copy the value of trade_no to trade_no_tmp + err = c.DB.Exec(` + UPDATE "PaymentRefund" SET "trade_no_tmp" = "trade_no"::TEXT WHERE "trade_no" IS NOT NULL; + `).Error + if err != nil { + return fmt.Errorf("failed to copy trade_no to trade_no_tmp: %w", err) + } + // 3. delete the original trade_no column + err = c.DB.Exec(` + ALTER TABLE "PaymentRefund" DROP COLUMN "trade_no"; + `).Error + if err != nil { + return fmt.Errorf("failed to drop column trade_no: %w", err) + } + // 4. rename trade_no_tmp to trade_no + err = c.DB.Exec(` + ALTER TABLE "PaymentRefund" RENAME COLUMN "trade_no_tmp" TO "trade_no"; + `).Error + if err != nil { + return fmt.Errorf( + "failed to rename column trade_no_tmp to trade_no: %w", + err, + ) + } + } + } + if columnType.Name() == "id" { + isPrimaryKey, _ := columnType.PrimaryKey() + isNullable, _ := columnType.Nullable() + if isPrimaryKey || !isNullable { + needMigratorPaymentRefundID = true + } + } + } + } else { + fmt.Println("PaymentRefund.trade_no column does not exist, skipping migration") + } + + // Migrate the PaymentRefund table: Rename the id field to order_id and remove the unique constraint + if needMigratorPaymentRefundID { + if err := c.migratorPaymentRefundTable(); err != nil { + return fmt.Errorf("failed to migrate PaymentRefund table: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.Payment{}, `activityType`) { + fmt.Println("add column activityType") + tableName := types.Payment{}.TableName() + err := c.DB.Exec(alterTableAddColumnSQL(tableName, `"activityType" TEXT`)).Error + if err != nil { + return fmt.Errorf("failed to add column activityType: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.Payment{}, "workspace_subscription_id") { + fmt.Println("add column workspace_subscription_id") + tableName := types.Payment{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL(tableName, `"workspace_subscription_id" uuid`), + ).Error + if err != nil { + return fmt.Errorf("failed to add column workspace_subscription_id: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.PaymentOrder{}, "workspace_subscription_id") { + fmt.Println("add column workspace_subscription_id to PaymentOrder") + tableName := types.PaymentOrder{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL(tableName, `"workspace_subscription_id" uuid`), + ).Error + if err != nil { + return fmt.Errorf( + "failed to add column workspace_subscription_id to PaymentOrder: %w", + err, + ) + } + } + if !c.DB.Migrator().HasColumn(&types.Payment{}, "stripe") { + fmt.Println("add column stripe to Payment") + tableName := types.Payment{}.TableName() + err := c.DB.Exec(alterTableAddColumnSQL(tableName, `"stripe" JSON`)).Error + if err != nil { + return fmt.Errorf("failed to add column stripe to Payment: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.PaymentOrder{}, "stripe") { + fmt.Println("add column stripe to Payment") + tableName := types.PaymentOrder{}.TableName() + err := c.DB.Exec(alterTableAddColumnSQL(tableName, `"stripe" JSON`)).Error + if err != nil { + return fmt.Errorf("failed to add column stripe to Payment: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.WorkspaceSubscriptionPlan{}, "ai_quota") { + fmt.Println("add column ai_quota to WorkspaceSubscriptionPlan") + tableName := types.WorkspaceSubscriptionPlan{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL(tableName, `"ai_quota" bigint NOT NULL DEFAULT 0`), + ).Error + if err != nil { + return fmt.Errorf("failed to add column ai_quota to WorkspaceSubscriptionPlan: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.WorkspaceSubscriptionTransaction{}, "pay_app") { + fmt.Println("add column pay_app to WorkspaceSubscriptionTransaction") + tableName := types.WorkspaceSubscriptionTransaction{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL(tableName, `"pay_app" TEXT NOT NULL DEFAULT ''`), + ).Error + if err != nil { + return fmt.Errorf("failed to add column pay_app to WorkspaceSubscriptionTransaction: %w", err) + } + } + if !c.Localdb.Migrator().HasColumn(&types.WorkspaceSubscriptionPlan{}, "ai_quota") { + fmt.Println("add column ai_quota to WorkspaceSubscriptionPlan") + tableName := types.WorkspaceSubscriptionPlan{}.TableName() + err := c.Localdb.Exec( + alterTableAddColumnSQL(tableName, `"ai_quota" bigint NOT NULL DEFAULT 0`), + ).Error + if err != nil { + return fmt.Errorf("failed to add column ai_quota to WorkspaceSubscriptionPlan: %w", err) + } + } + // TODO Processing encryptBalance column,to be deleted in the future + if c.DB.Migrator().HasColumn(&types.Account{}, "encryptBalance") { + // 将现有 NULL 值更新为 '' + // fmt.Println("updating NULL values to '' for encryptBalance") + // err := c.Exec(`UPDATE "Account" SET "encryptBalance" = '' WHERE "encryptBalance" IS NULL`).Error + // if err != nil { + // return fmt.Errorf("failed to update NULL values for encryptBalance: %v", err) + //} + + // 设置默认值 '' + fmt.Println("setting default value '' for encryptBalance") + err := c.DB.Exec(`ALTER TABLE "Account" ALTER COLUMN "encryptBalance" SET DEFAULT ''`).Error + if err != nil { + return fmt.Errorf("failed to set default value for encryptBalance: %w", err) + } + } + + // 处理 encryptDeductionBalance 列 + if c.DB.Migrator().HasColumn(&types.Account{}, "encryptDeductionBalance") { + //// 将现有 NULL 值更新为 '' + // fmt.Println("updating NULL values to '' for encryptDeductionBalance") + // err := c.Exec(`UPDATE "Account" SET "encryptDeductionBalance" = '' WHERE "encryptDeductionBalance" IS NULL`).Error + // if err != nil { + // return fmt.Errorf("failed to update NULL values for encryptDeductionBalance: %v", err) + //} + + // 设置默认值 '' + fmt.Println("setting default value '' for encryptDeductionBalance") + err := c.DB.Exec( + `ALTER TABLE "Account" ALTER COLUMN "encryptDeductionBalance" SET DEFAULT ''`, + ).Error + if err != nil { + return fmt.Errorf("failed to set default value for encryptDeductionBalance: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.Credits{}, `updated_at`) { + fmt.Println("add table `Credits` column updated_at") + tableName := types.Credits{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL( + tableName, + `"updated_at" TIMESTAMP(3) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`, + ), + ).Error + if err != nil { + return fmt.Errorf("failed to add column updated_at: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.Account{}, `updated_at`) { + fmt.Println("add table `Account` column updated_at") + tableName := types.Account{}.TableName() + err := c.DB.Exec( + alterTableAddColumnSQL( + tableName, + `"updated_at" TIMESTAMP(3) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`, + ), + ).Error + if err != nil { + return fmt.Errorf("failed to add column updated_at: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.Corporate{}, `type`) { + fmt.Println("add table `Corporate` column type") + tableName := types.Corporate{}.TableName() + err := c.DB.Exec(alterTableAddColumnSQL(tableName, `"type" TEXT`)).Error + if err != nil { + return fmt.Errorf("failed to add column type: %w", err) + } + } + if !c.DB.Migrator().HasColumn(&types.AccountTransaction{}, "credit_id_list") { + sqls := []string{ + `ALTER TABLE "AccountTransaction" ADD COLUMN IF NOT EXISTS "region" uuid;`, + `ALTER TABLE "AccountTransaction" ADD COLUMN IF NOT EXISTS "deduction_credit" bigint;`, + `ALTER TABLE "AccountTransaction" ADD COLUMN IF NOT EXISTS "billing_id_list" text[];`, + `ALTER TABLE "AccountTransaction" ADD COLUMN IF NOT EXISTS "credit_id_list" text[];`, + } + for _, sql := range sqls { + err := c.DB.Exec(sql).Error + if err != nil { + return fmt.Errorf("failed to add column credit_id_list: %w", err) + } + } + } + // alter table "CardInfo" + // drop constraint "CardInfo_card_token_key"; + // ALTER TABLE DROP CONSTRAINT, use DROP INDEX CASCADE instead + if c.DB.Migrator().HasColumn(&types.CardInfo{}, "card_token") { + // use DROP INDEX CASCADE instead + // DROP INDEX CASCADE instead + // sql := `DROP INDEX IF EXISTS "card_token_key" CASCADE;` + err := c.DB.Exec(`DROP INDEX IF EXISTS "CardInfo_card_token_key" CASCADE`).Error + if err != nil { + return fmt.Errorf("failed to drop unique constraint: %w", err) + } + } + + if !c.DB.Migrator().HasColumn(&types.Payment{}, "card_uid") { + sqls := []string{ + `ALTER TABLE "Payment" ADD COLUMN IF NOT EXISTS "card_uid" uuid;`, + `ALTER TABLE "Payment" ADD COLUMN IF NOT EXISTS "type" text;`, + `ALTER TABLE "Payment" ADD COLUMN IF NOT EXISTS "charge_source" text;`, + } + for _, sql := range sqls { + err := c.DB.Exec(sql).Error + if err != nil { + return fmt.Errorf("failed to add column credit_id_list: %w", err) + } + } + } + + // 增加状态列 + if !c.DB.Migrator().HasColumn(&types.Payment{}, "status") { + fmt.Println("add column status to payment") + // 注意:IF NOT EXISTS 避免重复报错 + sql := `ALTER TABLE "Payment" ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'PAID';` + if err := c.DB.Exec(sql).Error; err != nil { + return fmt.Errorf("failed to add payment.status column: %w", err) + } + } + return nil +} + +func NewCockRoach(globalURI, localURI string) (*Cockroach, error) { + dbLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logger.Error, + IgnoreRecordNotFoundError: true, + Colorful: true, + }) + db, err := gorm.Open(postgres.New(postgres.Config{ + DSN: globalURI, + PreferSimpleProtocol: true, + }), &gorm.Config{ + Logger: dbLogger, + PrepareStmt: true, + TranslateError: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to open global url %s : %w", globalURI, err) + } + localdb, err := gorm.Open(postgres.New(postgres.Config{ + DSN: localURI, + PreferSimpleProtocol: true, + }), &gorm.Config{ + Logger: dbLogger, + PrepareStmt: true, + TranslateError: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to open local url %s : %w", localURI, err) + } + if os.Getenv(EnvBaseBalance) != "" { + balance, err := strconv.ParseInt(os.Getenv(EnvBaseBalance), 10, 64) + if err == nil { + BaseBalance = balance + } + } + cockroach := &Cockroach{ + DB: db, + Localdb: localdb, + ZeroAccount: &types.Account{Balance: BaseBalance, DeductionBalance: 0}, + } + cockroach.ownerUsrUIDMap = &sync.Map{} + cockroach.ownerUsrIDMap = &sync.Map{} + cockroach.subscriptionPlans = &sync.Map{} + cockroach.workspaceSubPlans = &sync.Map{} + // TODO region with local + localRegionStr := os.Getenv(EnvLocalRegion) + if localRegionStr != "" { + cockroach.LocalRegion = &types.Region{ + UID: uuid.MustParse(localRegionStr), + } + } else { + return nil, errors.New("empty local region") + } + return cockroach, nil +} + +func CreateTableIfNotExist(db *gorm.DB, tables ...any) error { + for i := range tables { + table := tables[i] + if !db.Migrator().HasTable(table) { + if err := db.AutoMigrate(table); err != nil { + return fmt.Errorf("failed to auto migrate table %T: %w", table, err) + } + } + } + return nil +} + +// Close db connection +func (c *Cockroach) Close() error { + db, err := c.DB.DB() + if err != nil { + return fmt.Errorf("failed to get db: %w", err) + } + if err := db.Close(); err != nil { + return fmt.Errorf("failed to close db: %w", err) + } + db, err = c.Localdb.DB() + if err != nil { + return fmt.Errorf("failed to get localdb: %w", err) + } + return db.Close() +} + +func (c *Cockroach) GetGlobalDB() *gorm.DB { + return c.DB +} + +func (c *Cockroach) GetLocalDB() *gorm.DB { + return c.Localdb +} + +// RetryableTransaction wraps a GORM transaction and retries on CockroachDB retryable errors (SQLSTATE 40001) +func RetryableTransaction(db *gorm.DB, maxRetries int, fn func(tx *gorm.DB) error) error { + var err error + + for attempt := 0; attempt <= maxRetries; attempt++ { + err = db.Transaction(fn) + if err == nil { + return nil // success + } + + if isCockroachRetryableError(err) { + time.Sleep(backoffDuration(attempt)) + continue // retry + } else { + return err // not retryable, exit + } + } + + return err // retried max times, still failed +} + +func isCockroachRetryableError(err error) bool { + // Check for SQLSTATE 40001 or known Cockroach error strings + return strings.Contains(err.Error(), "SQLSTATE 40001") || + strings.Contains(err.Error(), "restart transaction") +} + +func backoffDuration(retry int) time.Duration { + base := 50 * time.Millisecond + _max := 1 * time.Second + + wait := base * time.Duration(1< _max { + wait = _max + } + return wait +} + +func (c *Cockroach) GetGiftCodeWithCode(code string) (*types.GiftCode, error) { + var giftCode types.GiftCode + if err := c.DB.Where(&types.GiftCode{Code: code}).First(&giftCode).Error; err != nil { + return nil, fmt.Errorf("failed to get gift code: %w", err) + } + return &giftCode, nil +} + +func (c *Cockroach) UseGiftCode(giftCode *types.GiftCode, userID string) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + var lockedGiftCode types.GiftCode + // Lock the gift code record for update + if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}). + Where(&types.GiftCode{ID: giftCode.ID}).First(&lockedGiftCode).Error; err != nil { + return fmt.Errorf("failed to lock gift code: %w", err) + } + + if lockedGiftCode.Used { + return errors.New("gift code has already been used") + } + + ops := &types.UserQueryOpts{ID: userID} + // Update the user's balance + if err := c.updateBalance(tx, ops, giftCode.CreditAmount, false, true); err != nil { + return fmt.Errorf("failed to update user balance: %w", err) + } + + message := "created by use gift code" + // Create an AccountTransaction record + accountTransaction := &types.AccountTransaction{ + ID: uuid.New(), + Type: "GiftCode", + UserUID: ops.UID, + DeductionBalance: 0, + Balance: giftCode.CreditAmount, + Message: &message, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + BillingID: giftCode.ID, + } + if err := tx.Create(accountTransaction).Error; err != nil { + return fmt.Errorf("failed to create account transaction: %w", err) + } + + // Mark the gift code as used + giftCode.Used = true + giftCode.UsedBy = ops.UID + giftCode.UsedAt = time.Now() + if err := tx.Save(giftCode).Error; err != nil { + return fmt.Errorf("failed to update gift code: %w", err) + } + + return nil + }) +} + +func (c *Cockroach) GetUserRealNameInfoByUserID(userID string) (*types.UserRealNameInfo, error) { + // get user info + userUID, err := c.getUserUIDByID(userID) + if err != nil { + return nil, fmt.Errorf("failed to get user uid: %w", err) + } + // get user realname info + var userRealNameInfo types.UserRealNameInfo + if err := c.DB.Where(&types.UserRealNameInfo{UserUID: userUID}).First(&userRealNameInfo).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, gorm.ErrRecordNotFound + } + return nil, fmt.Errorf("failed to get user real name info: %w", err) + } + return &userRealNameInfo, nil +} + +func (c *Cockroach) GetEnterpriseRealNameInfoByUserID( + userID string, +) (*types.EnterpriseRealNameInfo, error) { + // get user info + userUID, err := c.getUserUIDByID(userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + // get user realname info + var enterpriseRealNameInfo types.EnterpriseRealNameInfo + if err := c.DB.Where(&types.EnterpriseRealNameInfo{UserUID: userUID}).First(&enterpriseRealNameInfo).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, gorm.ErrRecordNotFound + } + return nil, fmt.Errorf("failed to get enterprise real name info: %w", err) + } + return &enterpriseRealNameInfo, nil +} + +func (c *Cockroach) CreateCorporate(account *types.Corporate) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + if account.UserUID == "" { + return errors.New("corporate uid is empty") + } + if account.ReceiptSerialNumber == "" { + return errors.New("corporate receiptSerialNumber is empty") + } + if account.PayerName == "" { + return errors.New("corporate payerName is empty") + } + if account.PaymentAmount < 0 { + return errors.New("corporate paymentAmount is zero") + } + id, err := gonanoid.New(12) + if err != nil { + return fmt.Errorf("failed to generate payment id: %w", err) + } + account.ID = id + pay := &types.Payment{} + pay.ID = account.ID + pay.UserUID, err = c.getUserUIDByID(account.UserUID) + if err != nil { + return fmt.Errorf("failed to get user uid: %w", err) + } + pay.Method = types.PaymentMethodCorporate + pay.TradeNO = account.ReceiptSerialNumber + pay.Amount = account.PaymentAmount + pay.Gift = account.GiftAmount + pay.Status = types.PaymentStatusPAID + if err := c.DB.Create(pay).Error; err != nil { + return fmt.Errorf("failed to create payment: %w", err) + } + if err := c.DB.Create(account).Error; err != nil { + return fmt.Errorf("failed to create corporate: %w", err) + } + amount := account.PaymentAmount + account.GiftAmount + if account.PaymentAmount > 0 { + if err := c.UpdateWithAccount(pay.UserUID, false, true, false, amount, tx); err != nil { + return fmt.Errorf("failed to update corporate payment amount: %w", err) + } + } + return nil + }) +} diff --git a/controllers/pkg/database/cockroach/accountv2_test.go b/controllers/pkg/database/cockroach/accountv2_test.go new file mode 100644 index 000000000000..649e6305067e --- /dev/null +++ b/controllers/pkg/database/cockroach/accountv2_test.go @@ -0,0 +1,146 @@ +// Copyright © 2024 sealos. +// +// 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 cockroach + +import ( + "os" + "testing" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +func TestCockroach_GetUserOauthProvider(t *testing.T) { + globalURI, localURI := requireCockroachTest(t) + ck, err := NewCockRoach(globalURI, localURI) + if err != nil { + t.Errorf("NewCockRoach() error = %v", err) + return + } + defer ck.Close() + + provider, err := ck.GetUserOauthProvider(&types.UserQueryOpts{ + Owner: "xxx", + }) + if err != nil { + t.Errorf("GetUserOauthProvider() error = %v", err) + return + } + t.Logf("provider: %+v", provider) +} + +func TestCockroach_GetAccountWithWorkspace(t *testing.T) { + globalURI, localURI := requireCockroachTest(t) + ck, err := NewCockRoach(globalURI, localURI) + if err != nil { + t.Errorf("NewCockRoach() error = %v", err) + return + } + defer ck.Close() + + account, err := ck.GetAccountWithWorkspace("ns-1c6gn6e0") + if err != nil { + t.Errorf("GetAccountWithWorkspace() error = %v", err) + return + } + t.Logf("account: %+v", account) +} + +func TestCockroach_InitTables(t *testing.T) { + globalURI, localURI := requireCockroachTest(t) + ck, err := NewCockRoach(globalURI, localURI) + if err != nil { + t.Errorf("NewCockRoach() error = %v", err) + return + } + defer ck.Close() + + // uid, err := uuid.Parse("9477dc81-de9a-48b0-b88e-5b3ec6c33a54") + // if err != nil { + // t.Fatalf("uuid.Parse() error = %v", err) + //} + //-2.77 + ops := &types.UserQueryOpts{ + // UID: uid, + // ID: "9F5NY4_lbS", + Owner: "6it2bra2", + IgnoreEmpty: true, + } + + userUID, err := ck.GetUser(ops) + if err != nil { + t.Fatalf("GetUserUID() error = %v", err) + } + t.Logf("userUID: %+v", userUID) + + // err = ck.InitTables() + // if err != nil { + // t.Errorf("InitTables() error = %v", err) + // return + //} + // + // + // err = ck.CreateCredits(&types.Credits{ + // UserUID: uid, + // Amount: 100000000, + // ExpireAt: time.Now().UTC().Add(10 * 365 * 24 * time.Hour), + // StartAt: time.Now().UTC(), + // Status: types.CreditsStatusActive, + // }) + // if err != nil { + // t.Fatalf("CreateCredits() error = %v", err) + //} + // + // err = ck.AddDeductionBalanceWithCredits(ops, 10_000000, []string{"order1", "order2"}) + // if err != nil { + // t.Fatalf("AddDeductionBalanceWithCredits() error = %v", err) + //} +} + +func TestCockroach_CreateCorporate(t *testing.T) { + globalURI, localURI := requireCockroachTest(t) + ck, err := NewCockRoach(globalURI, localURI) + if err != nil { + t.Errorf("NewCockRoach() error = %v", err) + return + } + defer ck.Close() + + cor := &types.Corporate{ + UserUID: "66EqYNUnLr", + ReceiptSerialNumber: uuid.New().String(), + PayerName: "payerName", + PaymentAmount: 1_000_000, + GiftAmount: 1_000_000, + } + err = ck.CreateCorporate(cor) + if err != nil { + t.Errorf("CreateCorporate() error = %v", err) + } + t.Logf("cor: %+v", cor) +} + +func requireCockroachTest(t *testing.T) (string, string) { + t.Helper() + if os.Getenv("RUN_COCKROACH_TESTS") != "true" { + t.Skip("set RUN_COCKROACH_TESTS=true to run CockroachDB tests") + } + for _, name := range []string{"GLOBAL_COCKROACH_URI", "LOCAL_COCKROACH_URI", "LOCAL_REGION"} { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } + return os.Getenv("GLOBAL_COCKROACH_URI"), os.Getenv("LOCAL_COCKROACH_URI") +} diff --git a/controllers/pkg/database/cockroach/billing_runtime_test.go b/controllers/pkg/database/cockroach/billing_runtime_test.go new file mode 100644 index 000000000000..581236ed6995 --- /dev/null +++ b/controllers/pkg/database/cockroach/billing_runtime_test.go @@ -0,0 +1,337 @@ +package cockroach + +import ( + "context" + "strings" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/testcontainers/testcontainers-go" + postgrescontainer "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func TestBillingDeductionWithPostgresRuntimeDoesNotCreateTransaction(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := postgrescontainer.Run(ctx, "postgres:16-alpine", + postgrescontainer.WithDatabase("account"), + postgrescontainer.WithUsername("account"), + postgrescontainer.WithPassword("account"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections").WithOccurrence(2), + ), + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate PostgreSQL: %v", err) + } + }) + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatal(err) + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&types.Account{}, &types.AccountTransaction{}); err != nil { + t.Fatal(err) + } + userUID := uuid.New() + if err := db.Create(&types.Account{ + UserUID: userUID, CreateRegionID: "test", Balance: 1000, + }).Error; err != nil { + t.Fatal(err) + } + regionUID := uuid.New() + account := &Cockroach{ + DB: db, Localdb: db, LocalRegion: &types.Region{UID: regionUID, Domain: "test"}, + ownerUsrUIDMap: &sync.Map{}, ownerUsrIDMap: &sync.Map{}, + } + for range 2 { + if err := account.AddDeductionBalance( + &types.UserQueryOpts{UID: userUID}, 125, + ); err != nil { + t.Fatal(err) + } + } + var stored types.Account + if err := db.First(&stored, `"userUid" = ?`, userUID).Error; err != nil { + t.Fatal(err) + } + if stored.DeductionBalance != 250 { + t.Fatalf("deduction balance = %d", stored.DeductionBalance) + } + var transactionCount int64 + if err := db.Model(&types.AccountTransaction{}).Count(&transactionCount).Error; err != nil { + t.Fatal(err) + } + if transactionCount != 0 { + t.Fatalf("transaction count = %d", transactionCount) + } +} + +func TestHistoricalBillingDeductionUsesCreditsActiveAtBillingTime(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := postgrescontainer.Run(ctx, "postgres:16-alpine", + postgrescontainer.WithDatabase("account"), + postgrescontainer.WithUsername("account"), + postgrescontainer.WithPassword("account"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections").WithOccurrence(2), + ), + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate PostgreSQL: %v", err) + } + }) + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatal(err) + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate( + &types.Account{}, &types.AccountTransaction{}, &types.Credits{}, + ); err != nil { + t.Fatal(err) + } + userUID := uuid.New() + if err := db.Create(&types.Account{ + UserUID: userUID, CreateRegionID: "test", Balance: 1000, + }).Error; err != nil { + t.Fatal(err) + } + creditID := uuid.New() + billingTime := time.Now().UTC().Add(-2 * time.Hour) + if err := db.Create(&types.Credits{ + ID: creditID, UserUID: userUID, Amount: 100, Status: types.CreditsStatusActive, + StartAt: billingTime.Add(-time.Hour), ExpireAt: billingTime.Add(time.Hour), + }).Error; err != nil { + t.Fatal(err) + } + regionUID := uuid.New() + account := &Cockroach{ + DB: db, Localdb: db, LocalRegion: &types.Region{UID: regionUID, Domain: "test"}, + ownerUsrUIDMap: &sync.Map{}, ownerUsrIDMap: &sync.Map{}, + } + if err := account.AddDeductionBalanceWithCreditsAt( + &types.UserQueryOpts{UID: userUID}, + 125, + []string{"historical-credit-order"}, + billingTime, + ); err != nil { + t.Fatal(err) + } + var storedCredit types.Credits + if err := db.First(&storedCredit, "id = ?", creditID).Error; err != nil { + t.Fatal(err) + } + if storedCredit.UsedAmount != 100 { + t.Fatalf("used credits = %d", storedCredit.UsedAmount) + } + var storedAccount types.Account + if err := db.First(&storedAccount, `"userUid" = ?`, userUID).Error; err != nil { + t.Fatal(err) + } + if storedAccount.DeductionBalance != 25 { + t.Fatalf("deduction balance = %d", storedAccount.DeductionBalance) + } + var transactionCount int64 + if err := db.Model(&types.AccountTransaction{}).Count(&transactionCount).Error; err != nil { + t.Fatal(err) + } + if transactionCount != 0 { + t.Fatalf("transaction count = %d", transactionCount) + } +} + +func TestBillingHistoryQueriesUseCompositeIndexes(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := postgrescontainer.Run(ctx, "postgres:16-alpine", + postgrescontainer.WithDatabase("account"), + postgrescontainer.WithUsername("account"), + postgrescontainer.WithPassword("account"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections").WithOccurrence(2), + ), + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate PostgreSQL: %v", err) + } + }) + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatal(err) + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.Exec(` + CREATE TABLE "Debt" ( + user_uid TEXT PRIMARY KEY, + account_debt_status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL + ); + CREATE TABLE "DebtStatusRecord" ( + id BIGINT PRIMARY KEY, + user_uid TEXT NOT NULL, + last_status TEXT NOT NULL, + create_at TIMESTAMPTZ NOT NULL + ); + CREATE TABLE "WorkspaceSubscriptionTransaction" ( + id BIGINT PRIMARY KEY, + region_domain TEXT NOT NULL, + workspace TEXT NOT NULL, + status TEXT NOT NULL, + pay_status TEXT NOT NULL, + operator TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL + ); + CREATE TABLE "Credits" ( + id BIGINT PRIMARY KEY, + user_uid TEXT NOT NULL, + amount BIGINT NOT NULL, + used_amount BIGINT NOT NULL, + expire_at TIMESTAMPTZ NOT NULL, + start_at TIMESTAMPTZ NOT NULL, + status TEXT NOT NULL + );`).Error; err != nil { + t.Fatal(err) + } + if err := ensureBillingQueryIndexes(db); err != nil { + t.Fatal(err) + } + if err := db.Exec(` + INSERT INTO "Debt" (user_uid, account_debt_status, created_at) + SELECT CASE WHEN i = 1 THEN 'target-user' ELSE 'user-' || i::TEXT END, + 'NormalPeriod', + now() - INTERVAL '1 day' + FROM generate_series(1, 1000) AS series(i);`).Error; err != nil { + t.Fatal(err) + } + if err := db.Exec(` + INSERT INTO "DebtStatusRecord" (id, user_uid, last_status, create_at) + SELECT i, + CASE WHEN i % 1000 = 0 THEN 'target-user' ELSE 'user-' || i::TEXT END, + 'NormalPeriod', + now() - (i || ' seconds')::INTERVAL + FROM generate_series(1, 100000) AS series(i);`).Error; err != nil { + t.Fatal(err) + } + if err := db.Exec(` + INSERT INTO "WorkspaceSubscriptionTransaction" ( + id, region_domain, workspace, status, pay_status, operator, updated_at + ) + SELECT i, + 'test-region', + CASE WHEN i % 1000 = 0 THEN 'target-workspace' ELSE 'workspace-' || i::TEXT END, + 'completed', + CASE WHEN i % 2 = 0 THEN 'paid' ELSE 'no_need' END, + CASE WHEN i % 1000 = 0 THEN 'canceled' WHEN i % 2 = 0 THEN 'created' ELSE 'canceled' END, + now() - (i || ' seconds')::INTERVAL + FROM generate_series(1, 100000) AS series(i);`).Error; err != nil { + t.Fatal(err) + } + if err := db.Exec(` + INSERT INTO "Credits" (id, user_uid, amount, used_amount, expire_at, start_at, status) + SELECT i, + CASE WHEN i % 1000 = 0 THEN 'target-user' ELSE 'user-' || i::TEXT END, + 100, + 0, + now() + INTERVAL '1 day', + now() - INTERVAL '1 day', + 'active' + FROM generate_series(1, 100000) AS series(i);`).Error; err != nil { + t.Fatal(err) + } + + plans := map[string]string{ + "debt first status": `EXPLAIN (ANALYZE, COSTS OFF) + SELECT d.user_uid, + COALESCE(first_record.last_status, d.account_debt_status) AS account_debt_status + FROM "Debt" AS d + LEFT JOIN LATERAL ( + SELECT r.last_status + FROM "DebtStatusRecord" AS r + WHERE r.user_uid = d.user_uid + AND r.create_at > now() - INTERVAL '2 hours' + ORDER BY r.create_at ASC, r.id ASC + LIMIT 1 + ) AS first_record ON TRUE + WHERE d.created_at <= now()`, + "subscription period": `EXPLAIN (ANALYZE, COSTS OFF) + SELECT * FROM "WorkspaceSubscriptionTransaction" + WHERE region_domain = 'test-region' + AND workspace IN ('target-workspace') + AND status = 'completed' + AND pay_status IN ('paid', 'no_need') + AND updated_at <= now()`, + "subscription terminal": `EXPLAIN (ANALYZE, COSTS OFF) + SELECT * FROM "WorkspaceSubscriptionTransaction" + WHERE region_domain = 'test-region' + AND workspace IN ('target-workspace') + AND status = 'completed' + AND operator IN ('canceled', 'deleted') + AND updated_at <= now()`, + "credits active period": `EXPLAIN (ANALYZE, COSTS OFF) + SELECT * FROM "Credits" + WHERE user_uid = 'target-user' + AND start_at <= now() + AND expire_at > now() + AND status = 'active' + ORDER BY expire_at ASC`, + } + wantIndexes := map[string]string{ + "debt first status": "idx_debt_record_user_time", + "subscription period": "idx_workspace_subscription_billing_history", + "subscription terminal": "idx_workspace_subscription_billing_history", + "credits active period": "idx_credits_active_period", + } + for name, query := range plans { + rows, err := db.Raw(query).Rows() + if err != nil { + t.Fatalf("%s explain: %v", name, err) + } + var plan strings.Builder + for rows.Next() { + var line string + if err := rows.Scan(&line); err != nil { + rows.Close() + t.Fatalf("%s scan explain: %v", name, err) + } + plan.WriteString(line) + } + if err := rows.Close(); err != nil { + t.Fatalf("%s close explain: %v", name, err) + } + planText := plan.String() + t.Logf("%s: %s", name, planText) + if !strings.Contains(planText, wantIndexes[name]) { + t.Fatalf("%s did not use %s: %s", name, wantIndexes[name], planText) + } + } + t.Logf("indexed historical billing queries completed against 100000-row tables") +} diff --git a/controllers/pkg/database/cockroach/user.go b/controllers/pkg/database/cockroach/user.go new file mode 100644 index 000000000000..a3b6f7f50b91 --- /dev/null +++ b/controllers/pkg/database/cockroach/user.go @@ -0,0 +1,26 @@ +package cockroach + +import ( + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +func (c *Cockroach) GetNotificationRecipient( + userUID uuid.UUID, +) (*types.NotificationRecipient, error) { + var result types.NotificationRecipient + err := c.DB.Model(&types.User{}). + Select(` + "User".nickname AS user_name, + COALESCE((SELECT op."providerId" FROM "OauthProvider" op WHERE op."userUid" = "User".uid AND op."providerType" = 'EMAIL' LIMIT 1), '') AS email, + COALESCE((SELECT op."providerId" FROM "OauthProvider" op WHERE op."userUid" = "User".uid AND op."providerType" = 'PHONE' LIMIT 1), '') AS phone_number, + "User".id AS user_id, + "User".uid AS user_uid + `). + Where("uid = ?", userUID). + Scan(&result).Error + if err != nil { + return nil, err + } + return &result, nil +} diff --git a/controllers/pkg/database/cockroach/workspace_subscription.go b/controllers/pkg/database/cockroach/workspace_subscription.go new file mode 100644 index 000000000000..890b302b2f78 --- /dev/null +++ b/controllers/pkg/database/cockroach/workspace_subscription.go @@ -0,0 +1,316 @@ +package cockroach + +import ( + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" + "gorm.io/gorm" +) + +func (c *Cockroach) GetWorkspaceSubscriptionPlan( + planName string, +) (*types.WorkspaceSubscriptionPlan, error) { + if planLoad, ok := c.workspaceSubPlans.Load(planName); ok { + plan, ok := planLoad.(*types.WorkspaceSubscriptionPlan) + if !ok { + return nil, fmt.Errorf( + "failed to assert subscription plan type from cache: %v", + planLoad, + ) + } + return plan, nil + } + var plan types.WorkspaceSubscriptionPlan + if c.Localdb.Migrator().HasTable(&types.WorkspaceSubscriptionPlan{}) { + if err := c.Localdb.Preload("Prices").Where(types.WorkspaceSubscriptionPlan{Name: planName}).First(&plan).Error; err == nil { + c.workspaceSubPlans.Store(planName, &plan) + return &plan, nil + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("failed to get subscription plan from localdb: %w", err) + } + } + if err := c.DB.Preload("Prices").Where(types.WorkspaceSubscriptionPlan{Name: planName}).First(&plan).Error; err != nil { + return nil, fmt.Errorf("failed to get subscription plan: %w", err) + } + c.workspaceSubPlans.Store(planName, &plan) + return &plan, nil +} + +func (c *Cockroach) GetWorkspaceSubscription( + workspace, regionDomain string, +) (*types.WorkspaceSubscription, error) { + var subscription types.WorkspaceSubscription + err := c.DB.Where("workspace = ? AND region_domain = ?", workspace, regionDomain). + First(&subscription). + Error + if err != nil { + return nil, err + } + return &subscription, err +} + +func (c *Cockroach) GetWorkspaceSubscriptionTraffic( + workspace, regionDomain string, +) (total, used int64, err error) { + result := &struct { + Total int64 `gorm:"column:total"` + Used int64 `gorm:"column:used"` + }{} + err = c.DB.Model(&types.WorkspaceTraffic{}). + Where("workspace = ? AND region_domain = ? AND expired_at > ?", workspace, regionDomain, time.Now()). + Select("SUM(total_bytes) as total, SUM(used_bytes) as used"). + Scan(result). + Error + if err != nil { + return 0, 0, err + } + return result.Total, result.Used, nil +} + +func (c *Cockroach) GetAIQuota(workspace, regionDomain string) (total, used int64, err error) { + result := &struct { + Total int64 `gorm:"column:total"` + Used int64 `gorm:"column:used"` + }{} + err = c.DB.Model(&types.WorkspaceAIQuotaPackage{}). + Where("workspace = ? AND region_domain = ? AND status = ? AND expired_at > ?", workspace, regionDomain, types.PackageStatusActive, time.Now()). + Select("SUM(total) as total, SUM(usage) as used"). + Scan(result). + Error + if err != nil { + return 0, 0, err + } + return result.Total, result.Used, nil +} + +// ListWorkspaceSubscription lists all subscriptions for a given user UID. +func (c *Cockroach) ListWorkspaceSubscription( + userUID uuid.UUID, +) ([]types.WorkspaceSubscription, error) { + var subscriptions []types.WorkspaceSubscription + err := c.DB.Where("user_uid = ?", userUID).Find(&subscriptions).Error + if err != nil { + return nil, fmt.Errorf("failed to list workspace subscriptions: %w", err) + } + return subscriptions, nil +} + +func (c *Cockroach) GetAllUnprocessedWorkspaceSubscriptionTransaction( + userUID uuid.UUID, +) ([]types.WorkspaceSubscriptionTransaction, error) { + var transactions []types.WorkspaceSubscriptionTransaction + err := c.DB.Where("user_uid = ? AND status IN (?, ?)", userUID, types.SubscriptionTransactionStatusPending, types.SubscriptionTransactionStatusProcessing). + Find(&transactions). + Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return []types.WorkspaceSubscriptionTransaction{}, nil + } + } + return transactions, nil +} + +func (c *Cockroach) GetLastWorkspaceSubscriptionTransaction( + workspace, regionDomain string, +) (*types.WorkspaceSubscriptionTransaction, error) { + transaction := &types.WorkspaceSubscriptionTransaction{} + err := c.DB.Where("workspace = ? AND region_domain = ?", workspace, regionDomain). + Order("created_at desc"). + First(transaction). + Error + if err != nil { + return nil, err + } + return transaction, nil +} + +// GetExpiredWorkspaceSubscriptions gets all expired workspace subscriptions with normal status and pay status +func (c *Cockroach) GetExpiredWorkspaceSubscriptions( + regionDomain string, +) ([]types.WorkspaceSubscription, error) { + var subscriptions []types.WorkspaceSubscription + now := time.Now() + + err := c.DB.Where(` + region_domain = ? AND + current_period_end_at <= ? AND + status = ? AND + pay_status IN (?, ?) + `, regionDomain, now, types.SubscriptionStatusNormal, types.SubscriptionPayStatusPaid, types.SubscriptionPayStatusNoNeed).Find(&subscriptions).Error + if err != nil { + return nil, fmt.Errorf("failed to get expired workspace subscriptions: %w", err) + } + return subscriptions, nil +} + +func AddWorkspaceSubscriptionTrafficPackage( + globalDB *gorm.DB, + subscriptionID uuid.UUID, + totalMiB int64, + expireAt time.Time, + from types.WorkspaceTrafficFrom, + fromID string, +) error { + totalBytes := totalMiB * 1024 * 1024 // Convert MiB to Bytes + // Get workspace subscription + var subscription types.WorkspaceSubscription + err := globalDB.Where(&types.WorkspaceSubscription{ID: subscriptionID}). + Find(&subscription). + Error + if err != nil { + return fmt.Errorf("failed to get workspace subscription: %w", err) + } + // Create new traffic package + trafficPackage := types.WorkspaceTraffic{ + ID: uuid.New(), + WorkspaceSubscriptionID: subscriptionID, + Workspace: subscription.Workspace, + RegionDomain: subscription.RegionDomain, + From: from, + FromID: fromID, + TotalBytes: totalBytes, + UsedBytes: 0, + ExpiredAt: expireAt, + Status: types.WorkspaceTrafficStatusActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = globalDB.Where("from_id = ?", fromID).FirstOrCreate(&trafficPackage).Error + if err != nil { + return fmt.Errorf("failed to create traffic package: %w", err) + } + return nil +} + +func AddWorkspaceSubscriptionAIQuotaPackage( + globalDB *gorm.DB, + subscriptionID uuid.UUID, + aiQuota int64, + expireAt time.Time, + from types.PackageFrom, + fromID string, +) error { + return AddWorkspaceSubscriptionAIQuotaPackageWithUpgrade( + globalDB, + subscriptionID, + aiQuota, + expireAt, + from, + fromID, + false, + 0, + ) +} + +// AddWorkspaceSubscriptionAIQuotaPackageWithUpgrade adds AI quota package with upgrade support +func AddWorkspaceSubscriptionAIQuotaPackageWithUpgrade( + globalDB *gorm.DB, + subscriptionID uuid.UUID, + aiQuota int64, + expireAt time.Time, + from types.PackageFrom, + fromID string, + isUpgrade bool, + oldPlanAIQuota int64, +) error { + if aiQuota <= 0 { + return nil + } + + // For upgrade scenarios, expire existing AI quota packages from the old plan + if isUpgrade && oldPlanAIQuota > 0 { + err := expireOldAIQuotaPackages(globalDB, subscriptionID, fromID) + if err != nil { + return fmt.Errorf("failed to expire old AI quota packages: %w", err) + } + } + + // Get workspace subscription + var subscription types.WorkspaceSubscription + err := globalDB.Where(&types.WorkspaceSubscription{ID: subscriptionID}). + Find(&subscription). + Error + if err != nil { + return fmt.Errorf("failed to get workspace subscription: %w", err) + } + // Create new AI quota package + aiQuotaPackage := types.WorkspaceAIQuotaPackage{ + ID: uuid.New(), + WorkspaceSubscriptionID: subscriptionID, + Workspace: subscription.Workspace, + RegionDomain: subscription.RegionDomain, + From: from, + FromID: fromID, + Total: aiQuota, + Usage: 0, + Status: types.PackageStatusActive, + ExpiredAt: expireAt, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = globalDB.Where("from_id = ?", fromID).FirstOrCreate(&aiQuotaPackage).Error + if err != nil { + return fmt.Errorf("failed to create AI quota package: %w", err) + } + return nil +} + +// expireOldAIQuotaPackages expires existing AI quota packages from old subscription plan +func expireOldAIQuotaPackages(globalDB *gorm.DB, subscriptionID uuid.UUID, _ string) error { + now := time.Now() + + // Update all existing active AI quota packages to expired status + // We don't need to exclude newFromID because the new package hasn't been created yet + // The newFromID parameter is kept for API consistency but not used in the query + err := globalDB.Model(&types.WorkspaceAIQuotaPackage{}). + Where("workspace_subscription_id = ? AND status = ?", + subscriptionID, types.PackageStatusActive). + Updates(map[string]any{ + "status": types.PackageStatusExpired, + "expired_at": now, + "updated_at": now, + }).Error + if err != nil { + return fmt.Errorf("failed to expire old AI quota packages: %w", err) + } + + return nil +} + +func (c *Cockroach) SetWorkspaceSubscriptionPlanList( + plans ...*types.WorkspaceSubscriptionPlan, +) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + for _, plan := range plans { + planWithoutPrices := *plan + planWithoutPrices.Prices = nil // temporarily remove the association + if err := tx.Save(&planWithoutPrices).Error; err != nil { + return fmt.Errorf("failed to save WorkspaceSubscriptionPlan %s: %w", plan.Name, err) + } + + if plan.ID == uuid.Nil { + return fmt.Errorf("WorkspaceSubscriptionPlan %s has no valid ID", plan.Name) + } + + // save the associated prices again + for i := range plan.Prices { + plan.Prices[i].ProductID = plan.ID // make sure the foreign keys are correct + if err := tx.Save(&plan.Prices[i]).Error; err != nil { + return fmt.Errorf("failed to save ProductPrice for plan %s: %w", plan.Name, err) + } + } + } + return nil + }) +} + +func (c *Cockroach) GetWorkspaceSubscriptionPlanList() ([]types.WorkspaceSubscriptionPlan, error) { + var plans []types.WorkspaceSubscriptionPlan + if err := c.DB.Preload("Prices").Find(&plans).Error; err != nil { + return nil, fmt.Errorf("failed to get subscription plan list: %w", err) + } + return plans, nil +} diff --git a/controllers/pkg/database/interface.go b/controllers/pkg/database/interface.go new file mode 100644 index 000000000000..87127319c867 --- /dev/null +++ b/controllers/pkg/database/interface.go @@ -0,0 +1,239 @@ +// Copyright © 2023 sealos. +// +// 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 database + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/common" + "github.com/labring/sealos/controllers/pkg/database/cockroach" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "go.mongodb.org/mongo-driver/bson/primitive" + "gorm.io/gorm" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Interface interface { + Account + Traffic + CVM + Creator +} + +type CVM interface { + GetPendingStateInstance(regionUID string) (cvmMap map[string][]types.CVMBilling, err error) + SetDoneStateInstance(instanceIDs ...primitive.ObjectID) error +} + +type Account interface { + GetBillingLastUpdateTime(owner string, _type common.Type) (bool, time.Time, error) + GetOwnersRecentUpdates(ownerList []string, checkTime time.Time) ([]string, error) + GetOwnerBillingsAt( + ownerList []string, + billingTime time.Time, + ) (map[string][]*resources.Billing, error) + GetUnsettledBillingsAt(billingTime time.Time) (map[string][]*resources.Billing, error) + GetBillingCheckpoint() (time.Time, bool, error) + SaveBillingCheckpoint(billingTime time.Time) error + GetTimeUsedNamespaceList(startTime, endTime time.Time) ([]string, error) + SaveBillings(billing ...*resources.Billing) error + SaveObjTraffic(obs ...*types.ObjectStorageTraffic) error + GetAllLatestObjTraffic(startTime, endTime time.Time) ([]types.ObjectStorageTraffic, error) + HandlerTimeObjBucketSentTraffic(startTime, endTime time.Time, bucket string) (int64, error) + GetTimeObjBucketBucket(startTime, endTime time.Time) ([]string, error) + GetUnsettingBillingHandler(owner string) ([]resources.BillingHandler, error) + UpdateBillingStatus(orderIDs []string, status resources.BillingStatus) error + GetUpdateTimeForCategoryAndPropertyFromMetering(category, property string) (time.Time, error) + GetAllPayment() ([]resources.Billing, error) + InitDefaultPropertyTypeLS() error + InitDefaultPropertyTypeLSWithDefaults() error + ReloadPropertyTypeLS() error + SavePropertyTypes(types []resources.PropertyType) error + GetBillingCount( + accountType common.Type, + startTime, endTime time.Time, + ) (count, amount int64, err error) + GenerateBillingData( + startTime, endTime time.Time, + prols *resources.PropertyTypeLS, + ownerToNS map[string][]string, + ) (map[string][]*resources.Billing, error) + InsertMonitor(ctx context.Context, monitors ...*resources.Monitor) error + GetDistinctMonitorCombinations(startTime, endTime time.Time) ([]resources.Monitor, error) + DropMonitorCollectionsOlderThan(days int) error + Disconnect(ctx context.Context) error + Creator +} + +type BillingRecordQuery struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Namespace string `json:"namespace,omitempty"` + StartTime v1.Time `json:"startTime"` + EndTime v1.Time `json:"endTime"` + OrderID string `json:"orderID,omitempty"` + Type common.Type `json:"type"` + AppType string `json:"appType,omitempty"` +} + +type Traffic interface { + GetTrafficSentBytes( + startTime, endTime time.Time, + namespace string, + _type uint8, + name string, + ) (int64, error) + GetTrafficRecvBytes( + startTime, endTime time.Time, + namespace string, + _type uint8, + name string, + ) (int64, error) + + GetPodTrafficSentBytes(startTime, endTime time.Time, namespace, name string) (int64, error) + GetPodTrafficRecvBytes(startTime, endTime time.Time, namespace, name string) (int64, error) + + GetNamespaceTraffic( + ctx context.Context, + startTime, endTime time.Time, + ) (result map[string]int64, err error) +} + +type AccountV2 interface { + Close() error + GetGlobalDB() *gorm.DB + GetLocalDB() *gorm.DB + GetUserCr(user *types.UserQueryOpts) (*types.RegionUserCr, error) + GetUser(ops *types.UserQueryOpts) (*types.User, error) + GetNotificationRecipient(userUID uuid.UUID) (*types.NotificationRecipient, error) + GetUserUID(ops *types.UserQueryOpts) (uuid.UUID, error) + GetUserID(ops *types.UserQueryOpts) (string, error) + GetAccount(user *types.UserQueryOpts) (*types.Account, error) + GetAccountWithCredits(userUID uuid.UUID) (*types.UsableBalanceWithCredits, error) + GetAvailableCredits(ops *types.UserQueryOpts) ([]types.Credits, error) + GetAccountConfig() (types.AccountConfig, error) + ReloadAccountConfig() (types.AccountConfig, error) + InsertAccountConfig(config *types.AccountConfig) error + GetRegions() ([]types.Region, error) + GetLocalRegion() types.Region + GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.OauthProvider, error) + GetWorkspace(namespaces ...string) ([]types.Workspace, error) + GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) + SetAccountCreateLocalRegion(account *types.Account, region string) error + CreateUser( + oAuth *types.OauthProvider, + regionUserCr *types.RegionUserCr, + user *types.User, + workspace *types.Workspace, + userWorkspace *types.UserWorkspace, + ) error + AddBalance(user *types.UserQueryOpts, balance int64) error + AddDeductionBalanceWithCredits(ops *types.UserQueryOpts, amount int64, orderIDs []string) error + AddDeductionBalanceWithCreditsAt( + ops *types.UserQueryOpts, + amount int64, + orderIDs []string, + at time.Time, + ) error + ReduceBalance(ops *types.UserQueryOpts, amount int64) error + ReduceDeductionBalance(ops *types.UserQueryOpts, amount int64) error + NewAccount(user *types.UserQueryOpts) (*types.Account, error) + NewAccountWithFreeSubscriptionPlan(ops *types.UserQueryOpts) (*types.Account, error) + GetSubscriptionPlan(planName string) (*types.SubscriptionPlan, error) + GetWorkspaceSubscriptionPlan(planName string) (*types.WorkspaceSubscriptionPlan, error) + GetWorkspaceSubscription(workspace, regionDomain string) (*types.WorkspaceSubscription, error) + GetWorkspaceSubscriptionTraffic(workspace, regionDomain string) (total, used int64, err error) + Payment(payment *types.Payment) error + PaymentWithFunc(payment *types.Payment, preDo, postDo func(tx *gorm.DB) error) error + GlobalTransactionHandler(funcs ...func(tx *gorm.DB) error) error + SavePayment(payment *types.Payment) error + GetUnInvoicedPaymentListWithIDs(ids []string) ([]types.Payment, error) + CreatePaymentOrder(order *types.PaymentOrder) error + CreateSubscription(subscription *types.Subscription) error + SetCardInfo(info *types.CardInfo) (uuid.UUID, error) + GetCardInfo(cardID, userUID uuid.UUID) (*types.CardInfo, error) + GetAllCardInfo(ops *types.UserQueryOpts) ([]types.CardInfo, error) + GetSubscription(ops *types.UserQueryOpts) (*types.Subscription, error) + GetSubscriptionPlanList() ([]types.SubscriptionPlan, error) + GetWorkspaceSubscriptionPlanList() ([]types.WorkspaceSubscriptionPlan, error) + SetWorkspaceSubscriptionPlanList(plans ...*types.WorkspaceSubscriptionPlan) error + SetSubscriptionPlanList(plans []types.SubscriptionPlan) error + GetCardList(ops *types.UserQueryOpts) ([]types.CardInfo, error) + DeleteCardInfo(id, userUID uuid.UUID) error + SetDefaultCard(cardID, userUID uuid.UUID) error + CreateAccount(ops *types.UserQueryOpts, account *types.Account) (*types.Account, error) + TransferAccount(from, to *types.UserQueryOpts, amount int64) error + TransferAccountAll(from, to *types.UserQueryOpts) error + AddDeductionBalance(user *types.UserQueryOpts, balance int64) error + AddDeductionBalanceWithDB(ops *types.UserQueryOpts, amount int64, tx *gorm.DB) error + AddDeductionBalanceWithFunc( + ops *types.UserQueryOpts, + amount int64, + preDo, postDo func() error, + ) error +} + +type Creator interface { + CreateBillingIfNotExist() error + // suffix by day, eg: monitor_20200101 + CreateMonitorTimeSeriesIfNotExist(collTime time.Time) error + CreateTTLTrafficTimeSeries() error +} + +const ( + MongoURI = "MONGO_URI" + CVMMongoURI = "CVM_MONGO_URI" + GlobalCockroachURI = "GLOBAL_COCKROACH_URI" + LocalCockroachURI = "LOCAL_COCKROACH_URI" + TrafficMongoURI = "TRAFFIC_MONGO_URI" +) + +var _ = AccountV2(&cockroach.Cockroach{}) + +func NewAccountV2(globalURI, localURI string) (AccountV2, error) { + return cockroach.NewCockRoach(globalURI, localURI) +} + +func InitRegionEnv(db *gorm.DB, localDomain string) error { + var regionENV []types.RegionConfig + if err := db.Model(&types.RegionConfig{}).Find(®ionENV).Error; err != nil && + !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get region env: %w", err) + } + // set global env + for _, envCfg := range regionENV { + if strings.ToUpper(envCfg.Region) == "GLOBAL" { + if err := os.Setenv(envCfg.Key, envCfg.Value); err != nil { + return fmt.Errorf("set global env error: %w", err) + } + } + } + // region env Cover + for _, envCfg := range regionENV { + if envCfg.Region == localDomain { + if err := os.Setenv(envCfg.Key, envCfg.Value); err != nil { + return fmt.Errorf("set region env error: %w", err) + } + } + } + return nil +} diff --git a/controllers/pkg/database/mongo/account.go b/controllers/pkg/database/mongo/account.go new file mode 100644 index 000000000000..59c61f145927 --- /dev/null +++ b/controllers/pkg/database/mongo/account.go @@ -0,0 +1,1280 @@ +// Copyright © 2023 sealos. +// +// 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 mongo + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "math" + "sort" + "strings" + "time" + + "github.com/labring/sealos/controllers/pkg/common" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/pkg/utils/logger" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + EnvAccountDBName = "ACCOUNT_DB_NAME" + EnvTrafficDBName = "TRAFFIC_DB_NAME" + EnvCVMDBName = "CVM_DB_NAME" + EnvCVMConn = "CVM_DB_CONN" + EnvTrafficConn = "TRAFFIC_CONN" +) + +const ( + DefaultAccountDBName = "sealos-resources" + DefaultTrafficDBName = "sealos-networkmanager" + DefaultAuthDBName = "sealos-auth" + DefaultCVMDBName = "sealos-cvm" + DefaultCVMConn = "cvm" + DefaultMeteringConn = "metering" + DefaultMonitorConn = "monitor" + DefaultBillingConn = "billing" + DefaultObjTrafficConn = "objectstorage-traffic" + DefaultUserConn = "user" + DefaultPricesConn = "prices" + DefaultPropertiesConn = "properties" + DefaultBillingCheckpointConn = "billing-checkpoint" + // TODO fix + DefaultTrafficConn = "traffic" +) + +type mongoDB struct { + Client *mongo.Client + AccountDB string + TrafficDB string + AuthDB string + CvmDB string + CvmConn string + UserConn string + MonitorConnPrefix string + MeteringConn string + BillingConn string + ObjTrafficConn string + PropertiesConn string + TrafficConn string +} + +type BillingRecordQueryItem struct { + Time metav1.Time `json:"time" bson:"time"` + BillingRecordQueryItemInline `json:",inline"` +} + +type BillingRecordQueryItemInline struct { + Name string `json:"name,omitempty" bson:"name,omitempty"` + OrderID string `json:"order_id" bson:"order_id"` + Namespace string `json:"namespace,omitempty" bson:"namespace,omitempty"` + Type common.Type `json:"type" bson:"type"` + AppType string `json:"appType,omitempty" bson:"appType,omitempty"` + Costs Costs `json:"costs,omitempty" bson:"costs,omitempty"` + // Amount = PaymentAmount + GiftAmount + Amount int64 `json:"amount,omitempty" bson:"amount"` + // when Type = Recharge, PaymentAmount is the amount of recharge + Payment *PaymentForQuery `json:"payment,omitempty" bson:"payment,omitempty"` +} + +type Costs map[string]int64 + +type PaymentForQuery struct { + Amount int64 `json:"amount,omitempty" bson:"amount,omitempty"` +} + +const ( + // Consumption 消费 + Consumption common.Type = iota +) + +type AccountBalanceSpecBSON struct { + // Time metav1.Time `json:"time" bson:"time"` + // If the Time field is of the time. time type, it cannot be converted to json crd, so use metav1.Time. However, metav1.Time cannot be inserted into mongo, so you need to convert it to time.Time + Time time.Time `json:"time" bson:"time"` + BillingRecordQueryItemInline `json:",inline" bson:",inline"` +} + +func (m *mongoDB) Disconnect(ctx context.Context) error { + return m.Client.Disconnect(ctx) +} + +func (m *mongoDB) GetBillingLastUpdateTime( + owner string, + _type common.Type, +) (bool, time.Time, error) { + // skip cvm billing time + filter := bson.M{ + "owner": owner, + "type": _type, + "app_type": bson.M{ + "$ne": resources.AppType[resources.CVM], + }, + } + findOneOptions := options.FindOne().SetSort(bson.D{primitive.E{Key: "time", Value: -1}}) + var result bson.M + err := m.getBillingCollection(). + FindOne(context.Background(), filter, findOneOptions). + Decode(&result) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return false, time.Time{}, nil + } + return false, time.Time{}, err + } + // Assuming that the `time` field is stored as a `primitive.DateTime` + if resultTime, ok := result["time"].(primitive.DateTime); ok { + return true, resultTime.Time().UTC(), nil + } + + return false, time.Time{}, fmt.Errorf( + "failed to convert time field to primitive.DateTime: %v", + result["time"], + ) +} + +func (m *mongoDB) GetOwnersRecentUpdates( + ownerList []string, + checkTime time.Time, +) ([]string, error) { + // MongoDB filter + filter := bson.M{ + "owner": bson.M{"$in": ownerList}, + "type": common.Consumption, + "app_type": bson.M{ + "$nin": []int{ + int(resources.AppType[resources.CVM]), + int(resources.AppType[resources.LLMToken]), + }, + }, + } + + // Aggregate query: Group by owner to get the latest time + pipeline := mongo.Pipeline{ + {{Key: "$match", Value: filter}}, + { + {Key: "$sort", Value: bson.D{{Key: "owner", Value: 1}, {Key: "time", Value: -1}}}, + }, // Sort by owner first, then by time in descending order + {{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$owner"}, + { + Key: "lastUpdateTime", + Value: bson.D{{Key: "$first", Value: "$time"}}, + }, // fetch latest Time + }}}, + } + // execute aggregate query + cursor, err := m.getBillingCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return nil, fmt.Errorf("failed to execute aggregate query: %w", err) + } + defer cursor.Close(context.Background()) + + // get all the data out at once + var results []struct { + Owner string `bson:"_id"` + LastUpdateRaw primitive.DateTime `bson:"lastUpdateTime"` + } + if err := cursor.All(context.Background(), &results); err != nil { + return nil, fmt.Errorf("failed to decode cursor: %w", err) + } + + // use map to store query results + latestUpdates := make(map[string]time.Time, len(results)) + for _, result := range results { + latestUpdates[result.Owner] = result.LastUpdateRaw.Time() + } + + // **In-memory processing: Filters owners that have been updated since checkTime** + var updatedOwners []string + for _, owner := range ownerList { + lastUpdateTime, exists := latestUpdates[owner] + if exists && (lastUpdateTime.After(checkTime) || lastUpdateTime.Equal(checkTime)) { + updatedOwners = append(updatedOwners, owner) + } + } + return updatedOwners, nil +} + +func (m *mongoDB) GetOwnerBillingsAt( + ownerList []string, + billingTime time.Time, +) (map[string][]*resources.Billing, error) { + result := make(map[string][]*resources.Billing) + if len(ownerList) == 0 { + return result, nil + } + filter := bson.M{ + "owner": ownerListFilter(ownerList), + "time": billingTime, + "type": common.Consumption, + "app_type": bson.M{"$nin": []int{ + int(resources.AppType[resources.CVM]), + int(resources.AppType[resources.LLMToken]), + }}, + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + cursor, err := m.getBillingCollection().Find(ctx, filter) + if err != nil { + return nil, fmt.Errorf("find owner billings: %w", err) + } + defer cursor.Close(ctx) + var billings []*resources.Billing + if err := cursor.All(ctx, &billings); err != nil { + return nil, fmt.Errorf("decode owner billings: %w", err) + } + for _, billing := range billings { + result[billing.Owner] = append(result[billing.Owner], billing) + } + return result, nil +} + +func (m *mongoDB) GetUnsettledBillingsAt( + billingTime time.Time, +) (map[string][]*resources.Billing, error) { + filter := bson.M{ + "time": billingTime, + "type": common.Consumption, + "status": resources.Unsettled, + "order_id": primitive.Regex{Pattern: "^bh_"}, + "app_type": bson.M{"$nin": []int{ + int(resources.AppType[resources.CVM]), + int(resources.AppType[resources.LLMToken]), + }}, + } + cursor, err := m.getBillingCollection().Find(context.Background(), filter) + if err != nil { + return nil, fmt.Errorf("find unsettled billings: %w", err) + } + defer cursor.Close(context.Background()) + var billings []*resources.Billing + if err := cursor.All(context.Background(), &billings); err != nil { + return nil, fmt.Errorf("decode unsettled billings: %w", err) + } + result := make(map[string][]*resources.Billing) + for _, billing := range billings { + result[billing.Owner] = append(result[billing.Owner], billing) + } + return result, nil +} + +func ownerListFilter(ownerList []string) bson.M { + return bson.M{"$in": ownerList} +} + +const billingCheckpointID = "account-hourly-billing" + +func (m *mongoDB) GetBillingCheckpoint() (time.Time, bool, error) { + var checkpoint resources.BillingCheckpoint + err := m.getBillingCheckpointCollection().FindOne( + context.Background(), + bson.M{"_id": billingCheckpointID}, + ).Decode(&checkpoint) + if errors.Is(err, mongo.ErrNoDocuments) { + return time.Time{}, false, nil + } + if err != nil { + return time.Time{}, false, fmt.Errorf("get billing checkpoint: %w", err) + } + return checkpoint.Time.UTC(), true, nil +} + +func (m *mongoDB) SaveBillingCheckpoint(billingTime time.Time) error { + _, err := m.getBillingCheckpointCollection().UpdateOne( + context.Background(), + bson.M{"_id": billingCheckpointID}, + bson.M{"$set": bson.M{"time": billingTime.UTC(), "updated_at": time.Now().UTC()}}, + options.Update().SetUpsert(true), + ) + if err != nil { + return fmt.Errorf("save billing checkpoint: %w", err) + } + return nil +} + +func (m *mongoDB) GetTimeUsedNamespaceList(startTime, endTime time.Time) ([]string, error) { + pipeline := mongo.Pipeline{ + { + { + Key: "$match", + Value: bson.D{ + { + Key: "time", + Value: bson.D{ + {Key: "$gte", Value: startTime}, + {Key: "$lt", Value: endTime}, + }, + }, + }, + }, + }, + {{Key: "$group", Value: bson.D{{Key: "_id", Value: "$category"}}}}, + { + { + Key: "$project", + Value: bson.D{{Key: "_id", Value: 0}, {Key: "namespace", Value: "$_id"}}, + }, + }, + } + cursor, err := m.getMonitorCollection(startTime).Aggregate(context.Background(), pipeline) + if err != nil { + return nil, fmt.Errorf("aggregate error: %w", err) + } + defer cursor.Close(context.Background()) + + namespaces := make([]string, 0) + for cursor.Next(context.Background()) { + var result struct { + Namespace string `bson:"namespace"` + } + err := cursor.Decode(&result) + if err != nil { + return nil, fmt.Errorf("decode error: %w", err) + } + namespaces = append(namespaces, result.Namespace) + } + if err = cursor.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return namespaces, nil +} + +func (m *mongoDB) GetUnsettingBillingHandler(owner string) ([]resources.BillingHandler, error) { + filter := bson.M{ + "owner": owner, + "status": bson.M{ + "$in": []resources.BillingStatus{ + resources.Unsettled, + }, + }, + } + findOptions := options.Find() + cur, err := m.getBillingCollection().Find(context.Background(), filter, findOptions) + if err != nil { + return nil, fmt.Errorf("find error: %w", err) + } + defer cur.Close(context.Background()) + results := make([]resources.BillingHandler, 0) + for cur.Next(context.Background()) { + var result resources.BillingHandler + if err := cur.Decode(&result); err != nil { + return nil, fmt.Errorf("decode error: %w", err) + } + results = append(results, result) + } + if err := cur.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return results, nil +} + +func (m *mongoDB) UpdateBillingStatus(orderIDs []string, status resources.BillingStatus) error { + // create a query filter + filter := bson.M{"order_id": bson.M{"$in": orderIDs}} + update := bson.M{ + "$set": bson.M{ + "status": status, + }, + } + _, err := m.getBillingCollection().UpdateMany(context.Background(), filter, update) + if err != nil { + return fmt.Errorf("update error: %w", err) + } + return nil +} + +func (m *mongoDB) SaveBillings(billing ...*resources.Billing) error { + if len(billing) == 0 { + return nil + } + models := make([]mongo.WriteModel, 0, len(billing)) + for _, b := range billing { + models = append(models, mongo.NewUpdateOneModel(). + SetFilter(bson.M{"owner": b.Owner, "order_id": b.OrderID}). + SetUpdate(bson.M{"$setOnInsert": b}). + SetUpsert(true)) + } + _, err := m.getBillingCollection().BulkWrite( + context.Background(), models, options.BulkWrite().SetOrdered(false), + ) + return err +} + +func (m *mongoDB) SaveObjTraffic(obs ...*types.ObjectStorageTraffic) error { + traffic := make([]any, len(obs)) + for i, ob := range obs { + traffic[i] = ob + } + _, err := m.getObjTrafficCollection().InsertMany(context.Background(), traffic) + return err +} + +func (m *mongoDB) GetAllLatestObjTraffic( + startTime, endTime time.Time, +) ([]types.ObjectStorageTraffic, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "time": bson.M{ + "$gt": startTime, + "$lte": endTime, + }, + }, + }, + { + "$sort": bson.M{"time": -1}, + }, + { + "$group": bson.M{ + "_id": bson.M{"user": "$user", "bucket": "$bucket"}, + "latestDoc": bson.M{"$first": "$$ROOT"}, + }, + }, + { + "$replaceRoot": bson.M{"newRoot": "$latestDoc"}, + }, + } + + cursor, err := m.getObjTrafficCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return nil, err + } + defer cursor.Close(context.Background()) + + var results []types.ObjectStorageTraffic + if err = cursor.All(context.Background(), &results); err != nil { + return nil, err + } + + return results, nil +} + +func (m *mongoDB) HandlerTimeObjBucketSentTraffic( + startTime, endTime time.Time, + bucket string, +) (int64, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "time": bson.M{ + "$gt": startTime, + "$lte": endTime, + }, + "bucket": bucket, + }, + }, + { + "$group": bson.M{ + "_id": nil, + "totalSent": bson.M{"$sum": "$sent"}, + }, + }, + } + cursor, err := m.getObjTrafficCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return 0, err + } + defer cursor.Close(context.Background()) + + var result struct { + TotalSent int64 `bson:"totalSent"` + } + if cursor.Next(context.Background()) { + if err := cursor.Decode(&result); err != nil { + return 0, err + } + return result.TotalSent, nil + } + if err := cursor.Err(); err != nil { + return 0, err + } + return 0, nil +} + +func (m *mongoDB) GetTimeObjBucketBucket(startTime, endTime time.Time) ([]string, error) { + pipeline := []bson.M{ + { + "$match": bson.M{ + "time": bson.M{ + "$gt": startTime, + "$lte": endTime, + }, + }, + }, + { + "$group": bson.M{ + "_id": nil, + "buckets": bson.M{"$addToSet": "$bucket"}, + }, + }, + } + cursor, err := m.getObjTrafficCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return nil, err + } + defer cursor.Close(context.Background()) + + var result struct { + Buckets []string `bson:"buckets"` + } + if cursor.Next(context.Background()) { + if err := cursor.Decode(&result); err != nil { + return nil, err + } + return result.Buckets, nil + } + if err := cursor.Err(); err != nil { + return nil, err + } + return nil, nil +} + +// InsertMonitor insert monitor data to mongodb collection monitor + time (eg: monitor_20200101) +// The monitor data is saved daily 2020-12-01 00:00:00 - 2020-12-01 23:59:59 => monitor_20201201 +func (m *mongoDB) InsertMonitor(ctx context.Context, monitors ...*resources.Monitor) error { + if len(monitors) == 0 { + return nil + } + manyMonitor := make([]any, 0, len(monitors)) + for i := range monitors { + manyMonitor = append(manyMonitor, monitors[i]) + } + _, err := m.getMonitorCollection(monitors[0].Time).InsertMany(ctx, manyMonitor) + return err +} + +func (m *mongoDB) GetDistinctMonitorCombinations( + startTime, endTime time.Time, +) ([]resources.Monitor, error) { + pipeline := mongo.Pipeline{ + {{Key: "$match", Value: bson.M{ + "time": bson.M{ + "$gte": startTime.UTC(), + "$lt": endTime.UTC(), + }, + }}}, + {{Key: "$group", Value: bson.M{ + "_id": bson.M{ + "category": "$category", + "name": "$name", + "type": "$type", + }, + }}}, + {{Key: "$project", Value: bson.M{ + "_id": 0, + "category": "$_id.category", + "name": "$_id.name", + "type": "$_id.type", + }}}, + } + cursor, err := m.getMonitorCollection(startTime).Aggregate(context.Background(), pipeline) + if err != nil { + return nil, fmt.Errorf("aggregate error: %w", err) + } + defer cursor.Close(context.Background()) + if !cursor.Next(context.Background()) { + return nil, nil + } + var monitors []resources.Monitor + if err := cursor.All(context.Background(), &monitors); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return monitors, nil +} + +func (m *mongoDB) GetAllPayment() ([]resources.Billing, error) { + filter := bson.M{ + "type": 1, + "payment.amount": bson.M{"$gt": 0}, + } + + cursor, err := m.getBillingCollection().Find(context.Background(), filter) + if err != nil { + return nil, fmt.Errorf("get all payment error: %w", err) + } + + var payments []resources.Billing + if err = cursor.All(context.Background(), &payments); err != nil { + return nil, fmt.Errorf("get all payment error: %w", err) + } + return payments, nil +} + +func (m *mongoDB) InitDefaultPropertyTypeLS() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cursor, err := m.getPropertiesCollection().Find(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("get all prices error: %w", err) + } + var properties []resources.PropertyType + if err = cursor.All(ctx, &properties); err != nil { + return fmt.Errorf("get all prices error: %w", err) + } + if len(properties) != 0 { + resources.DefaultPropertyTypeLS = resources.NewPropertyTypeLS(properties) + } + return nil +} + +// InitDefaultPropertyTypeLSWithDefaults initializes properties from database, +// and ensures basic resource types (cpu, memory, storage, ...) exist. +// If database is empty or missing basic resources, it will merge/write defaults. +func (m *mongoDB) InitDefaultPropertyTypeLSWithDefaults() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Fetch existing properties from database + cursor, err := m.getPropertiesCollection().Find(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("failed to get properties: %w", err) + } + defer cursor.Close(ctx) + + var existingProperties []resources.PropertyType + if err = cursor.All(ctx, &existingProperties); err != nil { + return fmt.Errorf("failed to decode properties: %w", err) + } + + // Check if database is empty or missing basic resources + needsInit := len(existingProperties) == 0 + missingBasicResources := m.findMissingBasicResources(existingProperties) + + // Merge with defaults if needed + var finalProperties []resources.PropertyType + switch { + case needsInit: + // Database is empty, use all defaults + finalProperties = resources.DefaultPropertyTypeList + if err := m.SavePropertyTypes(finalProperties); err != nil { + return fmt.Errorf("failed to save default properties: %w", err) + } + logger.Info("initialized properties with defaults", "count", len(finalProperties)) + case len(missingBasicResources) > 0: + // Merge existing properties with missing basic resources + finalProperties = m.mergeProperties(existingProperties, missingBasicResources) + // Upsert missing resources + for _, prop := range missingBasicResources { + filter := bson.M{"enum": prop.Enum} + update := bson.M{"$set": prop} + _, err := m.getPropertiesCollection(). + UpdateOne(ctx, filter, update, options.Update().SetUpsert(true)) + if err != nil { + return fmt.Errorf("failed to upsert property %s: %w", prop.Name, err) + } + } + logger.Info( + "merged missing basic resources", + "count", + len(missingBasicResources), + "resources", + missingBasicResources, + ) + // finalProperties = existingProperties + default: + // All basic resources exist, use existing properties + finalProperties = existingProperties + } + + // Update global DefaultPropertyTypeLS + if len(finalProperties) != 0 { + resources.DefaultPropertyTypeLS = resources.NewPropertyTypeLS(finalProperties) + } + + return nil +} + +// findMissingBasicResources checks which properties from DefaultPropertyTypeList are missing +func (m *mongoDB) findMissingBasicResources( + properties []resources.PropertyType, +) []resources.PropertyType { + var missing []resources.PropertyType + existingEnums := make(map[uint8]bool) + + // Build map of existing property enums + for _, prop := range properties { + existingEnums[prop.Enum] = true + } + + // Find missing properties from defaults + for _, defaultProp := range resources.DefaultPropertyTypeList { + if !existingEnums[defaultProp.Enum] { + missing = append(missing, defaultProp) + } + } + + return missing +} + +// mergeProperties merges existing properties with missing basic resources +func (m *mongoDB) mergeProperties( + existing, missing []resources.PropertyType, +) []resources.PropertyType { + // Create map of existing properties by enum + propMap := make(map[uint8]resources.PropertyType) + for _, prop := range existing { + propMap[prop.Enum] = prop + } + + // Add missing resources + for _, prop := range missing { + if _, exists := propMap[prop.Enum]; !exists { + propMap[prop.Enum] = prop + } + } + + // Convert back to slice + result := make([]resources.PropertyType, 0, len(propMap)) + for _, prop := range propMap { + result = append(result, prop) + } + + // Sort by enum for consistency + sort.Slice(result, func(i, j int) bool { + return result[i].Enum < result[j].Enum + }) + + return result +} + +func (m *mongoDB) ReloadPropertyTypeLS() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + cursor, err := m.getPropertiesCollection().Find(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("get all prices error: %w", err) + } + var properties []resources.PropertyType + if err = cursor.All(ctx, &properties); err != nil { + return fmt.Errorf("get all prices error: %w", err) + } + if len(properties) != 0 { + resources.DefaultPropertyTypeLS = resources.NewPropertyTypeLS(properties) + logger.Info("successfully reloaded property type ls", "count", len(properties)) + } else { + logger.Info("no properties found in database, using default properties") + } + return nil +} + +func (m *mongoDB) SavePropertyTypes(types []resources.PropertyType) error { + tps := make([]any, len(types)) + for i, b := range types { + tps[i] = b + } + _, err := m.getPropertiesCollection().InsertMany(context.Background(), tps) + return err +} + +func (m *mongoDB) GenerateBillingData( + startTime, endTime time.Time, + prols *resources.PropertyTypeLS, + ownerToNS map[string][]string, +) (map[string][]*resources.Billing, error) { + ownerMonitors, err := m.FetchOwnerMonitorRecords(startTime, endTime, ownerToNS) + if err != nil { + return nil, fmt.Errorf("failed to fetch monitor records: %w", err) + } + ownerBillings := make(map[string][]*resources.Billing) + for owner, monitors := range ownerMonitors { + billings, err := GenerateBillingDataFromRecords(monitors, prols, startTime, endTime, owner) + if err != nil { + return nil, fmt.Errorf("failed to generate billing data: %w", err) + } + if len(billings) == 0 { + continue + } + ownerBillings[owner] = billings + } + return ownerBillings, nil +} + +func (m *mongoDB) FetchOwnerMonitorRecords( + startTime, endTime time.Time, + ownerToNS map[string][]string, +) (map[string][]resources.Monitor, error) { + // Build the reverse index once so each monitor record has an O(1) owner lookup. + nsToOwner := make(map[string]string) + for owner, nsList := range ownerToNS { + for _, ns := range nsList { + nsToOwner[ns] = owner + } + } + namespaces := make([]string, 0, len(nsToOwner)) + for ns := range nsToOwner { + namespaces = append(namespaces, ns) + } + + // get all matching monitor records from mongodb + collection := m.getMonitorCollection(startTime) + filter := bson.M{ + "time": bson.M{"$gte": startTime, "$lt": endTime}, + "category": bson.M{"$in": namespaces}, // 查询所有涉及的 namespaces + } + + cursor, err := collection.Find(context.Background(), filter) + if err != nil { + return nil, fmt.Errorf("failed to find monitor records: %w", err) + } + defer cursor.Close(context.Background()) + + // reading mongodb data + var allRecords []resources.Monitor + if err := cursor.All(context.Background(), &allRecords); err != nil { + return nil, fmt.Errorf("failed to decode monitor records: %w", err) + } + + return groupMonitorRecordsByOwner(allRecords, nsToOwner), nil +} + +func groupMonitorRecordsByOwner( + records []resources.Monitor, + nsToOwner map[string]string, +) map[string][]resources.Monitor { + ownerMonitorRecords := make(map[string][]resources.Monitor) + for _, record := range records { + if owner, ok := nsToOwner[record.Category]; ok { + ownerMonitorRecords[owner] = append(ownerMonitorRecords[owner], record) + } + } + return ownerMonitorRecords +} + +func GenerateBillingDataFromRecords( + records []resources.Monitor, + prols *resources.PropertyTypeLS, + startTime, endTime time.Time, + owner string, +) (billings []*resources.Billing, err error) { + // Calculate the interval (minutes) to ensure that the divisor is not 0 + minutes := math.Max(endTime.Sub(startTime).Minutes(), 1) + + // 存储分组后的数据 + aggregatedMap := make(map[string]*struct { + resources.Monitor + UsedValues map[uint8][]int64 + Count int64 + }) + + // 分组 key 生成规则 + genGroupKey := func(rec resources.Monitor) string { + return fmt.Sprintf("%s/%d/%s", rec.Category, rec.Type, rec.Name) + } + + // 遍历所有记录,按分组键聚合 + for _, rec := range records { + key := genGroupKey(rec) + if _, ok := aggregatedMap[key]; !ok { + aggregatedMap[key] = &struct { + resources.Monitor + UsedValues map[uint8][]int64 + Count int64 + }{ + Monitor: rec, + UsedValues: make(map[uint8][]int64), + Count: 0, + } + } + aggregatedMap[key].Count++ + for k, v := range rec.Used { + // aggregatedMap[key].UsedValues[k] = append(aggregatedMap[key].UsedValues[k], v) + if _, exists := aggregatedMap[key].UsedValues[k]; !exists { + aggregatedMap[key].UsedValues[k] = make([]int64, 0, len(records)) + } + aggregatedMap[key].UsedValues[k] = append(aggregatedMap[key].UsedValues[k], v) + } + } + + // 存储最终计费数据 + type billingGroupKey struct { + appType uint8 + appName string + } + appCostsMap := make(map[string]map[billingGroupKey][]resources.AppCost) + nsTypeAmount := make(map[string]map[billingGroupKey]int64) + + calculateFinalUsed := func(values map[uint8][]int64, prols *resources.PropertyTypeLS, minutes float64) map[uint8]int64 { + finalUsed := make(map[uint8]int64) + for propKey, vals := range values { + if prop, ok := prols.EnumMap[propKey]; ok { + finalUsed[propKey] = computeUsedValue(vals, prop, minutes) + } + } + return finalUsed + } + // 计算最终 Used 数据 + for _, agg := range aggregatedMap { + finalUsed := calculateFinalUsed(agg.UsedValues, prols, minutes) + // 计算费用 + appCost := resources.AppCost{ + Type: agg.Type, + Name: agg.Name, + Used: finalUsed, + UsedAmount: make(map[uint8]int64), + } + var totalAmount int64 + for propKey, usedVal := range finalUsed { + if prop, ok := prols.EnumMap[propKey]; ok { + if prop.UnitPrice > 0 { + feeFloat := float64(usedVal) * prop.UnitPrice + if feeFloat > math.MaxInt64 { + return nil, fmt.Errorf("fee calculation overflow: %f", feeFloat) + } + fee := int64(math.Ceil(feeFloat)) + appCost.UsedAmount[propKey] = fee + totalAmount += fee + } + } + } + if totalAmount == 0 { + continue + } + appCost.Amount = totalAmount + groupKey := billingGroupKey{appType: agg.Type} + if agg.ParentType != 0 && agg.ParentName != "" { + groupKey = billingGroupKey{appType: agg.ParentType, appName: agg.ParentName} + } + ns := agg.Category + if _, ok := nsTypeAmount[ns]; !ok { + nsTypeAmount[ns] = make(map[billingGroupKey]int64) + } + nsTypeAmount[ns][groupKey] += totalAmount + + if _, ok := appCostsMap[ns]; !ok { + appCostsMap[ns] = make(map[billingGroupKey][]resources.AppCost) + } + appCostsMap[ns][groupKey] = append(appCostsMap[ns][groupKey], appCost) + } + billings = make([]*resources.Billing, 0) + + // 生成 Billing 数据 + for ns, appCostMap := range appCostsMap { + for groupKey, appCostList := range appCostMap { + amount := nsTypeAmount[ns][groupKey] + if amount <= 0 { + continue + } + billings = append(billings, &resources.Billing{ + OrderID: stableBillingOrderID( + owner, endTime, ns, groupKey.appType, groupKey.appName, + ), + Type: Consumption, + Namespace: ns, + AppType: groupKey.appType, + AppName: groupKey.appName, + AppCosts: appCostList, + Amount: amount, + Owner: owner, + Time: endTime, + Status: resources.Settled, + }) + } + } + return billings, nil +} + +func stableBillingOrderID( + owner string, + endTime time.Time, + namespace string, + appType uint8, + appName string, +) string { + key := fmt.Sprintf( + "%s\x00%s\x00%s\x00%d\x00%s", + owner, + endTime.UTC().Format(time.RFC3339), + namespace, + appType, + appName, + ) + sum := sha256.Sum256([]byte(key)) + return fmt.Sprintf("bh_%x", sum[:12]) +} + +func computeUsedValue(usedValues []int64, prop resources.PropertyType, minutes float64) int64 { + switch prop.PriceType { + case resources.DIF: + var maxVal int64 = -math.MaxInt64 + var minVal int64 = math.MaxInt64 + for _, v := range usedValues { + if v > maxVal { + maxVal = v + } + if v != 0 && v < minVal { + minVal = v + } + } + if maxVal > minVal { + return maxVal - minVal + } + return 0 + case resources.SUM: + var sum int64 + for _, v := range usedValues { + sum += v + } + return sum + default: + var sum int64 + for _, v := range usedValues { + sum += v + } + return int64(math.Round(float64(sum) / minutes)) + } +} + +func (m *mongoDB) GetUpdateTimeForCategoryAndPropertyFromMetering( + category, property string, +) (time.Time, error) { + filter := bson.M{"category": category, "property": property} + // sort by time desc + opts := options.FindOne().SetSort(bson.D{primitive.E{Key: "time", Value: -1}}) + + var result struct { + Time time.Time `bson:"time"` + } + err := m.getMeteringCollection().FindOne(context.Background(), filter, opts).Decode(&result) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + // No documents match the filter. Handle this case accordingly. + return time.Time{}, nil + } + return time.Time{}, err + } + return result.Time, nil +} + +func (m *mongoDB) GetBillingCount( + accountType common.Type, + startTime, endTime time.Time, +) (count, amount int64, err error) { + pipeline := bson.A{ + bson.M{ + "$match": bson.M{ + "type": accountType, + "time": bson.M{ + "$gte": startTime, + "$lte": endTime, + }, + }, + }, + bson.M{ + "$group": bson.M{ + "_id": nil, + "count": bson.M{"$sum": 1}, + "amount": bson.M{"$sum": "$amount"}, + }, + }, + } + + cursor, err := m.getBillingCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return 0, 0, err + } + defer cursor.Close(context.Background()) + + var result struct { + Count int64 `bson:"count"` + Amount int64 `bson:"amount"` + } + + if cursor.Next(context.Background()) { + if err := cursor.Decode(&result); err != nil { + return 0, 0, fmt.Errorf("failed to decode aggregation result: %w", err) + } + } + + return result.Count, result.Amount, nil +} + +func (m *mongoDB) getMeteringCollection() *mongo.Collection { + return m.Client.Database(m.AccountDB).Collection(m.MeteringConn) +} + +func (m *mongoDB) getMonitorCollection(collTime time.Time) *mongo.Collection { + // 2020-12-01 00:00:00 - 2020-12-01 23:59:59 + return m.Client.Database(m.AccountDB).Collection(m.getMonitorCollectionName(collTime)) +} + +func (m *mongoDB) getMonitorCollectionName(collTime time.Time) string { + // Calculate the suffix by day, for example, the suffix on the first day of 202012 is 20201201 + return fmt.Sprintf("%s_%s", m.MonitorConnPrefix, collTime.Format("20060102")) +} + +func (m *mongoDB) getBillingCollection() *mongo.Collection { + return m.Client.Database(m.AccountDB).Collection(m.BillingConn) +} + +func (m *mongoDB) getBillingCheckpointCollection() *mongo.Collection { + return m.Client.Database(m.AccountDB).Collection(DefaultBillingCheckpointConn) +} + +func (m *mongoDB) getObjTrafficCollection() *mongo.Collection { + return m.Client.Database(m.AccountDB).Collection(m.ObjTrafficConn) +} + +func (m *mongoDB) getPropertiesCollection() *mongo.Collection { + return m.Client.Database(m.AccountDB).Collection(m.PropertiesConn) +} + +func (m *mongoDB) CreateBillingIfNotExist() error { + ctx := context.Background() + exist, err := m.collectionExist(m.AccountDB, m.BillingConn) + if err != nil { + return err + } + if !exist { + if err := m.Client.Database(m.AccountDB).CreateCollection(ctx, m.BillingConn); err != nil { + return fmt.Errorf("failed to create collection for billing: %w", err) + } + } + + // create index + _, err = m.getBillingCollection().Indexes().CreateMany(ctx, []mongo.IndexModel{ + { + // unique index owner order_id + Keys: bson.D{ + primitive.E{Key: "owner", Value: 1}, + primitive.E{Key: "order_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + // owner + time + type indexes + Keys: bson.D{ + primitive.E{Key: "owner", Value: 1}, + primitive.E{Key: "time", Value: 1}, + primitive.E{Key: "type", Value: 1}, + }, + }, + { + // workspace consumption aggregation: equality filters before time range + Keys: bson.D{ + primitive.E{Key: "owner", Value: 1}, + primitive.E{Key: "status", Value: 1}, + primitive.E{Key: "time", Value: 1}, + }, + }, + { + // recover stable unsettled billings for one billing hour + Keys: bson.D{ + primitive.E{Key: "time", Value: 1}, + primitive.E{Key: "status", Value: 1}, + primitive.E{Key: "type", Value: 1}, + primitive.E{Key: "order_id", Value: 1}, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to create index for billing: %w", err) + } + return nil +} + +// CreateMonitorTimeSeriesIfNotExist creates the time series table for monitor +func (m *mongoDB) CreateMonitorTimeSeriesIfNotExist(collTime time.Time) error { + return m.CreateTimeSeriesIfNotExist(m.AccountDB, m.getMonitorCollectionName(collTime)) +} + +func (m *mongoDB) CreateTimeSeriesIfNotExist(dbName, collectionName string) error { + // Check if the collection already exists + if exist, err := m.collectionExist(dbName, collectionName); exist || err != nil { + return err + } + + // If the collection does not exist, create it + cmd := bson.D{ + primitive.E{Key: "create", Value: collectionName}, + primitive.E{Key: "timeseries", Value: bson.D{{Key: "timeField", Value: "time"}}}, + } + return m.Client.Database(dbName).RunCommand(context.TODO(), cmd).Err() +} + +func (m *mongoDB) CreateTTLTrafficTimeSeries() error { + // Check if the collection already exists + if exist, err := m.collectionExist(m.AccountDB, m.ObjTrafficConn); exist || err != nil { + return err + } + // If the collection does not exist, create it + cmd := bson.D{ + primitive.E{Key: "create", Value: m.ObjTrafficConn}, + primitive.E{Key: "timeseries", Value: bson.D{{Key: "timeField", Value: "time"}}}, + // default ttl set 30 days + primitive.E{Key: "expireAfterSeconds", Value: 30 * 24 * 60 * 60}, + } + return m.Client.Database(m.AccountDB).RunCommand(context.TODO(), cmd).Err() +} + +func (m *mongoDB) DropMonitorCollectionsOlderThan(days int) error { + db := m.Client.Database(m.AccountDB) + // Get the current time minus the number of days + cutoffDate := time.Now().UTC().AddDate(0, 0, -days) + cutoffName := m.getMonitorCollectionName(cutoffDate) + + collections, err := db.ListCollectionNames(context.Background(), bson.M{}) + if err != nil { + return err + } + for i := range collections { + // Check if the collection name starts with the prefix and is older than the cutoff date + if strings.HasPrefix(collections[i], m.MonitorConnPrefix) && collections[i] < cutoffName { + if err := db.Collection(collections[i]).Drop(context.TODO()); err != nil { + return err + } + logger.Info("dropped collection: ", collections[i]) + } + } + return nil +} + +func (m *mongoDB) collectionExist(dbName, collectionName string) (bool, error) { + // Check if the collection already exists + collections, err := m.Client.Database(dbName). + ListCollectionNames(context.Background(), bson.M{"name": collectionName}) + return len(collections) > 0, err +} + +func NewMongoInterface(ctx context.Context, url string) (database.Interface, error) { + client, err := mongo.Connect(ctx, options.Client().ApplyURI(url)) + if err != nil { + return nil, err + } + err = client.Ping(ctx, nil) + return &mongoDB{ + Client: client, + AccountDB: env.GetEnvWithDefault(EnvAccountDBName, DefaultAccountDBName), + TrafficDB: env.GetEnvWithDefault(EnvTrafficDBName, DefaultTrafficDBName), + CvmDB: env.GetEnvWithDefault(EnvCVMDBName, DefaultCVMDBName), + AuthDB: DefaultAuthDBName, + UserConn: DefaultUserConn, + MeteringConn: DefaultMeteringConn, + MonitorConnPrefix: DefaultMonitorConn, + BillingConn: DefaultBillingConn, + ObjTrafficConn: DefaultObjTrafficConn, + PropertiesConn: DefaultPropertiesConn, + TrafficConn: env.GetEnvWithDefault(EnvTrafficConn, DefaultTrafficConn), + CvmConn: env.GetEnvWithDefault(EnvCVMConn, DefaultCVMConn), + }, err +} diff --git a/controllers/pkg/database/mongo/account_test.go b/controllers/pkg/database/mongo/account_test.go new file mode 100644 index 000000000000..2ac4c4ef97d0 --- /dev/null +++ b/controllers/pkg/database/mongo/account_test.go @@ -0,0 +1,598 @@ +// Copyright © 2023 sealos. +// +// 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 mongo + +import ( + "context" + "fmt" + "os" + "reflect" + "strconv" + "testing" + "time" + + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "go.mongodb.org/mongo-driver/mongo" +) + +var testTime = time.Date(2023, time.May, 9, 5, 0, 0, 0, time.UTC) + +func TestGenerateBillingDataPreservesTypedGroupKey(t *testing.T) { + start := time.Date(2026, time.July, 29, 1, 0, 0, 0, time.UTC) + end := start.Add(time.Hour) + properties := resources.NewPropertyTypeLS([]resources.PropertyType{ + { + Name: "cpu", Enum: 0, PriceType: resources.AVG, UnitPrice: 1, + }, + }) + records := []resources.Monitor{ + { + Time: start, Category: "ns-owner", Type: 1, + ParentType: 255, ParentName: "parent/name", Name: "child", + Used: resources.EnumUsedMap{0: 60}, + }, + } + + billings, err := GenerateBillingDataFromRecords( + records, properties, start, end, "owner", + ) + if err != nil { + t.Fatal(err) + } + if len(billings) != 1 { + t.Fatalf("billing count = %d, want 1", len(billings)) + } + if billings[0].AppType != 255 || billings[0].AppName != "parent/name" { + t.Fatalf( + "billing group = (%d, %q), want (255, %q)", + billings[0].AppType, + billings[0].AppName, + "parent/name", + ) + } +} + +func TestMongoDB_SaveBillingsWithAccountBalance(t *testing.T) { + requireMongoTest(t) + type fields struct { + URL string + Client *mongo.Client + DBName string + MonitorConn string + MeteringConn string + BillingConn string + } + type args struct { + accountBalanceSpec *resources.Billing + } + + // Generate a large number of AccountBalanceSpec data + numRecords := 10 + accountBalanceSpecs := make([]*resources.Billing, numRecords+10) + + for i := range numRecords { + accountBalanceSpecs[i] = &resources.Billing{ + Time: testTime, + OrderID: fmt.Sprintf("random_order_id_%d", i+1), + Namespace: "ns-vd1k1dk3", + Owner: "vd1k1dk3", + Type: 0, + AppType: resources.AppType[resources.DB], + AppCosts: []resources.AppCost{ + { + UsedAmount: map[uint8]int64{ + resources.DefaultPropertyTypeLS.StringMap["cpu"].Enum: int64(1000 + i), + resources.DefaultPropertyTypeLS.StringMap["memory"].Enum: int64(2000 + i), + resources.DefaultPropertyTypeLS.StringMap["storage"].Enum: int64(3000 + i), + resources.DefaultPropertyTypeLS.StringMap["network"].Enum: int64(4000 + i), + }, + }, + }, + Amount: int64(6000 + 3*i), + } + } + for i := 10; i < numRecords+10; i++ { + accountBalanceSpecs[i] = &resources.Billing{ + Time: testTime, + OrderID: fmt.Sprintf("random_order_id_recharge%d", i+1), + Owner: "vd1k1dk3", + Type: 1, + Amount: int64(1000 + i), + } + } + + tests := []struct { + name string + fields fields + args args + wantErr bool + }{} + + for i, spec := range accountBalanceSpecs { + tests = append(tests, struct { + name string + fields fields + args args + wantErr bool + }{ + name: fmt.Sprintf( + "Test case %d: Save deduction record with owner 'ns-vd1k1dk3'", + i+1, + ), + args: args{accountBalanceSpec: spec}, + wantErr: false, + }) + } + + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := m.CreateBillingIfNotExist(); err != nil { + t.Fatalf("failed to create billing time series: error = %v", err) + } + if err := m.SaveBillings(tt.args.accountBalanceSpec); (err != nil) != tt.wantErr { + t.Fatalf("SaveBillingsWithAccountBalance() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestMongoDB_getBillingCollection(t *testing.T) { + type fields struct { + URL string + Client *mongo.Client + DBName string + MonitorConn string + MeteringConn string + BillingConn string + } + tests := []struct { + name string + fields fields + want *mongo.Collection + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mongoDB{ + Client: tt.fields.Client, + AccountDB: tt.fields.DBName, + MonitorConnPrefix: tt.fields.MonitorConn, + MeteringConn: tt.fields.MeteringConn, + BillingConn: tt.fields.BillingConn, + } + if got := m.getBillingCollection(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getBillingCollection() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMongoDB_getMeteringCollection(t *testing.T) { + type fields struct { + URL string + Client *mongo.Client + DBName string + MonitorConn string + MeteringConn string + BillingConn string + } + tests := []struct { + name string + fields fields + want *mongo.Collection + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mongoDB{ + Client: tt.fields.Client, + AccountDB: tt.fields.DBName, + MonitorConnPrefix: tt.fields.MonitorConn, + MeteringConn: tt.fields.MeteringConn, + BillingConn: tt.fields.BillingConn, + } + if got := m.getMeteringCollection(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getMeteringCollection() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMongoDB_getMonitorCollection(t *testing.T) { + type fields struct { + URL string + Client *mongo.Client + DBName string + MonitorConn string + MeteringConn string + BillingConn string + } + tests := []struct { + name string + fields fields + collTime time.Time + want *mongo.Collection + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mongoDB{ + Client: tt.fields.Client, + AccountDB: tt.fields.DBName, + MonitorConnPrefix: tt.fields.MonitorConn, + MeteringConn: tt.fields.MeteringConn, + BillingConn: tt.fields.BillingConn, + } + if got := m.getMonitorCollection(tt.collTime); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getMonitorCollection() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewMongoInterface(t *testing.T) { + type args struct { + ctx context.Context + URL string + } + tests := []struct { + name string + args args + want *mongoDB + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewMongoInterface(tt.args.ctx, tt.args.URL) + if (err != nil) != tt.wantErr { + t.Errorf("NewMongoInterface() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewMongoInterface() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMongoDB_GetBillingLastUpdateTime(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + exist, lastUpdateTime, err := m.GetBillingLastUpdateTime("vlemql0v", 0) + if err != nil { + t.Fatalf("failed to get billing last update time: error = %v", err) + } + if !exist { + t.Fatalf(" billing last update time not exist") + } + t.Logf("lastUpdateTime: %v", lastUpdateTime) +} + +func TestMongoDB_DropMonitorCollectionsOlderThan(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + // 0711 + if err = m.DropMonitorCollectionsOlderThan(30); err != nil { + t.Fatalf("failed to drop monitor collections older than 30 days: %v", err) + } +} + +/* +info generate billing data used {2 ns-7uyfrr47 pay-xy map[0:325 1:166 2:0]} + + limits: * 3 + cpu: 500m + memory: 256Mi +*/ + +func TestMongoDB_SetPropertyTypeLS(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + err = m.InitDefaultPropertyTypeLS() + if err != nil { + t.Fatalf("failed to get property type ls: %v", err) + } + t.Logf("propertyTypeLS: %+v", resources.DefaultPropertyTypeLS) + + for _, tp := range resources.DefaultPropertyTypeLS.Types { + t.Logf("propertyTypeLS type: %v", tp) + } + // err = m.SavePropertyTypes(resources.DefaultPropertyTypeLS.Types) + // if err != nil { + // t.Fatalf("failed to save property types: %v", err) + //} +} + +func Test_mongoDB_GetDistinctMonitorCombinations(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + queryTime := time.Now().UTC() + monitorCombinations, err := m.GetDistinctMonitorCombinations( + queryTime.Add(-time.Hour), + queryTime, + ) + if err != nil { + t.Fatalf("failed to get distinct monitor combinations: %v", err) + } + t.Logf("monitorCombinations: %v", monitorCombinations) +} + +func Test_mongoDB_CreateTTLTrafficTimeSeries(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + if err = m.CreateTTLTrafficTimeSeries(); err != nil { + t.Fatalf("failed to create TTL traffic time series: %v", err) + } + t.Logf("create TTL traffic time series success") +} + +func Test_mongoDB_SaveObjTraffic(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + traffic := make([]*types.ObjectStorageTraffic, 0, 10) + for i := range 10 { + traffic = append(traffic, &types.ObjectStorageTraffic{ + Time: time.Now().UTC(), + User: "user-" + strconv.Itoa(i), + Bucket: "bucket-" + strconv.Itoa(i), + TotalSent: int64(1000 + i), + Sent: int64(100 + i), + }) + } + if err = m.SaveObjTraffic(traffic...); err != nil { + t.Fatalf("failed to save object storage traffic: %v", err) + } + t.Logf("save object storage traffic success") +} + +func Test_mongoDB_GetAllLatestObjTraffic(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + traffic, err := m.GetAllLatestObjTraffic(time.Now().Add(-time.Hour), time.Now()) + if err != nil { + t.Fatalf("failed to save object storage traffic: %v", err) + } + t.Logf("save object storage traffic success") + for _, tf := range traffic { + t.Logf("traffic: %#+v", tf) + } +} + +func Test_mongoDB_HandlerTimeObjBucketSentTraffic(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + bytes, err := m.HandlerTimeObjBucketSentTraffic( + time.Now().UTC().Add(-time.Hour), + time.Now().UTC(), + "bucket-6", + ) + if err != nil { + t.Fatalf("failed to handle time object bucket usage: %v", err) + } + t.Logf("handle time object bucket usage success: %v", bytes) +} + +func Test_mongoDB_GetTimeObjBucketBucket(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + buckets, err := m.GetTimeObjBucketBucket(time.Now().UTC().Add(-10*time.Hour), time.Now().UTC()) + if err != nil { + t.Fatalf("failed to get time object bucket bucket: %v", err) + } + t.Logf("get time object bucket bucket success: len: %v", len(buckets)) + for _, bucket := range buckets { + t.Logf("bucket: %#+v", bucket) + } +} + +func Test_mongoDB_GetTimeUsedOwnerList(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + owners, err := m.GetTimeUsedNamespaceList(time.Now().UTC().Add(-time.Hour), time.Now().UTC()) + if err != nil { + t.Fatalf("failed to get time used owner list: %v", err) + } + t.Logf("get time used owner list success: %v", owners) +} + +func Test_mongoDB_GenerateBillingData(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + + prols := resources.DefaultPropertyTypeLS + ownerToNS := map[string][]string{ + "ax1uut8w": {"ns-tnw80mhk", "ns-ax1uut8w"}, + } + billings, err := m.GenerateBillingData( + time.Now().UTC().Add(-time.Hour), + time.Now().UTC(), + prols, + ownerToNS, + ) + if err != nil { + t.Fatalf("failed to generate billing data: %v", err) + } + for _, billing := range billings { + for _, bill := range billing { + t.Logf("%+v\n", bill) + } + } +} + +func Test_mongoDB_GetOwnersWithoutRecentUpdates(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + now := time.Now().UTC() + endHourTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local). + UTC() + owners, err := m.GetOwnersRecentUpdates([]string{"nfhmc74p"}, endHourTime) + if err != nil { + t.Fatalf("failed to get owners without recent updates: %v", err) + } + t.Logf("get owners without recent updates success: %v", owners) +} + +func requireMongoTest(t *testing.T) { + t.Helper() + if os.Getenv("RUN_MONGO_TESTS") != "true" { + t.Skip("set RUN_MONGO_TESTS=true to run MongoDB tests") + } + if os.Getenv("MONGODB_URI") == "" { + t.Skip("requires MONGODB_URI") + } +} diff --git a/controllers/pkg/database/mongo/billing_runtime_test.go b/controllers/pkg/database/mongo/billing_runtime_test.go new file mode 100644 index 000000000000..3766fcc62607 --- /dev/null +++ b/controllers/pkg/database/mongo/billing_runtime_test.go @@ -0,0 +1,304 @@ +package mongo + +import ( + "context" + "fmt" + "net" + "strings" + "testing" + "time" + + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func explainBillingFind(ctx context.Context, account *mongoDB, filter bson.D) (bson.M, error) { + var explain bson.M + err := account.Client.Database(account.AccountDB).RunCommand(ctx, bson.D{ + {Key: "explain", Value: bson.D{ + {Key: "find", Value: account.BillingConn}, + {Key: "filter", Value: filter}, + }}, + {Key: "verbosity", Value: "executionStats"}, + }).Decode(&explain) + return explain, err +} + +func TestBillingPersistenceWithMongoRuntime(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "mongo:7.0", + ExposedPorts: []string{"27017/tcp"}, + WaitingFor: wait.ForListeningPort("27017/tcp"). + WithStartupTimeout(2 * time.Minute), + }, + Started: true, + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate MongoDB: %v", err) + } + }) + host, err := container.Host(ctx) + if err != nil { + t.Fatal(err) + } + port, err := container.MappedPort(ctx, "27017/tcp") + if err != nil { + t.Fatal(err) + } + account, err := NewMongoInterface(ctx, "mongodb://"+net.JoinHostPort(host, port.Port())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := account.Disconnect(ctx); err != nil { + t.Errorf("disconnect MongoDB: %v", err) + } + }) + mongoAccount, ok := account.(*mongoDB) + if !ok { + t.Fatalf("account type = %T", account) + } + // Simulate an existing production collection so initialization must add + // indexes during an upgrade. + if err := mongoAccount.Client.Database(mongoAccount.AccountDB). + CreateCollection(ctx, mongoAccount.BillingConn); err != nil { + t.Fatal(err) + } + if err := account.CreateBillingIfNotExist(); err != nil { + t.Fatal(err) + } + if err := account.CreateBillingIfNotExist(); err != nil { + t.Fatalf("repeat index initialization: %v", err) + } + + end := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + billing := &resources.Billing{ + Time: end, OrderID: "stable-order", Owner: "owner", Namespace: "ns-owner", + Type: Consumption, AppType: 1, Amount: 100, Status: resources.Unsettled, + } + if err := account.SaveBillings(billing); err != nil { + t.Fatal(err) + } + billing.Status = resources.Settled + if err := account.SaveBillings(billing); err != nil { + t.Fatal(err) + } + + indexSpecs, err := mongoAccount.getBillingCollection().Indexes().ListSpecifications(ctx) + if err != nil { + t.Fatal(err) + } + const workspaceConsumptionIndexName = "owner_1_status_1_time_1" + hasWorkspaceConsumptionIndex := false + for _, indexSpec := range indexSpecs { + if indexSpec.Name == workspaceConsumptionIndexName { + hasWorkspaceConsumptionIndex = true + break + } + } + if !hasWorkspaceConsumptionIndex { + t.Fatalf("billing indexes do not include %q", workspaceConsumptionIndexName) + } + monitorTime := end.Add(-time.Hour) + namespaces, err := account.GetTimeUsedNamespaceList(monitorTime, end) + if err != nil || len(namespaces) != 0 { + t.Fatalf("missing monitor collection namespaces=%v err=%v", namespaces, err) + } + count, err := mongoAccount.getBillingCollection().CountDocuments(ctx, bson.M{ + "owner": "owner", "order_id": "stable-order", + }) + if err != nil { + t.Fatal(err) + } + if count != 1 { + t.Fatalf("billing count = %d", count) + } + var stored resources.Billing + if err := mongoAccount.getBillingCollection().FindOne(ctx, bson.M{ + "owner": "owner", "order_id": "stable-order", + }).Decode(&stored); err != nil { + t.Fatal(err) + } + if stored.Status != resources.Unsettled { + t.Fatalf("stored status = %v", stored.Status) + } + recoveryBillings := []*resources.Billing{ + { + Time: end, OrderID: "bh_recover", Owner: "recover-owner", Namespace: "ns-recover", + Type: Consumption, AppType: 1, Amount: 50, Status: resources.Unsettled, + }, + { + Time: end, OrderID: "bh_settled", Owner: "settled-owner", Namespace: "ns-settled", + Type: Consumption, AppType: 1, Amount: 50, Status: resources.Settled, + }, + { + Time: end, OrderID: "legacy-random", Owner: "legacy-owner", Namespace: "ns-legacy", + Type: Consumption, AppType: 1, Amount: 50, Status: resources.Unsettled, + }, + { + Time: end.Add(time.Hour), + OrderID: "bh_other-hour", + Owner: "other-owner", + Namespace: "ns-other", + Type: Consumption, AppType: 1, Amount: 50, Status: resources.Unsettled, + }, + } + if err := account.SaveBillings(recoveryBillings...); err != nil { + t.Fatal(err) + } + unsettled, err := account.GetUnsettledBillingsAt(end) + if err != nil { + t.Fatal(err) + } + if len(unsettled) != 1 || len(unsettled["recover-owner"]) != 1 || + unsettled["recover-owner"][0].OrderID != "bh_recover" { + t.Fatalf("unsettled billings = %#v", unsettled) + } + + if err := account.SaveBillingCheckpoint(end); err != nil { + t.Fatal(err) + } + checkpoint, exists, err := account.GetBillingCheckpoint() + if err != nil || !exists || !checkpoint.Equal(end) { + t.Fatalf("checkpoint=%v exists=%v err=%v", checkpoint, exists, err) + } +} + +func TestBillingQueriesUseIndexesWithMongoRuntime(t *testing.T) { + testcontainers.SkipIfProviderIsNotHealthy(t) + ctx := context.Background() + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "mongo:7.0", + ExposedPorts: []string{"27017/tcp"}, + WaitingFor: wait.ForListeningPort("27017/tcp"). + WithStartupTimeout(2 * time.Minute), + }, + Started: true, + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Errorf("terminate MongoDB: %v", err) + } + }) + host, err := container.Host(ctx) + if err != nil { + t.Fatal(err) + } + port, err := container.MappedPort(ctx, "27017/tcp") + if err != nil { + t.Fatal(err) + } + account, err := NewMongoInterface(ctx, "mongodb://"+net.JoinHostPort(host, port.Port())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := account.Disconnect(ctx); err != nil { + t.Errorf("disconnect MongoDB: %v", err) + } + }) + if err := account.CreateBillingIfNotExist(); err != nil { + t.Fatal(err) + } + + mongoAccount, ok := account.(*mongoDB) + if !ok { + t.Fatalf("account type = %T", account) + } + end := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + documents := make([]any, 0, 50000) + for i := 0; i < 50000; i++ { + owner := "owner-other" + billingTime := end.Add(time.Duration(i%24) * time.Hour) + status := resources.Settled + if i%1000 == 0 { + owner = "owner-target" + billingTime = end + status = resources.Unsettled + } + documents = append(documents, &resources.Billing{ + Time: billingTime, + OrderID: fmt.Sprintf("bh_%05d", i), + Type: Consumption, + Namespace: "ns-owner", + AppType: 1, + Amount: 1, + Owner: owner, + Status: status, + }) + } + if _, err := mongoAccount.getBillingCollection().InsertMany(ctx, documents); err != nil { + t.Fatal(err) + } + + ownerFilter := bson.D{ + {Key: "owner", Value: bson.M{"$in": []string{"owner-target"}}}, + {Key: "time", Value: end}, + {Key: "type", Value: Consumption}, + {Key: "app_type", Value: bson.M{"$nin": []int{int(resources.AppType[resources.CVM]), int(resources.AppType[resources.LLMToken])}}}, + } + unsettledFilter := bson.D{ + {Key: "time", Value: end}, + {Key: "status", Value: resources.Unsettled}, + {Key: "type", Value: Consumption}, + {Key: "order_id", Value: primitive.Regex{Pattern: "^bh_"}}, + {Key: "app_type", Value: bson.M{"$nin": []int{int(resources.AppType[resources.CVM]), int(resources.AppType[resources.LLMToken])}}}, + } + for name, filter := range map[string]bson.D{ + "owner billing lookup": ownerFilter, + "unsettled recovery lookup": unsettledFilter, + } { + explain, err := explainBillingFind(ctx, mongoAccount, filter) + if err != nil { + t.Fatalf("%s explain: %v", name, err) + } + plan := fmt.Sprint(explain["queryPlanner"]) + if !strings.Contains(plan, "IXSCAN") { + t.Fatalf("%s did not use an index: %s", name, plan) + } + stats := fmt.Sprint(explain["executionStats"]) + t.Logf("%s: %s", name, stats) + } +} + +func TestStableBillingOrderID(t *testing.T) { + end := time.Date(2026, time.July, 7, 10, 0, 0, 0, time.UTC) + first := stableBillingOrderID("owner", end, "ns-owner", 1, "app") + second := stableBillingOrderID("owner", end, "ns-owner", 1, "app") + if first != second { + t.Fatalf("IDs differ: %q %q", first, second) + } + if first == stableBillingOrderID("owner", end.Add(time.Hour), "ns-owner", 1, "app") { + t.Fatal("different billing windows share an ID") + } +} + +func TestGroupMonitorRecordsByOwner(t *testing.T) { + records := []resources.Monitor{ + {Category: "ns-a", Name: "a-1"}, + {Category: "ns-b", Name: "b-1"}, + {Category: "ns-a", Name: "a-2"}, + {Category: "unmapped", Name: "ignored"}, + } + grouped := groupMonitorRecordsByOwner(records, map[string]string{ + "ns-a": "owner-a", + "ns-b": "owner-b", + }) + if len(grouped) != 2 || len(grouped["owner-a"]) != 2 || len(grouped["owner-b"]) != 1 { + t.Fatalf("grouped records = %#v", grouped) + } +} diff --git a/controllers/pkg/database/mongo/cvm.go b/controllers/pkg/database/mongo/cvm.go new file mode 100644 index 000000000000..d3774e90c445 --- /dev/null +++ b/controllers/pkg/database/mongo/cvm.go @@ -0,0 +1,86 @@ +// Copyright © 2024 sealos. +// +// 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 mongo + +import ( + "context" + "errors" + "fmt" + + "github.com/labring/sealos/controllers/pkg/types" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func (m *mongoDB) GetPendingStateInstance( + regionUID string, +) (cvmMap map[string][]types.CVMBilling, err error) { + if regionUID == "" { + return nil, errors.New("region UID is empty") + } + filter := bson.M{ + "state": bson.M{ + "$eq": types.CVMBillingStatePending, + }, + "sealosRegionUid": bson.M{ + "$eq": regionUID, + }, + } + cur, err := m.getCVMCollection().Find(context.Background(), filter) + if err != nil { + return nil, fmt.Errorf("failed to find with filter: %w", err) + } + defer cur.Close(context.Background()) + cvm := make([]types.CVMBilling, 0) + cvmMap = make(map[string][]types.CVMBilling) + err = cur.All(context.Background(), &cvm) + if err != nil { + return nil, err + } + for i := range cvm { + userInfo := cvm[i].SealosUserUID + if cvmMap[userInfo] == nil { + cvmMap[userInfo] = make([]types.CVMBilling, 0) + } + cvmMap[userInfo] = append(cvmMap[userInfo], cvm[i]) + } + return cvmMap, nil +} + +func (m *mongoDB) SetDoneStateInstance(instanceIDs ...primitive.ObjectID) error { + if len(instanceIDs) == 0 { + return errors.New("instanceIDs is empty") + } + filter := bson.M{ + "_id": bson.M{ + "$in": instanceIDs, + }, + } + update := bson.M{ + "$set": bson.M{ + "state": types.CVMBillingStateDone, + }, + } + _, err := m.getCVMCollection().UpdateMany(context.Background(), filter, update) + if err != nil { + return fmt.Errorf("failed to update with filter: %w", err) + } + return nil +} + +func (m *mongoDB) getCVMCollection() *mongo.Collection { + return m.Client.Database(m.CvmDB).Collection(m.CvmConn) +} diff --git a/controllers/pkg/database/mongo/traffic.go b/controllers/pkg/database/mongo/traffic.go new file mode 100644 index 000000000000..18ffd907a50d --- /dev/null +++ b/controllers/pkg/database/mongo/traffic.go @@ -0,0 +1,225 @@ +// Copyright © 2024 sealos. +// +// 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 mongo + +import ( + "context" + "fmt" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +/* example: +{ + _id: ObjectId("60eea26373c4cdcb6356827d"), + traffic_meta: { + pod_name: "my-pod", + pod_namespace: "my-namespace", + pod_address: "100.64.0.1", + traffic_tag: "port:80", + pod_type: 1, + pod_type_name: "mongodb" + }, + timestamp: "2024-01-04T04:02:25", + sent_bytes: 31457280, + recv_bytes: 15728640 + } +*/ + +func (m *mongoDB) GetTrafficRecvBytes( + startTime, endTime time.Time, + namespace string, + _type uint8, + name string, +) (int64, error) { + return m.getTrafficBytes(false, startTime, endTime, namespace, _type, name) +} + +func (m *mongoDB) GetTrafficSentBytes( + startTime, endTime time.Time, + namespace string, + _type uint8, + name string, +) (int64, error) { + return m.getTrafficBytes(true, startTime, endTime, namespace, _type, name) +} + +func (m *mongoDB) GetPodTrafficSentBytes( + startTime, endTime time.Time, + namespace, name string, +) (int64, error) { + return m.getPodTrafficBytes(true, startTime, endTime, namespace, name) +} + +func (m *mongoDB) GetPodTrafficRecvBytes( + startTime, endTime time.Time, + namespace, name string, +) (int64, error) { + return m.getPodTrafficBytes(false, startTime, endTime, namespace, name) +} + +func (m *mongoDB) getPodTrafficBytes( + sent bool, + startTime, endTime time.Time, + namespace, name string, +) (int64, error) { + filter := bson.M{ + "traffic_meta.pod_namespace": namespace, + "traffic_meta.pod_name": name, + "timestamp": bson.M{ + "$gte": startTime, + "$lt": endTime, + }, + } + pipeline := mongo.Pipeline{ + bson.D{{Key: "$match", Value: filter}}, + } + if sent { + pipeline = append( + pipeline, + bson.D{ + { + Key: "$group", + Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "total", Value: bson.D{{Key: "$sum", Value: "$sent_bytes"}}}, + }, + }, + }, + ) + } else { + pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.D{{Key: "_id", Value: nil}, {Key: "total", Value: bson.D{{Key: "$sum", Value: "$recv_bytes"}}}}}}) + } + cur, err := m.getTrafficCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return 0, err + } + defer cur.Close(context.Background()) + total := int64(0) + for cur.Next(context.Background()) { + var result struct { + Total int64 `bson:"total"` + } + if err := cur.Decode(&result); err != nil { + return 0, err + } + total += result.Total + } + return total, nil +} + +func (m *mongoDB) getTrafficBytes( + sent bool, + startTime, endTime time.Time, + namespace string, + _type uint8, + name string, +) (int64, error) { + filter := bson.M{ + "traffic_meta.pod_namespace": namespace, + "traffic_meta.pod_type": _type, + "traffic_meta.pod_type_name": name, + "timestamp": bson.M{ + "$gte": startTime, + "$lte": endTime, + }, + } + pipeline := mongo.Pipeline{ + bson.D{{Key: "$match", Value: filter}}, + } + if sent { + pipeline = append( + pipeline, + bson.D{ + { + Key: "$group", + Value: bson.D{ + {Key: "_id", Value: nil}, + {Key: "total", Value: bson.D{{Key: "$sum", Value: "$sent_bytes"}}}, + }, + }, + }, + ) + } else { + pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.D{{Key: "_id", Value: nil}, {Key: "total", Value: bson.D{{Key: "$sum", Value: "$recv_bytes"}}}}}}) + } + cur, err := m.getTrafficCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return 0, err + } + defer cur.Close(context.Background()) + total := int64(0) + for cur.Next(context.Background()) { + var result struct { + Total int64 `bson:"total"` + } + if err := cur.Decode(&result); err != nil { + return 0, err + } + total += result.Total + } + return total, nil +} + +func (m *mongoDB) getTrafficCollection() *mongo.Collection { + return m.Client.Database(m.TrafficDB).Collection(m.TrafficConn) +} + +func (m *mongoDB) GetNamespaceTraffic( + ctx context.Context, + startTime, endTime time.Time, +) (resultMap map[string]int64, err error) { + collection := m.getTrafficCollection() + pipeline := mongo.Pipeline{ + {{Key: "$match", Value: bson.D{ + { + Key: "timestamp", + Value: bson.D{{Key: "$gte", Value: startTime}, {Key: "$lt", Value: endTime}}, + }, + {Key: "sent_bytes", Value: bson.D{{Key: "$gt", Value: 0}}}, + }}}, + {{Key: "$group", Value: bson.D{ + {Key: "_id", Value: "$traffic_meta.pod_namespace"}, + {Key: "total_sent_bytes", Value: bson.D{ + {Key: "$sum", Value: "$sent_bytes"}, + }}, + }}}, + } + cursor, err := collection.Aggregate(ctx, pipeline) + if err != nil { + return nil, fmt.Errorf("failed to execute aggregation: %w", err) + } + defer cursor.Close(ctx) + resultMap = make(map[string]int64) + for cursor.Next(ctx) { + var result struct { + Namespace string `bson:"_id"` + TotalSentBytes int64 `bson:"total_sent_bytes"` + } + if err := cursor.Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode result: %w", err) + } + if strings.HasPrefix(result.Namespace, "ns-") && result.TotalSentBytes > 0 { + resultMap[result.Namespace] = result.TotalSentBytes + } + } + if err := cursor.Err(); err != nil { + return nil, fmt.Errorf("failed to iterate cursor: %w", err) + } + return resultMap, nil +} diff --git a/controllers/pkg/database/mongo/traffic_test.go b/controllers/pkg/database/mongo/traffic_test.go new file mode 100644 index 000000000000..78b823965ee6 --- /dev/null +++ b/controllers/pkg/database/mongo/traffic_test.go @@ -0,0 +1,85 @@ +// Copyright © 2024 sealos. +// +// 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 mongo + +import ( + "context" + "os" + "strings" + "testing" + "time" +) + +// import ( +// "context" +// "os" +// "testing" +// "time" +//) +// +// func Test_mongoDB_GetPodTrafficSentBytes(t *testing.T) { +// dbCTX := context.Background() +// +// m, err := NewMongoInterface(dbCTX, os.Getenv("MONGO_URL")) +// if err != nil { +// t.Errorf("failed to connect mongo: error = %v", err) +// } +// defer func() { +// if err = m.Disconnect(dbCTX); err != nil { +// t.Errorf("failed to disconnect mongo: error = %v", err) +// } +// }() +// +// //2024-01-10T06:10:24.281+00:00-2024-01-10T06:12:24.281+00:00 +// startTime, _ := time.Parse(time.RFC3339, "2024-01-10T06:10:24.281+00:00") +// endTime, _ := time.Parse(time.RFC3339, "2024-01-10T07:12:24.281+00:00") +// t.Logf("startTime = %v, endTime = %v", startTime, endTime) +// bytes, err := m.GetTrafficSentBytes(startTime, endTime, "ns-8k7qhyy3", 3, "ros-minio-qzqtpjlv") +// if err != nil { +// t.Errorf("failed to get pod traffic sent bytes: error = %v", err) +// } +// t.Logf("bytes = %v", bytes) +//} + +func Test_mongoDB_GetNamespaceTraffic(t *testing.T) { + requireMongoTest(t) + dbCTX := context.Background() + + m, err := NewMongoInterface(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + now := time.Now().UTC() + trafficMap, err := m.GetNamespaceTraffic(dbCTX, now.Add(-1*time.Minute), now) + if err != nil { + t.Errorf("failed to get namespace traffic: error = %v", err) + } + for namespace, totalBytes := range trafficMap { + t.Logf("Namespace: %s, Total Bytes: %d", namespace, totalBytes) + if totalBytes <= 0 { + t.Errorf("Total bytes for namespace %s is negative: %d", namespace, totalBytes) + } + if !strings.HasPrefix(namespace, "ns-") { + t.Logf("Namespace %s is not prefixed with ns-", namespace) + } + } + execTime := time.Now().UTC() + t.Logf("Execution time: %v, Traffic Map: %d", execTime, len(trafficMap)) +} diff --git a/controllers/pkg/go.mod b/controllers/pkg/go.mod new file mode 100644 index 000000000000..396dbe492f95 --- /dev/null +++ b/controllers/pkg/go.mod @@ -0,0 +1,187 @@ +module github.com/labring/sealos/controllers/pkg + +go 1.25.0 + +replace ( + k8s.io/api => k8s.io/api v0.28.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 + k8s.io/client-go => k8s.io/client-go v0.28.3 + k8s.io/component-base => k8s.io/component-base v0.28.3 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.17.2 +) + +replace ( + github.com/labring/sealos/controllers/account => ../account + github.com/labring/sealos/controllers/user => ../user +) + +require ( + github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 + github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 + github.com/alibabacloud-go/tea v1.3.14 + github.com/alibabacloud-go/tea-utils/v2 v2.0.9 + github.com/containers/storage v1.50.2 + github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df + github.com/go-logr/logr v1.4.3 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 + github.com/matoous/go-nanoid/v2 v2.0.0 + github.com/minio/madmin-go/v3 v3.0.35 + github.com/minio/minio-go/v7 v7.0.64 + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 + github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_model v0.5.0 + github.com/prometheus/common v0.48.0 + github.com/prometheus/prom2json v1.3.3 + github.com/sirupsen/logrus v1.9.4 + github.com/smartwalle/alipay/v3 v3.2.25 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.11.1 + github.com/stripe/stripe-go/v74 v74.30.0 + github.com/stripe/stripe-go/v82 v82.5.1 + github.com/volcengine/volc-sdk-golang v1.0.229 + github.com/wechatpay-apiv3/wechatpay-go v0.2.17 + go.mongodb.org/mongo-driver v1.12.1 + go.uber.org/zap v1.26.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.5.4 + gorm.io/gorm v1.25.5 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + sigs.k8s.io/controller-runtime v0.17.2 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect + github.com/alibabacloud-go/openapi-util v0.1.0 // indirect + github.com/alibabacloud-go/tea-utils v1.3.1 // indirect + github.com/aliyun/credentials-go v1.4.5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.6.6 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runc v1.1.9 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/safchain/ethtool v0.3.0 // indirect + github.com/secure-io/sio-go v0.3.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.1 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/smartwalle/ncrypto v1.0.4 // indirect + github.com/smartwalle/ngx v1.0.9 // indirect + github.com/smartwalle/nsign v1.0.9 // indirect + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/testcontainers/testcontainers-go v0.42.0 // indirect + github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/client-go v12.0.0+incompatible // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/controllers/pkg/go.sum b/controllers/pkg/go.sum new file mode 100644 index 000000000000..bcb6dd27859b --- /dev/null +++ b/controllers/pkg/go.sum @@ -0,0 +1,1284 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6 h1:UTl97mt2qfavxveqCkaVg4tKaZUPzA9RKbFIRaIdtdg= +github.com/alibabacloud-go/dysmsapi-20170525/v3 v3.0.6/go.mod h1:UWpcGrWwTbES9QW7OQ7xDffukMJ/l7lzioixIz8+lgY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c= +github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk= +github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containers/storage v1.50.2 h1:Fys4BjFUVNRBEXlO70hFI48VW4EXsgnGisTpk9tTMsE= +github.com/containers/storage v1.50.2/go.mod h1:dpspZsUrcKD8SpTofvKWhwPDHD0MkO4Q7VE+oYdWkiA= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= +github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= +github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/madmin-go/v3 v3.0.35 h1:cCo5ZZpHA+rlBQbsAcwFwiuh/uHJmjVoDDx1G4+zaho= +github.com/minio/madmin-go/v3 v3.0.35/go.mod h1:4QN2NftLSV7MdlT50dkrenOMmNVHluxTvlqJou3hte8= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q= +github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= +github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= +github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.1.9 h1:XR0VIHTGce5eWPkaPesqTBrhW2yAcaraWfsEalNwQLM= +github.com/opencontainers/runc v1.1.9/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= +github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= +github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= +github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= +github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q= +github.com/smartwalle/alipay/v3 v3.2.25/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= +github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= +github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= +github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= +github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= +github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/stripe/stripe-go/v82 v82.5.1 h1:05q6ZDKoe8PLMpQV072obF74HCgP4XJeJYoNuRSX2+8= +github.com/stripe/stripe-go/v82 v82.5.1/go.mod h1:majCQX6AfObAvJiHraPi/5udwHi4ojRvJnnxckvHrX8= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/volcengine/volc-sdk-golang v1.0.229 h1:gOkDltTS6Fta8OyfYrbeY9bqCHHyiJuGYNJpR5MR+Fo= +github.com/volcengine/volc-sdk-golang v1.0.229/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= +github.com/wechatpay-apiv3/wechatpay-go v0.2.17 h1:i4YJA/6BqAbi2YfyPZBjpeEeO/+oa4UbKP4gSTRhhQg= +github.com/wechatpay-apiv3/wechatpay-go v0.2.17/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/pkg/gpu/nvidia.go b/controllers/pkg/gpu/nvidia.go new file mode 100644 index 000000000000..ea2afbd248ee --- /dev/null +++ b/controllers/pkg/gpu/nvidia.go @@ -0,0 +1,179 @@ +// Copyright © 2023 sealos. +// +// 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 gpu + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/json" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// nvidia labels for gpu +const ( + NvidiaGpuKey = "nvidia.com/gpu" + NvidiaCudaDriverMajorKey = "nvidia.com/cuda.driver.major" + NvidiaCudaDriverMinorKey = "nvidia.com/cuda.driver.minor" + NvidiaCudaDriverRevKey = "nvidia.com/cuda.driver.rev" + NvidiaCudaRuntimeMajorKey = "nvidia.com/cuda.runtime.major" + NvidiaCudaRuntimeMinorKey = "nvidia.com/cuda.runtime.minor" + NvidiaGfdTimestampKey = "nvidia.com/gfd.timestamp" + NvidiaGpuComputeMajorKey = "nvidia.com/gpu.compute.major" + NvidiaGpuComputeMinorKey = "nvidia.com/gpu.compute.minor" + NvidiaGpuCountKey = "nvidia.com/gpu.count" + NvidiaGpuDeployContainerToolkitKey = "nvidia.com/gpu.deploy.container-toolkit" + NvidiaGpuDeployDcgmKey = "nvidia.com/gpu.deploy.dcgm" + NvidiaGpuDeployDcgmExporterKey = "nvidia.com/gpu.deploy.dcgm-exporter" + NvidiaGpuDeployDevicePluginKey = "nvidia.com/gpu.deploy.device-plugin" + NvidiaGpuDeployDriverKey = "nvidia.com/gpu.deploy.driver" + NvidiaGpuDeployGpuFeatureDiscoveryKey = "nvidia.com/gpu.deploy.gpu-feature-discovery" + NvidiaGpuDeployNodeStatusExporterKey = "nvidia.com/gpu.deploy.node-status-exporter" + NvidiaGpuDeployOperatorValidatorKey = "nvidia.com/gpu.deploy.operator-validator" + NvidiaGpuFamilyKey = "nvidia.com/gpu.family" + NvidiaGpuMachineKey = "nvidia.com/gpu.machine" + NvidiaGpuMemoryKey = "nvidia.com/gpu.memory" + NvidiaGpuPresentKey = "nvidia.com/gpu.present" + NvidiaGpuProductKey = "nvidia.com/gpu.product" + NvidiaGpuReplicasKey = "nvidia.com/gpu.replicas" + NvidiaMigCapableKey = "nvidia.com/mig.capable" + NvidiaMigStrategyKey = "nvidia.com/mig.strategy" +) + +type NvidiaGPU struct { + GpuInfo Information + CudaInfo CudaInformation + GpuDeploy Deployment + GpuDetails DetailInformation + MigInfo MigInformation +} + +type Information struct { + Gpu string + GpuCount string + GpuPresent string + GpuProduct string + GpuReplicas string +} + +type CudaInformation struct { + CudaDriverMajor string + CudaDriverMinor string + CudaDriverRev string + CudaRuntimeMajor string + CudaRuntimeMinor string +} + +type Deployment struct { + GpuDeployContainerToolkit string + GpuDeployDcgm string + GpuDeployDcgmExporter string + GpuDeployDevicePlugin string + GpuDeployDriver string + GpuDeployGpuFeatureDiscovery string + GpuDeployNodeStatusExporter string + GpuDeployOperatorValidator string +} + +type DetailInformation struct { + GpuComputeMajor string + GpuComputeMinor string + GpuFamily string + GpuMachine string + GpuMemory string + GfdTimestamp string +} + +type MigInformation struct { + MigCapable string + MigStrategy string +} + +func GetGpuNodeExist(c client.Client) (bool, error) { + nodeList := &corev1.NodeList{} + err := c.List(context.Background(), nodeList) + if err != nil { + return false, err + } + + for _, node := range nodeList.Items { + if _, ok := node.Labels[NvidiaGpuKey]; ok { + return true, nil + } + } + return false, nil +} + +// nvidia.com/gpu + +func GetNodeGpuModel(c client.Client) (map[string]NvidiaGPU, error) { + nodeList := &corev1.NodeList{} + err := c.List(context.Background(), nodeList) + if err != nil { + return nil, err + } + + gpuModels := make(map[string]NvidiaGPU) + for _, node := range nodeList.Items { + gpu := NvidiaGPU{ + GpuInfo: Information{ + Gpu: node.Labels[NvidiaGpuKey], + GpuCount: node.Labels[NvidiaGpuCountKey], + GpuPresent: node.Labels[NvidiaGpuPresentKey], + GpuProduct: node.Labels[NvidiaGpuProductKey], + GpuReplicas: node.Labels[NvidiaGpuReplicasKey], + }, + CudaInfo: CudaInformation{ + CudaDriverMajor: node.Labels[NvidiaCudaDriverMajorKey], + CudaDriverMinor: node.Labels[NvidiaCudaDriverMinorKey], + CudaDriverRev: node.Labels[NvidiaCudaDriverRevKey], + CudaRuntimeMajor: node.Labels[NvidiaCudaRuntimeMajorKey], + CudaRuntimeMinor: node.Labels[NvidiaCudaRuntimeMinorKey], + }, + // fill in the rest similarly... + } + gpuModels[node.Name] = gpu + } + return gpuModels, nil +} + +const ( + Alias = "alias" + NodeInfoConfigmapNamespace = "node-system" + NodeInfoConfigmapName = "node-gpu-info" +) + +func GetGPUAlias(c client.Client) (map[string]string, error) { + /* + kubectl get cm -n node-system node-gpu-info -oyaml + apiVersion: v1 + data: + alias: '{"NVIDIA-GeForce-RTX-4090":"GeForce-RTX-4090"}' + */ + cm := &corev1.ConfigMap{} + if err := c.Get(context.Background(), client.ObjectKey{ + Namespace: NodeInfoConfigmapNamespace, + Name: NodeInfoConfigmapName, + }, cm); err != nil { + return nil, err + } + alias := make(map[string]string) + if aliasData := cm.Data[Alias]; aliasData != "" { + if err := json.Unmarshal([]byte(cm.Data[Alias]), &alias); err != nil { + return nil, err + } + } + return alias, nil +} diff --git a/controllers/pkg/license/claims.go b/controllers/pkg/license/claims.go new file mode 100644 index 000000000000..4e5c715ff1d9 --- /dev/null +++ b/controllers/pkg/license/claims.go @@ -0,0 +1,62 @@ +// Copyright © 2026 sealos. +// +// 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 license + +import ( + "github.com/golang-jwt/jwt/v4" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + "github.com/mitchellh/mapstructure" +) + +// Claims is the data structure of license claims +type Claims struct { + Type licensev1.LicenseType `json:"type"` + ClusterID string `json:"clusterID"` + Data ClaimData `json:"data"` + + jwt.RegisteredClaims +} + +type ClaimData map[string]any + +func (c *ClaimData) SwitchToClusterData(data *ClusterClaimData) error { + return mapstructure.Decode(c, data) +} + +// ClusterClaimData is the data structure of cluster license +// example: +// +// { +// "nodeCount": 3, +// "totalCPU": 6, +// "totalMemory": 12, +// "userCount": 100, +// } +type ClusterClaimData struct { + NodeCount int `json:"nodeCount" mapstructure:"nodeCount"` + TotalCPU int `json:"totalCPU" mapstructure:"totalCPU"` // in core + TotalMemory int `json:"totalMemory" mapstructure:"totalMemory"` // in GB + UserCount int `json:"userCount" mapstructure:"userCount"` +} + +// Compare compares the claims with the data +// return true if the claims is equal or lager to the data +func (c *ClusterClaimData) Compare(data *ClusterClaimData) bool { + if c.NodeCount > data.NodeCount || c.TotalCPU > data.TotalCPU || + c.TotalMemory > data.TotalMemory { + return false + } + return true +} diff --git a/controllers/pkg/license/key.go b/controllers/pkg/license/key.go new file mode 100644 index 000000000000..67e981661271 --- /dev/null +++ b/controllers/pkg/license/key.go @@ -0,0 +1,22 @@ +// Copyright © 2026 sealos. +// +// 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 license + +// encryptionKey is the key used to decode license token, base64 encoded +var encryptionKey = "ZHd6b2Nyc3NrcXdwZm5lb2twZmV5dGNxa2JnZ3Znem8=" + +func GetEncryptionKey() string { + return encryptionKey +} diff --git a/controllers/pkg/license/token.go b/controllers/pkg/license/token.go new file mode 100644 index 000000000000..c6c42b8e4800 --- /dev/null +++ b/controllers/pkg/license/token.go @@ -0,0 +1,59 @@ +// Copyright © 2026 sealos. +// +// 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 license + +import ( + "encoding/base64" + "errors" + + "github.com/golang-jwt/jwt/v4" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + "github.com/labring/sealos/controllers/pkg/crypto" +) + +func ParseToken(tokenString string) (*jwt.Token, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, + func(_ *jwt.Token) (any, error) { + decodeKey, err := base64.StdEncoding.DecodeString(GetEncryptionKey()) + if err != nil { + return nil, err + } + publicKey, err := crypto.ParseRSAPublicKeyFromAnyPEM(string(decodeKey)) + if err != nil { + return nil, err + } + return publicKey, nil + }) + if err != nil { + return nil, err + } + return token, nil +} + +func GetClaimsFromToken(tokenString string) (*Claims, error) { + token, err := ParseToken(tokenString) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*Claims) + if !ok { + return nil, errors.New("license claims type mismatch") + } + return claims, nil +} + +func GetClaimsFromLicense(license *licensev1.License) (*Claims, error) { + return GetClaimsFromToken(license.Spec.Token) +} diff --git a/controllers/pkg/notification/Makefile b/controllers/pkg/notification/Makefile new file mode 100644 index 000000000000..a4d4bafca6f3 --- /dev/null +++ b/controllers/pkg/notification/Makefile @@ -0,0 +1,107 @@ + +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.26.1 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v4.2.0 +CONTROLLER_TOOLS_VERSION ?= v0.11.3 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +$(KUSTOMIZE): $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) --output install_kustomize.sh && bash install_kustomize.sh $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); rm install_kustomize.sh; } + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/pkg/notification/README.md b/controllers/pkg/notification/README.md new file mode 100644 index 000000000000..bd5695b1c26d --- /dev/null +++ b/controllers/pkg/notification/README.md @@ -0,0 +1,20 @@ +## Notification + +Notification center of Sealos Cloud,Other controllers will send notification CR to user namespace. + +```go +type NotificationSpec struct { + Title string json:"title" + Message string json:"message" + TimeStamp int64 json:"timestamp" + From string json:"from,omitempty" // from which controller send the notification + Importance Type json:"importance,omitempty" +} +``` + +Frontend need to know the notification is read or unread,will using label to select,label key is `isRead`,value is True/False. + +How to deploy +```shell +kubectl apply -f deploy/manifests/deploy.yaml +``` \ No newline at end of file diff --git a/controllers/pkg/notification/api/v1/groupversion_info.go b/controllers/pkg/notification/api/v1/groupversion_info.go new file mode 100644 index 000000000000..e2a76c1e415f --- /dev/null +++ b/controllers/pkg/notification/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2023. + +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 v1 contains API Schema definitions for the notification v1 API group +// +kubebuilder:object:generate=true +// +groupName=notification.sealos.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "notification.sealos.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/controllers/pkg/notification/api/v1/notification_types.go b/controllers/pkg/notification/api/v1/notification_types.go new file mode 100644 index 000000000000..05e977c2e143 --- /dev/null +++ b/controllers/pkg/notification/api/v1/notification_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Type string + +const ( + High Type = "High" + Medium Type = "Medium" + Low Type = "Low" +) + +// NotificationSpec defines the desired state of Notification +// UserName and whether read will be set in label,because set in label is ease to query +type NotificationSpec struct { + Title string `json:"title"` + Message string `json:"message"` + Timestamp int64 `json:"timestamp"` + From string `json:"from,omitempty"` + Importance Type `json:"importance,omitempty"` + DesktopPopup bool `json:"desktopPopup,omitempty"` + I18n map[string]I18n `json:"i18ns,omitempty"` + StartTime *metav1.Time `json:"startTime,omitempty"` + EndTime *metav1.Time `json:"endTime,omitempty"` +} + +type I18n struct { + Title string `json:"title"` + Message string `json:"message"` + From string `json:"from,omitempty"` +} + +// NotificationStatus defines the observed state of Notification +type NotificationStatus struct{} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Notification is the Schema for the notifications API +type Notification struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NotificationSpec `json:"spec,omitempty"` + Status NotificationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// NotificationList contains a list of Notification +type NotificationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Notification `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Notification{}, &NotificationList{}) +} diff --git a/controllers/pkg/notification/api/v1/zz_generated.deepcopy.go b/controllers/pkg/notification/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..535c703a7654 --- /dev/null +++ b/controllers/pkg/notification/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,115 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2023. + +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 v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Notification) DeepCopyInto(out *Notification) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Notification. +func (in *Notification) DeepCopy() *Notification { + if in == nil { + return nil + } + out := new(Notification) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Notification) 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 *NotificationList) DeepCopyInto(out *NotificationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Notification, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationList. +func (in *NotificationList) DeepCopy() *NotificationList { + if in == nil { + return nil + } + out := new(NotificationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NotificationList) 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 *NotificationSpec) DeepCopyInto(out *NotificationSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationSpec. +func (in *NotificationSpec) DeepCopy() *NotificationSpec { + if in == nil { + return nil + } + out := new(NotificationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NotificationStatus) DeepCopyInto(out *NotificationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationStatus. +func (in *NotificationStatus) DeepCopy() *NotificationStatus { + if in == nil { + return nil + } + out := new(NotificationStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/pkg/notification/config/crd/bases/notification.sealos.io_notifications.yaml b/controllers/pkg/notification/config/crd/bases/notification.sealos.io_notifications.yaml new file mode 100644 index 000000000000..82daf9feea4b --- /dev/null +++ b/controllers/pkg/notification/config/crd/bases/notification.sealos.io_notifications.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: notifications.notification.sealos.io +spec: + group: notification.sealos.io + names: + kind: Notification + listKind: NotificationList + plural: notifications + singular: notification + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Notification is the Schema for the notifications API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NotificationSpec defines the desired state of Notification + UserName and whether read will be set in label,because set in label + is ease to query + properties: + desktopPopup: + type: boolean + endTime: + format: date-time + type: string + from: + type: string + i18ns: + additionalProperties: + properties: + from: + type: string + message: + type: string + title: + type: string + required: + - message + - title + type: object + type: object + importance: + type: string + message: + type: string + startTime: + format: date-time + type: string + timestamp: + format: int64 + type: integer + title: + type: string + required: + - message + - timestamp + - title + type: object + status: + description: NotificationStatus defines the observed state of Notification + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/pkg/notification/config/crd/kustomization.yaml b/controllers/pkg/notification/config/crd/kustomization.yaml new file mode 100644 index 000000000000..31b816ed5b56 --- /dev/null +++ b/controllers/pkg/notification/config/crd/kustomization.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/notification.sealos.io_notifications.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_notifications.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_notifications.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controllers/pkg/notification/config/crd/kustomizeconfig.yaml b/controllers/pkg/notification/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..9c99fc52fb20 --- /dev/null +++ b/controllers/pkg/notification/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controllers/pkg/notification/config/crd/patches/cainjection_in_notifications.yaml b/controllers/pkg/notification/config/crd/patches/cainjection_in_notifications.yaml new file mode 100644 index 000000000000..4c23cd683fc1 --- /dev/null +++ b/controllers/pkg/notification/config/crd/patches/cainjection_in_notifications.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: notifications.notification.sealos.io diff --git a/controllers/pkg/notification/utils/build_test.go b/controllers/pkg/notification/utils/build_test.go new file mode 100644 index 000000000000..aac9a1d56b1e --- /dev/null +++ b/controllers/pkg/notification/utils/build_test.go @@ -0,0 +1,26 @@ +/* +Copyright 2023. + +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 utils + +import ( + "fmt" + "testing" +) + +func TestEncodeTime(_ *testing.T) { + fmt.Println(encodeTime()) +} diff --git a/controllers/pkg/notification/utils/builder.go b/controllers/pkg/notification/utils/builder.go new file mode 100644 index 000000000000..e41e693e20a4 --- /dev/null +++ b/controllers/pkg/notification/utils/builder.go @@ -0,0 +1,195 @@ +/* +Copyright 2023. + +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 utils + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "io" + "strconv" + "strings" + "time" + + v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Kind string + +const ( + General Kind = "General" + Admin Kind = "Admin" +) + +const ( + GeneralPrefix = "ns-" // general notification prefix +) + +const ( + idLength = 12 + letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789" +) + +// Receiver is the struct that contains the notification information. +type Receiver struct { + context.Context + client.Client + receivers []string +} + +func NewReceiver(ctx context.Context, client client.Client) *Receiver { + return &Receiver{ + Context: ctx, + Client: client, + } +} + +func (rv *Receiver) AddReceiver(receiver string) *Receiver { + rv.receivers = append(rv.receivers, receiver) + return rv +} + +// Cache of the NamespaceCache caches the namespaces in the cluster +// categorized by filters. +func (rv *Receiver) AddReceivers(receivers []string) *Receiver { + rv.receivers = append(rv.receivers, receivers...) + return rv +} + +// NotificationPackage is the struct that contains the notification information. +type NoticeEventQueue struct { + Events []Event +} + +// AddToEvents adds the notification to the provided NoticeEventQueue. +func (nb *Builder) AddToEventQueue(neq *NoticeEventQueue) { + neq.Events = append(neq.Events, Event{ + ID: RandStrings(idLength), + Title: nb.Title, + From: nb.From, + Message: nb.Message, + Level: nb.Level, + Kind: nb.Kind, + }) +} + +// Event is the struct that contains the notification information. +type Event struct { + // ID is the unique ID of the notification. + ID string + Title string + From string + Message string + Kind Kind + Level v1.Type +} + +// Builder is the struct that contains the notification information. +type Builder struct { + Kind Kind + Title string + From string + Message string + Level v1.Type +} + +func (nb *Builder) WithTitle(title string) *Builder { + nb.Title = title + return nb +} + +func (nb *Builder) WithFrom(from string) *Builder { + nb.From = from + return nb +} + +func (nb *Builder) WithMessage(message string) *Builder { + nb.Message = message + return nb +} + +func (nb *Builder) WithLevel(level v1.Type) *Builder { + nb.Level = level + return nb +} + +func (nb *Builder) WithType(kind Kind) *Builder { + nb.Kind = kind + return nb +} + +/* +NewNotificationEvent creates a new NotificationEvent with the provided title, message, kind, sender (from), and optional severity level. + +Parameters: +- title: The title of the notification. +- message: The message content of the notification. +- kind: The kind of the notification (e.g., General, Personal, Admin). +- from: The sender of the notification. +- level: The optional severity level of the notification. +- target: An optional parameter specifying the intended recipient(s) of the notification. If provided, the notification will be directed towards these targets. + +Return values: +- A NotificationEvent struct populated with the provided values and a unique ID generated for this notification. +- An error object if the provided level exceeds 1, in which case an error message "level parameter cannot exceed 1" will be returned. + +The function also generates a unique ID for each NotificationEvent. If the generation of the ID fails, it uses the current Unix timestamp as the ID. +*/ +func NewNotificationEvent(title, message string, kind Kind, from string, level v1.Type) Event { + id, err := randStringBytes(idLength) + if err != nil || id == "" { + id = strings.ToLower(strconv.Itoa(int(time.Now().Unix()))) + } + + return Event{ + ID: id, + Title: title, + Message: message, + From: from, + Kind: kind, + Level: level, + } +} + +func RandStrings(n int) string { + str, err := randStringBytes(n) + if err != nil || str == "" { + str = encodeTime() + } + return str +} + +func randStringBytes(n int) (string, error) { + bytes := make([]byte, n) + if _, err := io.ReadFull(rand.Reader, bytes); err != nil { + return "", err + } + for i, b := range bytes { + bytes[i] = letterBytes[b%byte(len(letterBytes))] + } + return string(bytes), nil +} + +// encodeTime encodes current Unix timestamp to a base64 string +func encodeTime() string { + current := time.Now().Unix() + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(current)) // #nosec G115 + return base64.StdEncoding.EncodeToString(buf) +} diff --git a/controllers/pkg/notification/utils/manager.go b/controllers/pkg/notification/utils/manager.go new file mode 100644 index 000000000000..cf9137613ca4 --- /dev/null +++ b/controllers/pkg/notification/utils/manager.go @@ -0,0 +1,136 @@ +/* +Copyright 2023. + +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 utils + +import ( + "context" + "time" + + "github.com/go-logr/logr" + v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" + "github.com/labring/sealos/controllers/pkg/utils/logger" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// the best practice of notification api is the following: +// 1. use a notification builder to build a notification event queue,such as: +// Queue := NotificationQueue{} +// {&Builder}.WithFrom("from").WithMessage("message").WithType(General).WithLevel(Info).AddToEventQueue(&Queue) +// +// OR immediately build a notification event: +// notification := NewNotificationEvent("title", "message", General, "from", Info) +// +// 2. decide the receiver of the notification, +// step one: create a reveiverList, such as: +// receiver := Receiver{} +// if just one, you can use the following: +// receiver.SetReceiver("namespace", General) +// if you have a "Broadcasting Task", you can use the following: +// receiver.Cache(&corev1.NamespaceList{}) +// +// 3. use a notification manager to send the notification event queue to the receiver, such as: +// manager := NotificationManager{} +// manager.Load(receiver, events) +// manager.Run() + +// Features of the notification api: +// 1. support multiple receivers +// 2. support high concurrency +// 3. reduce the pressure of the kubernetes api server +// 4. decoupling of message generation and delivery, along with error tolerance mechanisms +// 5. allow users to focus solely on important tasks. + +type Manager struct { + ctx context.Context + client client.Client + logger logr.Logger + batchSize int + channelSize int + queue []v1.Notification +} + +func NewNotificationManager(ctx context.Context, client client.Client, + logger logr.Logger, batchSize, channelSize int, +) *Manager { + return &Manager{ + ctx: ctx, + client: client, + logger: logger, + batchSize: batchSize, + channelSize: channelSize, + } +} + +// Run of the NotificationManager runs the notification manager. +// It writes the notifications in batches +func (nm *Manager) Run() { + pool := NewPool(nm.batchSize) + pool.Run(nm.channelSize) + for _, notification := range nm.queue { + pool.Add(func() { + err := write(nm.ctx, nm.client, ¬ification) + if err != nil { + logger.Error(err, "Failed to Do Notification Write Opt") + } + }) + } + pool.Wait() +} + +func (nm *Manager) Load(receivers *Receiver, events []Event) *Manager { + for _, event := range events { + nm.loadNotification(receivers.receivers, event) + } + return nm +} + +func (nm *Manager) loadNotification(receivers []string, event Event) { + for _, receiver := range receivers { + nm.queue = append(nm.queue, newNotification(receiver, event)) + } +} + +func write( + ctx context.Context, + client client.Client, + obj client.Object, + opts ...client.CreateOption, +) error { + err := client.Create(ctx, obj, opts...) + if err != nil && apierrors.IsAlreadyExists(err) { + return nil + } + return err +} + +func newNotification(receiver string, event Event) v1.Notification { + return v1.Notification{ + ObjectMeta: metav1.ObjectMeta{ + Name: event.ID, + Namespace: receiver, + }, + Spec: v1.NotificationSpec{ + Importance: event.Level, + Message: event.Message, + Timestamp: time.Now().Unix(), + From: event.From, + Title: event.Title, + }, + } +} diff --git a/controllers/pkg/notification/utils/pool.go b/controllers/pkg/notification/utils/pool.go new file mode 100644 index 000000000000..370b25fd85fc --- /dev/null +++ b/controllers/pkg/notification/utils/pool.go @@ -0,0 +1,54 @@ +/* +Copyright 2023. + +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 utils + +import "sync" + +type Pool struct { + wg sync.WaitGroup + work chan func() +} + +func NewPool(size int) *Pool { + p := &Pool{ + work: make(chan func(), size), + } + return p +} + +func (p *Pool) Add(f func()) { + p.work <- func() { + f() + } +} + +func (p *Pool) Wait() { + close(p.work) + p.wg.Wait() +} + +func (p *Pool) Run(size int) { + p.wg.Add(size) + for range size { + go func() { + for f := range p.work { + f() + } + p.wg.Done() + }() + } +} diff --git a/controllers/pkg/objectstorage/interface.go b/controllers/pkg/objectstorage/interface.go new file mode 100644 index 000000000000..65dc7a93547e --- /dev/null +++ b/controllers/pkg/objectstorage/interface.go @@ -0,0 +1,23 @@ +// Copyright © 2023 sealos. +// +// 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 objectstorage + +import "github.com/minio/minio-go/v7" + +type Interface interface { + GetUserObjectStorageSize(client *minio.Client, username string) (int64, int64, error) + + GetUserObjectStorageFlow(client *minio.Client, promURL, username string) (int64, error) +} diff --git a/controllers/pkg/objectstorage/metric_parser.go b/controllers/pkg/objectstorage/metric_parser.go new file mode 100644 index 000000000000..6d0fccd67a21 --- /dev/null +++ b/controllers/pkg/objectstorage/metric_parser.go @@ -0,0 +1,459 @@ +// +// Copyright (c) 2015-2023 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package objectstorage + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/hex" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "time" + "unicode/utf8" + + jwtgo "github.com/golang-jwt/jwt/v4" + "github.com/minio/minio-go/v7/pkg/s3utils" + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/prom2json" +) + +// MetricsClient implements MinIO metrics operations +type MetricsClient struct { + /// JWT token for authentication + jwtToken string + // Indicate whether we are using https or not + secure bool + // Parsed endpoint url provided by the user. + endpointURL *url.URL + // Needs allocation. + httpClient *http.Client +} + +// metricsRequestData - is container for all the values to make a +// request. +type metricsRequestData struct { + relativePath string // URL path relative to admin API base endpoint +} + +// NewMetricsClient - instantiate minio metrics client honoring Prometheus format +func NewMetricsClient( + endpoint, accessKeyID, secretAccessKey string, + secure bool, +) (*MetricsClient, error) { + jwtToken, err := getPrometheusToken(accessKeyID, secretAccessKey) + if err != nil { + return nil, err + } + + endpointURL, err := getEndpointURL(endpoint, secure) + if err != nil { + return nil, err + } + return privateNewMetricsClient(endpointURL, jwtToken, secure) +} + +// BucketUsageTotalBytesMetrics - returns Bucket Usage Total Metrics in Prometheus format +func (client *MetricsClient) BucketUsageTotalBytesMetrics( + ctx context.Context, +) ([]*prom2json.Family, error) { + return client.fetchMetrics(ctx, "bucket", []string{"minio_bucket_usage_total_bytes"}) +} + +// BucketUsageAndTrafficBytesMetrics - returns Bucket Usage And Traffic Metrics in Prometheus format +func (client *MetricsClient) BucketUsageAndTrafficBytesMetrics( + ctx context.Context, +) ([]*prom2json.Family, error) { + return client.fetchMetrics( + ctx, + "bucket", + []string{ + "minio_bucket_usage_total_bytes", + "minio_bucket_traffic_sent_bytes", + "minio_bucket_traffic_received_bytes", + }, + ) +} + +// fetchMetrics - returns Metrics of given subsystem in Prometheus format +func (client *MetricsClient) fetchMetrics( + ctx context.Context, + subSystem string, + metricsName []string, +) ([]*prom2json.Family, error) { + reqData := metricsRequestData{ + relativePath: "/v2/metrics/" + subSystem, + } + + // Execute GET on /minio/v2/metrics/ + resp, err := client.executeGetRequest(ctx, reqData) + if err != nil { + return nil, err + } + defer closeResponse(resp) + + if resp.StatusCode != http.StatusOK { + return nil, httpRespToErrorResponse(resp) + } + return parsePrometheusResults(resp.Body, metricsName) +} + +// closeResponse close non nil response with any response Body. +// convenient wrapper to drain any remaining data on response body. +// +// Subsequently this allows golang http RoundTripper +// to re-use the same connection for future requests. +func closeResponse(resp *http.Response) { + // Callers should close resp.Body when done reading from it. + // If resp.Body is not closed, the Client's underlying RoundTripper + // (typically Transport) may not be able to re-use a persistent TCP + // connection to the server for a subsequent "keep-alive" request. + if resp != nil && resp.Body != nil { + // Drain any remaining Body and then close the connection. + // Without this closing connection would disallow re-using + // the same connection for future uses. + // - http://stackoverflow.com/a/17961593/4465767 + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } +} + +func parsePrometheusResults( + reader io.Reader, + prefix []string, +) (results []*prom2json.Family, err error) { + filteredReader, err := filterMetricsByPrefix(reader, prefix) + if err != nil { + return nil, err + } + mfChan := make(chan *dto.MetricFamily) + errChan := make(chan error) + + go func() { + defer close(errChan) + err = prom2json.ParseReader(filteredReader, mfChan) + if err != nil { + errChan <- err + } + }() + + for mf := range mfChan { + for i := range prefix { + if strings.Contains(mf.GetName(), prefix[i]) { + results = append(results, prom2json.NewFamily(mf)) + } + } + } + if err := <-errChan; err != nil { + return nil, err + } + return results, nil +} + +func filterMetricsByPrefix(reader io.Reader, prefix []string) (io.Reader, error) { + var buf bytes.Buffer + for { + line, err := readLine(reader) + if errors.Is(err, io.EOF) { + break + } else if err != nil { + return nil, err + } + + if bytes.HasPrefix(line, []byte("#")) { + continue + } + + for i := range prefix { + if bytes.HasPrefix(line, []byte(prefix[i])) { + if _, err := buf.Write(line); err != nil { + return nil, err + } + } + } + } + return &buf, nil +} + +func readLine(reader io.Reader) ([]byte, error) { + line := make([]byte, 0) + for { + b := make([]byte, 1) + _, err := reader.Read(b) + if err != nil { + return nil, err + } + line = append(line, b[0]) + if b[0] == '\n' { + break + } + } + return line, nil +} + +// httpRespToErrorResponse returns a new encoded ErrorResponse +// structure as error. +func httpRespToErrorResponse(resp *http.Response) error { + if resp == nil || resp.Body == nil { + msg := "Response is empty." + return ErrInvalidArgument(msg) + } + + defer closeResponse(resp) + // Limit to 100K + body, err := io.ReadAll(io.LimitReader(resp.Body, 100<<10)) + if err != nil { + return ResponseError{ + Code: resp.Status, + Message: fmt.Sprintf("Failed to read server response: %s.", err), + } + } + + var errResp ResponseError + // Decode the json error + err = json.Unmarshal(body, &errResp) + if err != nil { + // We might get errors as XML, try that. + xmlErr := xml.Unmarshal(body, &errResp) + + if xmlErr != nil { + bodyString := string(body) + if !utf8.Valid(body) { + bodyString = hex.EncodeToString(body) + } + if len(bodyString) > 1024 { + bodyString = bodyString[:1021] + "..." + } + return ResponseError{ + Code: resp.Status, + Message: fmt.Sprintf( + "Failed to parse server response (%s): %s", + err.Error(), + bodyString, + ), + } + } + } + return errResp +} + +// executeGetRequest - instantiates a Get method and performs the request +func (client *MetricsClient) executeGetRequest( + ctx context.Context, + reqData metricsRequestData, +) (res *http.Response, err error) { + req, err := client.newGetRequest(ctx, reqData) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+client.jwtToken) + return client.httpClient.Do(req) +} + +// newGetRequest - instantiate a new HTTP GET request +func (client *MetricsClient) newGetRequest( + ctx context.Context, + reqData metricsRequestData, +) (req *http.Request, err error) { + targetURL, err := client.makeTargetURL(reqData) + if err != nil { + return nil, err + } + + return http.NewRequestWithContext(ctx, http.MethodGet, targetURL.String(), nil) +} + +// makeTargetURL make a new target url. +func (client *MetricsClient) makeTargetURL(r metricsRequestData) (*url.URL, error) { + if client.endpointURL == nil { + return nil, errors.New("enpointURL cannot be nil") + } + + host := client.endpointURL.Host + scheme := client.endpointURL.Scheme + prefix := libraryMinioURLPrefix + + urlStr := scheme + "://" + host + prefix + r.relativePath + return url.Parse(urlStr) +} + +const ( + defaultPrometheusJWTExpiry = 100 * 365 * 24 * time.Hour + libraryMinioURLPrefix = "/minio" + prometheusIssuer = "prometheus" + + // metricsRespBodyLimit = 20 << 20 // 10 MiB +) + +// getPrometheusToken creates a JWT from MinIO access and secret keys +func getPrometheusToken(accessKey, secretKey string) (string, error) { + jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.RegisteredClaims{ + ExpiresAt: jwtgo.NewNumericDate(time.Now().UTC().Add(defaultPrometheusJWTExpiry)), + Subject: accessKey, + Issuer: prometheusIssuer, + }) + + return jwt.SignedString([]byte(secretKey)) +} + +func privateNewMetricsClient( + endpointURL *url.URL, + jwtToken string, + secure bool, +) (*MetricsClient, error) { + clnt := new(MetricsClient) + clnt.jwtToken = jwtToken + clnt.secure = secure + clnt.endpointURL = endpointURL + clnt.httpClient = &http.Client{ + Transport: DefaultTransport(secure), + } + return clnt, nil +} + +// getEndpointURL - construct a new endpoint. +func getEndpointURL(endpoint string, secure bool) (*url.URL, error) { + if strings.Contains(endpoint, ":") { + host, _, err := net.SplitHostPort(endpoint) + if err != nil { + return nil, err + } + if !s3utils.IsValidIP(host) && !s3utils.IsValidDomain(host) { + msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards." + return nil, ErrInvalidArgument(msg) + } + } else if !s3utils.IsValidIP(endpoint) && !s3utils.IsValidDomain(endpoint) { + msg := "Endpoint: " + endpoint + " does not follow ip address or domain name standards." + return nil, ErrInvalidArgument(msg) + } + + // If secure is false, use 'http' scheme. + scheme := "https" + if !secure { + scheme = "http" + } + + // Strip the obvious :443 and :80 from the endpoint + // to avoid the signature mismatch error. + if secure && strings.HasSuffix(endpoint, ":443") { + endpoint = strings.TrimSuffix(endpoint, ":443") + } + if !secure && strings.HasSuffix(endpoint, ":80") { + endpoint = strings.TrimSuffix(endpoint, ":80") + } + + // Construct a secured endpoint URL. + endpointURLStr := scheme + "://" + endpoint + endpointURL, err := url.Parse(endpointURLStr) + if err != nil { + return nil, err + } + + // Validate incoming endpoint URL. + return endpointURL, isValidEndpointURL(endpointURL.String()) +} + +// Verify if input endpoint URL is valid. +func isValidEndpointURL(endpointURL string) error { + if endpointURL == "" { + return ErrInvalidArgument("Endpoint url cannot be empty.") + } + url, err := url.Parse(endpointURL) + if err != nil { + return ErrInvalidArgument("Endpoint url cannot be parsed.") + } + if url.Path != "/" && url.Path != "" { + return ErrInvalidArgument("Endpoint url cannot have fully qualified paths.") + } + return nil +} + +// DefaultTransport - this default transport is similar to +// http.DefaultTransport but with additional param DisableCompression +// is set to true to avoid decompressing content with 'gzip' encoding. +var DefaultTransport = func(secure bool) http.RoundTripper { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 5 * time.Second, + KeepAlive: 15 * time.Second, + FallbackDelay: 100 * time.Millisecond, + }).DialContext, + MaxIdleConns: 1024, + MaxIdleConnsPerHost: 1024, + ResponseHeaderTimeout: 60 * time.Second, + IdleConnTimeout: 60 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + // Set this value so that the underlying transport round-tripper + // doesn't try to auto decode the body of objects with + // content-encoding set to `gzip`. + // + // Refer: + // https://golang.org/src/net/http/transport.go?h=roundTrip#L1843 + DisableCompression: true, + } + + if secure { + tr.TLSClientConfig = &tls.Config{ + // Can't use SSLv3 because of POODLE and BEAST + // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher + // Can't use TLSv1.1 because of RC4 cipher usage + MinVersion: tls.VersionTLS12, + } + } + return tr +} + +// ErrInvalidArgument - Invalid argument response. +func ErrInvalidArgument(message string) error { + return ResponseError{ + Code: "InvalidArgument", + Message: message, + RequestID: "minio", + } +} + +// ResponseError - Is the typed error returned by all API operations. +type ResponseError struct { + XMLName xml.Name `xml:"Error" json:"-"` + Code string `xml:"Code" json:"code"` + Message string `xml:"Message" json:"message"` + BucketName string `xml:"BucketName,omitempty" json:"bucket_name,omitempty"` + Key string `xml:"Key,omitempty" json:"key,omitempty"` + RequestID string `xml:"RequestId" json:"requestID,omitempty"` + HostID string `xml:"HostId" json:"hostID,omitempty"` + + // Region where the bucket is located. This header is returned + // only in HEAD bucket and ListObjects response. + Region string `xml:"-" json:"region,omitempty"` +} + +// Error - Returns HTTP error string +func (e ResponseError) Error() string { + return e.Message +} diff --git a/controllers/pkg/objectstorage/objectstorage.go b/controllers/pkg/objectstorage/objectstorage.go new file mode 100644 index 000000000000..15428db15477 --- /dev/null +++ b/controllers/pkg/objectstorage/objectstorage.go @@ -0,0 +1,477 @@ +// Copyright © 2023 sealos. +// +// 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 objectstorage + +import ( + "context" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/minio/madmin-go/v3" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/prometheus/client_golang/api" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + "github.com/prometheus/prom2json" +) + +func NewOSAdminClient(endpoint, accessKey, secretKey string) (*madmin.AdminClient, error) { + opts := &madmin.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: false, + } + + return madmin.NewWithOptions(endpoint, opts) +} + +func NewOSClient(endpoint, accessKey, secretKey string) (*minio.Client, error) { + opts := &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: false, + } + + return minio.New(endpoint, opts) +} + +func ListUserObjectStorageBucket(client *minio.Client, username string) ([]string, error) { + buckets, err := client.ListBuckets(context.Background()) + if err != nil { + return nil, err + } + + var expectBuckets []string + for _, bucket := range buckets { + if strings.HasPrefix(bucket.Name, username) { + expectBuckets = append(expectBuckets, bucket.Name) + } + } + return expectBuckets, nil +} + +func ListAllObjectStorageBucket(client *minio.Client) ([]string, error) { + buckets, err := client.ListBuckets(context.Background()) + if err != nil { + return nil, err + } + allBuckets := make([]string, 0, len(buckets)) + for _, bucket := range buckets { + allBuckets = append(allBuckets, bucket.Name) + } + return allBuckets, nil +} + +func GetObjectStorageSize(client *minio.Client, bucket string) (int64, int64) { + objects := client.ListObjects(context.Background(), bucket, minio.ListObjectsOptions{ + Recursive: true, + }) + var totalSize int64 + var objectsCount int64 + for object := range objects { + totalSize += object.Size + objectsCount++ + } + return totalSize, objectsCount +} + +func GetObjectStorageFlow( + promURL, bucket, instance string, + startTime, endTime time.Time, +) (int64, error) { + flow, err := QueryPrometheus(promURL, bucket, instance, startTime, endTime) + if err != nil { + return 0, fmt.Errorf("failed to query prometheus, bucket: %v, err: %w", bucket, err) + } + return flow, nil +} + +func GetUserObjectStorageSize(client *minio.Client, username string) (int64, int64, error) { + buckets, err := ListUserObjectStorageBucket(client, username) + if err != nil { + return 0, 0, fmt.Errorf("failed to list object storage buckets: %w", err) + } + + var totalSize int64 + var objectsCount int64 + for _, bucketName := range buckets { + size, count := GetObjectStorageSize(client, bucketName) + totalSize += size + objectsCount += count + } + return totalSize, objectsCount, nil +} + +func GetUserObjectStorageFlow( + client *minio.Client, + promURL, username, instance string, + startTime, endTime time.Time, +) (int64, error) { + buckets, err := ListUserObjectStorageBucket(client, username) + if err != nil { + return 0, fmt.Errorf("failed to list object storage buckets: %w", err) + } + + var totalFlow int64 + for _, bucketName := range buckets { + flow, err := QueryPrometheus(promURL, bucketName, instance, startTime, endTime) + if err != nil { + return 0, fmt.Errorf("failed to query prometheus, bucket: %v, err: %w", bucketName, err) + } + totalFlow += flow + } + + return totalFlow, nil +} + +var timeoutDuration = time.Duration( + env.GetInt64EnvWithDefault(EnvPromQueryObsTimeoutSecEnv, 10), +) * time.Second + +const ( + EnvPromQueryObsTimeoutSecEnv = "PROM_QUERY_OBS_TIMEOUT_SEC" + timeFormat = time.DateTime +) + +var bytePattern = regexp.MustCompile(`\d+`) + +func QueryPrometheus( + host, bucketName, instance string, + startTime, endTime time.Time, +) (int64, error) { + client, err := api.NewClient(api.Config{ + Address: host, + }) + if err != nil { + return 0, fmt.Errorf("failed to create Prometheus client: %w", err) + } + + v1api := v1.NewAPI(client) + ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration) + defer cancel() + + rcvdQuery := fmt.Sprintf( + "sum(minio_bucket_traffic_received_bytes{bucket=\"%s\", instance=\"%s\"})", + bucketName, + instance, + ) + rcvdValues, err := queryPrometheus(ctx, v1api, rcvdQuery, startTime, endTime) + if err != nil { + return 0, fmt.Errorf("failed to query Prometheus: %w", err) + } + + if rcvdValues[0] <= 0 || rcvdValues[1] <= 0 { + fmt.Println( + "[Warning] The metrics retrieved by vector-metrics are less than or equal to 0.", + "bucket:", + bucketName, + ) + fmt.Printf( + "received bytes: {startTime: {time: %v, value: %v}, endTime: {time: %v, value: %v}}\n", + startTime.Format(timeFormat), + rcvdValues[0], + endTime.Format(timeFormat), + rcvdValues[1], + ) + return 0, nil + } + + sentQuery := fmt.Sprintf( + "sum(minio_bucket_traffic_sent_bytes{bucket=\"%s\", instance=\"%s\"})", + bucketName, + instance, + ) + sentValues, err := queryPrometheus(ctx, v1api, sentQuery, startTime, endTime) + if err != nil { + return 0, fmt.Errorf("failed to query Prometheus: %w", err) + } + + if sentValues[0] <= 0 || sentValues[1] <= 0 { + fmt.Println( + "[Warning] The metrics retrieved by vector-metrics are less than or equal to 0.", + "bucket:", + bucketName, + ) + fmt.Printf( + "sent bytes: {startTime: {time: %v, value: %v}, endTime: {time: %v, value: %v}}\n", + startTime.Format(timeFormat), + sentValues[0], + endTime.Format(timeFormat), + sentValues[1], + ) + return 0, nil + } + + receivedDiff := rcvdValues[1] - rcvdValues[0] + sentDiff := sentValues[1] - sentValues[0] + + fmt.Printf( + "bucket: %v, received bytes in duration: %v, sent bytes in duration: %v\n", + bucketName, + receivedDiff, + sentDiff, + ) + fmt.Printf( + "received bytes: {startTime: {time: %v, value: %v}, endTime: {time: %v, value: %v}}\n", + startTime.Format(timeFormat), + rcvdValues[0], + endTime.Format(timeFormat), + rcvdValues[1], + ) + fmt.Printf( + "sent bytes: {startTime: {time: %v, value: %v}, endTime: {time: %v, value: %v}}\n", + startTime.Format(timeFormat), + sentValues[0], + endTime.Format(timeFormat), + sentValues[1], + ) + + return rcvdValues[1] + sentValues[1] - rcvdValues[0] - sentValues[0], nil +} + +func queryPrometheus( + ctx context.Context, + api v1.API, + query string, + startTime, endTime time.Time, +) ([]int64, error) { + result1, _, err := api.Query(ctx, query, startTime, v1.WithTimeout(timeoutDuration)) + if err != nil { + return nil, err + } + + result2, _, err := api.Query(ctx, query, endTime, v1.WithTimeout(timeoutDuration)) + if err != nil { + return nil, err + } + + val1, val2 := extractValues(result1, result2) + return []int64{val1, val2}, nil +} + +func extractValues(result1, result2 model.Value) (int64, int64) { + rcvdStr1 := bytePattern.FindString(result1.String()) + rcvdStr2 := bytePattern.FindString(result2.String()) + val1, _ := strconv.ParseInt(rcvdStr1, 10, 64) + val2, _ := strconv.ParseInt(rcvdStr2, 10, 64) + return val1, val2 +} + +type MetricData struct { + // key: bucket name, value: usage + Usage map[string]int64 + // key: bucket name, value: traffic sent + Sent map[string]int64 + // key: bucket name, value: traffic received + Received map[string]int64 +} + +type Metrics map[string]MetricData + +func QueryUserUsage(client *MetricsClient) (Metrics, error) { + obMetrics := make(Metrics) + bucketMetrics, err := client.BucketUsageTotalBytesMetrics(context.TODO()) + if err != nil { + return nil, fmt.Errorf("failed to get bucket metrics: %w", err) + } + for _, bucketMetric := range bucketMetrics { + if !isUsageBytesTargetMetric(bucketMetric.Name) { + continue + } + for _, metrics := range bucketMetric.Metrics { + promMetrics, ok := metrics.(prom2json.Metric) + if !ok { + return nil, errors.New("failed to convert metrics to prom2json.Metric") + } + floatValue, err := strconv.ParseFloat(promMetrics.Value, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s to float value", promMetrics.Value) + } + intValue := int64(floatValue) + if bucket := promMetrics.Labels["bucket"]; bucket != "" { + user := getUserWithBucket(bucket) + metricData, exists := obMetrics[user] + if !exists { + metricData = MetricData{ + Usage: make(map[string]int64), + } + } + metricData.Usage[bucket] += intValue + obMetrics[user] = metricData + } + } + } + + return obMetrics, err +} + +func QueryUserUsageAndTraffic(client *MetricsClient) (Metrics, error) { + obMetrics := make(Metrics) + bucketMetrics, err := client.BucketUsageAndTrafficBytesMetrics(context.TODO()) + if err != nil { + return nil, fmt.Errorf("failed to get bucket traffic metrics: %w", err) + } + + // Initialize a map to count the number of metrics per bucket for sent traffic + bucketSentCounts := make(map[string]int) + // Seed the random number generator + + for _, bucketMetric := range bucketMetrics { + if !isUsageAndTrafficBytesTargetMetric(bucketMetric.Name) { + continue + } + + for _, metrics := range bucketMetric.Metrics { + promMetrics, ok := metrics.(prom2json.Metric) + if !ok { + return nil, errors.New("failed to convert metrics to prom2json.Metric") + } + floatValue, err := strconv.ParseFloat(promMetrics.Value, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s to float value", promMetrics.Value) + } + intValue := int64(floatValue) + if bucket := promMetrics.Labels["bucket"]; bucket != "" { + // fmt.Println("debug info", "type:", bucketMetric.Name, "promMetrics:", promMetrics) + user := getUserWithBucket(bucket) + if user == "" { + // fmt.Println("debug info", "false bucket:", bucket) + continue + } + // fmt.Println("debug info", "true bucket:", bucket, "user:", user) + metricData, exists := obMetrics[user] + if !exists { + metricData = MetricData{ + Usage: make(map[string]int64), + Sent: make(map[string]int64), + Received: make(map[string]int64), + } + } + if bucketMetric.Name == "minio_bucket_usage_total_bytes" { + metricData.Usage[bucket] += intValue + } + if bucketMetric.Name == "minio_bucket_traffic_sent_bytes" { + // Skip this data point to simulate loss + // if strings.HasPrefix(bucket, "961a5c2b") && rand.Intn(10) == 0 { + // fmt.Printf("Simulated data loss for bucket %s, value %d\n", bucket, intValue) + // continue + // } + metricData.Sent[bucket] += intValue + // Increment the counter for this bucket + bucketSentCounts[bucket]++ + } + if bucketMetric.Name == "minio_bucket_traffic_received_bytes" { + metricData.Received[bucket] += intValue + } + obMetrics[user] = metricData + } + } + } + + // After processing all metrics, check the counts for sent traffic + for user, metricData := range obMetrics { + for bucket, count := range bucketSentCounts { + if _, ok := metricData.Sent[bucket]; ok { + if count < 4 { + // Zero out if less than 4 metrics were added + metricData.Sent[bucket] = -1 + } + // if strings.HasPrefix(bucket, "961a5c2b") { + // fmt.Println("bucket:", bucket, ", sent:", sent) + // } + } + } + obMetrics[user] = metricData + } + + return obMetrics, err +} + +func isUsageBytesTargetMetric(name string) bool { + targetMetrics := []string{ + "minio_bucket_usage_total_bytes", + } + for _, target := range targetMetrics { + if name == target { + return true + } + } + return false +} + +func isUsageAndTrafficBytesTargetMetric(name string) bool { + targetMetrics := []string{ + "minio_bucket_usage_total_bytes", + "minio_bucket_traffic_sent_bytes", + "minio_bucket_traffic_received_bytes", + } + for _, target := range targetMetrics { + if name == target { + return true + } + } + return false +} + +func getUserWithBucket(bucket string) string { + re := regexp.MustCompile(`^([a-zA-Z0-9]{8})-(.*)$`) + matches := re.FindStringSubmatch(bucket) + if len(matches) == 3 { + return matches[1] + } + return "" +} + +/* +/pvc-03392f69-a7b0-4a52-a839-82d9c586d96f/ns-eemtkfj3/halo-faxdridb-pg-bd87bb8c-a128-44cc-b076-6228593b16ee/postgresql/halo-faxdridb-pg-yhxnjm/halo-faxdridb-pg-yhxnjm.tar.gz + 1/pvc-03392f69-a7b0-4a52-a839-82d9c586d96f + 2/pvc-03392f69-a7b0-4a52-a839-82d9c586d96f + 2/ns-eemtkfj3 + 3/ns-eemtkfj3 + 3/halo-faxdridb-pg-bd87bb8c-a128-44cc-b076-6228593b16ee + 4/halo-faxdridb-pg-bd87bb8c-a128-44cc-b076-6228593b16ee + 4/postgresql + 5/postgresql + 5/halo-faxdridb-pg-yhxnjm + 6/halo-faxdridb-pg-yhxnjm + 6/halo-faxdridb-pg-yhxnjm.tar.gz +*/ + +// func GetUserBakFileSize(client *minio.Client) map[string]int64 { +// bucket := "file-backup" +// userUsageMap := make(map[string]int64) +// objectsCh := client.ListObjects(context.Background(), bucket, minio.ListObjectsOptions{Recursive: true}) +// for object := range objectsCh { +// user := extractNamespace(object.Key) +// if user != "" { +// userUsageMap[user] += object.Size +// } +// } +// +// return userUsageMap +//} + +// func extractNamespace(input string) string { +// re := regexp.MustCompile(`ns-(\w+)`) +// matches := re.FindStringSubmatch(input) +// if len(matches) < 2 { +// return "" +// } +// return matches[1] +//} diff --git a/controllers/pkg/objectstorage/objectstorage_test.go b/controllers/pkg/objectstorage/objectstorage_test.go new file mode 100644 index 000000000000..2d3dfc169099 --- /dev/null +++ b/controllers/pkg/objectstorage/objectstorage_test.go @@ -0,0 +1,132 @@ +// Copyright © 2023 sealos. +// +// 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 objectstorage + +import ( + "fmt" + "os" + "strconv" + "testing" + "time" +) + +func TestGetUserObjectStorageFlow(t *testing.T) { + requireObjectStorageTest(t, + "MINIO_ENDPOINT", + "MINIO_ACCESS_KEY", + "MINIO_SECRET_KEY", + "PROM_URL", + "MINIO_USERNAME", + "MINIO_INSTANCE", + ) + cli, err := NewOSClient( + os.Getenv("MINIO_ENDPOINT"), + os.Getenv("MINIO_ACCESS_KEY"), + os.Getenv("MINIO_SECRET_KEY"), + ) + if err != nil { + t.Fatal(err) + } + start := time.Now().Truncate(time.Hour).Add(-time.Hour) + bytes, err := GetUserObjectStorageFlow( + cli, + os.Getenv("PROM_URL"), + os.Getenv("MINIO_USERNAME"), + os.Getenv("MINIO_INSTANCE"), + start, + start.Add(time.Hour), + ) + if err != nil { + t.Fatal(err) + } + t.Log(ConvertBytes(bytes)) +} + +func ConvertBytes(bytes int64) string { + switch { + case bytes < 1024: + return strconv.FormatInt(bytes, 10) + "B" + case bytes < 1024*1024: + return strconv.FormatFloat(float64(bytes)/1024, 'f', 2, 64) + "KB" + case bytes < 1024*1024*1024: + return strconv.FormatFloat(float64(bytes)/1024/1024, 'f', 2, 64) + "MB" + default: + return strconv.FormatFloat(float64(bytes)/1024/1024/1024, 'f', 2, 64) + "GB" + } +} + +func TestQueryUserUsage(t *testing.T) { + requireObjectStorageTest(t, + "OBJECTSTORAGE_METRICS_ENDPOINT", + "OBJECTSTORAGE_METRICS_USERNAME", + "OBJECTSTORAGE_METRICS_PASSWORD", + ) + obClient, err := NewMetricsClient( + os.Getenv("OBJECTSTORAGE_METRICS_ENDPOINT"), + os.Getenv("OBJECTSTORAGE_METRICS_USERNAME"), + os.Getenv("OBJECTSTORAGE_METRICS_PASSWORD"), + false, + ) + if err != nil { + t.Fatal(err) + } + metrics, err := QueryUserUsage(obClient) + if err != nil { + t.Fatal(err) + } + for _, metric := range metrics { + fmt.Println(metric) + } +} + +func TestQueryUserTraffic(t *testing.T) { + requireObjectStorageTest(t, + "OBJECTSTORAGE_METRICS_ENDPOINT", + "OBJECTSTORAGE_METRICS_USERNAME", + "OBJECTSTORAGE_METRICS_PASSWORD", + ) + obClient, err := NewMetricsClient( + os.Getenv("OBJECTSTORAGE_METRICS_ENDPOINT"), + os.Getenv("OBJECTSTORAGE_METRICS_USERNAME"), + os.Getenv("OBJECTSTORAGE_METRICS_PASSWORD"), + false, + ) + if err != nil { + t.Fatal(err) + } + metrics, err := QueryUserUsageAndTraffic(obClient) + if err != nil { + t.Fatal(err) + } + + for user, metric := range metrics { + fmt.Println("user:", user) + fmt.Println("usage:", metric.Usage) + fmt.Println("sent:", metric.Sent) + fmt.Println("received:", metric.Received) + } +} + +func requireObjectStorageTest(t *testing.T, envNames ...string) { + t.Helper() + if os.Getenv("RUN_OBJECTSTORAGE_TESTS") != "true" { + t.Skip("set RUN_OBJECTSTORAGE_TESTS=true to run object storage tests") + } + for _, name := range envNames { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } +} diff --git a/controllers/pkg/pay/alipay_payment.go b/controllers/pkg/pay/alipay_payment.go new file mode 100644 index 000000000000..99b5edefb6b8 --- /dev/null +++ b/controllers/pkg/pay/alipay_payment.go @@ -0,0 +1,154 @@ +package pay + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/account" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/smartwalle/alipay/v3" +) + +type AlipayPayment struct { + client *alipay.Client +} + +func NewAlipayPayment() (*AlipayPayment, error) { + client, err := alipay.New( + os.Getenv(account.AlipayAppID), + os.Getenv(account.AlipayPrivateKey), + env.GetBoolWithDefault(account.PayIsProduction, true), + ) + if err != nil { + return nil, fmt.Errorf("alipay client init failed: %w", err) + } + // err = client.LoadAliPayPublicKey(os.Getenv(account.AlipayPublicKey)) + // if err != nil { + // return nil, fmt.Errorf("load alipay public key failed: %v", err) + //} + if err = client.LoadAppCertPublicKey(os.Getenv(account.AlipayAppCertPublicKey)); err != nil { + return nil, fmt.Errorf("load appCertPublicKey failed: %w", err) + } + if err = client.LoadAliPayRootCert(os.Getenv(account.AlipayRootCert)); err != nil { + return nil, fmt.Errorf("load alipayRootCert failed: %w", err) + } + if err = client.LoadAlipayCertPublicKey(os.Getenv(account.AlipayCertPublicKey)); err != nil { + return nil, fmt.Errorf("load alipayCertPublicKey failed: %w", err) + } + return &AlipayPayment{client}, nil +} + +// CreatePayment Create a payment and return the payment URL and order number +func (a *AlipayPayment) CreatePayment(amount int64, _, _ string) (string, string, error) { + p := alipay.TradePagePay{} + p.Subject = "sealos_cloud_pay" + p.OutTradeNo = uuid.NewString() + p.TotalAmount = fmt.Sprintf( + "%.2f", + float64(amount)/1_000_000, + ) // the unit of the amount is converted to a dollar + p.ProductCode = "FAST_INSTANT_TRADE_PAY" + p.QRPayMode = "4" // order code mode: Alipay renders the cashier page (with official QR code), embedded by the frontend via iframe + p.QRCodeWidth = "210" + p.TimeoutExpress = "10m" + url, err := a.client.TradePagePay(p) + if err != nil { + return "", "", err + } + return p.OutTradeNo, url.String(), nil +} + +// GetPaymentDetails check the status of your payment +func (a *AlipayPayment) GetPaymentDetails(sessionID string) (string, int64, error) { + resp, err := a.client.TradeQuery(context.Background(), alipay.TradeQuery{ + OutTradeNo: sessionID, + }) + if err != nil { + return PaymentUnknown, 0, err + } + amount, _ := strconv.ParseFloat(resp.TotalAmount, 64) + amountInt := int64(amount * 1_000_000) + // state mapping + var status string + // the type of notification that is triggered + // notification type description it is enabled by default + // tradeStatus.TRADE_CLOSED transaction-closed 1 + // tradeStatus.TRADE_FINISHED the-transaction-is-closed 1 + // tradeStatus.TRADE_SUCCESS the-payment-was-successful 1 + // tradeStatus.WAIT_BUYER_PAY deal-creation 0 + switch resp.TradeStatus { + case "TRADE_SUCCESS": + status = PaymentSuccess + case "WAIT_BUYER_PAY": + status = PaymentProcessing + case "TRADE_CLOSED", "TRADE_FINISHED": + status = PaymentExpired + default: + status = PaymentUnknown + } + return status, amountInt, nil +} + +// ExpireSession close the order +func (a *AlipayPayment) ExpireSession(payment string) error { + _, err := a.client.TradeClose(context.Background(), alipay.TradeClose{ + OutTradeNo: payment, + }) + return err +} + +// RefundPayment refund +func (a *AlipayPayment) RefundPayment(option RefundOption) (string, string, error) { + ctx := context.Background() + + // query the order to get the payment time + qresp, err := a.client.TradeQuery(ctx, alipay.TradeQuery{ + OutTradeNo: option.TradeNo, + }) + if err != nil { + return "", "", fmt.Errorf("failed to query Alipay order: %w", err) + } + + // use sendpaydate to verify the time + if qresp.SendPayDate == "" { + return "", "", fmt.Errorf( + "the payment time of order %s is unknown, and it is impossible to determine the refund time", + option.TradeNo, + ) + } + paidAt, err := time.ParseInLocation(time.DateTime, qresp.SendPayDate, time.Local) + if err != nil { + return "", "", fmt.Errorf("failed to parse the payment time %w", err) + } + if time.Since(paidAt) > 365*24*time.Hour { + return "", "", fmt.Errorf( + "order %s has exceeded the one-year refund period and cannot be refunded", + option.TradeNo, + ) + } + + outRequestNo := uuid.NewString() + + // Amount Unit Conversion: option. The unit of Amount is "cent", and the SDK API requires "yuan" and supports two decimal places + refundAmt := fmt.Sprintf("%.2f", float64(option.Amount)/1_000_000) + + req := alipay.TradeRefund{ + OutTradeNo: option.TradeNo, // Merchant's original order number, choose one of the two with TradeNo + OutRequestNo: outRequestNo, // The number of this refund request, guaranteed idempotent + RefundAmount: refundAmt, // The amount of this refund, in "yuan", supports two decimal places + RefundReason: "refund for order " + option.OrderID, + } + + resp, err := a.client.TradeRefund(context.Background(), req) + if err != nil { + return "", "", fmt.Errorf("alipay TradeRefund error: %w", err) + } + + // Response parsing: resp. RefundFee is the amount of the refund, in yuan and string type + // It can also be used according to the resp. FundChange or resp. RefundStatus for further judgment + return outRequestNo, resp.RefundFee, nil +} diff --git a/controllers/pkg/pay/alipay_payment_test.go b/controllers/pkg/pay/alipay_payment_test.go new file mode 100644 index 000000000000..6375a846cbdf --- /dev/null +++ b/controllers/pkg/pay/alipay_payment_test.go @@ -0,0 +1,96 @@ +package pay + +import ( + "os" + "testing" + "time" + + "github.com/labring/sealos/controllers/pkg/account" +) + +// TestCreatePaymentIntegration test payment creation +func TestCreatePaymentIntegration(t *testing.T) { + requirePaymentTest(t, + account.AlipayAppID, + account.AlipayPrivateKey, + account.AlipayAppCertPublicKey, + account.AlipayRootCert, + account.AlipayCertPublicKey, + account.PayIsProduction, + ) + ap, err := NewAlipayPayment() + if err != nil { + t.Fatalf("NewAlipayPayment() failed: %v", err) + } + // place an order of $1 + tradeNo, qrURL, err := ap.CreatePayment(1_000_000, "test-user", "unit tests create payments") + if err != nil { + t.Fatalf("CreatePayment() failed: %v", err) + } + t.Logf("CreatePayment success: tradeNo=%s, qrURL=%s", tradeNo, qrURL) +} + +// Full E2E Test: Payment → Inquiries → Refunds +func TestSandbox_EndToEnd(t *testing.T) { + requirePaymentTest(t, + account.AlipayAppID, + account.AlipayPrivateKey, + account.AlipayAppCertPublicKey, + account.AlipayRootCert, + account.AlipayCertPublicKey, + account.PayIsProduction, + ) + ap, err := NewAlipayPayment() + if err != nil { + t.Fatalf("NewAlipayPayment() failed: %v", err) + } + + // place an order + const amount = 20_000 + outTradeNo, qrURL, err := ap.CreatePayment( + amount, + "sandbox-user", + "sandbox_environment_testing", + ) + if err != nil { + t.Fatalf("CreatePayment() failed: %v", err) + } + t.Logf("the order was successfully placed:outTradeNo=%s, qrURL=%s", outTradeNo, qrURL) + + t.Log("the simulated payment is complete") + + time.Sleep(30 * time.Second) + + // check the status of your payment + status, _, err := ap.GetPaymentDetails(outTradeNo) + if err != nil { + t.Fatalf("GetPaymentDetails() failed: %v", err) + } + if status != PaymentSuccess { + t.Fatalf("expected status %s, got %s", PaymentSuccess, status) + } + t.Logf("payment status:%s", status) + + // refund process + refundNo, refundFee, err := ap.RefundPayment(RefundOption{ + OrderID: "sandbox-order-001", + TradeNo: outTradeNo, + Amount: 0.7 * amount, + }) + if err != nil { + t.Fatalf("RefundPayment() failed: %v", err) + } + t.Logf("the refund was successful:refundNo=%s, refundFee=%s", refundNo, refundFee) +} + +func requirePaymentTest(t *testing.T, envNames ...string) { + t.Helper() + if os.Getenv("RUN_PAYMENT_TESTS") != "true" { + t.Skip("set RUN_PAYMENT_TESTS=true to run payment provider tests") + } + for _, name := range envNames { + if os.Getenv(name) == "" { + t.Skipf("requires %s", name) + } + } +} diff --git a/controllers/pkg/pay/common.go b/controllers/pkg/pay/common.go new file mode 100644 index 000000000000..b830d9908fd4 --- /dev/null +++ b/controllers/pkg/pay/common.go @@ -0,0 +1,29 @@ +// Copyright © 2023 sealos. +// +// 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 pay + +const ( + PaymentNotPaid = "notpaid" + PaymentProcessing = "processing" + PaymentFailed = "failed" + PaymentExpired = "expired" + PaymentSuccess = "success" + PaymentUnknown = "unknown" +) + +const ( + DefaultPort string = "443" + DefaultDomain string = "cloud.sealos.io" +) diff --git a/controllers/pkg/pay/interface.go b/controllers/pkg/pay/interface.go new file mode 100644 index 000000000000..537a901c9e13 --- /dev/null +++ b/controllers/pkg/pay/interface.go @@ -0,0 +1,45 @@ +// Copyright © 2023 sealos. +// +// 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 pay + +type RefundOption struct { + OrderID string `json:"order_id"` + RefundID string `json:"refund_id"` + TradeNo string `json:"trade_no"` + Amount int64 `json:"amount"` +} + +type Interface interface { + // amount = sealos amount + CreatePayment(amount int64, user, describe string) (tradeNo, codeURL string, err error) + RefundPayment(option RefundOption) (refundNo, refundID string, err error) + GetPaymentDetails(sessionID string) (string, int64, error) + ExpireSession(payment string) error +} + +func NewPayHandler(paymentMethod string) (Interface, error) { + switch paymentMethod { + case "stripe": + return &StripePayment{}, nil + case "wechat": + return &WechatPayment{}, nil + case "alipay": + return NewAlipayPayment() + default: + // return nil, fmt.Errorf("unsupported payment method: %s", paymentMethod) + // TODO Now set it as the default wechat, and modify it a few days later + return &WechatPayment{}, nil + } +} diff --git a/controllers/pkg/pay/stripe.go b/controllers/pkg/pay/stripe.go new file mode 100644 index 000000000000..b0b815fa693f --- /dev/null +++ b/controllers/pkg/pay/stripe.go @@ -0,0 +1,192 @@ +// Copyright © 2023 sealos. +// +// 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 pay + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/stripe/stripe-go/v74" + session2 "github.com/stripe/stripe-go/v74/checkout/session" + "github.com/stripe/stripe-go/v74/refund" +) + +var DefaultURL = "https://" + env.GetEnvWithDefault("DOMAIN", DefaultDomain) + +const ( + stripeSuccessPostfix = "STRIPE_SUCCESS_POSTFIX" + stripeCancelPostfix = "STRIPE_CANCEL_POSTFIX" + stripeCurrency = "STRIPE_CURRENCY" +) + +var Currency string + +func init() { + if port := os.Getenv("PORT"); port != "" { + DefaultURL = fmt.Sprintf("%s:%s", DefaultURL, port) + } + currency := strings.ToLower(strings.TrimSpace(os.Getenv(stripeCurrency))) + if currency != USD { + currency = CNY + } + Currency = currency +} + +func (s StripePayment) CreatePayment(amount int64, _, _ string) (string, string, error) { + session, err := CreateCheckoutSession( + amount/10000, + Currency, + DefaultURL+os.Getenv(stripeSuccessPostfix), + DefaultURL+os.Getenv(stripeCancelPostfix), + ) + if err != nil { + return "", "", err + } + return session.ID, "", nil +} + +func (s StripePayment) GetPaymentDetails(sessionID string) (string, int64, error) { + ses, err := GetSession(sessionID) + if err != nil { + return "", 0, err + } + switch ses.Status { + case stripe.CheckoutSessionStatusComplete: + return PaymentSuccess, ses.AmountTotal * 10000, nil + case stripe.CheckoutSessionStatusExpired: + return PaymentExpired, 0, nil + case stripe.CheckoutSessionStatusOpen: + return PaymentProcessing, 0, nil + default: + return PaymentUnknown, 0, fmt.Errorf("unknown order status: %s", ses.Status) + } +} + +func (s StripePayment) ExpireSession(sessionID string) error { + status, _, _ := s.GetPaymentDetails(sessionID) + if status == PaymentSuccess || status == PaymentExpired { + return nil + } + _, err := ExpireSession(sessionID) + if err != nil { + return err + } + return nil +} + +// RefundPayment processes a refund request with robust validation and error handling +// Returns (refundNo, refundID, error) +func (s StripePayment) RefundPayment(opt RefundOption) (string, string, error) { + // Input validation + if err := validateRefundOption(opt); err != nil { + return "", "", fmt.Errorf("invalid refund request: %w", err) + } + refundNo := uuid.NewString() + if opt.RefundID != "" { + refundNo = opt.RefundID + } + + // Check if Stripe API key is configured + if stripe.Key == "" { + return "", "", errors.New("stripe API key not configured") + } + + params := &stripe.CheckoutSessionParams{} + + session, err := session2.Get(opt.TradeNo, params) + if err != nil { + return "", "", fmt.Errorf( + "failed to retrieve session for trade no %s: %w", + opt.TradeNo, + err, + ) + } + + if session.PaymentStatus != "paid" { + return "", "", fmt.Errorf("failed to refund: session %s is not paid", opt.TradeNo) + } + + if session.PaymentIntent == nil || session.PaymentIntent.ID == "" { + return "", "", fmt.Errorf( + "failed to refund: payment intent ID is nil for session %s", + opt.TradeNo, + ) + } + + // Build refund parameters with comprehensive metadata + refundParams := &stripe.RefundParams{ + Params: stripe.Params{ + Metadata: map[string]string{ + "order_id": opt.OrderID, + "refund_id": refundNo, + }, + }, + PaymentIntent: stripe.String(session.PaymentIntent.ID), + } + + // Handle partial vs full refund + if opt.Amount > 0 { + // Validate that the amount is reasonable (convert to cents for Stripe) + refundParams.Amount = stripe.Int64(opt.Amount / 10000) // Convert to cents + } else { + return "", "", errors.New("refund amount must be greater than zero") + } + + // Create the refund + refundResult, err := refund.New(refundParams) + if err != nil { + return "", "", fmt.Errorf("failed to create refund for charge %s: %w", opt.TradeNo, err) + } + + // Validate the refund result + if refundResult == nil { + return "", "", errors.New("refund result is nil") + } + + // Check refund status and provide appropriate feedback + switch refundResult.Status { + case stripe.RefundStatusSucceeded: + return refundResult.ID, refundResult.ID, nil + case stripe.RefundStatusPending: + return refundResult.ID, refundResult.ID, fmt.Errorf( + "refund pending: refund ID %s is being processed", + refundResult.ID, + ) + case stripe.RefundStatusFailed: + return "", "", fmt.Errorf("refund failed: %s", refundResult.FailureReason) + case stripe.RefundStatusCanceled: + return "", "", fmt.Errorf("refund canceled: refund ID %s was canceled", refundResult.ID) + default: + return refundResult.ID, refundResult.ID, fmt.Errorf( + "refund created with unknown status: %s", + refundResult.Status, + ) + } +} + +// validateRefundOption validates the refund request parameters +func validateRefundOption(opt RefundOption) error { + if opt.TradeNo == "" { + return errors.New("trade_no (charge ID) is required") + } + if opt.Amount <= 0 { + return errors.New("refund amount cannot be negative") + } + return nil +} diff --git a/controllers/pkg/pay/stripe_payment.go b/controllers/pkg/pay/stripe_payment.go new file mode 100644 index 000000000000..ce98939c3992 --- /dev/null +++ b/controllers/pkg/pay/stripe_payment.go @@ -0,0 +1,84 @@ +// Copyright © 2023 sealos. +// +// 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 pay + +import ( + "os" + "time" + + "github.com/stripe/stripe-go/v74" + "github.com/stripe/stripe-go/v74/checkout/session" +) + +const StripeAPIKEY = "STRIPE_API_KEY" + +type StripePayment struct{} + +func init() { + stripe.Key = os.Getenv(StripeAPIKEY) +} + +// const currency +const ( + USD = "usd" + CNY = "cny" +) + +const sessionExpirationTime = 30 * time.Minute + +func CreateCheckoutSession( + amount int64, + currency, successURL, cancelURL string, +) (*stripe.CheckoutSession, error) { + expireAt := time.Now().UTC().Add(sessionExpirationTime).Unix() + params := &stripe.CheckoutSessionParams{ + PaymentMethodTypes: stripe.StringSlice([]string{ + "card", + }), + ExpiresAt: &expireAt, + LineItems: []*stripe.CheckoutSessionLineItemParams{ + { + PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{ + UnitAmount: stripe.Int64(amount), + Currency: stripe.String(currency), + ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{ + Name: stripe.String("Sealos Recharge"), + }, + }, + Quantity: stripe.Int64(1), + }, + }, + Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), + AllowPromotionCodes: stripe.Bool(true), + SuccessURL: stripe.String(successURL), + CancelURL: stripe.String(cancelURL), + } + + s, err := session.New(params) + if err != nil { + return nil, err + } + + return s, nil +} + +func GetSession(sessionID string) (*stripe.CheckoutSession, error) { + return session.Get(sessionID, nil) +} + +// ExpireSession +func ExpireSession(sessionID string) (*stripe.CheckoutSession, error) { + return session.Expire(sessionID, nil) +} diff --git a/controllers/pkg/pay/stripe_payment_test.go b/controllers/pkg/pay/stripe_payment_test.go new file mode 100644 index 000000000000..8909049036dd --- /dev/null +++ b/controllers/pkg/pay/stripe_payment_test.go @@ -0,0 +1,86 @@ +// Copyright © 2024 sealos. +// +// 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 pay + +import ( + "os" + "testing" +) + +func TestCreateCheckoutSession(t *testing.T) { + requirePaymentTest(t, StripeAPIKEY) + successURL := os.Getenv("STRIPE_TEST_SUCCESS_URL") + cancelURL := os.Getenv("STRIPE_TEST_CANCEL_URL") + if successURL == "" || cancelURL == "" { + t.Skip("requires STRIPE_TEST_SUCCESS_URL and STRIPE_TEST_CANCEL_URL") + } + stripe, err := CreateCheckoutSession( + 2000, + "cny", + successURL, + cancelURL, + ) + if err != nil { + t.Error(err) + } + t.Log(stripe.ID) +} + +// func setupEnv_stripe() { +// const ( +// APIKEY = "" +// ) +// // check that the environment variables are set +// if os.Getenv(StripeAPIKEY) == "" { +// err := os.Setenv(StripeAPIKEY, APIKEY) +// if err != nil { +// log.Fatalf("failed to set environment variables: %v", err) +// } +// } +// +// // sandboxEnvironment +// err := os.Setenv(StripeAPIKEY, "true") +// if err != nil { +// return +// } +//} + +// func TestStripsPayment_PaymentAndRefund(t *testing.T) { +// setupEnv_stripe() +// stripe.Key = os.Getenv(StripeAPIKEY) +// +// // 1. create a payment intent +// piParams := &stripe.PaymentIntentParams{ +// Amount: stripe.Int64(2000), // 2000 分 = ¥20.00 +// Currency: stripe.String(string(stripe.CurrencyCNY)), +// PaymentMethodTypes: stripe.StringSlice([]string{"card"}), // pay with a card +// } +// pi, err := paymentintent.New(piParams) +// if err != nil { +// t.Fatalf("failed to create a payment intent: %v", err) +// } +// t.Logf("PaymentIntent the creation is successful,ID=%s", pi.ID) +// +// // 2. perform a partial refund for that payment intent +// refundParams := &stripe.RefundParams{ +// PaymentIntent: stripe.String(pi.ID), +// Amount: stripe.Int64(1000), // refund 1000 cent = ¥10.00 +// } +// r, err := refund.New(refundParams) +// if err != nil { +// t.Fatalf("refund failed: %v", err) +// } +// t.Logf("the refund was successful,Refund ID=%s,refund amount=%d", r.ID, r.Amount) +//} diff --git a/controllers/pkg/pay/test/README.md b/controllers/pkg/pay/test/README.md new file mode 100644 index 000000000000..e842edd7de89 --- /dev/null +++ b/controllers/pkg/pay/test/README.md @@ -0,0 +1,49 @@ + +Change the stripe key in templates/index.html to your public key. Test keys can be found [here ↗](https://stripe.com/docs/keys#test-live-modes). + +```shell +sed -i 's/stripe_public_key/your_stripe_public_key/g' templates/index.html +``` + +use test private keys in the test environment. And to run the main program, use the following command: +```shell +STRIPE_API_KEY="your_stripe_api_key" go run main.go +``` + +```shell +huaijiahui@huaijiahuideMacBook-Pro test % curl -X POST localhost:8080/create-checkout-session +{"sessionId":"cs_test_a1W0VrXpzOTQHq5dpC2KWADTV3A0QB6moN1gbt3DZPz9uV3WuSvoUmRac9"}% +huaijiahui@huaijiahuideMacBook-Pro test % curl -X GET 'localhost:8080/get-session?session_id=cs_test_a1W0VrXpzOTQHq5dpC2KWADTV3A0QB6moN1gbt3DZPz9uV3WuSvoUmRac9' +{"message":"ok","status":"open"}% +huaijiahui@huaijiahuideMacBook-Pro test % curl -X GET 'localhost:8080/expire-session?session_id=cs_test_a1W0VrXpzOTQHq5dpC2KWADTV3A0QB6moN1gbt3DZPz9uV3WuSvoUmRac9' +{"message":"ok","status":"expired"}% +huaijiahui@huaijiahuideMacBook-Pro test % curl -X GET 'localhost:8080/get-session?session_id=cs_test_a1W0VrXpzOTQHq5dpC2KWADTV3A0QB6moN1gbt3DZPz9uV3WuSvoUmRac9' +{"message":"ok","status":"expired"}% +``` + +[open in browser](localhost:8080) + +Open your browser and navigate to localhost:8080. Click the `Checkout` button to open the Stripe payment page. Enter your credit card information and click the `Pay` button. Upon successful payment, you'll be redirected to the pre-specified success page. +> Note: In the test environment, you can use the official test credit card number provided by Stripe, such as `4242424242424242`. The expiration date can be any time in the future, and the CVC can be `any number`. For more details, visit [https://stripe.com/docs/testing#cards ↗](https://stripe.com/docs/testing#cards) + +![img.png](img.png) + +The `CreateCheckoutSession` function is defined as follows: +```shell +func CreateCheckoutSession(amount int64, currency, successURL, cancelURL string) (*stripe.CheckoutSession, error) +``` +- `currency`: The type of currency you use, such as: usd, cny (lowercase only) +- `amount`: Here, the amount is scaled by a factor of 1:100. For example, for 1 unit of currency, set `amount=100`. +- `successURL`, `cancelURL`: These are the URLs to which the user will be redirected upon successful payment or cancellation. + +The `GetSession` function is defined as follows: +```shell +func GetSession(sessionID string) (*stripe.CheckoutSession, error) +``` +- `sessionID`: This is the session ID returned by Stripe upon successful payment. After creating a session, you can use this function to poll the payment status and handle corresponding business processes accordingly. + +The `ExpireSession` function is defined as follows: +```shell +func ExpireSession(sessionID string) (*stripe.CheckoutSession, error) +``` +- `sessionID`: This is the session ID returned by Stripe upon successful payment. Once the session exceeds the expected payment time, you can call this function to handle the expiration process. diff --git a/controllers/pkg/pay/test/img.png b/controllers/pkg/pay/test/img.png new file mode 100644 index 000000000000..fe499df4c4e1 Binary files /dev/null and b/controllers/pkg/pay/test/img.png differ diff --git a/controllers/pkg/pay/test/main.go b/controllers/pkg/pay/test/main.go new file mode 100644 index 000000000000..ed914cdb6761 --- /dev/null +++ b/controllers/pkg/pay/test/main.go @@ -0,0 +1,80 @@ +// Copyright © 2023 sealos. +// +// 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 ( +// "fmt" +// "os" +// "path" +// "runtime" +// +// "github.com/labring/sealos/controllers/pkg/pay" +// +// "github.com/gin-gonic/gin" +//) +// +// func main() { +// router := gin.Default() +// // 获取当前文件运行的路径 +// _, filename, _, _ := runtime.Caller(0) +// dir := path.Join(path.Dir(filename), "templates") +// +// // 加载HTML目录 +// router.LoadHTMLGlob(dir + "/*") +// +// router.GET("/", func(c *gin.Context) { +// // 渲染index.html文件 +// c.HTML(200, "index.html", nil) +// }) +// +// router.POST("/create-checkout-session", func(c *gin.Context) { +// s, err := pay.CreateCheckoutSession(2000, "cny", "https://cloud.sealos.io", "https://cloud.sealos.io") +// if err != nil { +// c.JSON(500, gin.H{"error": err.Error()}) +// return +// } +// fmt.Println("Successfully created checkout session:", s.ID) +// c.JSON(200, gin.H{"sessionId": s.ID}) +// }) +// //get session +// router.GET("/get-session", func(c *gin.Context) { +// sessionID := c.Query("session_id") +// ses, err := pay.GetSession(sessionID) +// if err != nil { +// c.JSON(500, gin.H{"error": err.Error()}) +// return +// } +// fmt.Println("Successfully created get session:", ses) +// c.JSON(200, gin.H{"message": "ok", "status": ses.Status}) +// }) +// //expire session +// router.GET("/expire-session", func(c *gin.Context) { +// sessionID := c.Query("session_id") +// ses, err := pay.ExpireSession(sessionID) +// if err != nil { +// c.JSON(500, gin.H{"error": err.Error()}) +// return +// } +// fmt.Println("Successfully expire session:", ses) +// c.JSON(200, gin.H{"message": "ok", "status": ses.Status}) +// }) +// +// err := router.Run(":8080") +// if err != nil { +// fmt.Println("Error running server:", err) +// os.Exit(1) +// } +//} diff --git a/controllers/pkg/pay/test/templates/index.html b/controllers/pkg/pay/test/templates/index.html new file mode 100644 index 000000000000..be22b5b9acfc --- /dev/null +++ b/controllers/pkg/pay/test/templates/index.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/controllers/pkg/pay/wechat.go b/controllers/pkg/pay/wechat.go new file mode 100644 index 000000000000..09f918389f6e --- /dev/null +++ b/controllers/pkg/pay/wechat.go @@ -0,0 +1,125 @@ +// Copyright © 2023 sealos. +// +// 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 pay + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" +) + +func (w WechatPayment) CreatePayment(amount int64, user, describe string) (string, string, error) { + tradeNO := GetRandomString(32) + codeURL, err := WechatPay(amount/10000, user, tradeNO, describe, "") + if err != nil { + return "", "", err + } + return tradeNO, codeURL, nil +} + +func (w WechatPayment) GetPaymentDetails(sessionID string) (string, int64, error) { + orderResp, err := QueryOrder(sessionID) + if err != nil { + return "", 0, err + } + switch *orderResp.TradeState { + case StatusSuccess: + return PaymentSuccess, *orderResp.Amount.Total * 10000, nil + case StatusProcessing: + return PaymentProcessing, 0, nil + case StatusNotPay: + return PaymentNotPaid, 0, nil + case StatusFail: + return PaymentFailed, 0, errors.New("order failed") + default: + return PaymentUnknown, 0, fmt.Errorf("unknown order status: %s", *orderResp.TradeState) + } +} + +func (w WechatPayment) ExpireSession(_ string) error { + return nil +} + +func (w WechatPayment) RefundPayment(option RefundOption) (string, string, error) { + // check the order and get SuccessTime + orderResp, err := QueryOrder(option.TradeNo) + if err != nil { + return "", "", fmt.Errorf("failed to query wechat order: %w", err) + } + if orderResp.SuccessTime != nil { + // The SuccessTime format is generally RFC3339 + paidAt, err := time.Parse(time.RFC3339, *orderResp.SuccessTime) + if err != nil { + return "", "", fmt.Errorf("failed to resolve the payment time: %w", err) + } + if time.Since(paidAt) > 365*24*time.Hour { + return "", "", fmt.Errorf( + "order %s has exceeded the one-year refund period and cannot be refunded", + option.TradeNo, + ) + } + } else { + return "", "", fmt.Errorf("order %s has not been paid or the payment time is unknown and cannot be refunded", option.TradeNo) + } + + _, paidAmount, err := w.GetPaymentDetails(option.TradeNo) + if err != nil { + return "", "", fmt.Errorf("failed to query the payment order: %w", err) + } + + // generate a merchant refund number + refundNo := uuid.NewString() + if option.RefundID != "" { + refundNo = option.RefundID + } + + // Amount Unit Conversion: option. Amount is the "cent", which is the same as CreatePayment (amount/10000) + refundAmt := option.Amount / 10000 + + ctx := context.Background() + client, err := NewClient(ctx) + if err != nil { + return "", "", fmt.Errorf("new wechat pay client err: %w", err) + } + + req := refunddomestic.CreateRequest{ + OutTradeNo: core.String(option.TradeNo), + OutRefundNo: core.String(refundNo), + Reason: core.String("refund for order " + option.OrderID), + Amount: &refunddomestic.AmountReq{ + Total: core.Int64(paidAmount / 10000), + Refund: core.Int64(refundAmt), + Currency: core.String("CNY"), + }, + } + + // call the refund api + svc := refunddomestic.RefundsApiService{Client: client} + resp, _, err := svc.Create(ctx, req) + if err != nil { + return refundNo, "", fmt.Errorf("call Refund API error: %w", err) + } + if resp == nil || resp.RefundId == nil { + return refundNo, "", errors.New("empty refund response") + } + + // Return: Merchant Refund Number & WeChat Refund Number + return refundNo, *resp.RefundId, nil +} diff --git a/controllers/pkg/pay/wechat_payment.go b/controllers/pkg/pay/wechat_payment.go new file mode 100644 index 000000000000..57753edcc543 --- /dev/null +++ b/controllers/pkg/pay/wechat_payment.go @@ -0,0 +1,157 @@ +// Copyright © 2022 sealos. +// +// 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 pay + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "log" + "os" + "time" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" + "github.com/wechatpay-apiv3/wechatpay-go/utils" +) + +type WechatPayment struct{} + +// ENV keys +const ( + WechatPrivateKey = "WechatPrivateKey" + MchID = "MchID" + MchCertificateSerialNumber = "MchCertificateSerialNumber" + MchAPIv3Key = "MchAPIv3Key" + AppID = "AppID" + NotifyCallbackURL = "NotifyCallbackURL" + + StatusSuccess = "SUCCESS" + StatusProcessing = "PROCESSING" + StatusNotPay = "NOTPAY" + StatusFail = "FAILED" + DefaultCallbackURL = "https://sealos.io/payment/wechat/callback" +) + +func NewClient(ctx context.Context, opts ...core.ClientOption) (*core.Client, error) { + mchID := os.Getenv(MchID) // 商户号 + mchCertificateSerialNumber := os.Getenv(MchCertificateSerialNumber) // 商户证书序列号 + mchAPIv3Key := os.Getenv(MchAPIv3Key) // 商户APIv3密钥 + mchPrivateKey, err := utils.LoadPrivateKey(os.Getenv(WechatPrivateKey)) + if err != nil { + log.Print("private key is: ", os.Getenv(WechatPrivateKey)) + return nil, fmt.Errorf("load merchant private key error: %w", err) + } + opts = append( + opts, + option.WithWechatPayAutoAuthCipher( + mchID, + mchCertificateSerialNumber, + mchPrivateKey, + mchAPIv3Key, + ), + ) + return core.NewClient(ctx, opts...) +} + +func QueryOrder(orderID string) (*payments.Transaction, error) { + ctx := context.Background() + client, err := NewClient(context.Background()) + if err != nil { + return nil, fmt.Errorf("new wechat pay client err:%w", err) + } + svc := native.NativeApiService{Client: client} + resp, _, err := svc.QueryOrderByOutTradeNo(ctx, + native.QueryOrderByOutTradeNoRequest{ + Mchid: core.String(os.Getenv(MchID)), + OutTradeNo: core.String(orderID), + }, + ) + if err != nil { + return nil, fmt.Errorf("call QueryOrder err:%w", err) + } + return resp, nil +} + +// 1 ¥ = amount 100 +func WechatPay(amount int64, user, tradeNO, describe, callback string) (string, error) { + ctx := context.Background() + client, err := NewClient(context.Background()) + if err != nil { + return "", fmt.Errorf("new wechat pay client err:%w", err) + } + + if tradeNO == "" { + tradeNO = GetRandomString(32) + } + if tradeNO == "" { + return "", errors.New("generate tradeNO failed") + } + if describe == "" { + describe = "sealos cloud recharge" + } + if callback == "" { + callback = DefaultCallbackURL + } + svc := native.NativeApiService{Client: client} + resp, _, err := svc.Prepay(ctx, + native.PrepayRequest{ + Appid: core.String(os.Getenv(AppID)), + Mchid: core.String(os.Getenv(MchID)), + Description: core.String(describe), + OutTradeNo: core.String(tradeNO), + TimeExpire: core.Time(time.Now()), + Attach: core.String(user), + NotifyUrl: core.String(callback), + GoodsTag: core.String("sealos recharge"), + SupportFapiao: core.Bool(false), + Amount: &native.Amount{ + Currency: core.String("CNY"), + Total: core.Int64(amount), + }, + Detail: &native.Detail{ + CostPrice: core.Int64(608800), + GoodsDetail: []native.GoodsDetail{ + { + GoodsName: core.String("sealos cloud recharge"), + MerchantGoodsId: core.String("ABC"), + Quantity: core.Int64(1), + UnitPrice: core.Int64(828800), + WechatpayGoodsId: core.String("1001"), + }, + }, + }, + SettleInfo: &native.SettleInfo{ + ProfitSharing: core.Bool(false), + }, + }, + ) + if err != nil { + return "", fmt.Errorf("call Prepay err:%w", err) + } + return *resp.CodeUrl, nil +} + +func GetRandomString(n int) string { + randBytes := make([]byte, n/2) + if _, err := rand.Read(randBytes); err != nil { + return "" + } + return hex.EncodeToString(randBytes) +} diff --git a/controllers/pkg/pay/wechat_payment_test.go b/controllers/pkg/pay/wechat_payment_test.go new file mode 100644 index 000000000000..cfbfe35da3d4 --- /dev/null +++ b/controllers/pkg/pay/wechat_payment_test.go @@ -0,0 +1,70 @@ +package pay + +import ( + "fmt" + "testing" + "time" +) + +func TestWechatPayment_PaymentAndRefund(t *testing.T) { + requirePaymentTest(t, + MchID, + WechatPrivateKey, + MchCertificateSerialNumber, + MchAPIv3Key, + AppID, + NotifyCallbackURL, + ) + // initialize the wechat pay object + wechatPayment := WechatPayment{} + + user := "test_user" + amount := int64(10000) // The amount to be paid is in "cents", e.g. 10,000 cents = 100 RMB + describe := "test_payouts" + + // create a payment order + tradeNo, codeURL, err := wechatPayment.CreatePayment(amount, user, describe) + if err != nil { + t.Fatalf("failed to create a payment order: %v", err) + } + + // print the payment order information + fmt.Printf("the payment order has been created successfully\n") + fmt.Printf("merchant order number %s\n", tradeNo) + fmt.Printf("payment qr code link %s\n", codeURL) + + time.Sleep(40 * time.Second) + + // check the status of your payment order + status, paidAmount, err := wechatPayment.GetPaymentDetails(tradeNo) + if err != nil { + t.Fatalf("failed to query the payment order: %v", err) + } + + // print the status of the payment order + fmt.Printf("payment order status: %s\n", status) + fmt.Printf("payment amount %d cent\n", paidAmount) + + // determine whether the payment was successful + // if status != StatusSuccess { + // t.Fatalf("The payment was unsuccessful and no refund can be made") + //} + + // make a refund + refundOption := RefundOption{ + TradeNo: tradeNo, + OrderID: tradeNo, // can be set to be the same as the order number + Amount: amount, // refund amount + } + + // invoke the refund method + refundNo, refundID, err := wechatPayment.RefundPayment(refundOption) + if err != nil { + t.Fatalf("refund failed: %v", err) + } + + // print the refund information + fmt.Printf("the refund was successful!\n") + fmt.Printf("merchant refund number: %s\n", refundNo) + fmt.Printf("wechat refund number: %s\n", refundID) +} diff --git a/controllers/pkg/resources/named.go b/controllers/pkg/resources/named.go new file mode 100644 index 000000000000..e9daa6fa61fc --- /dev/null +++ b/controllers/pkg/resources/named.go @@ -0,0 +1,150 @@ +// Copyright © 2023 sealos. +// +// 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 resources + +import ( + "strings" + + "github.com/labring/sealos/controllers/pkg/utils/label" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + DBPodLabelInstanceKey = "app.kubernetes.io/instance" + DBPodLabelComponentNameKey = "apps.kubeblocks.io/component-name" + TerminalIDLabelKey = "TerminalID" + AppLabelKey = "app" + AppDeployLabelKey = "cloud.sealos.io/app-deploy-manager" + AppStoreDeployLabelKey = "cloud.sealos.io/deploy-on-sealos" + JobNameLabelKey = "job-name" + ACMEChallengeKey = "acme.cert-manager.io/http01-solver" + KubeBlocksBackUpName = "kubeblocks-backup-data" + dataProtectionBackupRepoKey = "dataprotection.kubeblocks.io/backup-repo-name" + InstanceLabelKey = "app.kubernetes.io/instance" +) + +type ResourceNamed struct { + _name string + // db or app or terminal or job or other + _type string + parentType string + parentName string + labels map[string]string +} + +func NewResourceNamed(cr client.Object) *ResourceNamed { + labels := cr.GetLabels() + p := &ResourceNamed{labels: labels} + switch { + case cr.GetName() == KubeBlocksBackUpName || labels[dataProtectionBackupRepoKey] != "": + p._type = DBBackup + p._name = KubeBlocksBackUpName + if labels[InstanceLabelKey] != "" { + p._name = labels[InstanceLabelKey] + } + case labels[DBPodLabelComponentNameKey] != "": + p._type = DB + p._name = labels[DBPodLabelInstanceKey] + case labels[TerminalIDLabelKey] != "" || (labels[label.AppManagedBy] == label.DefaultManagedBy && labels[label.AppPartOf] == "terminal"): + p._type = TERMINAL + p._name = "" + case labels[label.AppPartOf] == "devbox": + p._type = DevBox + p._name = labels[label.AppName] + case labels[AppLabelKey] != "": + p._type = APP + p._name = labels[AppLabelKey] + case labels[AppDeployLabelKey] != "": + p._type = APP + p._name = labels[AppDeployLabelKey] + case labels[JobNameLabelKey] != "": + p._type = JOB + p._name = strings.SplitN(labels[JobNameLabelKey], "-", 2)[0] + case labels[ACMEChallengeKey] != "": + p._type = APP + p._name = getACMEResolverName(cr) + default: + p._type = OTHER + p._name = "" + } + return p +} + +func (r *ResourceNamed) SetInstanceParent(instances map[string]struct{}) { + for ins := range instances { + if strings.HasPrefix(r._name, ins) { + r.parentType = AppStore + r.parentName = ins + } + } +} + +func NewObjStorageResourceNamed(bucket string) *ResourceNamed { + return &ResourceNamed{ + _type: ObjectStorage, + _name: bucket, + } +} + +const ( + acmesolver = "acmesolver" + acmesolverContainerArgsDomainPrefix = "--domain=" +) + +func getACMEResolverName(obj client.Object) string { + pod, ok := obj.(*corev1.Pod) + if !ok { + return "" + } + for _, container := range pod.Spec.Containers { + if container.Name != acmesolver { + continue + } + for _, arg := range container.Args { + if strings.HasPrefix(arg, acmesolverContainerArgsDomainPrefix) { + return acmesolver + "-" + strings.TrimPrefix( + arg, + acmesolverContainerArgsDomainPrefix, + ) + } + } + } + return pod.Name +} + +func (r *ResourceNamed) Type() uint8 { + return AppType[r._type] +} + +func (r *ResourceNamed) ParentType() uint8 { + return AppType[r.parentType] +} + +func (r *ResourceNamed) ParentName() string { + return r.parentName +} + +func (r *ResourceNamed) TypeString() string { + return r._type +} + +func (r *ResourceNamed) Name() string { + return r._name +} + +func (r *ResourceNamed) String() string { + return r._type + "/" + r._name +} diff --git a/controllers/pkg/resources/resources.go b/controllers/pkg/resources/resources.go new file mode 100644 index 000000000000..3b9e4a1f3140 --- /dev/null +++ b/controllers/pkg/resources/resources.go @@ -0,0 +1,662 @@ +// Copyright © 2023 sealos. +// +// 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 resources + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/common" + "github.com/labring/sealos/controllers/pkg/gpu" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "go.mongodb.org/mongo-driver/bson/primitive" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +/* +Unified base unit: + +CPU: millicore (mCore) (1 core = 1000 millicores) 0.067 / 1000 * 1000000 = 67 unit price +Memory: Mebibytes (MiB) 0.033 / 1024 (2^30) * 1000000 = 33 unit price +Storage: Mebibytes (MiB) 0.0021 / 1024 * 1000000 = 2 unit price +Network bandwidth: Mebibytes (MiB) 0.00078125 / 1024 * 1000000 = 781 unit price +*/ + +// | property | Price | Detail | +// | ------------ | ----- | -------------- | +// | Cpu | 67 | mCore unit | +// | Memory | 33 | Mebibytes unit | +// | Disk | 2 | Mebibytes unit | +// | Mongodb | 1 | feature... | +// | Minio | 1 | Mebibytes unit | +// | Infra-Cpu | 67 | mCore unit | +// | Infra-Memory | 33 | Mebibytes unit | +// | Infra-Disk | 2 | Mebibytes unit | +// +// price: 1000000 = 1¥ + +type Price struct { + Property string `json:"property" bson:"property"` + Price int64 `json:"price" bson:"price"` + Detail string `json:"detail" bson:"detail"` + // Unit string `json:"unit" bson:"unit"` +} + +// | Category | property | Time | value | | +// | ---------- | ------------ | ------- | ----- | ---- | +// | Namespace1 | cpu | timestamp1 | 5 | | +// | Namespace1 | memory | timestamp1 | 100 | | +// | Namespace1 | disk | timestamp1 | 30 | | +// | appid1 | mongodb | timestamp1 | 100 | | +// | appid1 | Minio | timestamp1 | 100 | | +// | Namespace2 | Infra-Cpu | timestamp1 | 5 | | +// | Namespace2 | cpu | timestamp1 | 100 | | +// | Namespace2 | Infra-Memory | timestamp1 | 30 | | +// | Namespace2 | Infra-Disk | timestamp1 | 100 | | +// | Namespace2 | memory | timestamp1 | 100 | | +// | Namespace2 | disk | timestamp1 | 100 | | + +// Composite index: category, property, time, speed up query +type Monitor struct { + Time time.Time `json:"time" bson:"time"` + // equal namespace + Category string `json:"category" bson:"category"` + Type uint8 `json:"type" bson:"type"` + ParentType uint8 `json:"parent_type" bson:"parent_type"` + ParentName string `json:"parent_name" bson:"parent_name"` + Name string `json:"name" bson:"name"` + Used EnumUsedMap `json:"used" bson:"used"` + Property string `json:"property,omitempty" bson:"property,omitempty"` +} + +type ActiveBilling struct { + ID primitive.ObjectID `json:"_id,omitempty" bson:"_id,omitempty"` + Time time.Time `json:"time,omitempty" bson:"time"` + Namespace string `json:"namespace" bson:"namespace"` + AppType string `json:"app_type" bson:"app_type"` + AppName string `json:"app_name" bson:"app_name"` + Used UsedMap `json:"used,omitempty" bson:"used,omitempty"` + Amount int64 `json:"amount" bson:"amount,omitempty"` + Owner string `json:"owner" bson:"owner,omitempty"` + UserUID uuid.UUID `json:"user_uid" bson:"user_uid"` + Status ConsumptionStatus `json:"status" bson:"status"` + // Rule string `json:"rule" bson:"rule,omitempty"` +} + +type ConsumptionStatus string + +const ( + Consumed ConsumptionStatus = "consumed" + Processing ConsumptionStatus = "processing" + Unconsumed ConsumptionStatus = "unconsumed" + ErrorConsumed ConsumptionStatus = "error_consumed" +) + +type BillingType int + +type Billing struct { + Time time.Time `json:"time" bson:"time"` + OrderID string `json:"order_id" bson:"order_id"` + Type common.Type `json:"type" bson:"type"` + // Name string `json:"name" bson:"name"` + Namespace string `json:"namespace" bson:"namespace"` + // Used Used `json:"used" bson:"used"` + // UsedAmount Used `json:"used_amount" bson:"used_amount"` + + AppCosts []AppCost `json:"app_costs,omitempty" bson:"app_costs,omitempty"` + AppName string `json:"app_name,omitempty" bson:"app_name,omitempty"` + AppType uint8 `json:"app_type,omitempty" bson:"app_type,omitempty"` + + Amount int64 `json:"amount" bson:"amount,omitempty"` + Owner string `json:"owner" bson:"owner,omitempty"` + // 0: 未结算 1: 已结算 + Status BillingStatus `json:"status" bson:"status"` + // if type = Consumption, then payment is not nil + Payment *Payment `json:"payment" bson:"payment,omitempty"` + // if type = Transfer, then transfer is not nil + Transfer *Transfer `json:"transfer" bson:"transfer,omitempty"` + Detail string `json:"detail" bson:"detail,omitempty"` + // UserUID uuid.UUID `json:"user_uid" bson:"user_uid,omitempty"` +} + +type BillingCheckpoint struct { + ID string `json:"id" bson:"_id"` + Time time.Time `json:"time" bson:"time"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` +} + +type Payment struct { + Method string `json:"method" bson:"method"` + UserID string `json:"user_id" bson:"user_id"` + Amount int64 `json:"amount,omitempty"` + TradeNO string `json:"tradeNO,omitempty"` + // CodeURL is the codeURL of wechatpay + CodeURL string `json:"codeURL,omitempty"` +} + +type Transfer struct { + From string `json:"from" bson:"from,omitempty"` + To string `json:"to" bson:"to,omitempty"` + Amount int64 `json:"amount" bson:"amount"` +} + +type AppCost struct { + Type uint8 `json:"type" bson:"type"` + Used EnumUsedMap `json:"used" bson:"used"` + UsedAmount EnumUsedMap `json:"used_amount" bson:"used_amount"` + Amount int64 `json:"amount" bson:"amount,omitempty"` + Name string `json:"name" bson:"name"` +} + +type BillingHandler struct { + OrderID string `json:"order_id" bson:"order_id"` + Time time.Time `json:"time" bson:"time"` + Amount int64 `json:"amount" bson:"amount,omitempty"` + Status BillingStatus `json:"status" bson:"status,omitempty"` +} + +type BillingStatus int + +const ( + // 0: 未结算 1: 已结算 + Unsettled BillingStatus = iota + Settled + Subscription +) + +const ( + // Consumption 消费 + Consumption common.Type = iota + // Subconsumption 子消费 + SubConsumption +) + +const ( + // DB = 1 + // APP = 2 + // TERMINAL = 3 + // JOB = 4 + // OTHER = 5 + + db = iota + 1 + app + terminal + job + other + objectStorage + cvm + appStore + dbBackup + devBox + llmToken +) + +const ( + DB = "DB" + APP = "APP" + TERMINAL = "TERMINAL" + JOB = "JOB" + OTHER = "OTHER" + ObjectStorage = "OBJECT-STORAGE" + CVM = "CLOUD-VM" + AppStore = "APP-STORE" + DBBackup = "DB-BACKUP" + DevBox = "DEV-BOX" + LLMToken = "LLM-TOKEN" +) + +var AppType = map[string]uint8{ + DB: db, APP: app, TERMINAL: terminal, JOB: job, OTHER: other, ObjectStorage: objectStorage, CVM: cvm, AppStore: appStore, DBBackup: dbBackup, DevBox: devBox, LLMToken: llmToken, +} + +var AppTypeReverse = map[uint8]string{ + db: DB, app: APP, terminal: TERMINAL, job: JOB, other: OTHER, objectStorage: ObjectStorage, cvm: CVM, appStore: AppStore, dbBackup: DBBackup, devBox: DevBox, llmToken: LLMToken, +} + +// resource consumption +type EnumUsedMap map[uint8]int64 + +type UsedMap map[string]float64 + +type PropertyType struct { + // For the monitoring storage enumeration type, use uint 8 to save memory + // 0 cpu, 1 memory, 2 storage, 3 network ... expandable + Name string `json:"name" bson:"name"` + Alias string `json:"alias" bson:"alias"` + Enum uint8 `json:"enum" bson:"enum"` + // AVG, SUM, DIF value. The cumulative value is the average value by default + PriceType string `json:"price_type,omitempty" bson:"price_type,omitempty"` + // Price = UsedAmount (avg || accumulated-value || difference-value) / Unit * UnitPrice + UnitPrice float64 `json:"unit_price" bson:"unit_price"` + ViewPrice float64 `json:"view_price" bson:"view_price"` + EncryptUnitPrice string `json:"encrypt_unit_price" bson:"encrypt_unit_price"` + Unit resource.Quantity `json:"-" bson:"-"` + // ::= 0 | 1 | ... | 9 + // ::= | + // ::= | . | . | . + // ::= "+" | "-" + // ::= | + // ::= | | + // ::= Ki | Mi | Gi | Ti | Pi | Ei + // + // (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) + // + // ::= m | "" | k | M | G | T | P | E + // + // (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) + // + // ::= "e" | "E" + UnitString string `json:"unit" bson:"unit"` + // charging cycle second + UnitPeriod string `json:"unit_period,omitempty" bson:"unit_period,omitempty"` +} + +type PropertyTypeLS struct { + Types []PropertyType + StringMap map[string]PropertyType + EnumMap map[uint8]PropertyType +} + +const ( + // average value + AVG = "AVG" + // accumulated value + SUM = "SUM" + // difference value + DIF = "DIF" +) + +var DefaultPropertyTypeList = []PropertyType{ + { + Name: "cpu", + Enum: 0, + PriceType: AVG, + // raw price: 67 + UnitPrice: 2.237442922, + UnitString: "1m", + }, + { + Name: "memory", + Enum: 1, + PriceType: AVG, + // raw price: 33 + UnitPrice: 1.092501427, + UnitString: "1Mi", + }, + { + Name: "storage", + Enum: 2, + PriceType: AVG, + // raw price: 21 + UnitPrice: 0, + UnitString: "1Mi", + }, + { + Name: "network", + Enum: 3, + PriceType: SUM, + UnitPrice: 0, + UnitString: "1Mi", + }, + { + // monitor unit: 1 node port = 1000 unit + Name: "services.nodeports", + Enum: 4, + PriceType: AVG, + UnitPrice: 2.083, + ViewPrice: 2083, + UnitString: "1", + }, +} + +var DefaultPropertyTypeLS = newPropertyTypeLS(DefaultPropertyTypeList) + +func ConvertEnumUsedToString(costs map[uint8]int64) (costsMap map[string]int64) { + costsMap = make(map[string]int64, len(costs)) + for k, v := range costs { + costsMap[DefaultPropertyTypeLS.EnumMap[k].Name] = v + } + return costsMap +} + +func NewPropertyTypeLS(types []PropertyType) (ls *PropertyTypeLS) { + return newPropertyTypeLS(types) +} + +func newPropertyTypeLS(types []PropertyType) (ls *PropertyTypeLS) { + ls = &PropertyTypeLS{ + Types: types, + StringMap: make(PropertyTypeStringMap, len(types)), + EnumMap: make(PropertyTypeEnumMap, len(types)), + } + for i := range types { + if types[i].Unit == (resource.Quantity{}) && types[i].UnitString != "" { + types[i].Unit = resource.MustParse(types[i].UnitString) + } + ls.EnumMap[types[i].Enum] = types[i] + ls.StringMap[types[i].Name] = types[i] + } + return ls +} + +type PropertyTypeEnumMap map[uint8]PropertyType + +type PropertyTypeStringMap map[string]PropertyType + +type PropertyList []PropertyType + +// GpuResourcePrefix GPUResource = gpu- + gpu.Product ; ex. gpu-tesla-v100 +const GpuResourcePrefix = "gpu-" + +const ( + ResourceGPU corev1.ResourceName = gpu.NvidiaGpuKey + ResourceNetwork = "network" +) + +const ( + ResourceRequestGpu corev1.ResourceName = "requests." + gpu.NvidiaGpuKey + ResourceLimitGpu corev1.ResourceName = "limits." + gpu.NvidiaGpuKey + ResourceObjectStorageSize corev1.ResourceName = "objectstorage/size" + ResourceObjectStorageBucket corev1.ResourceName = "objectstorage/bucket" +) + +func NewGpuResource(product string) corev1.ResourceName { + return corev1.ResourceName(GpuResourcePrefix + product) +} + +func IsGpuResource(resource string) bool { + return strings.HasPrefix(resource, GpuResourcePrefix) +} + +func GetGpuResourceProduct(resource string) string { + return strings.TrimPrefix(resource, GpuResourcePrefix) +} + +func GetDefaultResourceQuota(ns, name string) *corev1.ResourceQuota { + return &corev1.ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: corev1.ResourceQuotaSpec{ + Hard: DefaultResourceQuotaHard(), + }, + } +} + +func GetLimit0Quota(ns, name string) *corev1.ResourceQuota { + return &corev1.ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: corev1.ResourceQuotaSpec{ + Hard: Limit0Quota, + }, + } +} + +var Limit0Quota = corev1.ResourceList{} + +func init() { + zeroQuantity := func() (q resource.Quantity) { + q.Set(0) + return q + } + for k := range DefaultResourceQuotaHard() { + Limit0Quota[k] = zeroQuantity() + } +} + +func GetDefaultLimitRange(ns, name string) *corev1.LimitRange { + return &corev1.LimitRange{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: corev1.LimitRangeSpec{ + Limits: DefaultLimitRangeLimits(), + }, + } +} + +const ( + QuotaLimitsCPU = "QUOTA_LIMITS_CPU" + QuotaLimitsMemory = "QUOTA_LIMITS_MEMORY" + QuotaLimitsStorage = "QUOTA_LIMITS_STORAGE" + QuotaLimitsGPU = "QUOTA_LIMITS_GPU" + QuotaLimitsPods = "QUOTA_LIMITS_PODS" + QuotaLimitsNodePorts = "QUOTA_LIMITS_NODE_PORTS" + QuotaObjectStorageSize = "QUOTA_OBJECT_STORAGE_SIZE" + QuotaObjectStorageBucket = "QUOTA_OBJECT_STORAGE_BUCKET" + + LimitRangeCPU = "LIMIT_RANGE_CPU" + LimitRangeMemory = "LIMIT_RANGE_MEMORY" + LimitRangeEphemeralStorage = "LIMIT_RANGE_EPHEMERAL_STORAGE" + + LimitRangeRepCPU = "LIMIT_RANGE_REP_CPU" + LimitRangeRepMemory = "LIMIT_RANGE_REP_MEMORY" + LimitRangeRepEphemeralStorage = "LIMIT_RANGE_REP_EPHEMERAL_STORAGE" +) + +const ( + DefaultQuotaLimitsCPU = "16" + DefaultQuotaLimitsMemory = "64Gi" + DefaultQuotaLimitsStorage = "100Gi" + DefaultQuotaLimitsGPU = "8" + DefaultQuotaLimitsNodePorts = "10" + DefaultQuotaLimitsPods = "20" + DefaultQuotaObjectStorageSize = "20Gi" + DefaultQuotaObjectStorageBucket = "5" +) + +func DefaultResourceQuotaHard() corev1.ResourceList { + return corev1.ResourceList{ + ResourceRequestGpu: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsGPU, DefaultQuotaLimitsGPU), + ), + ResourceLimitGpu: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsGPU, DefaultQuotaLimitsGPU), + ), + corev1.ResourceLimitsCPU: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsCPU, DefaultQuotaLimitsCPU), + ), + corev1.ResourceLimitsMemory: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsMemory, DefaultQuotaLimitsMemory), + ), + corev1.ResourceRequestsStorage: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsStorage, DefaultQuotaLimitsStorage), + ), + corev1.ResourceLimitsEphemeralStorage: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsStorage, DefaultQuotaLimitsStorage), + ), + corev1.ResourcePods: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsPods, DefaultQuotaLimitsPods), + ), + corev1.ResourceServicesNodePorts: resource.MustParse( + env.GetEnvWithDefault(QuotaLimitsNodePorts, DefaultQuotaLimitsNodePorts), + ), + ResourceObjectStorageSize: resource.MustParse( + env.GetEnvWithDefault(QuotaObjectStorageSize, DefaultQuotaObjectStorageSize), + ), + ResourceObjectStorageBucket: resource.MustParse( + env.GetEnvWithDefault(QuotaObjectStorageBucket, DefaultQuotaObjectStorageBucket), + ), + // TODO storage.diskio.read, storage.diskio.write + } +} + +func ParseResourceLimitWithSubscription( + plans []types.SubscriptionPlan, +) (map[string]corev1.ResourceList, error) { + subPlansLimit := make(map[string]corev1.ResourceList) + for i := range plans { + // max_resources: {"cpu":"128","memory":"256Gi","storage":"500Gi"} + res := plans[i].MaxResources + if res == "" { + subPlansLimit[plans[i].Name] = DefaultResourceQuotaHard() + } else { + var maxResources map[string]string + if err := json.Unmarshal([]byte(res), &maxResources); err != nil { + return nil, fmt.Errorf("parse max_resources failed: %w", err) + } + rl := make(corev1.ResourceList) + for k, v := range maxResources { + _v, err := ParseCustomQuantity(v) + if err != nil { + return nil, fmt.Errorf("parse %s failed: %w", k, err) + } + switch k { + case "cpu": + rl[corev1.ResourceLimitsCPU] = _v + case "memory": + rl[corev1.ResourceLimitsMemory] = _v + case "storage": + rl[corev1.ResourceRequestsStorage] = _v + case "nodeports": + rl[corev1.ResourceServicesNodePorts] = _v + case corev1.ResourcePods.String(): + rl[corev1.ResourcePods] = _v + case corev1.ResourceServices.String(): + rl[corev1.ResourceServices] = _v + case ResourceObjectStorageSize.String(): + rl[ResourceObjectStorageSize] = _v + case ResourceObjectStorageBucket.String(): + rl[ResourceObjectStorageBucket] = _v + } + } + subPlansLimit[plans[i].Name] = rl + } + } + return subPlansLimit, nil +} + +type HasNameAndMaxResources interface { + GetName() string + GetMaxResources() string +} + +func ParseResourceLimitWithPlans[T HasNameAndMaxResources]( + plans []T, +) (map[string]corev1.ResourceList, error) { + subPlansLimit := make(map[string]corev1.ResourceList) + + for _, plan := range plans { + res := plan.GetMaxResources() + if res == "" { + subPlansLimit[plan.GetName()] = DefaultResourceQuotaHard() + continue + } + + var maxResources map[string]string + if err := json.Unmarshal([]byte(res), &maxResources); err != nil { + return nil, fmt.Errorf("parse max_resources failed: %w", err) + } + + rl := make(corev1.ResourceList) + for k, v := range maxResources { + _v, err := ParseCustomQuantity(v) + if err != nil { + return nil, fmt.Errorf("parse %s failed: %w", k, err) + } + switch k { + case "cpu": + rl[corev1.ResourceLimitsCPU] = _v + case "memory": + rl[corev1.ResourceLimitsMemory] = _v + case "storage": + rl[corev1.ResourceRequestsStorage] = _v + case "nodeports": + rl[corev1.ResourceServicesNodePorts] = _v + case corev1.ResourcePods.String(): + rl[corev1.ResourcePods] = _v + case corev1.ResourceServices.String(): + rl[corev1.ResourceServices] = _v + case ResourceObjectStorageSize.String(): + rl[ResourceObjectStorageSize] = _v + case ResourceObjectStorageBucket.String(): + rl[ResourceObjectStorageBucket] = _v + } + } + subPlansLimit[plan.GetName()] = rl + } + return subPlansLimit, nil +} + +func ParseCustomQuantity(s string) (resource.Quantity, error) { + s = strings.Replace(s, "GiB", "Gi", 1) + s = strings.Replace(s, "MiB", "Mi", 1) + return resource.ParseQuantity(s) +} + +func DefaultLimitRangeLimits() []corev1.LimitRangeItem { + return []corev1.LimitRangeItem{ + { + Type: corev1.LimitTypeContainer, + Default: defaultLimitRange, + DefaultRequest: defaultLimitRangeReq, + }, + } +} + +var defaultLimitRange, defaultLimitRangeReq = getLimitRangeDefault(), getLimitRangeReq() + +func getLimitRangeDefault() corev1.ResourceList { + rcList := corev1.ResourceList{} + cpu, memory, ephemeralStorage := resource.MustParse( + env.GetEnvWithDefault(LimitRangeCPU, "50m"), + ), resource.MustParse( + env.GetEnvWithDefault(LimitRangeMemory, "64Mi"), + ), resource.MustParse( + env.GetEnvWithDefault(LimitRangeEphemeralStorage, "100Mi"), + ) + if !cpu.IsZero() { + rcList[corev1.ResourceCPU] = cpu + } + if !memory.IsZero() { + rcList[corev1.ResourceMemory] = memory + } + if !ephemeralStorage.IsZero() { + rcList[corev1.ResourceEphemeralStorage] = ephemeralStorage + } + return rcList +} + +func getLimitRangeReq() corev1.ResourceList { + rcList := corev1.ResourceList{} + cpu, memory, ephemeralStorage := resource.MustParse( + env.GetEnvWithDefault(LimitRangeRepCPU, "50m"), + ), resource.MustParse( + env.GetEnvWithDefault(LimitRangeRepMemory, "64Mi"), + ), resource.MustParse( + env.GetEnvWithDefault(LimitRangeRepEphemeralStorage, "100Mi"), + ) + if !cpu.IsZero() { + rcList[corev1.ResourceCPU] = cpu + } + if !memory.IsZero() { + rcList[corev1.ResourceMemory] = memory + } + if !ephemeralStorage.IsZero() { + rcList[corev1.ResourceEphemeralStorage] = ephemeralStorage + } + return rcList +} diff --git a/controllers/pkg/types/activity.go b/controllers/pkg/types/activity.go new file mode 100644 index 000000000000..995b65eb1c36 --- /dev/null +++ b/controllers/pkg/types/activity.go @@ -0,0 +1,228 @@ +// Copyright © 2023 sealos. +// +// 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 types + +import ( + "fmt" + "strconv" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type UserRechargeDiscount struct { + DefaultActiveType ActivityType `json:"defaultActiveType,omitempty" bson:"defaultActiveType,omitempty"` + DefaultSteps map[int64]int64 `json:"defaultSteps" bson:"defaultSteps"` + FirstRechargeSteps map[int64]int64 `json:"firstRechargeDiscount" bson:"firstRechargeDiscount"` +} + +// TODO the following structures will be deleted +type Activity struct { + gorm.Model + ActivityType string `gorm:"uniqueIndex"` + PhaseOrder string + Phases []Phase +} + +type Phase struct { + gorm.Model + ActivityID uint `gorm:"index"` + Name string + GiveAmount int64 + RechargeDiscount RechargeDiscountInfo `gorm:"embedded"` +} + +type RechargeDiscount struct { + DiscountRates []float64 `json:"discountRates"` + DiscountSteps []int64 `json:"discountSteps"` + SpecialDiscount map[int64]int64 `json:"specialDiscount" gorm:"type:jsonb"` +} + +type RechargeDiscountInfo struct { + LimitTimes int64 `gorm:"default:0"` + LimitDuration string + RechargeDiscount +} + +type UserActivity struct { + gorm.Model + Name string + UserID uuid.UUID `gorm:"index"` + CurrentPhase string + ActivityID uint `gorm:"index"` + Phases []UserPhase +} + +type UserPhase struct { + gorm.Model + UserActivityID uuid.UUID `gorm:"index"` + Name string + StartTime time.Time + EndTime time.Time + RechargeNums int64 + GiveAmount int64 +} + +// type RechargeDiscount struct { +// LimitTimes int64 `json:"limitTimes,omitempty"` +// LimitDuration string `json:"limitDuration,omitempty"` +// DiscountRates []float64 `json:"discountRates,omitempty"` +// DiscountSteps []int64 `json:"discountSteps,omitempty"` +// SpecialDiscount map[int64]int64 `json:"specialDiscount,omitempty"` +//} +// +// type Phase struct { +// Name string `json:"name"` +// GiveAmount int64 `json:"giveAmount"` +// RechargeDiscount RechargeDiscount `json:",inline"` +//} +// +// type Activity struct { +// ActivityType string `json:"activityType"` +// Phases map[string]Phase `json:"phases"` +// PhaseOrder string `json:"phaseOrder"` +//} +// +// type UserPhase struct { +// Name string `json:"name"` +// //RFC339 time format +// StartTime time.Time `json:"startTime"` +// EndTime time.Time `json:"endTime"` +// RechargeNums int64 `json:"rechargeNums"` +// GiveAmount int64 `json:"giveAmount"` +//} +// +// type UserActivity struct { +// CurrentPhase string `json:"currentPhase"` +// Phases map[string]*UserPhase +//} + +type Activities map[string]*Activity + +type UserActivities map[string]*UserActivity + +// func ParseUserActivities(annotations map[string]string) (UserActivities, error) { +// userActivities := make(map[string]*UserActivity) +// +// for key, value := range annotations { +// parts := strings.Split(key, ".") +// +// if len(parts) == 3 && parts[0] == "activity" && parts[2] == "current-phase" { +// if _, exists := userActivities[parts[1]]; !exists { +// userActivities[parts[1]] = &UserActivity{ +// Phases: make(map[string]*UserPhase), +// } +// } +// userActivities[parts[1]].CurrentPhase = value +// } +// +// if len(parts) == 4 && parts[0] == "activity" { +// activityType := parts[1] +// phase := parts[2] +// +// if _, exists := userActivities[activityType]; !exists { +// userActivities[activityType] = &UserActivity{ +// Phases: make(map[string]*UserPhase), +// } +// } +// +// if _, exists := userActivities[activityType].Phases[phase]; !exists { +// userActivities[activityType].Phases[phase] = &UserPhase{Name: phase} +// } +// var err error +// switch parts[3] { +// case "startTime": +// fmt.Println(value) +// userActivities[activityType].Phases[phase].StartTime, err = time.Parse(time.RFC3339, value) +// if err != nil { +// return nil, fmt.Errorf("parse start time failed: %w", err) +// } +// case "endTime": +// userActivities[activityType].Phases[phase].EndTime, err = time.Parse(time.RFC3339, value) +// if err != nil { +// return nil, fmt.Errorf("parse end time failed: %w", err) +// } +// case "rechargeNums": +// userActivities[activityType].Phases[phase].RechargeNums, err = strconv.ParseInt(value, 10, 64) +// if err != nil { +// return nil, fmt.Errorf("parse %s to recharge nums failed: %w", value, err) +// } +// case "giveAmount": +// userActivities[activityType].Phases[phase].GiveAmount, err = strconv.ParseInt(value, 10, 64) +// if err != nil { +// return nil, fmt.Errorf("parse %s to give amount failed: %w", value, err) +// } +// } +// } +// } +// return userActivities, nil +//} + +// func SetUserPhaseRechargeTimes(annotations map[string]string, activityType string, phase string, rechargeNums int64) map[string]string { +// annotations[fmt.Sprintf("activity.%s.%s.rechargeNums", activityType, phase)] = fmt.Sprintf("%d", rechargeNums) +// return annotations +//} + +func SetUserPhaseGiveAmount( + annotations map[string]string, + activityType, phase string, + giveAmount int64, +) map[string]string { + annotations[fmt.Sprintf("activity.%s.%s.giveAmount", activityType, phase)] = strconv.FormatInt( + giveAmount, + 10, + ) + return annotations +} + +// func GetUserActivityDiscount(activities Activities, userActivities *UserActivities) (activityType string, returnPhase *Phase, returnErr error) { +// if activities == nil || userActivities == nil { +// returnErr = fmt.Errorf("activities is nil") +// return +// } +// for aType, userActivity := range *userActivities { +// activity := activities[aType] +// for _, phase := range activity.Phases { +// if phase.Name == userActivity.CurrentPhase { +// +// for _, userPhase := range userActivity.Phases { +// if userPhase.Name == userActivity.CurrentPhase { +// +// if phase.RechargeDiscount.LimitTimes > 0 && userPhase.RechargeNums >= phase.RechargeDiscount.LimitTimes { +// return +// } +// if phase.RechargeDiscount.LimitDuration != "" { +// duration, err := time.ParseDuration(phase.RechargeDiscount.LimitDuration) +// if err != nil { +// returnErr = fmt.Errorf("parse duration failed: %w", err) +// return +// } +// if time.Now().After(userActivity.Phases[userActivity.CurrentPhase].EndTime.Add(duration)) { +// continue +// } +// } +// return aType, &phase, nil +// +// } +// } +// } +// } +// returnErr = fmt.Errorf("user activity not exist") +// return +// } +// returnErr = fmt.Errorf("user activity not exist") +// return +//} diff --git a/controllers/pkg/types/config.go b/controllers/pkg/types/config.go new file mode 100644 index 000000000000..d680d746def9 --- /dev/null +++ b/controllers/pkg/types/config.go @@ -0,0 +1,37 @@ +/* +Copyright 2024. + +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 types + +type Configs struct { + Type ConfigType `json:"type" gorm:"type:varchar(255);not null,primaryKey"` + Data string `json:"data" gorm:"type:jsonb"` +} + +type ConfigType string + +const AccountConfigType ConfigType = "account" + +type AccountConfig struct { + TaskProcessRegion string `json:"taskProcessRegion"` + FirstRechargeDiscountSteps map[int64]int64 `json:"firstRechargeDiscountSteps"` + DefaultDiscountSteps map[int64]int64 `json:"defaultDiscountSteps"` + DefaultActiveType ActivityType `json:"defaultActiveType,omitempty"` +} + +func (c Configs) TableName() string { + return "Configs" +} diff --git a/controllers/pkg/types/credits.go b/controllers/pkg/types/credits.go new file mode 100644 index 000000000000..772d13e27d6f --- /dev/null +++ b/controllers/pkg/types/credits.go @@ -0,0 +1,78 @@ +// Copyright © 2025 sealos. +// +// 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 types + +import ( + "time" + + "github.com/google/uuid" +) + +// TODO: 加索引 +// Credits 表示用户的credits信息 +type Credits struct { + ID uuid.UUID `json:"id" gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` // credits ID + UserUID uuid.UUID `json:"user_uid" gorm:"column:user_uid;type:uuid"` // 用户ID + Amount int64 `json:"amount" gorm:"column:amount;type:bigint"` // 总额度 + UsedAmount int64 `json:"used_amount" gorm:"column:used_amount;type:bigint"` // 已使用额度 + FromID string `json:"from_id" gorm:"column:from_id;type:text"` // 来源ID + FromType CreditsFromType `json:"from_type" gorm:"column:from_type;type:text"` // 来源分类 + ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;type:timestamp"` // 过期时间 + CreatedAt time.Time `json:"created_at" gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp"` // 创建时间 + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;type:timestamp(3) with time zone;autoUpdateTime;default:current_timestamp"` // 更新时间 + StartAt time.Time `json:"start_at" gorm:"column:start_at;type:timestamp"` // 开始时间 + Status CreditsStatus `json:"status" gorm:"column:status;type:text"` // 状态 +} + +type ( + CreditsStatus string + CreditsRecordType string + CreditsRecordReason string + + CreditsFromType string +) + +const ( + CreditsStatusActive CreditsStatus = "active" + CreditsStatusExpired CreditsStatus = "expired" + CreditsStatusUsedUp CreditsStatus = "used_up" + + CreditsFromTypeSubscription CreditsFromType = "subscription" + + CreditsRecordTypeIssue CreditsRecordType = "issue" + CreditsRecordTypeConsume CreditsRecordType = "consume" + + CreditsRecordReasonResourceAccountTransaction CreditsRecordReason = "AccountTransaction" +) + +// CreditsTransaction 表示credits的使用或发放记录 +type CreditsTransaction struct { + ID uuid.UUID `json:"id"` // 记录ID + UserUID uuid.UUID `json:"user_uid"` // 用户ID + AccountTransactionID *uuid.UUID `json:"account_transaction_id,omitempty"` // 关联的AccountTransaction ID + RegionUID uuid.UUID `json:"region_uid"` // 区域ID + CreditsID uuid.UUID `json:"credits_id"` // 关联的Credits ID + UsedAmount int64 `json:"used_amount"` // 使用额度 + CreatedAt time.Time `json:"created_at"` // 操作时间 + Reason CreditsRecordReason `json:"reason"` // 操作原因(如"AccountTransaction") +} + +func (Credits) TableName() string { + return "Credits" +} + +func (CreditsTransaction) TableName() string { + return "CreditsTransaction" +} diff --git a/controllers/pkg/types/cvm.go b/controllers/pkg/types/cvm.go new file mode 100644 index 000000000000..952ef9a3425e --- /dev/null +++ b/controllers/pkg/types/cvm.go @@ -0,0 +1,55 @@ +// Copyright © 2024 sealos. +// +// 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 types + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// CVMBilling represents the billing information of a CloudVirtualMachine instance +type CVMBilling struct { + ID primitive.ObjectID `json:"_id" bson:"_id"` + InstanceName string `json:"instanceName"` + // Namespace string `json:"namespace"` + // VirtualMachinePackageId string `json:"virtualMachinePackageId"` + StartAt time.Time `json:"startAt"` + EndAt time.Time `json:"endAt"` + // CloudProvider CvmCloudProvider `json:"cloudProvider"` + // SealosUserID string `json:"sealosUserId"` + SealosUserUID string `json:"sealosUserUid"` + SealosRegionUID string `json:"sealosRegionUid"` + SealosRegionDomain string `json:"sealosRegionDomain"` + Amount float64 `json:"amount"` + Detail struct { + Instance float64 `json:"instance"` + Network float64 `json:"network"` + Disk float64 `json:"disk"` + } + State CvmBillingStatus `json:"state"` +} + +const ( + CVMBillingStatePending = "Pending" + CVMBillingStateDone = "Done" + + CVMCloudProviderTencent = "tencent" +) + +type ( + CvmBillingStatus string + CvmCloudProvider string +) diff --git a/controllers/pkg/types/dbquery.go b/controllers/pkg/types/dbquery.go new file mode 100644 index 000000000000..7511e7b701ec --- /dev/null +++ b/controllers/pkg/types/dbquery.go @@ -0,0 +1,69 @@ +// Copyright © 2024 sealos. +// +// 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 types + +import ( + "time" + + "github.com/google/uuid" +) + +type UserQueryOpts struct { + UID uuid.UUID + WithOutCache bool + ID string + Namespace string + Owner string + IgnoreEmpty bool +} + +type GetTransfersReq struct { + *UserQueryOpts + // can be empty to get all transfers + TransferID string `json:"transferID"` + + // 0: all, 1: in, 2: out + Type TransferType `json:"type"` + LimitReq ` json:",inline"` +} + +type TransferType int + +const ( + TypeTransferAll TransferType = iota + TypeTransferIn + TypeTransferOut +) + +type TimeRange struct { + StartTime time.Time `json:"startTime" bson:"startTime" example:"2021-01-01T00:00:00Z"` + EndTime time.Time `json:"endTime" bson:"endTime" example:"2021-12-01T00:00:00Z"` +} + +type GetTransfersResp struct { + Transfers []Transfer `json:"transfers"` + LimitResp ` json:",inline"` +} + +type LimitReq struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + TimeRange ` json:",inline"` +} + +type LimitResp struct { + Total int64 `json:"total"` + TotalPage int64 `json:"totalPage"` +} diff --git a/controllers/pkg/types/debt.go b/controllers/pkg/types/debt.go new file mode 100644 index 000000000000..e063be8b126e --- /dev/null +++ b/controllers/pkg/types/debt.go @@ -0,0 +1,109 @@ +package types + +import ( + "time" + + "github.com/google/uuid" +) + +const ( + DebtNamespaceAnnoStatusKey = "debt.sealos/status" + NormalDebtNamespaceAnnoStatus = "Normal" + SuspendDebtNamespaceAnnoStatus = "Suspend" + SuspendCompletedDebtNamespaceAnnoStatus = "SuspendCompleted" + FinalDeletionDebtNamespaceAnnoStatus = "FinalDeletion" + FinalDeletionCompletedDebtNamespaceAnnoStatus = "FinalDeletionCompleted" + FinalDeletionReplayAnnotationKey = "debt.sealos/final-deletion-replay" + ResumeDebtNamespaceAnnoStatus = "Resume" + ResumeCompletedDebtNamespaceAnnoStatus = "ResumeCompleted" + TerminateSuspendDebtNamespaceAnnoStatus = "TerminateSuspend" + TerminateSuspendCompletedDebtNamespaceAnnoStatus = "TerminateSuspendCompleted" + + NetworkStatusAnnoKey = "network.sealos.io/status" + WorkspaceSubscriptionStatusAnnoKey = "subscription.sealos.io/status" + WorkspaceSubscriptionStatusUpdateTimeAnnoKey = "subscription.sealos.io/status-update-time" + NetworkSuspend = "Suspend" + NetworkSuspendCompleted = "SuspendCompleted" + NetworkResume = "Resume" + NetworkResumeCompleted = "ResumeCompleted" +) + +// DebtStatusType 定义债务状态类型 +type DebtStatusType string + +// 定义状态常量 +const ( + NormalPeriod DebtStatusType = "NormalPeriod" + LowBalancePeriod DebtStatusType = "LowBalancePeriod" + CriticalBalancePeriod DebtStatusType = "CriticalBalancePeriod" + DebtPeriod DebtStatusType = "DebtPeriod" + DebtDeletionPeriod DebtStatusType = "DebtDeletionPeriod" + FinalDeletionPeriod DebtStatusType = "FinalDeletionPeriod" +) + +const ( + DebtPrefix = "debt-" + DaySecond = 24 * 60 * 60 +) + +var StatusMap = map[DebtStatusType]int{ + NormalPeriod: 0, + LowBalancePeriod: 1, + CriticalBalancePeriod: 2, + DebtPeriod: 3, + DebtDeletionPeriod: 4, + FinalDeletionPeriod: 5, +} + +var ( + NonDebtStates = []DebtStatusType{NormalPeriod, LowBalancePeriod, CriticalBalancePeriod} + DebtStates = []DebtStatusType{DebtPeriod, DebtDeletionPeriod, FinalDeletionPeriod} +) + +func ContainDebtStatus(statuses []DebtStatusType, status DebtStatusType) bool { + for _, s := range statuses { + if s == status { + return true + } + } + return false +} + +// Debt 表示 debts 表 +type Debt struct { + UserUID uuid.UUID `gorm:"column:user_uid;type:uuid;not null;primary_key"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;default:current_timestamp"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;default:current_timestamp"` // 更新时间 + AccountDebtStatus DebtStatusType `gorm:"column:account_debt_status;not null" json:"account_debt_status,omitempty"` + StatusRecords []DebtStatusRecord `gorm:"foreignKey:UserUID;references:UserUID"` +} + +// DebtStatusRecord 表示 debt_status_records 表 +type DebtStatusRecord struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:user_uid;type:uuid;not null" json:"user_uid"` // 外键,关联 User.ID + LastStatus DebtStatusType `gorm:"column:last_status" json:"last_status,omitempty"` + CurrentStatus DebtStatusType `gorm:"column:current_status" json:"current_status,omitempty"` + CreateAt time.Time `gorm:"column:create_at;not null;autoCreateTime;default:current_timestamp" json:"create_at,omitempty"` +} + +func (Debt) TableName() string { + return "Debt" +} + +func (DebtStatusRecord) TableName() string { + return "DebtStatusRecord" +} + +type DebtResumeDeductionBalanceTransaction struct { + ID uuid.UUID `json:"id" gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `json:"user_uid" gorm:"column:user_uid;type:uuid;not null"` + BeforeDeductionBalance int64 `json:"before_deduction_balance" gorm:"column:before_deduction_balance;not null"` + AfterDeductionBalance int64 `json:"after_deduction_balance" gorm:"column:after_deduction_balance;not null"` + BeforeBalance int64 `json:"before_balance" gorm:"column:before_balance;not null"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at;autoCreateTime;default:current_timestamp"` +} + +func (DebtResumeDeductionBalanceTransaction) TableName() string { + return "DebtResumeDeductionBalanceTransaction" +} diff --git a/controllers/pkg/types/enum.go b/controllers/pkg/types/enum.go new file mode 100644 index 000000000000..859e1a17eb55 --- /dev/null +++ b/controllers/pkg/types/enum.go @@ -0,0 +1,204 @@ +package types + +import ( + "database/sql/driver" + "fmt" +) + +type SubscriptionStatus string + +const ( + SubscriptionStatusNormal SubscriptionStatus = "NORMAL" + // initialize the state of the subscription space the pause state + SubscriptionStatusPaused SubscriptionStatus = "PAUSED" + SubscriptionStatusDebt SubscriptionStatus = "DEBT" + SubscriptionStatusDebtPreDeletion SubscriptionStatus = "DEBT_PRE_DELETION" + SubscriptionStatusDebtFinalDeletion SubscriptionStatus = "DEBT_FINAL_DELETION" + SubscriptionStatusDeleted SubscriptionStatus = "DELETED" +) + +func (s *SubscriptionStatus) Scan(value any) error { + if value == nil { + *s = "" + return nil + } + sv, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan SubscriptionStatus: %v", value) + } + *s = SubscriptionStatus(sv) + return nil +} + +func (s *SubscriptionStatus) Value() (driver.Value, error) { + return string(*s), nil +} + +type SubscriptionOperator string + +const ( + SubscriptionTransactionTypeCreated SubscriptionOperator = "created" + SubscriptionTransactionTypeUpgraded SubscriptionOperator = "upgraded" + SubscriptionTransactionTypeDowngraded SubscriptionOperator = "downgraded" + SubscriptionTransactionTypeCanceled SubscriptionOperator = "canceled" + SubscriptionTransactionTypeDeleted SubscriptionOperator = "deleted" + SubscriptionTransactionTypeRenewed SubscriptionOperator = "renewed" + + // SubscriptionResumed Resume Renewal (cancel subscription can be selected to cancel the cancel subscription status within a certain period before the end of the current cycle) + SubscriptionTransactionTypeResumed SubscriptionOperator = "resumed" + // 续订余额失败状态 + SubscriptionTransactionTypeRenewFailed SubscriptionOperator = "renew_failed" + + // 到期未续费 + SubscriptionTransactionTypeDebt SubscriptionOperator = "debt" + + SubscriptionTransactionTypeOther SubscriptionOperator = "other" + // SubscriptionTransactionTypePayStatusChanged SubscriptionOperator = "pay_status_changed" +) + +func (o *SubscriptionOperator) Scan(value any) error { + if value == nil { + *o = "" + return nil + } + ov, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan SubscriptionOperator: %v", value) + } + *o = SubscriptionOperator(ov) + return nil +} + +func (o *SubscriptionOperator) Value() (driver.Value, error) { + return string(*o), nil +} + +type PaymentMethod string + +const ( + PaymentMethodErrAndUseBalance PaymentMethod = "err_and_use_balance" // 支付失败转余额支付 + PaymentMethodCorporate PaymentMethod = "corporate" // 企业转账支付 + PaymentMethodBalance PaymentMethod = "balance" // 余额支付 + PaymentMethodStripe PaymentMethod = "stripe" // Stripe 支付 +) + +type PayApp string + +const ( + PayAppCostcenter PayApp = "system-costcenter" + PayAppBrain PayApp = "system-brain" +) + +func (p PayApp) IsValid() bool { + switch p { + case PayAppCostcenter, PayAppBrain: + return true + default: + return false + } +} + +type SubscriptionPayStatus string + +const ( + SubscriptionPayStatusPending SubscriptionPayStatus = "pending" + SubscriptionPayStatusProcessing SubscriptionPayStatus = "processing" + SubscriptionPayStatusCanceled SubscriptionPayStatus = "canceled" + SubscriptionPayStatusPaid SubscriptionPayStatus = "paid" + SubscriptionPayStatusUnpaid SubscriptionPayStatus = "unpaid" + SubscriptionPayStatusNoNeed SubscriptionPayStatus = "no_need" + SubscriptionPayStatusFailed SubscriptionPayStatus = "failed" + SubscriptionPayStatusExpired SubscriptionPayStatus = "expired" + // 表示续订失败后,使用余额支付成功 + SubscriptionPayStatusFailedAndUseBalance SubscriptionPayStatus = "failed_and_use_balance" +) + +func (p *SubscriptionPayStatus) Scan(value any) error { + if value == nil { + *p = "" + return nil + } + pv, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan SubscriptionPayStatus: %v", value) + } + *p = SubscriptionPayStatus(pv) + return nil +} + +func (p *SubscriptionPayStatus) Value() (driver.Value, error) { + return string(*p), nil +} + +type SubscriptionPeriod string + +const ( + SubscriptionPeriodMonthly SubscriptionPeriod = "1m" + SubscriptionPeriodYearly SubscriptionPeriod = "1y" +) + +func (p *SubscriptionPeriod) Scan(value any) error { + if value == nil { + *p = "" + return nil + } + pv, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan SubscriptionPeriod: %v", value) + } + *p = SubscriptionPeriod(pv) + return nil +} + +func (p SubscriptionPeriod) Value() (driver.Value, error) { + return string(p), nil +} + +type WorkspaceTrafficStatus string + +const ( + WorkspaceTrafficStatusActive WorkspaceTrafficStatus = "active" + WorkspaceTrafficStatusExhausted WorkspaceTrafficStatus = "exhausted" + WorkspaceTrafficStatusUsedUp WorkspaceTrafficStatus = "used_up" + WorkspaceTrafficStatusExpired WorkspaceTrafficStatus = "expired" +) + +func (t *WorkspaceTrafficStatus) Scan(value any) error { + if value == nil { + *t = "" + return nil + } + tv, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan WorkspaceTrafficStatus: %v", value) + } + *t = WorkspaceTrafficStatus(tv) + return nil +} + +func (t WorkspaceTrafficStatus) Value() (driver.Value, error) { + return string(t), nil +} + +type SubscriptionTransactionStatus string + +const ( + SubscriptionTransactionStatusCompleted SubscriptionTransactionStatus = "completed" + SubscriptionTransactionStatusPending SubscriptionTransactionStatus = "pending" + SubscriptionTransactionStatusProcessing SubscriptionTransactionStatus = "processing" + SubscriptionTransactionStatusFailed SubscriptionTransactionStatus = "failed" + SubscriptionTransactionStatusCanceled SubscriptionTransactionStatus = "canceled" +) + +func (s *SubscriptionTransactionStatus) Scan(value any) error { + if value == nil { + *s = "" + return nil + } + sv, ok := value.(string) + if !ok { + return fmt.Errorf("failed to scan SubscriptionTransactionStatus: %v", value) + } + *s = SubscriptionTransactionStatus(sv) + return nil +} diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go new file mode 100644 index 000000000000..ffd1397a76bf --- /dev/null +++ b/controllers/pkg/types/global.go @@ -0,0 +1,439 @@ +// Copyright © 2024 sealos. +// +// 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 types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +type Account struct { + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;default:gen_random_uuid();primary_key"` + ActivityBonus int64 `gorm:"column:activityBonus;type:bigint;not null"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;autoUpdateTime;default:current_timestamp"` + CreateRegionID string `gorm:"type:text;not null"` + Balance int64 + DeductionBalance int64 +} + +func (Account) TableName() string { + return "Account" +} + +type UsableBalanceWithCredits struct { + UserUID uuid.UUID + Balance int64 // Separate balance + DeductionBalance int64 // Separate deduction balance + UsableCredits int64 + CreateRegionID string +} + +type BalanceWithCredits struct { + UserUID uuid.UUID `json:"userUid"` + Balance int64 `json:"balance"` + DeductionBalance int64 `json:"deductionBalance"` + Credits int64 `json:"credits"` + DeductionCredits int64 `json:"deductionCredits"` + CreateRegionID string `json:"createRegionId"` +} + +type Region struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + DisplayName string `gorm:"column:displayName;type:text"` + Location string `gorm:"column:location;type:text"` + Domain string `gorm:"column:domain;type:text;not null;unique"` + Description string `gorm:"column:description;type:text"` +} + +type RegionDescription struct { + Provider string `json:"provider"` + Serial string `json:"serial"` + Description map[string]string `json:"description"` +} + +func RegionDescriptionJSON(data RegionDescription) string { + jsonString := `{ + "provider": "` + data.Provider + `", + "serial": "` + data.Serial + `", + "description": {` + + for key, value := range data.Description { + jsonString += `"` + key + `": "` + value + `",` + } + + jsonString = jsonString[:len(jsonString)-1] + + jsonString += `} + }` + + return jsonString +} + +// RegionUserCr is located in the region +type RegionUserCr struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + CrName string `gorm:"type:text;column:crName;not null;unique"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3);default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3);default:current_timestamp"` +} + +type OauthProvider struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"` + ProviderType OauthProviderType `gorm:"column:providerType;type:text;not null"` + ProviderID string `gorm:"column:providerId;type:text;not null"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3);default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3);default:current_timestamp"` + Password string `gorm:"type:text"` +} + +type Transfer struct { + ID string `gorm:"type:text;primary_key"` + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + FromUserUID uuid.UUID `gorm:"column:fromUserUid;type:uuid;not null"` + FromUserID string `gorm:"column:fromUserId;type:text;not null"` + ToUserUID uuid.UUID `gorm:"column:toUserUid;type:uuid;not null"` + ToUserID string `gorm:"column:toUserId;type:text;not null"` + Amount int64 `gorm:"type:bigint;not null"` + Remark string `gorm:"type:text;not null"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` +} + +func (Transfer) TableName() string { + return "UserTransfer" +} + +type User struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;default:current_timestamp"` + AvatarURI string `gorm:"column:avatarUri;type:text"` + Nickname string `gorm:"type:text"` + ID string `gorm:"type:text;not null;unique"` + Name string `gorm:"type:text;not null"` + Status UserStatus `gorm:"column:status;type:UserStatus;default:'NORMAL_USER'::defaultdb.public.'UserStatus';not null"` +} + +type UserStatus string + +const ( + UserStatusNormal UserStatus = "NORMAL_USER" + UserStatusLock UserStatus = "LOCK_USER" +) + +func (User) TableName() string { + return "User" +} + +type Workspace struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + ID string `gorm:"type:text;not null;unique"` + DisplayName string `gorm:"column:displayName;type:text;not null"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;default:current_timestamp"` +} + +type UserWorkspace struct { + UID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primary_key"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;default:current_timestamp"` + WorkspaceUID uuid.UUID `gorm:"column:workspaceUid;type:uuid;not null"` + UserCrUID uuid.UUID `gorm:"column:userCrUid;type:uuid;not null"` + HandlerUID uuid.UUID `gorm:"column:handlerUid;type:uuid"` + Role Role `gorm:"type:Role;default:'DEVELOPER'::defaultdb.public.'Role';not null"` + Status JoinStatus + IsPrivate bool `gorm:"column:isPrivate;type:boolean;not null"` + JoinAt time.Time `gorm:"column:joinAt;type:timestamp(3) with time zone"` +} + +type ( + Role string + JoinStatus string + OauthProviderType string +) + +const ( + OauthProviderTypePhone OauthProviderType = "PHONE" + OauthProviderTypeEmail OauthProviderType = "EMAIL" + OauthProviderTypePassword OauthProviderType = "PASSWORD" + OauthProviderTypeGithub OauthProviderType = "GITHUB" + // OauthProviderTypeWechat OauthProviderType = "WECHAT" + + RoleOwner Role = "OWNER" + // RoleDeveloper Role = "DEVELOPER" + // RoleManager Role = "MANAGER" + + JoinStatusInWorkspace JoinStatus = "IN_WORKSPACE" +) + +func (UserWorkspace) TableName() string { + return "UserWorkspace" +} + +func (Workspace) TableName() string { + return "Workspace" +} + +func (OauthProvider) TableName() string { + return "OauthProvider" +} + +func (Region) TableName() string { + return "Region" +} + +func (RegionUserCr) TableName() string { + return "UserCr" +} + +type InvoiceStatus string + +const ( + PendingInvoiceStatus = "PENDING" + ProcessingInvoiceStatus = "PROCESSING" + CompletedInvoiceStatus = "COMPLETED" + RejectedInvoiceStatus = "REJECTED" +) + +type Invoice struct { + ID string `gorm:"type:text;primary_key" json:"id" bson:"id"` + UserID string `gorm:"type:text;not null" json:"userID" bson:"userID"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp" json:"createdAt" bson:"createdAt"` + UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp" json:"updatedAt" bson:"updatedAt"` + Detail string `gorm:"type:text;not null" json:"detail" bson:"detail"` + Remark string `gorm:"type:text" json:"remark" bson:"remark"` + TotalAmount int64 `gorm:"type:bigint;not null" json:"totalAmount" bson:"totalAmount"` + // Pending, Completed, Rejected + Status InvoiceStatus `gorm:"type:text;not null" json:"status" bson:"status"` +} + +type InvoicePayment struct { + InvoiceID string `gorm:"type:text"` + PaymentID string `gorm:"type:text;primary_key"` + Amount int64 `gorm:"type:bigint;not null"` +} + +func (Invoice) TableName() string { + return "Invoice" +} + +func (InvoicePayment) TableName() string { + return "InvoicePayment" +} + +type GiftCode struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + Code string `gorm:"column:code;type:text;not null;unique"` + CreditAmount int64 `gorm:"column:creditAmount;type:bigint;default:0;not null"` + Used bool `gorm:"column:used;type:boolean;default:false;not null"` + UsedBy uuid.UUID `gorm:"column:usedBy;type:uuid"` + UsedAt time.Time `gorm:"column:usedAt;type:timestamp(3) with time zone"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + ExpiredAt time.Time `gorm:"column:expiredAt;type:timestamp(3) with time zone"` + Comment string `gorm:"column:comment;type:text"` +} + +func (GiftCode) TableName() string { + return "GiftCode" +} + +type AccountTransaction struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + RegionUID uuid.UUID `gorm:"column:region;type:uuid;"` + Type string `gorm:"column:type;type:text"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid"` + DeductionBalance int64 `gorm:"column:deduction_balance;type:bigint"` + Balance int64 `gorm:"column:balance;type:bigint"` + DeductionCredit int64 `gorm:"column:deduction_credit;type:bigint"` + Message *string `gorm:"column:message;type:text"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) with time zone;default:current_timestamp"` + BillingID uuid.UUID `gorm:"column:billing_id;type:uuid"` + BillingIDList pq.StringArray `gorm:"column:billing_id_list;type:text[]"` + CreditIDList pq.StringArray `gorm:"column:credit_id_list;type:text[]"` + BalanceBefore int64 `gorm:"column:balance_before;type:bigint"` + DeductionBalanceBefore int64 `gorm:"column:deduction_balance_before;type:bigint"` +} + +func (AccountTransaction) TableName() string { + return "AccountTransaction" +} + +type UserRealNameInfo struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"` + RealName *string `gorm:"column:realName;type:text"` + IDCard *string `gorm:"column:idCard;type:text"` + Phone *string `gorm:"column:phone;type:text"` + IsVerified bool `gorm:"column:isVerified;type:boolean;default:false"` + IDVerifyFailedTimes int `gorm:"column:idVerifyFailedTimes;type:integer;default:0"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;autoUpdateTime"` + AdditionalInfo json.RawMessage `gorm:"column:additionalInfo;type:jsonb"` +} + +func (UserRealNameInfo) TableName() string { + return "UserRealNameInfo" +} + +type EnterpriseRealNameInfo struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"` + EnterpriseName *string `gorm:"column:enterpriseName;type:text"` + EnterpriseQualification *string `gorm:"column:enterpriseQualification;type:text"` + LegalRepresentativePhone *string `gorm:"column:legalRepresentativePhone;type:text"` + IsVerified bool `gorm:"column:isVerified;type:boolean;default:false"` + VerificationStatus *string `gorm:"column:verificationStatus;type:text"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;autoUpdateTime"` + AdditionalInfo json.RawMessage `gorm:"column:additionalInfo;type:jsonb"` + SupportingMaterials json.RawMessage `gorm:"column:supportingMaterials;type:jsonb"` +} + +func (EnterpriseRealNameInfo) TableName() string { + return "EnterpriseRealNameInfo" +} + +type UserInfo struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"` + FirstName string `gorm:"column:firstname;type:text;default:''::STRING"` + LastName string `gorm:"column:lastname;type:text;default:''::STRING"` + Config *UserInfoConfig `gorm:"column:config;type:jsonb"` + // Config datatypes.JSO `gorm:"column:config;type:jsonb" json:"config"` +} + +func (UserInfo) TableName() string { + return "UserInfo" +} + +func (j *UserInfoConfig) Scan(value any) error { + if value == nil { + return nil + } + b, ok := value.([]byte) + if !ok { + return fmt.Errorf("failed to unmarshal JSONB: %v", value) + } + return json.Unmarshal(b, j) +} + +func (j *UserInfoConfig) Value() (driver.Value, error) { + if j == nil { + return nil, nil + } + b, err := json.Marshal(j) + if err != nil { + return nil, err + } + return string(b), nil +} + +type UserInfoConfig struct { + Github Github `json:"github"` +} + +type Github struct { + CreatedAt string `json:"created_at"` + Login string `json:"login"` + Name any `json:"name"` + ID int `json:"id"` + // AvatarURL string `json:"avatar_url"` + // Bio interface{} `json:"bio"` + // Blog string `json:"blog"` + // Collaborators int `json:"collaborators"` + // Company interface{} `json:"company"` + // DiskUsage int `json:"disk_usage"` + // Email interface{} `json:"email"` + // EventsURL string `json:"events_url"` + // Followers int `json:"followers"` + // FollowersURL string `json:"followers_url"` + // Following int `json:"following"` + // FollowingURL string `json:"following_url"` + // GistsURL string `json:"gists_url"` + // GravatarID string `json:"gravatar_id"` + // Hireable interface{} `json:"hireable"` + // HtmlURL string `json:"html_url"` + // Location interface{} `json:"location"` + // NodeID string `json:"node_id"` + // NotificationEmail interface{} `json:"notification_email"` + // OrganizationsURL string `json:"organizations_url"` + // OwnedPrivateRepos int `json:"owned_private_repos"` + // Plan Plan `json:"plan"` + // PrivateGists int `json:"private_gists"` + // PublicGists int `json:"public_gists"` + // PublicRepos int `json:"public_repos"` + // ReceivedEventsURL string `json:"received_events_url"` + // ReposURL string `json:"repos_url"` + // SiteAdmin bool `json:"site_admin"` + // StarredURL string `json:"starred_url"` + // SubscriptionsURL string `json:"subscriptions_url"` + // TotalPrivateRepos int `json:"total_private_repos"` + // TwitterUsername interface{} `json:"twitter_username"` + // TwoFactorAuthentication bool `json:"two_factor_authentication"` + // Type string `json:"type"` + // UpdatedAt string `json:"updated_at"` + // URL string `json:"url"` + // UserViewType string `json:"user_view_type"` +} + +// type Plan struct { +// Collaborators int `json:"collaborators"` +// Name string `json:"name"` +// PrivateRepos int `json:"private_repos"` +// Space int `json:"space"` +//} + +type RegionConfig struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + // domain_region_key: domain_key + Key string `gorm:"column:key;type:text"` + Value string `gorm:"column:value;type:text"` + Region string `gorm:"column:region;type:text"` +} + +func (RegionConfig) TableName() string { + return "RegionConfig" +} + +type NotificationRecipient struct { + UserName string `json:"user_name" gorm:"column:user_name"` + Email string `json:"email,omitempty" gorm:"column:email"` + PhoneNumber string `json:"phone_number,omitempty" gorm:"column:phone_number"` + UserID string `json:"user_id,omitempty" gorm:"column:user_id"` + UserUID uuid.UUID `json:"user_uid,omitempty" gorm:"column:user_uid"` +} + +// UserAlertNotificationAccount 用户自定义告警通知账号 +type UserAlertNotificationAccount struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:user_uid;type:uuid;not null;uniqueIndex:idx_user_provider_type_id"` + ProviderType OauthProviderType `gorm:"column:provider_type;type:text;not null;uniqueIndex:idx_user_provider_type_id"` + ProviderID string `gorm:"column:provider_id;type:text;not null;uniqueIndex:idx_user_provider_type_id"` + IsEnabled bool `gorm:"column:is_enabled;type:boolean;not null;default:true"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(3);default:current_timestamp"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3);default:current_timestamp"` +} + +func (UserAlertNotificationAccount) TableName() string { + return "UserAlertNotificationAccount" +} diff --git a/controllers/pkg/types/notification.go b/controllers/pkg/types/notification.go new file mode 100644 index 000000000000..ab1254f4c2be --- /dev/null +++ b/controllers/pkg/types/notification.go @@ -0,0 +1 @@ +package types diff --git a/controllers/pkg/types/package.go b/controllers/pkg/types/package.go new file mode 100644 index 000000000000..4e193e7fb036 --- /dev/null +++ b/controllers/pkg/types/package.go @@ -0,0 +1,40 @@ +package types + +import ( + "time" + + "github.com/google/uuid" +) + +// AIQuotaPackage 对应 ai_quota_package 表的结构体 +type WorkspaceAIQuotaPackage struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id" bson:"id"` + Workspace string `gorm:"type:varchar(50);not null;index:idx_workspace;column:workspace" json:"workspace" bson:"workspace"` + RegionDomain string `gorm:"type:varchar(50);not null;index:idx_region_domain;column:region_domain" json:"region_domain" bson:"region_domain"` + // PackageType string `gorm:"type:varchar(50);not null;check:package_type IN ('FREE','SUBSCRIPTION','PAY_AS_YOU_GO')" json:"package_type" bson:"package_type"` + Total int64 `gorm:"type:bigint;not null;default:0" json:"total_quota" bson:"total_quota"` + Usage int64 `gorm:"type:bigint;not null;default:0" json:"usage" bson:"usage"` + From PackageFrom `gorm:"type:varchar(50)" json:"from" bson:"from"` + FromID string `gorm:"type:varchar(50)" json:"from_id" bson:"from_id"` + // RemainingQuota float64 `gorm:"type:decimal(15,2);generated:total_quota - usage" json:"remaining_quota" bson:"remaining_quota"` + Status PackageStatus `gorm:"type:varchar(20);not null;default:'active'" json:"status" bson:"status"` + WorkspaceSubscriptionID uuid.UUID `gorm:"type:uuid" json:"workspace_subscription_id" bson:"workspace_subscription_id"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;not null;default:current_timestamp" json:"created_at" bson:"created_at"` + UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;not null;default:current_timestamp;autoUpdateTime" json:"updated_at" bson:"updated_at"` + ExpiredAt time.Time `gorm:"type:timestamp(3) with time zone;not null;default:current_timestamp" json:"expired_at" bson:"expired_at"` +} + +func (WorkspaceAIQuotaPackage) TableName() string { + return "WorkspaceAIQuotaPackage" +} + +// PackageStatus 定义套餐状态 +type PackageStatus string + +const ( + PackageStatusActive PackageStatus = "active" + PackageStatusExpired PackageStatus = "expired" + PackageStatusInactive PackageStatus = "inactive" + + PKGFromWorkspaceSubscription = "workspace_subscription" +) diff --git a/controllers/pkg/types/payment.go b/controllers/pkg/types/payment.go new file mode 100644 index 000000000000..646a1bce532c --- /dev/null +++ b/controllers/pkg/types/payment.go @@ -0,0 +1,314 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" +) + +type PaymentRaw struct { + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"` + RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + RegionUserOwner string `gorm:"column:regionUserOwner;type:text"` + Method PaymentMethod `gorm:"type:text;not null"` + Amount int64 `gorm:"type:bigint;not null"` + Gift int64 `gorm:"type:bigint"` + // 订单号 + TradeNO string `gorm:"type:text;unique;not null"` + // CodeURL is the codeURL of wechatpay + CodeURL string `gorm:"type:text"` + InvoicedAt bool `gorm:"type:boolean;default:false"` + Remark string `gorm:"type:text"` + ActivityType ActivityType `gorm:"type:text;column:activityType"` + Message string `gorm:"type:text;not null"` + // TODO 初始化判断 新加字段 + CardUID *uuid.UUID `gorm:"type:uuid"` + Type PaymentType `gorm:"type:text"` // 交易类型: AccountRecharge, Subscription,UpgradeSubscription... + ChargeSource ChargeSource `gorm:"type:text"` // 支付来源: 余额支付, 新卡支付, 绑卡支付, Stripe支付 + Status PaymentStatus `gorm:"type:text;column:status;default:'PAID'"` // 支付状态: PAID, REFUNDED, EXPIRED + WorkspaceSubscriptionID *uuid.UUID `gorm:"type:uuid;column:workspace_subscription_id;not null"` + Stripe *StripePay `gorm:"column:stripe;type:json"` // Stripe 相关信息 +} + +type StripePay struct { + SubscriptionID string `json:"subscriptionId"` + CustomerID string `json:"customerId"` + InvoiceID string `json:"invoiceId"` + SessionID string `json:"sessionId"` +} + +func (s *StripePay) Value() (driver.Value, error) { + if s == nil { + return nil, nil + } + data, err := json.Marshal(s) + if err != nil { + return nil, err + } + return string(data), nil +} + +func (s *StripePay) Scan(value any) error { + if value == nil { + *s = StripePay{} + return nil + } + var data []byte + switch value := value.(type) { + case []byte: + data = value + case string: + data = []byte(value) + default: + return fmt.Errorf("unexpected type for StripePay: %T", value) + } + if err := json.Unmarshal(data, s); err != nil { + return fmt.Errorf("failed to unmarshal JSON into StripePay: %w", err) + } + return nil +} + +type ChargeSource string + +const ( + ChargeSourceBalance ChargeSource = "balance" + ChargeSourceNewCard ChargeSource = "CARD" + ChargeSourceBindCard ChargeSource = "BIND_CARD" + ChargeSourceStripe ChargeSource = "stripe" // Stripe 相关支付 +) + +type PaymentOrder struct { + ID string `gorm:"type:text;primary_key"` + PaymentRaw + // 支付状态 + Status PaymentOrderStatus `gorm:"type:text;column:status;not null"` +} + +type ( + PaymentStatus string + PaymentOrderStatus string + CardPaymentStatus string + PaymentType string +) + +const ( + PaymentStatusPAID PaymentStatus = "PAID" + PaymentStatusRefunded PaymentStatus = "REFUNDED" + PaymentStatusExpired PaymentStatus = "EXPIRED" +) + +const ( + // PaymentOrderStatusPending TODO will delete + PaymentOrderStatusPending PaymentOrderStatus = "PENDING" + PaymentOrderStatusSuccess PaymentOrderStatus = "SUCCESS" + PaymentOrderStatusFailed PaymentOrderStatus = "FAILED" + + CardPaymentStatusActive CardPaymentStatus = "ACTIVE" + // "paymentStatus": "FAIL", + // "paymentResultCode": "ACCESS_DENIED", + // "paymentResultMessage": "Access denied.", + CardPaymentStatusFail CardPaymentStatus = "FAIL" +) + +const ( + PaymentTypeAccountRecharge PaymentType = "ACCOUNT_RECHARGE" + PaymentTypeSubscription PaymentType = "SUBSCRIPTION" +) + +func (PaymentOrder) TableName() string { + return "PaymentOrder" +} + +type ActivityType string + +const ( + ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE" +) + +type Payment struct { + ID string `gorm:"type:text;primary_key"` + PaymentRaw +} + +func (Payment) TableName() string { + return "Payment" +} + +type CardInfo struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + UserUID uuid.UUID `gorm:"type:uuid;not null"` + CardNo string `gorm:"type:text"` + CardBrand string `gorm:"type:text"` + CardToken string `gorm:"type:text"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + NetworkTransactionID string `gorm:"type:text"` + Default bool `gorm:"type:boolean;default:false"` + // 上次支付状态 + LastPaymentStatus PaymentOrderStatus `gorm:"type:text"` +} + +func (CardInfo) TableName() string { + return "CardInfo" +} + +// PaymentNotificationType 支付通知类型 +const ( + PaymentResultNotification = "PAYMENT_RESULT" + PaymentPendingNotification = "PAYMENT_PENDING" +) + +// PaymentNotification 支付通知请求结构体 +type PaymentNotification struct { + NotifyType string `json:"notifyType"` + Result Result `json:"result"` + PaymentRequestID string `json:"paymentRequestId"` + PaymentID string `json:"paymentId"` + PaymentAmount Amount `json:"paymentAmount"` + PaymentCreateTime time.Time `json:"paymentCreateTime"` + PaymentTime *time.Time `json:"paymentTime,omitempty"` + PspCustomerInfo *PspCustomerInfo `json:"pspCustomerInfo,omitempty"` + CustomsDeclarationAmount *Amount `json:"customsDeclarationAmount,omitempty"` + GrossSettlementAmount *Amount `json:"grossSettlementAmount,omitempty"` + SettlementQuote *Quote `json:"settlementQuote,omitempty"` + AcquirerReferenceNo string `json:"acquirerReferenceNo,omitempty"` + PaymentResultInfo any `json:"paymentResultInfo,omitempty"` + PromotionResult []PromotionResult `json:"promotionResult,omitempty"` + PaymentMethodType string `json:"paymentMethodType,omitempty"` +} + +// PspCustomerInfo PSP客户信息 +type PspCustomerInfo struct { + PspName string `json:"pspName,omitempty"` + PspCustomerID string `json:"pspCustomerId,omitempty"` + DisplayCustomerID string `json:"displayCustomerId,omitempty"` +} + +// Quote 汇率信息 +type Quote struct { + Guaranteed bool `json:"guaranteed,omitempty"` + QuoteCurrencyPair string `json:"quoteCurrencyPair"` + QuoteExpiryTime *time.Time `json:"quoteExpiryTime,omitempty"` + QuoteID string `json:"quoteId,omitempty"` + QuotePrice string `json:"quotePrice"` + QuoteStartTime *time.Time `json:"quoteStartTime,omitempty"` +} + +// PromotionResult 优惠结果 +type PromotionResult struct { + PromotionType string `json:"promotionType"` + Discount *Discount `json:"discount,omitempty"` +} + +// Discount 折扣信息 +type Discount struct { + // 根据实际需求添加字段 + DiscountAmount Amount `json:"discountAmount"` +} + +// Result 通用结果结构体 +type Result struct { + ResultCode string `json:"resultCode"` + ResultMessage string `json:"resultMessage"` + ResultStatus string `json:"resultStatus"` +} + +const ( + NotifyTypePaymentResult = "PAYMENT_RESULT" + NotifyTypeCaptureResult = "CAPTURE_RESULT" +) + +const OrderClosedResultCode = "ORDER_IS_CLOSED" + +// Amount 通用金额结构体 +type Amount struct { + Currency string `json:"currency"` + Value string `json:"value"` +} + +// CommonResponse 通用响应结构体 +type CommonResponse struct { + Result Result `json:"result"` +} + +func (c *CommonResponse) Raw() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +// NewSuccessResponse 创建成功响应 +func NewSuccessResponse() CommonResponse { + return CommonResponse{ + Result: Result{ + ResultCode: "SUCCESS", + ResultMessage: "success", + ResultStatus: "S", + }, + } +} + +// CaptureNotification 请款通知请求结构体 +type CaptureNotification struct { + Result Result `json:"result"` + NotifyType string `json:"notifyType"` // 通知类型,固定为CAPTURE_RESULT + CaptureRequestID string `json:"captureRequestId"` // 商户分配的请款请求ID + PaymentID string `json:"paymentId"` // 支付ID + CaptureID string `json:"captureId"` // 请款ID + CaptureAmount Amount `json:"captureAmount"` // 请款金额 + CaptureTime *time.Time `json:"captureTime,omitempty"` // 请款完成时间 + AcquirerReferenceNo string `json:"acquirerReferenceNo,omitempty"` // 收单机构交易ID +} + +// CaptureResponse 请款通知响应结构体 +type CaptureResponse struct { + Result Result `json:"result"` +} + +// Raw 返回JSON格式的字节数组 +func (c *CaptureResponse) Raw() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +type PaymentRefund struct { + RowID string `json:"rowid" gorm:"type:uuid;column:rowid;primaryKey;default:gen_random_uuid()"` // CockroachDB 主键 + TradeNo string `json:"tradeNo" gorm:"type:string;not null"` + OrderID string `json:"OrderId" gorm:"type:string;not null"` // 外键 跟payment关联,不再唯一 + ID string `json:"Id" gorm:"type:string"` // 保持兼容性 + Method PaymentMethod `json:"method" gorm:"type:varchar(255);not null"` // 退款方式 + // OutTradeNo string `json:"outTradeNo" gorm:"type:uuid"` + RefundNo string `json:"refundNo" gorm:"type:string;not null"` + RefundAmount int64 `json:"refundAmount" gorm:"type:float;not null"` + DeductAmount int64 `json:"deductAmount" gorm:"type:float;not null"` // 从 account的 balance里面扣款 + CreatedAt time.Time `json:"createdAt" gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + RefundReason string `json:"refundReason" gorm:"type:text"` +} + +func (PaymentRefund) TableName() string { + return "PaymentRefund" +} + +type Corporate struct { + UserUID string `json:"userUid" gorm:"type:string;not null"` + ID string `json:"Id" gorm:"type:string;not null"` + ReceiptSerialNumber string `json:"receiptSerialNumber" gorm:"type:string;not null"` + PayerName string `json:"payerName" gorm:"type:varchar(255);not null"` + PaymentAmount int64 `json:"paymentAmount" gorm:"type:float;not null"` + GiftAmount int64 `json:"giftAmount" gorm:"type:float;not null"` + PayDate time.Time `json:"payDate" gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + CreationDate time.Time `json:"creationDate" gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + Type string `json:"type" gorm:"type:varchar(255);not null"` +} + +func (Corporate) TableName() string { + return "Corporate" +} diff --git a/controllers/pkg/types/subscription.go b/controllers/pkg/types/subscription.go new file mode 100644 index 000000000000..010f05c0a79f --- /dev/null +++ b/controllers/pkg/types/subscription.go @@ -0,0 +1,211 @@ +package types + +import ( + "fmt" + "regexp" + "strconv" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +type Subscription struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 订阅 ID + PlanID uuid.UUID `gorm:"type:uuid;column:plan_id"` // 计划 ID + PlanName string `gorm:"type:varchar(50);column:plan_name"` // 计划名称 + UserUID uuid.UUID `gorm:"unique;not null;type:uuid;column:user_uid"` // 用户 ID + Status SubscriptionStatus `gorm:"type:varchar(50);column:status"` // 状态 + StartAt time.Time `gorm:"column:start_at;autoCreateTime"` // 开始时间 + UpdateAt time.Time `gorm:"column:update_at;autoCreateTime"` // 更新时间 + ExpireAt time.Time `gorm:"column:expire_at;autoCreateTime"` // 过期时间 + CardID *uuid.UUID `gorm:"type:uuid;column:card_id"` // 银行卡 ID + NextCycleDate time.Time `gorm:"column:next_cycle_date"` // 下一个周期的日期 +} + +// 订阅变更记录表 +type SubscriptionTransaction struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // ID + SubscriptionID uuid.UUID `gorm:"type:uuid;not null;index;column:subscription_id"` // 关联的订阅 ID + UserUID uuid.UUID `gorm:"type:uuid;not null;index;column:user_uid"` // 用户 ID + OldPlanID uuid.UUID `gorm:"type:uuid;column:old_plan_id"` // 旧的订阅计划 ID + NewPlanID uuid.UUID `gorm:"type:uuid;column:new_plan_id"` // 新的订阅计划 ID + OldPlanName string `gorm:"type:varchar(50);column:old_plan_name"` // 旧的订阅计划名称 + NewPlanName string `gorm:"type:varchar(50);column:new_plan_name"` // 新的订阅计划名称 + OldPlanStatus SubscriptionStatus `gorm:"type:varchar(50);column:old_plan_status"` // 旧的订阅状态 + Operator SubscriptionOperator `gorm:"type:varchar(50);column:operator"` // 操作类型(created/upgraded/downgraded/canceled/renewed) + StartAt time.Time `gorm:"column:start_at"` // 变更开始时间 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 + Status SubscriptionTransactionStatus `gorm:"type:varchar(50);column:status"` // 状态 + PayStatus SubscriptionPayStatus `gorm:"type:varchar(50);column:pay_status"` // 支付状态 + PayID string `gorm:"type:text;column:pay_id"` // 支付订单号 + Amount int64 `gorm:"type:bigint;column:amount"` // 金额 +} + +type SubscriptionPlan struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 计划 ID + Name string `gorm:"unique;not null;column:name;type:text"` // 计划名称 + Description string `gorm:"type:text;column:description"` // 描述 + Amount int64 `gorm:"type:bigint;column:amount"` // 金额 + GiftAmount int64 `gorm:"type:bigint;column:gift_amount"` // 赠送金额 + Period string `gorm:"type:varchar(50);column:period"` // 周期 + UpgradePlanList pq.StringArray `gorm:"type:text[];column:upgrade_plan_list"` // 可升级的计划列表 + DowngradePlanList pq.StringArray `gorm:"type:text[];column:downgrade_plan_list"` // 可降级的计划列表 + // <0 Unrestricted + MaxSeats int `gorm:"not null;column:max_seats"` // 最大席位数 + MaxWorkspaces int `gorm:"not null;column:max_workspaces"` // 最大 Workspace 数量 + MaxResources string `gorm:"column:max_resources"` // 最大资源数: map[string]string: {"cpu": "4", "memory": "8Gi", "storage": "100Gi"} + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 + // Most Popular + MostPopular bool `gorm:"column:most_popular"` +} + +type ( + TransactionFrom string + BillingCycle string +) + +const ( + FreeSubscriptionPlanName = "Free" + + ProductPriceBillingCycleYearly BillingCycle = "yearly" + ProductPriceBillingCycleQuarterly BillingCycle = "quarterly" + ProductPriceBillingCycleMonthly BillingCycle = "monthly" + ProductPriceBillingCycleWeekly BillingCycle = "weekly" + ProductPriceBillingCycleDaily BillingCycle = "daily" + ProductPriceBillingCycleOneTime BillingCycle = "one_time" + + TransactionFromUser TransactionFrom = "user" + TransactionFromAdmin TransactionFrom = "admin" + TransactionFromSystem TransactionFrom = "system" + TransactionFromReferral TransactionFrom = "referral" +) + +func DayPeriod(days int) SubscriptionPeriod { + return SubscriptionPeriod(strconv.Itoa(days) + "d") +} + +func WeekPeriod(weeks int) SubscriptionPeriod { + return SubscriptionPeriod(strconv.Itoa(weeks) + "w") +} + +func MonthPeriod(months int) SubscriptionPeriod { + return SubscriptionPeriod(strconv.Itoa(months) + "m") +} + +func ParsePeriod(period SubscriptionPeriod) (time.Duration, error) { + re := regexp.MustCompile(`^(\d+)([dhwmy])$`) + matches := re.FindStringSubmatch(string(period)) + if len(matches) != 3 { + return 0, fmt.Errorf("invalid duration format: %s", period) + } + value, err := strconv.Atoi(matches[1]) + if err != nil { + return 0, fmt.Errorf("invalid number in duration: %s", matches[1]) + } + unit := matches[2] + switch unit { + case "h": + return time.Duration(value) * time.Hour, nil + case "d": + return time.Duration(value) * 24 * time.Hour, nil + case "w": + return time.Duration(value) * 7 * 24 * time.Hour, nil + case "m": + return time.Duration(value) * 30 * 24 * time.Hour, nil + case "y": + return time.Duration(value) * 365 * 24 * time.Hour, nil + default: + return 0, fmt.Errorf("unsupported unit: %s", unit) + } +} + +type ProductPrice struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index:idx_product_cycle,unique"` + BillingCycle SubscriptionPeriod `gorm:"type:varchar(20);not null;index:idx_product_cycle,unique"` // 计费周期, 年/季/月/周/天/次 + Price int64 `gorm:"type:bigint;column:price"` // 价格 + OriginalPrice int64 `gorm:"type:bigint;column:original_price"` + StripePrice *string `gorm:"type:varchar(100);column:stripe_price"` // Stripe 价格 ID + // Currency string `gorm:"type:varchar(10);not null;column:currency"` // 货币 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 +} + +func (p SubscriptionPlan) GetName() string { + return p.Name +} + +func (p SubscriptionPlan) GetMaxResources() string { + return p.MaxResources +} + +func (Subscription) TableName() string { + return "Subscription" +} + +func (SubscriptionPlan) TableName() string { + return "SubscriptionPlan" +} + +func (SubscriptionTransaction) TableName() string { + return "SubscriptionTransaction" +} + +func (ProductPrice) TableName() string { + return "ProductPrice" +} + +type AccountRegionUserTask struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // ID + // RegionUID uuid.UUID `gorm:"type:uuid;not null;index;column:region_uid"` // Region ID + RegionDomain string `gorm:"type:varchar(50);not null;column:region_domain"` // Region Domain + UserUID uuid.UUID `gorm:"type:uuid;not null;index;column:user_uid"` // 用户 ID + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + // flush-quota + Type AccountRegionUserTaskType `gorm:"column:type"` // 类型 + // TaskID uuid.UUID `gorm:"type:uuid;column:task_id"` // 任务 ID + // Executed bool `gorm:"column:executed"` // 是否已执行 + StartAt time.Time `gorm:"column:start_at"` // 开始时间 + EndAt time.Time `gorm:"column:end_at"` // 结束时间 + Status AccountRegionUserTaskStatus +} + +func (AccountRegionUserTask) TableName() string { + return "AccountRegionUserTask" +} + +type AccountRegionUserTaskType string + +type AccountRegionUserTaskStatus string + +const ( + AccountRegionUserTaskTypeFlushQuota AccountRegionUserTaskType = "flush-quota" + AccountRegionUserTaskTypeFlushDebt AccountRegionUserTaskType = "flush-debt" + + AccountRegionUserTaskStatusPending AccountRegionUserTaskStatus = "pending" + AccountRegionUserTaskStatusCompleted AccountRegionUserTaskStatus = "completed" + AccountRegionUserTaskStatusFailed AccountRegionUserTaskStatus = "failed" +) + +type UserKYC struct { + UserUID uuid.UUID `gorm:"type:uuid;not null;primaryKey;column:user_uid"` // 用户 ID + Status KYCStatus `gorm:"type:varchar(50);column:status"` // KYC 状态 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 + NextAt time.Time `gorm:"column:next_at"` // 下次credits时间 +} + +func (UserKYC) TableName() string { + return "UserKYC" +} + +type KYCStatus string + +const ( + UserKYCStatusPending KYCStatus = "pending" + UserKYCStatusCompleted KYCStatus = "completed" + UserKYCStatusFailed KYCStatus = "failed" +) diff --git a/controllers/pkg/types/task.go b/controllers/pkg/types/task.go new file mode 100644 index 000000000000..00bc27641e35 --- /dev/null +++ b/controllers/pkg/types/task.go @@ -0,0 +1,80 @@ +/* +Copyright 2024. + +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 types + +import ( + "time" + + "github.com/google/uuid" +) + +// Task represents the Task model in Go with GORM annotations. +type Task struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + Title string `gorm:"column:title;type:text;not null" json:"title"` + Description string `gorm:"column:description;type:text;not null" json:"description"` + Reward int64 `gorm:"column:reward;type:bigint;not null" json:"reward"` + Order int `gorm:"column:order;type:integer;not null" json:"order"` + IsActive bool `gorm:"column:isActive;type:boolean;default:true;not null" json:"isActive"` + IsNewUserTask bool `gorm:"column:isNewUserTask;type:boolean;default:false;not null" json:"isNewUserTask"` + TaskType TaskType `gorm:"column:taskType;type:TaskType;not null" json:"taskType"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp;not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` +} + +// UserTask represents the UserTask model in Go with GORM annotations. +type UserTask struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null" json:"userUid"` + TaskID uuid.UUID `gorm:"column:taskId;type:uuid;not null" json:"taskId"` + Status TaskStatus `gorm:"column:status;type:TaskStatus;not null" json:"status"` + RewardStatus TaskStatus `gorm:"column:rewardStatus;type:TaskStatus;not null" json:"rewardStatus"` + CompletedAt time.Time `gorm:"column:completedAt;type:timestamp(3);not null" json:"completedAt"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp;not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` + + // User User `gorm:"foreignKey:UserUid;references:UID" json:"user"` + // Task Task `gorm:"foreignKey:TaskId;references:ID" json:"task"` +} + +// TableName specifies the table name for GORM +func (Task) TableName() string { + return "Task" +} + +// TableName specifies the table name for GORM +func (UserTask) TableName() string { + return "UserTask" +} + +// TaskType represents the TaskType enum in Go. +type TaskType string + +// const ( +// TaskTypeLaunchpad TaskType = "LAUNCHPAD" +// TaskTypeCostcenter TaskType = "COSTCENTER" +// TaskTypeDatabase TaskType = "DATABASE" +// TaskTypeDesktop TaskType = "DESKTOP" +//) + +// TaskStatus represents the TaskStatus enum in Go. +type TaskStatus string + +const ( + TaskStatusNotCompleted TaskStatus = "NOT_COMPLETED" + TaskStatusCompleted TaskStatus = "COMPLETED" +) diff --git a/controllers/pkg/types/traffic.go b/controllers/pkg/types/traffic.go new file mode 100644 index 000000000000..1658314e4aa0 --- /dev/null +++ b/controllers/pkg/types/traffic.go @@ -0,0 +1,83 @@ +// Copyright © 2024 sealos. +// +// 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 types + +import ( + "time" + + "github.com/google/uuid" +) + +type ObjectStorageTraffic struct { + Time time.Time `json:"time" bson:"time"` + User string `json:"user" bson:"user"` + Bucket string `json:"bucket" bson:"bucket"` + // bytes + TotalSent int64 `json:"totalSent" bson:"totalSent"` + + // The sent traffic since the last time + Sent int64 `json:"sent" bson:"sent"` +} + +type UserTimeRangeTraffic struct { + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;autoUpdateTime;default:current_timestamp"` + NextCleanTime time.Time `gorm:"type:timestamp(3) with time zone"` + UserUID uuid.UUID `gorm:"type:uuid;primaryKey" json:"user_uid" bson:"user_uid"` + SentBytes int64 `gorm:"type:bigint;default:0" json:"sent_bytes" bson:"sent_bytes"` + Status UserTimeRangeTrafficStatus `gorm:"type:varchar(20);default:'processing'" json:"status" bson:"status"` +} + +func (UserTimeRangeTraffic) TableName() string { + return "UserTimeRangeTraffic" +} + +type UserTimeRangeTrafficStatus string + +const ( + UserTimeRangeTrafficStatusUsedUp UserTimeRangeTrafficStatus = "used_up" + UserTimeRangeTrafficStatusProcessing UserTimeRangeTrafficStatus = "processing" + UserTimeRangeTrafficStatusSkip UserTimeRangeTrafficStatus = "skip" + + UserTimeRangeTrafficStatusRecovering UserTimeRangeTrafficStatus = "recovering" +) + +type WorkspaceTraffic struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id" bson:"id"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + UpdatedAt time.Time `gorm:"type:timestamp(3) with time zone;autoUpdateTime;default:current_timestamp"` + ExpiredAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp"` + Workspace string `gorm:"type:varchar(50);not null;index:idx_workspace_region_domain;column:workspace"` // Workspace 名称 + RegionDomain string `gorm:"type:varchar(50);not null;index:idx_workspace_region_domain;column:region_domain"` // Region Domain + WorkspaceSubscriptionID uuid.UUID `gorm:"type:uuid" json:"workspace_subscription_id" bson:"workspace_subscription_id"` + Status WorkspaceTrafficStatus `gorm:"type:workspace_traffic_status;default:'active'" json:"status" bson:"status"` + From WorkspaceTrafficFrom `gorm:"type:varchar(50)" json:"from" bson:"from"` + FromID string `gorm:"type:varchar(50)" json:"from_id" bson:"from_id"` + TotalBytes int64 `gorm:"type:bigint;default:0" json:"total_bytes" bson:"total_bytes"` + UsedBytes int64 `gorm:"type:bigint;default:0" json:"used_bytes" bson:"used_bytes"` +} + +type ( + WorkspaceTrafficFrom string + PackageFrom string +) + +const ( + WorkspaceTrafficFromWorkspaceSubscription WorkspaceTrafficFrom = "workspace_subscription" +) + +func (WorkspaceTraffic) TableName() string { + return "WorkspaceTraffic" +} diff --git a/controllers/pkg/types/workspace_subscription.go b/controllers/pkg/types/workspace_subscription.go new file mode 100644 index 000000000000..14a6c067a8d4 --- /dev/null +++ b/controllers/pkg/types/workspace_subscription.go @@ -0,0 +1,145 @@ +package types + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +type WorkspaceSubscription struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 订阅 ID + PlanName string `gorm:"type:varchar(50);column:plan_name"` // 计划名称 + Workspace string `gorm:"type:varchar(50);column:workspace;uniqueIndex:idx_workspace_region_domain"` + RegionDomain string `gorm:"type:varchar(50);column:region_domain;uniqueIndex:idx_workspace_region_domain"` + UserUID uuid.UUID `gorm:"type:uuid;index:idx_workspace_subscription_user_uid;column:user_uid"` // 用户 ID + Status SubscriptionStatus `gorm:"type:subscription_status;column:status"` // 状态 + PayStatus SubscriptionPayStatus `gorm:"type:subscription_pay_status;column:pay_status"` // 支付状态 + PayMethod PaymentMethod `gorm:"type:string;column:pay_method"` // 支付方式 + Stripe *StripePay `gorm:"column:stripe;type:json"` // Stripe 相关信息 + TrafficStatus WorkspaceTrafficStatus `gorm:"type:workspace_traffic_status;default:'active';column:traffic_status"` // 流量状态 + // StartAt time.Time `gorm:"column:start_at;autoCreateTime"` // 开始时间 + CurrentPeriodStartAt time.Time `gorm:"column:current_period_start_at"` // 当前周期开始时间 + CurrentPeriodEndAt time.Time `gorm:"column:current_period_end_at"` // 当前周期结束时间 + CancelAtPeriodEnd bool `gorm:"column:cancel_at_period_end;default:false"` // 是否在当前周期结束时取消订阅 + + CancelAt time.Time `gorm:"column:cancel_at"` // 取消订阅时间 + CreateAt time.Time `gorm:"column:create_at"` // 创建时间 + UpdateAt time.Time `gorm:"column:update_at;autoCreateTime"` // 更新时间 + ExpireAt *time.Time `gorm:"column:expire_at"` // 过期时间 + Traffic []WorkspaceTraffic `gorm:"foreignKey:WorkspaceSubscriptionID;references:ID"` // 关联的流量数据 + AIQuota []WorkspaceAIQuotaPackage `gorm:"foreignKey:WorkspaceSubscriptionID;references:ID"` // 关联的AI配额数据 +} + +// TODO CREATE INDEX IF NOT EXISTS idx_pending_transactions ON "WorkspaceSubscriptionTransaction" (pay_status, start_at, status, region_domain); +type WorkspaceSubscriptionTransaction struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // ID + From TransactionFrom `gorm:"type:varchar(50);column:from;type:text"` // 变更来源, 例如: "user" / "admin" / "referral" + Workspace string `gorm:"type:varchar(50);not null;index:idx_workspace_region_domain;column:workspace"` // Workspace 名称 + RegionDomain string `gorm:"type:varchar(50);not null;index:idx_workspace_region_domain;column:region_domain"` // Region Domain + UserUID uuid.UUID `gorm:"type:uuid;column:user_uid"` // 用户 ID + OldPlanName string `gorm:"type:varchar(50);column:old_plan_name"` // 旧的订阅计划名称 + NewPlanName string `gorm:"type:varchar(50);column:new_plan_name"` // 新的订阅计划名称 + OldPlanStatus SubscriptionStatus `gorm:"type:subscription_status;column:old_plan_status"` // 旧的订阅状态 + Operator SubscriptionOperator `gorm:"type:subscription_operator;column:operator"` // 操作类型(created/upgraded/downgraded/canceled/renewed/pay_status_changed) + StartAt time.Time `gorm:"column:start_at"` // 变更开始时间 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 + Status SubscriptionTransactionStatus `gorm:"type:subscription_transaction_status;column:status"` // 状态 + StatusDesc string `gorm:"type:varchar(255);column:status_desc"` // 状态描述 + PayStatus SubscriptionPayStatus `gorm:"type:subscription_pay_status;column:pay_status"` // 支付状态 + PayID string `gorm:"type:text;column:pay_id"` // 支付订单号 + PayApp PayApp `gorm:"type:text;column:pay_app"` // Stripe 回跳后打开的应用 + Period SubscriptionPeriod `gorm:"type:text;column:period"` // 周期, 默认1一个月,年或者月 + Amount int64 `gorm:"type:bigint;column:amount"` // 金额 +} + +type WorkspaceSubscriptionPlan struct { + ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` // 计划 ID + Name string `gorm:"unique;not null;column:name;type:text"` // 计划名称 + Description string `gorm:"type:text;column:description"` // 描述 + UpgradePlanList pq.StringArray `gorm:"type:text[];column:upgrade_plan_list"` // 可升级的计划列表 + DowngradePlanList pq.StringArray `gorm:"type:text[];column:downgrade_plan_list"` // 可降级的计划列表 + MaxSeats int `gorm:"not null;column:max_seats"` // 最大席位数 + MaxResources string `gorm:"column:max_resources"` // 最大资源数: map[string]string: {"cpu": "4", "memory": "8Gi", "storage": "100Gi"} + Traffic int64 `gorm:"type:bigint;column:traffic"` // 包含流量包大小, 单位: MB + AIQuota int64 `gorm:"type:bigint;column:ai_quota"` // 包含AI配额大小 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` // 创建时间 + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` // 更新时间 + Order int `gorm:"column:order"` // 排序号 + Tags pq.StringArray `gorm:"type:text[];column:tags"` // 标签分类 + Prices []ProductPrice `gorm:"foreignKey:ProductID;references:ID"` // 一对多关联 +} + +func (p WorkspaceSubscriptionPlan) GetName() string { + return p.Name +} + +func (p WorkspaceSubscriptionPlan) GetMaxResources() string { + return p.MaxResources +} + +func (p WorkspaceSubscriptionPlan) CanBeUpgraded(planName string) bool { + for _, plan := range p.UpgradePlanList { + if plan == planName { + return true + } + } + return false +} + +func (WorkspaceSubscription) TableName() string { + return "WorkspaceSubscription" +} + +func (WorkspaceSubscriptionTransaction) TableName() string { + return "WorkspaceSubscriptionTransaction" +} + +func (WorkspaceSubscriptionPlan) TableName() string { + return "WorkspaceSubscriptionPlan" +} + +// Resource represents the JSON structure +type WorkspaceFeatureResource struct { + CPU string `json:"cpu"` + Memory string `json:"memory"` + Storage string `json:"storage"` + NodePorts string `json:"nodeports,omitempty"` + Traffic int64 `json:"traffic,omitempty"` +} + +func ParseMaxResource(res string, traffic int64) ([]string, error) { + var resource WorkspaceFeatureResource + err := json.Unmarshal([]byte(res), &resource) + if err != nil { + return nil, err + } + if traffic > 0 { + resource.Traffic = traffic + } + return parseResource(resource) +} + +func parseResource(res WorkspaceFeatureResource) ([]string, error) { + result := []string{ + res.CPU + " vCPU", + strings.TrimSuffix(res.Memory, "Gi") + "GB RAM", + strings.TrimSuffix(res.Storage, "Gi") + "GB Disk", + res.NodePorts + " / NodePorts", + } + + // Handle nodeports (traffic) if present + if res.Traffic != 0 { + if res.Traffic > 1024 { + trafficGB := float64(res.Traffic) / 1024.0 + result = append(result, fmt.Sprintf("%.2fGB Traffic", trafficGB)) + } else { + result = append(result, fmt.Sprintf("%dMB Traffic", res.Traffic)) + } + } + return result, nil +} diff --git a/controllers/pkg/user_notify/contact_provider_memory.go b/controllers/pkg/user_notify/contact_provider_memory.go new file mode 100644 index 000000000000..750428bf334a --- /dev/null +++ b/controllers/pkg/user_notify/contact_provider_memory.go @@ -0,0 +1,79 @@ +// Copyright © 2024 sealos. +// +// 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 usernotify + +import ( + "context" + "fmt" + "sync" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +type MemoryContactProvider struct { + contacts map[uuid.UUID]*types.NotificationRecipient + mutex sync.RWMutex +} + +func NewMemoryContactProvider() *MemoryContactProvider { + return &MemoryContactProvider{ + contacts: make(map[uuid.UUID]*types.NotificationRecipient), + } +} + +func (p *MemoryContactProvider) GetUserContact( + ctx context.Context, + userUID uuid.UUID, +) (*types.NotificationRecipient, error) { + p.mutex.RLock() + defer p.mutex.RUnlock() + + contact, exists := p.contacts[userUID] + if !exists { + return nil, fmt.Errorf("user contact not found for user: %s", userUID) + } + + return contact, nil +} + +func (p *MemoryContactProvider) SetUserContact( + userUID uuid.UUID, + recipient *types.NotificationRecipient, +) { + p.mutex.Lock() + defer p.mutex.Unlock() + + p.contacts[userUID] = recipient +} + +func (p *MemoryContactProvider) RemoveUserContact(userUID uuid.UUID) { + p.mutex.Lock() + defer p.mutex.Unlock() + + delete(p.contacts, userUID) +} + +func (p *MemoryContactProvider) GetAllContacts() map[uuid.UUID]*types.NotificationRecipient { + p.mutex.RLock() + defer p.mutex.RUnlock() + + result := make(map[uuid.UUID]*types.NotificationRecipient) + for k, v := range p.contacts { + result[k] = v + } + + return result +} diff --git a/controllers/pkg/user_notify/content_generator.go b/controllers/pkg/user_notify/content_generator.go new file mode 100644 index 000000000000..d6bbf597fa01 --- /dev/null +++ b/controllers/pkg/user_notify/content_generator.go @@ -0,0 +1,369 @@ +// Copyright © 2024 sealos. +// +// 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 usernotify + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/labring/sealos/controllers/pkg/types" +) + +// DefaultContentGenerator 默认内容生成器 +type DefaultContentGenerator struct { + config map[NotificationMethod]ProviderConfig +} + +// NewDefaultContentGenerator 创建默认内容生成器 +func NewDefaultContentGenerator( + config map[NotificationMethod]ProviderConfig, +) *DefaultContentGenerator { + return &DefaultContentGenerator{config: config} +} + +// GenerateContent 生成通知内容 +func (g *DefaultContentGenerator) GenerateContent( + event *NotificationEvent, + method NotificationMethod, +) (title, content, templateID string, err error) { + switch event.EventType { + case EventTypeDebtStatusChange: + return g.generateDebtContent(method, event.EventData, event.Recipient) + case EventTypeSubscriptionStatusChange, + EventTypeSubscriptionOperationDone, + EventTypeSubscriptionPaymentDone, + EventTypeWorkspaceSubscriptionCreatedSuccess, + EventTypeWorkspaceSubscriptionCreatedFailed, + EventTypeWorkspaceSubscriptionUpgradedSuccess, + EventTypeWorkspaceSubscriptionUpgradedFailed, + EventTypeWorkspaceSubscriptionRenewedSuccess, + EventTypeWorkspaceSubscriptionRenewedBalanceFallback, + EventTypeWorkspaceSubscriptionRenewedFailed, + EventTypeWorkspaceSubscriptionDebt, + EventTypeWorkspaceSubscriptionDebtPreDeletion, + EventTypeTrafficUsageAlert: + return g.generateWorkspaceSubscriptionContent(method, event) + case EventTypeTrafficStatusChange /*EventTypeTrafficUsageAlert*/ : + return g.generateTrafficContent(method, event.EventData, event.Recipient) + case EventTypeCustom: + return g.generateCustomContent(method, event.EventData, event.Recipient) + default: + return "", "", "", fmt.Errorf("unsupported event type: %s", event.EventType) + } +} + +// generateDebtContent 生成债务相关通知内容 +func (g *DefaultContentGenerator) generateDebtContent( + method NotificationMethod, + eventData map[string]any, + recipient types.NotificationRecipient, +) (title, content, templateID string, err error) { + var debtData DebtEventData + dataBytes, err := json.Marshal(eventData) + if err != nil { + return "", "", "", fmt.Errorf("failed to marshal debt event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &debtData); err != nil { + return "", "", "", fmt.Errorf("failed to parse debt event data: %w", err) + } + + // 获取模板ID + if config, exists := g.config[method]; exists { + templateID = config.GetVMSTemplateID(EventTypeDebtStatusChange) + switch method { + case NotificationMethodSMS: + templateID = config.GetSMSTemplateCode(EventTypeDebtStatusChange) + case NotificationMethodEmail: + templateID = config.GetEmailTemplate(EventTypeDebtStatusChange) + } + } + + switch debtData.CurrentStatus { + case types.DebtPeriod: + if debtData.LastStatus != types.DebtPeriod { + // 首次进入欠费状态 + title = "账户欠费通知" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的账户余额不足,请及时充值以避免服务中断。", + map[string]any{ + "UserName": recipient.UserName, + }, + ) + } + case types.DebtDeletionPeriod: + title = "账户欠费警告" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的账户已欠费{{.DebtDays}}天,请尽快充值以避免服务中断。", + map[string]any{ + "UserName": recipient.UserName, + "DebtDays": debtData.DebtDays, + }, + ) + case types.FinalDeletionPeriod: + title = "资源清理预警" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的账户已欠费{{.DebtDays}}天,系统将在24小时内进行资源清理,请立即充值!", + map[string]any{ + "UserName": recipient.UserName, + "DebtDays": debtData.DebtDays, + }, + ) + case types.NormalPeriod: + if debtData.LastStatus != types.NormalPeriod { + // 恢复正常状态 + title = "账户恢复通知" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的账户已恢复正常,感谢您的及时充值。", + map[string]any{ + "UserName": recipient.UserName, + }, + ) + } + default: + return "", "", "", fmt.Errorf("unsupported debt status: %s", debtData.CurrentStatus) + } + + return title, content, templateID, nil +} + +func (g *DefaultContentGenerator) generateWorkspaceSubscriptionContent( + method NotificationMethod, + event *NotificationEvent, +) (title, content, templateID string, err error) { + var subData WorkspaceSubscriptionEventData + dataBytes, err := json.Marshal(event.EventData) + if err != nil { + return "", "", "", fmt.Errorf("failed to marshal subscription event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &subData); err != nil { + return "", "", "", fmt.Errorf("failed to parse subscription event data: %w", err) + } + + // 获取模板ID + if config, exists := g.config[method]; exists { + templateID = config.GetVMSTemplateID(EventTypeSubscriptionOperationDone) + switch method { + case NotificationMethodSMS: + templateID = config.GetSMSTemplateCode(EventTypeSubscriptionOperationDone) + case NotificationMethodEmail: + templateID, err = config.GenerateEmailContent(event) + if err != nil { + return "", "", "", fmt.Errorf("failed to generate email content: %w", err) + } + } + } + + var contentTmpl string + switch event.EventType { + case EventTypeWorkspaceSubscriptionCreatedSuccess: + title = "Workspace Subscription Created Successfully" + // contentTmpl = "尊敬的{{.UserName}},您的工作空间 {{.PlanName}} 订阅已成功创建。" + contentTmpl = "Dear {{.UserName}}, your workspace subscription for {{.PlanName}} has been successfully created." + case EventTypeWorkspaceSubscriptionCreatedFailed: + title = "Workspace Subscription Creation Failed" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅创建失败,原因:{{.ErrorReason}}。" + contentTmpl = "Dear {{.UserName}}, your subscription for {{.PlanName}} failed to be created. Reason: {{.ErrorReason}}." + case EventTypeWorkspaceSubscriptionUpgradedSuccess: + title = "Workspace Subscription Upgraded Successfully" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅已成功升级。" + contentTmpl = "Dear {{.UserName}}, your subscription for {{.PlanName}} has been successfully upgraded." + case EventTypeWorkspaceSubscriptionUpgradedFailed: + title = "Workspace Subscription Upgrade Failed" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅升级失败,原因:{{.ErrorReason}}。" + contentTmpl = "Dear {{.UserName}}, your subscription upgrade for {{.PlanName}} failed. Reason: {{.ErrorReason}}." + case EventTypeWorkspaceSubscriptionRenewedSuccess: + title = "Workspace Subscription Renewed Successfully" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅已成功续订。" + contentTmpl = "Dear {{.UserName}}, your subscription for {{.PlanName}} has been successfully renewed." + case EventTypeWorkspaceSubscriptionRenewedBalanceFallback: + title = "Workspace Subscription Renewed with Balance Fallback" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅续订因余额不足已切换至备用支付方式。" + contentTmpl = "Dear {{.UserName}}, your subscription for {{.PlanName}} has been renewed using your balance due to insufficient funds." + case EventTypeWorkspaceSubscriptionRenewedFailed: + title = "Workspace Subscription Renewal Failed" + // contentTmpl = "尊敬的{{.UserName}},您的{{.PlanName}}订阅续订失败,原因:{{.ErrorReason}}。" + contentTmpl = "Dear {{.UserName}}, your subscription renewal for {{.PlanName}} failed. Reason: {{.ErrorReason}}." + case EventTypeWorkspaceSubscriptionDebt, EventTypeWorkspaceSubscriptionDebtPreDeletion: + title = "Workspace Subscription Debt Notice" + // contentTmpl = "尊敬的{{.UserName}},您的工作空间{{.Workspace}}订阅已进入欠费状态,请及时充值以避免服务中断。" + contentTmpl = "Dear {{.UserName}}, your workspace subscription for {{.Workspace}} is in debt status. Please recharge promptly to avoid service interruption." + case EventTypeTrafficUsageAlert: + title = "Workspace Traffic Usage Alert" + // contentTmpl = "尊敬的{{.UserName}},您的工作空间{{.Workspace}}流量使用已达到{{.UsagePercent}}%,请注意控制使用。" + contentTmpl = "Dear {{.UserName}}, your workspace {{.Workspace}} has used {{.UsagePercent}}% of its traffic. Please monitor your usage." + } + content = g.formatContent(method, contentTmpl, map[string]any{ + "UserName": event.Recipient.UserName, + "PlanName": subData.NewPlanName, + "ErrorReason": subData.ErrorReason, + "Workspace": subData.WorkspaceName, + "Amount": subData.Amount, + "Domain": subData.Domain, + }) + return title, content, templateID, nil +} + +// generateTrafficContent 生成流量相关通知内容 +func (g *DefaultContentGenerator) generateTrafficContent( + method NotificationMethod, + eventData map[string]any, + recipient types.NotificationRecipient, +) (title, content, templateID string, err error) { + var trafficData WorkspaceSubscriptionTrafficEventData + dataBytes, err := json.Marshal(eventData) + if err != nil { + return "", "", "", fmt.Errorf("failed to marshal traffic event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &trafficData); err != nil { + return "", "", "", fmt.Errorf("failed to parse traffic event data: %w", err) + } + + // 获取模板ID + if config, exists := g.config[method]; exists { + templateID = config.GetVMSTemplateID(EventTypeTrafficStatusChange) + switch method { + case NotificationMethodSMS: + templateID = config.GetSMSTemplateCode(EventTypeTrafficStatusChange) + case NotificationMethodEmail: + templateID = config.GetEmailTemplate(EventTypeTrafficStatusChange) + } + } + + switch trafficData.Status { + case /*types.WorkspaceTrafficStatusExhausted,*/ types.WorkspaceTrafficStatusUsedUp: + title = "流量已用尽" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的工作空间{{.Workspace}}流量已用尽,请及时购买流量包。", + map[string]any{ + "UserName": recipient.UserName, + "Workspace": trafficData.Workspace, + }, + ) + default: + if trafficData.UsagePercent >= 80 { + title = "流量预警" + content = g.formatContent( + method, + "尊敬的{{.UserName}},您的工作空间{{.Workspace}}流量使用已达到{{.UsagePercent}}%,请注意控制使用。", + map[string]any{ + "UserName": recipient.UserName, + "Workspace": trafficData.Workspace, + "UsagePercent": trafficData.UsagePercent, + }, + ) + } + } + + return title, content, templateID, nil +} + +// generateCustomContent 生成自定义通知内容 +func (g *DefaultContentGenerator) generateCustomContent( + method NotificationMethod, + eventData map[string]any, + recipient types.NotificationRecipient, +) (title, content, templateID string, err error) { + var customData CustomEventData + dataBytes, err := json.Marshal(eventData) + if err != nil { + return "", "", "", fmt.Errorf("failed to marshal custom event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &customData); err != nil { + return "", "", "", fmt.Errorf("failed to parse custom event data: %w", err) + } + + // 获取模板ID + if config, exists := g.config[method]; exists { + templateID = config.GetVMSTemplateID(EventTypeCustom) + switch method { + case NotificationMethodSMS: + templateID = config.GetSMSTemplateCode(EventTypeCustom) + case NotificationMethodEmail: + templateID = config.GetEmailTemplate(EventTypeCustom) + } + } + + title = customData.Title + content = g.formatContent(method, customData.Content, map[string]any{ + "UserName": recipient.UserName, + }) + + // 合并额外数据 + if customData.ExtraData != nil { + for key, value := range customData.ExtraData { + placeholder := fmt.Sprintf("{{.%s}}", key) + content = strings.ReplaceAll(content, placeholder, fmt.Sprintf("%v", value)) + } + } + + return title, content, templateID, nil +} + +// formatContent 格式化内容,根据不同的通知方式进行适配 +func (g *DefaultContentGenerator) formatContent( + method NotificationMethod, + template string, + data map[string]any, +) string { + content := template + + // 基本参数替换 + for key, value := range data { + placeholder := fmt.Sprintf("{{.%s}}", key) + content = strings.ReplaceAll(content, placeholder, fmt.Sprintf("%v", value)) + } + + // 根据通知方式进行内容适配 + switch method { + case NotificationMethodSMS: + // SMS内容简化,确保在短信字数限制内 + content = g.simplifySMSContent(content) + case NotificationMethodEmail: + // Email可以包含更丰富的内容和格式 + content = g.enrichEmailContent(content) + case NotificationMethodVMS: + // VMS需要语音友好的内容 + content = g.optimizeVMSContent(content) + } + + return content +} + +// simplifySMSContent 简化SMS内容 +func (g *DefaultContentGenerator) simplifySMSContent(content string) string { + // 移除敬语,简化表达 + content = strings.ReplaceAll(content, "尊敬的", "") + content = strings.ReplaceAll(content, "请及时", "请") + content = strings.ReplaceAll(content, "以避免服务中断", "") + return content +} + +// enrichEmailContent 丰富Email内容 +func (g *DefaultContentGenerator) enrichEmailContent(content string) string { + // Email可以添加更多详细信息和格式 + return content + "\n\n如有疑问,请联系技术支持。\n\n此邮件由Sealos系统自动发送,请勿回复。" +} + +// optimizeVMSContent 优化VMS语音内容 +func (g *DefaultContentGenerator) optimizeVMSContent(content string) string { + content = strings.ReplaceAll(content, "%", "百分之") + return content +} diff --git a/controllers/pkg/user_notify/providers.go b/controllers/pkg/user_notify/providers.go new file mode 100644 index 000000000000..5a631bb7033f --- /dev/null +++ b/controllers/pkg/user_notify/providers.go @@ -0,0 +1,1092 @@ +// Copyright © 2024 sealos. +// +// 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 usernotify + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + "strings" + "time" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v3/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/go-gomail/gomail" + "github.com/sirupsen/logrus" + "github.com/volcengine/volc-sdk-golang/service/vms" +) + +// BaseProvider 基础通知提供者 +type BaseProvider struct { + Name string + Available bool + Config ProviderConfig +} + +func (p *BaseProvider) GetName() string { + return p.Name +} + +func (p *BaseProvider) IsAvailable() bool { + return p.Available && p.Config.IsEnabled +} + +func (p *BaseProvider) GetSupportedMethods() []NotificationMethod { + switch p.Name { + case "vms": + return []NotificationMethod{NotificationMethodVMS} + case "email": + return []NotificationMethod{NotificationMethodEmail} + case "sms": + return []NotificationMethod{NotificationMethodSMS} + default: + return []NotificationMethod{} + } +} + +// VMSProvider 火山语音通知提供者 +type VMSProvider struct { + BaseProvider +} + +// NewVMSProvider 创建VMS提供者 +func NewVMSProvider(config ProviderConfig) *VMSProvider { + provider := &VMSProvider{ + BaseProvider: BaseProvider{ + Name: "vms", + Available: false, + Config: config, + }, + } + + // 检查配置是否完整 + if config.VMSAPIKey == "" || config.VMSAPISecret == "" { + log.Printf("VMS provider configuration incomplete, disabling provider") + return provider + } + + provider.Available = true + log.Printf("VMS provider initialized successfully") + return provider +} + +func (p *VMSProvider) Send( + ctx context.Context, + message *NotificationMessage, +) (*NotificationResult, error) { + if !p.IsAvailable() { + return nil, errors.New("VMS provider is not available") + } + + if message.Recipient.PhoneNumber == "" { + return nil, errors.New("phone number is required for VMS notification") + } + + result := &NotificationResult{ + UserUID: message.UserUID, + EventType: message.EventType, + Method: message.Method, + SentAt: time.Now(), + } + + // 实际发送VMS通知 + log.Printf("Sending VMS notification to %s: %s", message.Recipient.PhoneNumber, message.Title) + + // 构建VMS参数,包括模板变量 + phoneParam := make(map[string]any) + if message.EventData != nil { + // 根据事件数据构建phone参数 + if workspaceName, ok := message.EventData["workspace_name"]; ok { + phoneParam["workspace_name"] = workspaceName + } + if usedPercentage, ok := message.EventData["used_percentage"]; ok { + phoneParam["used_percentage"] = usedPercentage + } + // 可以添加更多参数映射 + } + + var paramList []*vms.SingleParam + param := &vms.SingleParam{ + Phone: message.Recipient.PhoneNumber, + Type: 1, + TriggerTime: &vms.JsonTime{Time: result.SentAt}, + Resource: message.TemplateID, // 使用TemplateID作为Resource + NumberPoolNo: p.Config.VMSNumberPool, + SingleOpenId: message.Recipient.PhoneNumber + "-" + result.SentAt.Format(time.DateOnly), + } + + // 如果有参数,添加PhoneParam + if len(phoneParam) > 0 { + param.PhoneParam = phoneParam + } + + paramList = append(paramList, param) + + req := &vms.SingleAppendRequest{ + List: paramList, + } + + // 发送VMS + vmResult, statusCode, err := vms.DefaultInstance.SingleBatchAppend(req) + if err != nil { + result.Success = false + result.Error = fmt.Sprintf("failed to send VMS: %v", err) + return result, err + } + + if vmResult.ResponseMetadata.Error != nil { + result.Success = false + result.Error = fmt.Sprintf("VMS API error: %v", vmResult.ResponseMetadata.Error) + return result, fmt.Errorf("VMS API error: %v", vmResult.ResponseMetadata.Error) + } + + if statusCode != 200 { + result.Success = false + result.Error = fmt.Sprintf("VMS failed with status code: %d", statusCode) + return result, fmt.Errorf("VMS failed with status code: %d", statusCode) + } + + // 构建响应数据 + responseData := map[string]any{ + "template_id": message.TemplateID, + "phone": message.Recipient.PhoneNumber, + "title": message.Title, + "content": message.Content, + "sent_at": result.SentAt, + "provider": "vms", + "status_code": statusCode, + "vms_result": vmResult.Result, + "single_open_id": paramList[0].SingleOpenId, + "phone_param": phoneParam, // 记录传递的模板参数 + } + + if responseBytes, err := json.Marshal(responseData); err == nil { + result.ProviderResponse = string(responseBytes) + } + + result.Success = true + log.Printf( + "VMS notification sent successfully to %s, status: %d", + message.Recipient.PhoneNumber, + statusCode, + ) + return result, nil +} + +// EmailProvider 邮箱通知提供者 +type EmailProvider struct { + BaseProvider +} + +// NewEmailProvider 创建Email提供者 +func NewEmailProvider(config ProviderConfig) *EmailProvider { + provider := &EmailProvider{ + BaseProvider: BaseProvider{ + Name: "email", + Available: false, + Config: config, + }, + } + + // 检查配置是否完整 + if config.SMTPHost == "" || config.SMTPUsername == "" || config.SMTPPassword == "" || + config.FromEmail == "" { + log.Printf("Email provider configuration incomplete, disabling provider") + return provider + } + + provider.Available = true + log.Printf("Email provider initialized successfully") + return provider +} + +func (p *EmailProvider) Send( + ctx context.Context, + message *NotificationMessage, +) (*NotificationResult, error) { + if !p.IsAvailable() { + return nil, errors.New("email provider is not available") + } + + if message.Recipient.Email == "" { + return nil, errors.New("email address is required for email notification") + } + + result := &NotificationResult{ + UserUID: message.UserUID, + EventType: message.EventType, + Method: message.Method, + SentAt: time.Now(), + } + + // 实际发送邮件通知 + log.Printf("Sending email notification to %s: %s", message.Recipient.Email, message.Title) + + // 确定邮件内容:优先使用HTML模板,否则使用简单内容 + var emailContent string + if message.TemplateID != "" { + // 使用HTML模板并替换变量 + // emailContent = p.renderEmailTemplate(message.TemplateID, message.Content, message.EventData, message.Recipient) + emailContent = message.TemplateID + } else { + // 退回到使用简单内容,包装为基本HTML + emailContent = p.wrapSimpleContent(message.Content) + } + + // 构建邮件消息 + m := gomail.NewMessage() + m.SetHeader("To", message.Recipient.Email) + m.SetAddressHeader("From", p.Config.FromEmail, p.Config.FromName) + m.SetHeader("Subject", message.Title) + m.SetBody("text/html", emailContent) + + // 创建邮件发送器 + d := gomail.NewDialer( + p.Config.SMTPHost, + p.Config.SMTPPort, + p.Config.SMTPUsername, + p.Config.SMTPPassword, + ) + + // 发送邮件 + if err := d.DialAndSend(m); err != nil { + result.Success = false + result.Error = fmt.Sprintf("failed to send email: %v", err) + return result, fmt.Errorf("failed to send email: %w", err) + } + + // 构建响应数据 + responseData := map[string]any{ + "template_id": message.TemplateID, + "email": message.Recipient.Email, + "subject": message.Title, + "content": message.Content, + "sent_at": result.SentAt, + "provider": "email", + "smtp_host": p.Config.SMTPHost, + "smtp_port": p.Config.SMTPPort, + "from_email": p.Config.FromEmail, + } + + if responseBytes, err := json.Marshal(responseData); err == nil { + result.ProviderResponse = string(responseBytes) + } + + result.Success = true + log.Printf("Email notification sent successfully to %s", message.Recipient.Email) + return result, nil +} + +// renderEmailTemplate 渲染邮件HTML模板并替换变量 +// func (p *EmailProvider) renderEmailTemplate( +// templateContent, content string, +// eventData map[string]any, +// recipient types.NotificationRecipient, +// ) string { +// renderedTemplate := templateContent +// +// // 基本变量替换 +// renderedTemplate = strings.ReplaceAll(renderedTemplate, "{{.content}}", content) +// renderedTemplate = strings.ReplaceAll(renderedTemplate, "{{.UserName}}", recipient.UserName) +// +// // 替换事件数据中的变量 +// if eventData != nil { +// if workspaceName, ok := eventData["workspace_name"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.workspace_name}}", +// fmt.Sprintf("%v", workspaceName), +// ) +// } +// if usedPercentage, ok := eventData["used_percentage"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.used_percentage}}", +// fmt.Sprintf("%v", usedPercentage), +// ) +// } +// if planName, ok := eventData["plan_name"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.plan_name}}", +// fmt.Sprintf("%v", planName), +// ) +// } +// if errorReason, ok := eventData["error_reason"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.error_reason}}", +// fmt.Sprintf("%v", errorReason), +// ) +// } +// if amount, ok := eventData["amount"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.amount}}", +// fmt.Sprintf("%v", amount), +// ) +// } +// if expireDays, ok := eventData["expire_days"]; ok { +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// "{{.expire_days}}", +// fmt.Sprintf("%v", expireDays), +// ) +// } +// +// // 处理其他动态变量 +// for key, value := range eventData { +// placeholder := fmt.Sprintf("{{.%s}}", key) +// renderedTemplate = strings.ReplaceAll( +// renderedTemplate, +// placeholder, +// fmt.Sprintf("%v", value), +// ) +// } +// } +// +// return renderedTemplate +//} + +type EmailData struct { + UserName string + Title string + AlertMessage string + Content string + WarnContent string + BorderColor string + PlanDetails *PlanDetails // Optional + Recommendation string // Optional +} + +// PlanDetails represents the plan card details +type PlanDetails struct { + Title string + Dates string + Location string + Features []string +} + +const WorkspaceSubscriptionEventEmailRenderTmpl = ` + + + + + Sealos - {{.Title}} + + + + +
+ + + + +
+ + + + +
+
+ Sealos Logo +
+ Sealos +
+

+ Application-Centric Intelligent Cloud Operating System +

+
+ + + +
+

Hi {{.UserName}},

+ +

+ {{.AlertMessage}} +

+ {{if .WarnContent}} +

+ {{.WarnContent}} +

+ {{else}}{{if .Content}} +

+ {{.Content}} +

+ {{end}} + {{end}} + + {{if .PlanDetails}} + + + + +
+ + + + + + + + +
+

{{.PlanDetails.Title}}

+
+
+ {{.PlanDetails.Dates}} +
+
+
{{.PlanDetails.Location}}
+
+ +
+ {{range .PlanDetails.Features}} + + + + + +
+
+
+ {{.}} +
+ {{end}} +
+
+ {{end}} + + {{if .Recommendation}} + + Upgrade Plan + +
+ {{end}} + + + + ` + +// EventConfig defines the configuration for each event type +type EventConfig struct { + TitleTemplate string + AlertTemplate string + WarnAlertTemplate string + Content string + BorderColor string + Features []string + Recommendation string + DatesFormat string +} + +// eventConfigs maps event types to their configurations +var eventConfigs = map[EventType]EventConfig{ + EventTypeTrafficUsageAlert: { + TitleTemplate: "%s Region %s Workspace Resource %s", + AlertTemplate: "This is a heads-up that your %s resource usage in the %s region for the %s workspace has exceeded %s.", + Content: "To avoid any potential service disruption, please review your current usage and consider upgrading your plan.", + BorderColor: "#ffa500", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Until %s", + }, + EventTypeWorkspaceSubscriptionCreatedSuccess: { + TitleTemplate: "%s Region %s Space Subscription Created Successfully", + AlertTemplate: `Welcome to Sealos! +You have successfully subscribed to the %s plan for the %s space in the %s region.`, + Content: "Your plan is active and will automatically renew on %s.", + BorderColor: "#e5e5e5", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + DatesFormat: "Until %s", + }, + EventTypeWorkspaceSubscriptionCreatedFailed: { + TitleTemplate: "%s Region %s workspace Subscription Creation Failed", + AlertTemplate: `We were unable to process your subscription to the %s plan for the %s workspace in the %s region.`, + Content: "Please contact support for assistance.", + BorderColor: "#ff0000", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Until %s", + }, + EventTypeWorkspaceSubscriptionRenewedSuccess: { + TitleTemplate: "%s Region %s Workspace Subscription Renewed", + AlertTemplate: `Your subscription to the %s plan for the %s workspace in the %s region has been successfully renewed.`, + Content: "Thank you for your continued trust in Sealos.", + BorderColor: "#e5e5e5", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + DatesFormat: "Until %s", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, + EventTypeWorkspaceSubscriptionRenewedFailed: { + TitleTemplate: "%s Region %s Workspace Subscription Renewal Failed", + AlertTemplate: `We were unable to renew your %s plan for the %s workspace in the %s region. Your service will be suspended on your plan's expiration date.`, + Content: "To prevent service interruption and data loss, please update your payment information as soon as possible. Your resources will be permanently deleted 7 days after the expiration date of %s.", + BorderColor: "#ff0000", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Until %s", + }, + // EventTypeWorkspaceSubscriptionRenewedBalanceFallback 订阅支付失败,自动使用余额支付,需要通知用户订阅支付失败,但使用cloud balance成功 + EventTypeWorkspaceSubscriptionRenewedBalanceFallback: { + TitleTemplate: "%s Region %s Workspace Subscription Renewed with Cloud Balance", + AlertTemplate: `Your subscription to the %s plan for the %s workspace in the %s region was successfully renewed using your cloud balance.Dear User, Your subscription auto-renewal has failed. However, we have successfully deducted the payment from your account balance, and your subscription has been renewed. Please check your payment method to ensure smooth auto-renewals in the future. If you have any questions, please contact our customer support.`, + Content: "Thank you for your continued trust in Sealos. Please ensure your payment information is up to date to avoid future interruptions.", + BorderColor: "#e5e5e5", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + DatesFormat: "Until %s", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, + EventTypeWorkspaceSubscriptionExpired: { + TitleTemplate: "%s Region %s Workspace Subscription Expired", + AlertTemplate: "Your %s plan for the %s workspace in the %s region has expired, and your service is now suspended.", + Content: "Your resources will be permanently deleted in %d days. To restore your service and prevent data loss, please renew your subscription now.", + BorderColor: "#ff0000", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Until %s", + }, + EventTypeWorkspaceSubscriptionExpiredDeleteResources: { + TitleTemplate: "%s Region %s Workspace Resources Deleted", + AlertTemplate: "As your subscription for the %s space in the %s region was not renewed within the 7-day grace period, your associated resources have now been permanently deleted.", + Content: "We're sorry to see you go. If you wish to use Sealos services again in the future, you can start a new subscription at any time.", + BorderColor: "#ff0000", + Features: []string{"4 vCPU", "4GB RAM", "1GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Expired on %s", + }, + EventTypeWorkspaceSubscriptionUpgradedSuccess: { + TitleTemplate: "%s Region %s Workspace Subscription Upgraded", + AlertTemplate: "You have successfully upgraded to the %s plan! Your new plan is effective immediately for the %s workspace in the %s region.", + Content: "Enjoy the new features! Your plan will automatically renew on %s.", + BorderColor: "#e5e5e5", + Features: []string{"8 vCPU", "16GB RAM", "10GB Disk"}, + DatesFormat: "Until %s", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, + EventTypeWorkspaceSubscriptionUpgradedFailed: { + TitleTemplate: "%s Region %s Workspace Subscription Upgrade Failed", + AlertTemplate: "We were unable to process your upgrade to the %s plan for the %s workspace in the %s region. Your subscription will remain on your current plan.", + Content: "Please contact support for assistance.", + BorderColor: "#ff0000", + Features: []string{"8 vCPU", "16GB RAM", "10GB Disk"}, + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + DatesFormat: "Current Until %s", + }, + EventTypeWorkspaceSubscriptionDowngradedSuccess: { + TitleTemplate: "%s Region %s Workspace Subscription Downgraded", + AlertTemplate: "You have successfully scheduled a downgrade to the %s plan for the %s workspace in the %s region.", + Content: "This change will take effect at the start of your next billing cycle on %s.", + BorderColor: "#e5e5e5", + Features: []string{"2 vCPU", "2GB RAM", "500MB Disk"}, + DatesFormat: "Effective from %s", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, + EventTypeWorkspaceSubscriptionDowngradedFailed: { + TitleTemplate: "%s Region %s Workspace Subscription Downgrade Failed", + AlertTemplate: "We were unable to process your downgrade to the %s plan for the %s workspace in the %s region. Your subscription will remain on your current plan.", + Content: "Please contact support for assistance.", + BorderColor: "#ff0000", + Features: []string{"2 vCPU", "2GB RAM", "500MB Disk"}, + DatesFormat: "Current Until %s", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, + EventTypeWorkspaceSubscriptionDebt: { + TitleTemplate: "Workspace Subscription Expiration Warning in %s Region %s Workspace", + AlertTemplate: "Your %s Plan for the %s workspace in the %s region has expired. Your service will be suspended on your plan's expiration date.", + BorderColor: "#ff0000", + WarnAlertTemplate: "To prevent service interruption and data loss, please update your payment information as soon as possible. Your resources will be permanently deleted 7 days after the expiration date.", + // Content: `To prevent service interruption and data loss, please update your plan as soon as possible. Your resources will be permanently deleted 7 days after the expiration date.`, + Content: "Your subscription will delete in %d days. Please renew your subscription to continue enjoying our services.", + Recommendation: "https://usw.sealos.io/?openapp=system-costcenter®ion=%s&workspace=%s", + }, +} + +// generateEmailContent generates email content based on the event type +func generateEmailContent(event *NotificationEvent) (*EmailData, error) { + config, ok := eventConfigs[event.EventType] + if !ok { + return nil, fmt.Errorf("unsupported event type: %s", event.EventType) + } + + var data EmailData + data.BorderColor = config.BorderColor + data.Content = config.Content + + switch event.EventType { + case EventTypeTrafficUsageAlert: + var trafficData WorkspaceSubscriptionTrafficEventData + dataBytes, err := json.Marshal(event.EventData) + if err != nil { + return nil, fmt.Errorf("failed to marshal traffic event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &trafficData); err != nil { + return nil, fmt.Errorf("failed to parse traffic event data: %w", err) + } + // Determine title suffix based on UsagePercent + titleSuffix := "Usage Alert" + if trafficData.UsagePercent >= 100 { + titleSuffix = "Exhausted" + data.Content = "Please upgrade your plan immediately to ensure continued operation." + data.BorderColor = "#ff0000" + } + data.Title = fmt.Sprintf( + config.TitleTemplate, + trafficData.RegionDomain, + trafficData.Workspace, + titleSuffix, + ) + data.AlertMessage = fmt.Sprintf( + strings.ReplaceAll( + config.AlertTemplate, + `%s`, + `%s`, + ), + "Traffic", + trafficData.RegionDomain, + trafficData.Workspace, + strconv.Itoa(trafficData.UsagePercent)+"%", + ) + data.Recommendation = fmt.Sprintf( + config.Recommendation, + trafficData.RegionDomain, + trafficData.Workspace, + ) + data.PlanDetails = &PlanDetails{ + Title: trafficData.PlanName, + Dates: fmt.Sprintf(config.DatesFormat, trafficData.ExpirationDate), + Location: fmt.Sprintf("%s/%s", trafficData.RegionDomain, trafficData.Workspace), + Features: trafficData.Features, + } + if trafficData.UsagePercent >= 100 { + data.Content = "Please upgrade your plan immediately to ensure continued operation." + data.BorderColor = "#ff0000" + } + case EventTypeWorkspaceSubscriptionDebt: + var subData WorkspaceSubscriptionDebtEventData + dataBytes, err := json.Marshal(event.EventData) + if err != nil { + return nil, fmt.Errorf("failed to marshal subscription debt event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &subData); err != nil { + return nil, fmt.Errorf("failed to parse subscription debt event data: %w", err) + } + data.Title = fmt.Sprintf(config.TitleTemplate, subData.RegionDomain, subData.WorkspaceName) + data.AlertMessage = fmt.Sprintf( + config.AlertTemplate, + subData.PlanName, + subData.WorkspaceName, + subData.RegionDomain, + ) + data.Content = fmt.Sprintf( + config.Content, + subData.PlanName, + subData.WorkspaceName, + subData.RegionDomain, + ) + data.WarnContent = config.WarnAlertTemplate + data.Recommendation = fmt.Sprintf( + config.Recommendation, + subData.RegionDomain, + subData.WorkspaceName, + ) + data.AlertMessage = fmt.Sprintf( + strings.ReplaceAll( + config.AlertTemplate, + `%s`, + `%s`, + ), + subData.PlanName, + subData.WorkspaceName, + subData.RegionDomain, + ) + data.WarnContent = config.WarnAlertTemplate + default: + var subData WorkspaceSubscriptionEventData + dataBytes, err := json.Marshal(event.EventData) + if err != nil { + return nil, fmt.Errorf("failed to marshal subscription event data: %w", err) + } + if err := json.Unmarshal(dataBytes, &subData); err != nil { + return nil, fmt.Errorf("failed to parse subscription event data: %w", err) + } + data.Title = fmt.Sprintf(config.TitleTemplate, subData.RegionDomain, subData.WorkspaceName) + + data.AlertMessage = fmt.Sprintf( + strings.ReplaceAll( + config.AlertTemplate, + `%s`, + `%s`, + ), + subData.NewPlanName, + subData.WorkspaceName, + subData.RegionDomain, + ) + data.PlanDetails = &PlanDetails{ + Title: subData.NewPlanName, + Dates: fmt.Sprintf(config.DatesFormat, subData.ExpirationDate), + Location: fmt.Sprintf("%s/%s", subData.RegionDomain, subData.WorkspaceName), + Features: subData.Features, + } + if config.Recommendation != "" { + data.Recommendation = fmt.Sprintf( + config.Recommendation, + subData.RegionDomain, + subData.WorkspaceName, + ) + } + if event.EventType == EventTypeWorkspaceSubscriptionExpired { + data.Content = fmt.Sprintf(config.Content, subData.DaysRemaining) + } + switch event.EventType { + case EventTypeWorkspaceSubscriptionUpgradedSuccess, + EventTypeWorkspaceSubscriptionCreatedSuccess: + data.Content = fmt.Sprintf(config.Content, subData.NextPayDate) + case EventTypeWorkspaceSubscriptionRenewedFailed: + data.Content = fmt.Sprintf(config.Content, subData.NewPlanName) + } + } + data.UserName = event.Recipient.UserName + + fmt.Printf("Generated Email Data: %+v\n", data) + return &data, nil +} + +// wrapSimpleContent 将简单文本内容包装为基本HTML +func (p *EmailProvider) wrapSimpleContent(content string) string { + return fmt.Sprintf(` + + + + + Sealos通知 + + + +
+

%s

+
+ + +`, content) +} + +// SMSProvider 阿里短信通知提供者 +type SMSProvider struct { + BaseProvider +} + +// NewSMSProvider 创建SMS提供者 +func NewSMSProvider(config ProviderConfig) *SMSProvider { + provider := &SMSProvider{ + BaseProvider: BaseProvider{ + Name: "sms", + Available: false, + Config: config, + }, + } + + // 检查配置是否完整 + if config.SMSAccessKeyID == "" || config.SMSAccessKeySecret == "" || + config.SMSDefaultTemplate == "" { + log.Printf("SMS provider configuration incomplete, disabling provider") + return provider + } + + provider.Available = true + log.Printf("SMS provider initialized successfully") + return provider +} + +func (p *SMSProvider) Send( + ctx context.Context, + message *NotificationMessage, +) (*NotificationResult, error) { + if !p.IsAvailable() { + return nil, errors.New("SMS provider is not available") + } + // 准备短信模板参数 + templateParam := "" + if message.EventData != nil { + if templateParamBytes, err := json.Marshal(message.EventData); err == nil { + templateParam = string(templateParamBytes) + } + } + + if message.Recipient.PhoneNumber == "" || templateParam == "" { + // TODO: skip null phone number or empty template param + return nil, nil + } + + result := &NotificationResult{ + UserUID: message.UserUID, + EventType: message.EventType, + Method: message.Method, + SentAt: time.Now(), + } + + // 实际发送短信通知 + log.Printf("Sending SMS notification to %s: %s", message.Recipient.PhoneNumber, message.Title) + + // 创建阿里云SMS客户端 + config := &openapi.Config{ + AccessKeyId: tea.String(p.Config.SMSAccessKeyID), + AccessKeySecret: tea.String(p.Config.SMSAccessKeySecret), + Endpoint: tea.String(p.Config.SMSEndpoint), + } + + client, err := dysmsapi20170525.NewClient(config) + if err != nil { + result.Success = false + result.Error = fmt.Sprintf("failed to create SMS client: %v", err) + return result, err + } + + // 构建发送短信请求 + sendSmsRequest := &dysmsapi20170525.SendSmsRequest{ + PhoneNumbers: tea.String(message.Recipient.PhoneNumber), + SignName: tea.String(p.Config.SMSSignName), + TemplateCode: tea.String(message.TemplateID), + TemplateParam: tea.String(templateParam), + } + + // 发送短信 + runtime := &util.RuntimeOptions{} + resp, err := client.SendSmsWithOptions(sendSmsRequest, runtime) + if err != nil { + result.Success = false + result.Error = fmt.Sprintf("failed to send SMS: %v", err) + return result, err + } + + // 检查响应结果 + if resp.Body.Code == nil || *resp.Body.Code != "OK" { + errCode := "UNKNOWN" + errMsg := "Unknown error" + if resp.Body.Code != nil { + errCode = *resp.Body.Code + } + if resp.Body.Message != nil { + errMsg = *resp.Body.Message + } + result.Success = false + result.Error = fmt.Sprintf("SMS send failed with code %s: %s", errCode, errMsg) + return result, fmt.Errorf("SMS send failed with code %s: %s", errCode, errMsg) + } + + // 构建响应数据 + responseData := map[string]any{ + "template_code": message.TemplateID, + "phone": message.Recipient.PhoneNumber, + "content": message.Content, + "template_param": templateParam, + "sent_at": result.SentAt, + "provider": "sms", + "sign_name": p.Config.SMSSignName, + "response_code": *resp.Body.Code, + "request_id": *resp.Body.RequestId, + } + + if resp.Body.BizId != nil { + responseData["biz_id"] = *resp.Body.BizId + } + + if responseBytes, err := json.Marshal(responseData); err == nil { + result.ProviderResponse = string(responseBytes) + } + + result.Success = true + log.Printf( + "SMS notification sent successfully to %s, RequestId: %s", + message.Recipient.PhoneNumber, + *resp.Body.RequestId, + ) + return result, nil +} + +// ProviderManager 通知提供者管理器 +type ProviderManager struct { + providers map[NotificationMethod]NotificationProvider + contactProvider UserContactProvider + contentGen NotificationContentGenerator +} + +// NewProviderManager 创建提供者管理器 +func NewProviderManager( + configs map[NotificationMethod]ProviderConfig, + contactProvider UserContactProvider, +) *ProviderManager { + manager := &ProviderManager{ + providers: make(map[NotificationMethod]NotificationProvider), + contactProvider: contactProvider, + contentGen: NewDefaultContentGenerator(configs), + } + + // 初始化各种提供者 + if vmsConfig, exists := configs[NotificationMethodVMS]; exists { + manager.providers[NotificationMethodVMS] = NewVMSProvider(vmsConfig) + } + + if emailConfig, exists := configs[NotificationMethodEmail]; exists { + manager.providers[NotificationMethodEmail] = NewEmailProvider(emailConfig) + } + + if smsConfig, exists := configs[NotificationMethodSMS]; exists { + manager.providers[NotificationMethodSMS] = NewSMSProvider(smsConfig) + } + + return manager +} + +// GetProvider 获取指定方法的提供者 +func (m *ProviderManager) GetProvider(method NotificationMethod) (NotificationProvider, bool) { + provider, exists := m.providers[method] + return provider, exists +} + +// GetAvailableProviders 获取所有可用的提供者 +func (m *ProviderManager) GetAvailableProviders() []NotificationProvider { + var available []NotificationProvider + for _, provider := range m.providers { + if provider.IsAvailable() { + available = append(available, provider) + } + } + return available +} + +// SendEvent 发送事件通知 +func (m *ProviderManager) SendEvent( + ctx context.Context, + event *NotificationEvent, +) ([]*NotificationResult, error) { + results := make([]*NotificationResult, 0) + var errors []string + + // 如果事件中没有接收者信息,尝试获取 + if m.contactProvider != nil { + contact, err := m.contactProvider.GetUserContact(ctx, event.UserUID) + if err != nil { + return nil, fmt.Errorf("failed to get user contact: %w", err) + } else if contact != nil { + // 补充接收者信息 + event.Recipient = *contact + } + } + logrus.Infof("recipient info: %+v", event.Recipient) + + // 为每种通知方式生成消息并发送 + for _, method := range event.Methods { + provider, exists := m.GetProvider(method) + if !exists { + // TODO 临时去掉 + // return nil, fmt.Errorf("failed to get provider for method: %s", method) + continue + } + + if !provider.IsAvailable() { + errors = append(errors, fmt.Sprintf("provider %s is not available", provider.GetName())) + continue + } + switch provider.GetName() { + case "email": + if event.Recipient.Email == "" && !event.NotIgnoreIfNoContact { + // return nil, fmt.Errorf("email required for email notification") + continue + } + case "sms", "vms": + if event.Recipient.PhoneNumber == "" && !event.NotIgnoreIfNoContact { + continue + } + } + + // 生成通知内容 + title, content, templateID, err := m.contentGen.GenerateContent(event, method) + if err != nil { + errors = append( + errors, + fmt.Sprintf("failed to generate content for %s: %v", method, err), + ) + continue + } + + // 创建通知消息 + message := &NotificationMessage{ + UserUID: event.UserUID, + EventType: event.EventType, + Method: method, + Priority: event.Priority, + Title: title, + Content: content, + Recipient: event.Recipient, + EventData: event.EventData, + TemplateID: templateID, + Timestamp: event.Timestamp, + } + + // 发送通知 + result, err := provider.Send(ctx, message) + if err != nil { + // 创建失败结果 + result = &NotificationResult{ + UserUID: event.UserUID, + EventType: event.EventType, + Method: method, + Success: false, + Error: err.Error(), + SentAt: time.Now(), + } + errors = append(errors, fmt.Sprintf("failed to send %s notification: %v", method, err)) + } + + results = append(results, result) + } + + var finalError error + if len(errors) > 0 { + finalError = fmt.Errorf("notification sending errors: %s", strings.Join(errors, "; ")) + } + + return results, finalError +} + +// SetContactProvider 设置用户联系方式提供者 +func (m *ProviderManager) SetContactProvider(provider UserContactProvider) { + m.contactProvider = provider +} + +// SetContentGenerator 设置内容生成器 +func (m *ProviderManager) SetContentGenerator(generator NotificationContentGenerator) { + m.contentGen = generator +} diff --git a/controllers/pkg/user_notify/service.go b/controllers/pkg/user_notify/service.go new file mode 100644 index 000000000000..dc1ae415d4fe --- /dev/null +++ b/controllers/pkg/user_notify/service.go @@ -0,0 +1,657 @@ +// Copyright © 2024 sealos. +// +// 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 usernotify + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +// EventNotificationService 事件驱动的通知服务 +type EventNotificationService interface { + // 发送事件通知 + SendEventNotification( + ctx context.Context, + event *NotificationEvent, + ) ([]*NotificationResult, error) + + // 债务状态变更事件 + HandleDebtStatusChange( + ctx context.Context, + userUID uuid.UUID, + lastStatus, currentStatus types.DebtStatusType, + debtDays int, + methods []NotificationMethod, + ) ([]*NotificationResult, error) + + // 订阅相关事件 + HandleWorkspaceSubscriptionEvent( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + operator types.SubscriptionOperator, + methods []NotificationMethod, + ) ([]*NotificationResult, error) + + // 流量相关事件 + HandleTrafficEvent( + ctx context.Context, + userUID uuid.UUID, + status types.WorkspaceTrafficStatus, + usagePercent int, + workspace string, + methods []NotificationMethod, + ) ([]*NotificationResult, error) + + // 自定义事件 + HandleCustomEvent( + ctx context.Context, + userUID uuid.UUID, + title, content string, + extraData map[string]any, + methods []NotificationMethod, + ) ([]*NotificationResult, error) +} + +// eventNotificationServiceImpl 事件通知服务实现 +type eventNotificationServiceImpl struct { + providerManager *ProviderManager +} + +// NewEventNotificationService 创建事件通知服务 +func NewEventNotificationService( + configs map[NotificationMethod]ProviderConfig, + contactProvider UserContactProvider, +) EventNotificationService { + providerManager := NewProviderManager(configs, contactProvider) + + return &eventNotificationServiceImpl{ + providerManager: providerManager, + } +} + +// SendEventNotification 发送事件通知 +func (s *eventNotificationServiceImpl) SendEventNotification( + ctx context.Context, + event *NotificationEvent, +) ([]*NotificationResult, error) { + // 设置默认值 + if event.Priority == "" { + event.Priority = NotificationPriorityNormal + } + if event.Timestamp.IsZero() { + event.Timestamp = time.Now() + } + + // 发送通知 + results, err := s.providerManager.SendEvent(ctx, event) + + // 记录发送日志 + for _, result := range results { + if result.Success { + log.Printf("Event notification sent successfully: UserUID=%s, EventType=%s, Method=%s", + result.UserUID, result.EventType, result.Method) + } else { + log.Printf("Event notification failed: UserUID=%s, EventType=%s, Method=%s, Error=%s", + result.UserUID, result.EventType, result.Method, result.Error) + } + } + + return results, err +} + +// HandleDebtStatusChange 处理债务状态变更事件 +func (s *eventNotificationServiceImpl) HandleDebtStatusChange( + ctx context.Context, + userUID uuid.UUID, + lastStatus, currentStatus types.DebtStatusType, + debtDays int, + methods []NotificationMethod, +) ([]*NotificationResult, error) { + // 只在状态真正发生变化时发送通知 + if lastStatus == currentStatus { + return nil, nil + } + + // 构建事件数据 + eventData := map[string]any{ + "last_status": string(lastStatus), + "current_status": string(currentStatus), + "debt_days": debtDays, + } + + // 根据债务状态设置优先级 + var priority NotificationPriority + switch currentStatus { + case types.FinalDeletionPeriod: + priority = NotificationPriorityCritical + case types.DebtDeletionPeriod: + priority = NotificationPriorityHigh + case types.DebtPeriod: + priority = NotificationPriorityHigh + default: + priority = NotificationPriorityNormal + } + + event := &NotificationEvent{ + UserUID: userUID, + EventType: EventTypeDebtStatusChange, + EventData: eventData, + Methods: methods, + Priority: priority, + Timestamp: time.Now(), + } + + return s.SendEventNotification(ctx, event) +} + +// HandleSubscriptionEvent 处理订阅相关事件 +func (s *eventNotificationServiceImpl) HandleWorkspaceSubscriptionEvent( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + operator types.SubscriptionOperator, + methods []NotificationMethod, +) ([]*NotificationResult, error) { + // 构建事件数据 + // eventData := map[string]interface{}{ + // "workspace_name": workspace, + // "operator": string(operator), + // "old_plan_name": oldPlan, + // "new_plan_name": newPlan, + // "pay_status": string(payStatus), + //} + // + // if errorReason != "" { + // eventData["error_reason"] = errorReason + //} + eventDataMap := eventData.ToMap() + payStatus := eventDataMap["pay_status"] + // payMethod := eventDataMap["pay_method"] + // 根据操作类型设置优先级 + var priority NotificationPriority + if payStatus == types.SubscriptionPayStatusFailed || + operator == types.SubscriptionTransactionTypeCanceled { + priority = NotificationPriorityHigh + } else { + priority = NotificationPriorityNormal + } + var eventType EventType + if eventDataMap["type"] != nil { + if et, ok := eventDataMap["type"].(EventType); ok { + eventType = et + } + } + if eventType == "" { + switch operator { + case types.SubscriptionTransactionTypeCreated: + if payStatus == types.SubscriptionPayStatusPaid { + eventType = EventTypeWorkspaceSubscriptionCreatedSuccess + } else { + eventType = EventTypeWorkspaceSubscriptionCreatedFailed + } + case types.SubscriptionTransactionTypeUpgraded: + if payStatus == types.SubscriptionPayStatusPaid { + eventType = EventTypeWorkspaceSubscriptionUpgradedSuccess + } else { + eventType = EventTypeWorkspaceSubscriptionUpgradedFailed + } + case types.SubscriptionTransactionTypeDowngraded: + case types.SubscriptionTransactionTypeRenewed: + switch payStatus { + case types.SubscriptionPayStatusPaid: + eventType = EventTypeWorkspaceSubscriptionRenewedSuccess + case types.SubscriptionPayStatusFailedAndUseBalance: + eventType = EventTypeWorkspaceSubscriptionRenewedBalanceFallback + default: + eventType = EventTypeWorkspaceSubscriptionRenewedFailed + } + } + } + + event := &NotificationEvent{ + UserUID: userUID, + EventType: eventType, + EventData: eventData.ToMap(), + Methods: methods, + Priority: priority, + Timestamp: time.Now(), + } + + return s.SendEventNotification(ctx, event) +} + +// HandleTrafficEvent 处理流量相关事件 +func (s *eventNotificationServiceImpl) HandleTrafficEvent( + ctx context.Context, + userUID uuid.UUID, + status types.WorkspaceTrafficStatus, + usagePercent int, + workspace string, + methods []NotificationMethod, +) ([]*NotificationResult, error) { + // 构建事件数据 + eventData := map[string]any{ + "status": string(status), + "usage_percent": usagePercent, + "workspace": workspace, + } + + // 根据流量状态设置优先级 + var priority NotificationPriority + switch status { + case types.WorkspaceTrafficStatusExhausted, types.WorkspaceTrafficStatusUsedUp: + priority = NotificationPriorityHigh + default: + switch { + case usagePercent >= 90: + priority = NotificationPriorityHigh + case usagePercent >= 80: + priority = NotificationPriorityNormal + default: + return nil, nil // 低于80%不发送通知 + } + } + + event := &NotificationEvent{ + UserUID: userUID, + EventType: EventTypeTrafficStatusChange, + EventData: eventData, + Methods: methods, + Priority: priority, + Timestamp: time.Now(), + } + + return s.SendEventNotification(ctx, event) +} + +// HandleCustomEvent 处理自定义事件 +func (s *eventNotificationServiceImpl) HandleCustomEvent( + ctx context.Context, + userUID uuid.UUID, + title, content string, + extraData map[string]any, + methods []NotificationMethod, +) ([]*NotificationResult, error) { + // 构建事件数据 + eventData := map[string]any{ + "title": title, + "content": content, + } + + if extraData != nil { + eventData["extra_data"] = extraData + } + + event := &NotificationEvent{ + UserUID: userUID, + EventType: EventTypeCustom, + EventData: eventData, + Methods: methods, + Priority: NotificationPriorityNormal, + Timestamp: time.Now(), + } + + return s.SendEventNotification(ctx, event) +} + +// NotificationHelper 通知助手,提供便捷的事件发送方法 +type NotificationHelper struct { + service EventNotificationService +} + +// NewNotificationHelper 创建通知助手 +func NewNotificationHelper(service EventNotificationService) *NotificationHelper { + return &NotificationHelper{ + service: service, + } +} + +// SendDebtNotification 发送欠费通知 +func (h *NotificationHelper) SendDebtNotification( + ctx context.Context, + userUID uuid.UUID, + methods []NotificationMethod, +) error { + _, err := h.service.HandleDebtStatusChange( + ctx, + userUID, + types.NormalPeriod, + types.DebtPeriod, + 0, + methods, + ) + return err +} + +// SendDebt3DaysNotification 发送欠费3天通知 +func (h *NotificationHelper) SendDebt3DaysNotification( + ctx context.Context, + userUID uuid.UUID, + methods []NotificationMethod, +) error { + _, err := h.service.HandleDebtStatusChange( + ctx, + userUID, + types.DebtPeriod, + types.DebtDeletionPeriod, + 3, + methods, + ) + return err +} + +// SendDebt7DaysPreCleanupNotification 发送欠费7天预清理通知 +func (h *NotificationHelper) SendDebt7DaysPreCleanupNotification( + ctx context.Context, + userUID uuid.UUID, + methods []NotificationMethod, +) error { + _, err := h.service.HandleDebtStatusChange( + ctx, + userUID, + types.DebtDeletionPeriod, + types.FinalDeletionPeriod, + 7, + methods, + ) + return err +} + +// SendTrafficLow80Notification 发送流量不足80%通知 +func (h *NotificationHelper) SendTrafficLow80Notification( + ctx context.Context, + userUID uuid.UUID, + usagePercent int, + methods []NotificationMethod, +) error { + _, err := h.service.HandleTrafficEvent( + ctx, + userUID, + types.WorkspaceTrafficStatusActive, + usagePercent, + "default", + methods, + ) + return err +} + +// SendTrafficExhaustedNotification 发送流量已用尽通知 +func (h *NotificationHelper) SendTrafficExhaustedNotification( + ctx context.Context, + userUID uuid.UUID, + methods []NotificationMethod, +) error { + _, err := h.service.HandleTrafficEvent( + ctx, + userUID, + types.WorkspaceTrafficStatusExhausted, + 100, + "default", + methods, + ) + return err +} + +// SendWorkspaceSubscriptionCreateSuccessNotification 发送创建订阅成功通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionCreateSuccessNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeCreated, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionCreateFailedNotification 发送创建订阅失败通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionCreateFailedNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeCreated, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionRenewSuccessNotification 发送续订成功通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionRenewSuccessNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeRenewed, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionUpgradeSuccessNotification 发送升级成功通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionUpgradeSuccessNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeUpgraded, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionUpgradeFailedNotification 发送升级失败通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionUpgradeFailedNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeUpgraded, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionDowngradeSuccessNotification 发送降级成功通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionDowngradeSuccessNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeDowngraded, + methods, + ) + return err +} + +// SendWorkspaceSubscriptionRenewFailedNotification 发送续订失败通知 +func (h *NotificationHelper) SendWorkspaceSubscriptionRenewFailedNotification( + ctx context.Context, + userUID uuid.UUID, + eventData EventData, + methods []NotificationMethod, +) error { + _, err := h.service.HandleWorkspaceSubscriptionEvent( + ctx, + userUID, + eventData, + types.SubscriptionTransactionTypeRenewed, + methods, + ) + return err +} + +// SendDatabaseResourceLowNotification 发送数据库资源不足通知 +func (h *NotificationHelper) SendDatabaseResourceLowNotification( + ctx context.Context, + userUID uuid.UUID, + databaseName string, + usagePercent int, + methods []NotificationMethod, +) error { + extraData := map[string]any{ + "database_name": databaseName, + "usage_percent": usagePercent, + } + title := "数据库资源预警" + content := fmt.Sprintf("您的数据库%s资源使用量已达到%d%%,请注意优化。", databaseName, usagePercent) + + _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods) + return err +} + +// SendDatabaseAbnormalNotification 发送数据库异常通知 +func (h *NotificationHelper) SendDatabaseAbnormalNotification( + ctx context.Context, + userUID uuid.UUID, + databaseName, errorReason string, + methods []NotificationMethod, +) error { + extraData := map[string]any{ + "database_name": databaseName, + "error_reason": errorReason, + } + title := "数据库异常通知" + content := fmt.Sprintf("您的数据库%s出现异常:%s", databaseName, errorReason) + + _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods) + return err +} + +// SendCustomNotification 发送自定义通知 +func (h *NotificationHelper) SendCustomNotification( + ctx context.Context, + userUID uuid.UUID, + title, content string, + methods []NotificationMethod, + priority NotificationPriority, + extraData map[string]any, +) error { + _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods) + return err +} + +// SendScheduledNotification 发送定时通知 +func (h *NotificationHelper) SendScheduledNotification( + ctx context.Context, + userUID uuid.UUID, + eventType EventType, + scheduleAt time.Time, + methods []NotificationMethod, + extraData map[string]any, +) error { + // 事件驱动的通知系统不支持定时发送,这个功能需要外部调度系统来实现 + // 这里直接发送通知 + title := "定时通知" + content := "这是一个定时通知" + if extraData != nil { + if t, ok := extraData["title"].(string); ok { + title = t + } + if c, ok := extraData["content"].(string); ok { + content = c + } + } + + _, err := h.service.HandleCustomEvent(ctx, userUID, title, content, extraData, methods) + return err +} + +// BatchNotificationRequest 批量通知请求 +type BatchNotificationRequest struct { + UserUID uuid.UUID `json:"user_uid"` + EventType EventType `json:"event_type"` + Methods []NotificationMethod `json:"methods"` + Priority NotificationPriority `json:"priority"` + Title string `json:"title"` + Content string `json:"content"` + TemplateData map[string]any `json:"template_data"` +} + +// SendBatchNotifications 批量发送通知 +func (h *NotificationHelper) SendBatchNotifications( + ctx context.Context, + requests []BatchNotificationRequest, +) ([]*NotificationResult, error) { + var allResults []*NotificationResult + var errors []string + + for _, req := range requests { + extraData := req.TemplateData + if extraData == nil { + extraData = make(map[string]any) + } + + results, err := h.service.HandleCustomEvent( + ctx, + req.UserUID, + req.Title, + req.Content, + extraData, + req.Methods, + ) + if err != nil { + errors = append( + errors, + fmt.Sprintf("failed to send notification to user %s: %v", req.UserUID, err), + ) + } + + allResults = append(allResults, results...) + } + + var finalError error + if len(errors) > 0 { + finalError = fmt.Errorf("batch notification errors: %v", errors) + } + + return allResults, finalError +} diff --git a/controllers/pkg/user_notify/types.go b/controllers/pkg/user_notify/types.go new file mode 100644 index 000000000000..d7a81187272b --- /dev/null +++ b/controllers/pkg/user_notify/types.go @@ -0,0 +1,395 @@ +// Copyright © 2024 sealos. +// +// 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 usernotify + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "text/template" + "time" + + "github.com/google/uuid" + "github.com/labring/sealos/controllers/pkg/types" +) + +// NotificationMethod 通知方式枚举 +type NotificationMethod string + +const ( + NotificationMethodVMS NotificationMethod = "vms" + NotificationMethodEmail NotificationMethod = "email" + NotificationMethodSMS NotificationMethod = "sms" +) + +// NotificationPriority 通知优先级枚举 +type NotificationPriority string + +const ( + NotificationPriorityLow NotificationPriority = "low" + NotificationPriorityNormal NotificationPriority = "normal" + NotificationPriorityHigh NotificationPriority = "high" + NotificationPriorityCritical NotificationPriority = "critical" +) + +// EventType 事件类型,基于现有的业务状态 +type EventType string + +const ( + // 债务相关事件 + EventTypeDebtStatusChange EventType = "debt_status_change" + + // 订阅相关事件 + EventTypeSubscriptionStatusChange EventType = "subscription_status_change" + EventTypeSubscriptionRenewalFailed EventType = "subscription_renewal_failed" + + EventTypeSubscriptionOperationDone EventType = "subscription_operation_done" + EventTypeSubscriptionPaymentDone EventType = "subscription_payment_done" + + EventTypeUniversalWorkspaceSubscriptionEvent EventType = "universal_workspace_subscription_event" + + EventTypeWorkspaceSubscriptionCreatedSuccess EventType = "workspace_subscription_created_success" + EventTypeWorkspaceSubscriptionCreatedFailed EventType = "workspace_subscription_created_failed" + EventTypeWorkspaceSubscriptionUpgradedSuccess EventType = "workspace_subscription_upgraded_success" + EventTypeWorkspaceSubscriptionUpgradedFailed EventType = "workspace_subscription_upgraded_failed" + EventTypeWorkspaceSubscriptionDowngradedSuccess EventType = "workspace_subscription_downgraded_success" + EventTypeWorkspaceSubscriptionDowngradedFailed EventType = "workspace_subscription_downgraded_failed" + EventTypeWorkspaceSubscriptionRenewedSuccess EventType = "workspace_subscription_renewed_success" + EventTypeWorkspaceSubscriptionRenewedBalanceFallback EventType = "workspace_subscription_renewed_balance_fallback" + EventTypeWorkspaceSubscriptionRenewedFailed EventType = "workspace_subscription_renewed_failed" + EventTypeWorkspaceSubscriptionExpired EventType = "workspace_subscription_expired" + EventTypeWorkspaceSubscriptionExpiredDeleteResources EventType = "workspace_subscription_expired_delete_resources" + + // 流量相关事件 + EventTypeTrafficStatusChange EventType = "traffic_status_change" + EventTypeTrafficUsageAlert EventType = "traffic_usage_alert" + + // 债务预警事件 + // 债务到期 + EventTypeWorkspaceSubscriptionDebt EventType = "workspace_subscription_debt" + // 债务删除预警 + EventTypeWorkspaceSubscriptionDebtPreDeletion EventType = "workspace_subscription_debt_pre_deletion" + // 债务最终删除预警 + EventTypeWorkspaceSubscriptionDebtFinalDeletion EventType = "workspace_subscription_debt_final_deletion" + + // 自定义事件 + EventTypeCustom EventType = "custom" +) + +// NotificationEvent 通知事件结构(输入) +type NotificationEvent struct { + UserUID uuid.UUID `json:"user_uid"` + EventType EventType `json:"event_type"` + EventData map[string]any `json:"event_data"` + Recipient types.NotificationRecipient `json:"recipient,omitempty"` + Methods []NotificationMethod `json:"methods"` + Priority NotificationPriority `json:"priority,omitempty"` + Timestamp time.Time `json:"timestamp"` + // Whether to ignore users without contact information, the default is false to ignore + NotIgnoreIfNoContact bool `json:"ignore_if_no_contact,omitempty"` +} + +type EventData interface { + ToMap() map[string]any +} + +func (t *WorkspaceSubscriptionTrafficEventData) ToMap() map[string]any { + return map[string]any{ + "type": t.Type, + "status": t.Status, + "used_percentage": t.UsagePercent, + "total_bytes": t.TotalBytes, + "used_bytes": t.UsedBytes, + "workspace_name": t.Workspace, + "region_domain": t.RegionDomain, + "plan_name": t.PlanName, + "features": t.Features, + "expiration_date": t.ExpirationDate, + } +} + +func (t *WorkspaceSubscriptionTrafficEventData) GetType() EventType { + return t.Type +} + +func (t *DebtEventData) ToMap() map[string]any { + return map[string]any{ + "type": t.Type, + "last_status": t.LastStatus, + "current_status": t.CurrentStatus, + "debt_days": t.DebtDays, + } +} + +func (t *DebtEventData) GetType() EventType { + return t.Type +} + +func (t *WorkspaceSubscriptionDebtEventData) ToMap() map[string]any { + return map[string]any{ + "type": t.Type, + "last_status": t.LastStatus, + "current_status": t.CurrentStatus, + "debt_days": t.DebtDays, + "region_domain": t.RegionDomain, + "workspace_name": t.WorkspaceName, + "expiration_date": t.ExpirationDate, + "plan_name": t.PlanName, + } +} + +func (t *WorkspaceSubscriptionEventData) GetType() EventType { + return t.Type +} + +func (t *CustomEventData) GetType() EventType { + return t.Type +} + +func (t *WorkspaceSubscriptionEventData) ToMap() map[string]any { + return map[string]any{ + "type": t.Type, + "region_domain": t.RegionDomain, + "expiration_date": t.ExpirationDate, + "workspace_name": t.WorkspaceName, + "domain": t.Domain, + "operator": t.Operator, + "status": t.Status, + "pay_status": t.PayStatus, + "pay_method": t.PayMethod, + "days_remaining": t.DaysRemaining, + "old_plan_name": t.OldPlanName, + "new_plan_name": t.NewPlanName, + "amount": t.Amount, + "error_reason": t.ErrorReason, + "next_pay_date": t.NextPayDate, + "features": t.Features, + } +} + +func (t *CustomEventData) ToMap() map[string]any { + return map[string]any{ + "type": t.Type, + "title": t.Title, + "content": t.Content, + "extra_data": t.ExtraData, + } +} + +// NotificationMessage 通知消息结构(内部处理) +type NotificationMessage struct { + UserUID uuid.UUID `json:"user_uid"` + EventType EventType `json:"event_type"` + Method NotificationMethod `json:"method"` + Priority NotificationPriority `json:"priority"` + Title string `json:"title"` + Content string `json:"content"` + Recipient types.NotificationRecipient `json:"recipient"` + EventData map[string]any `json:"event_data"` + TemplateID string `json:"template_id,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +// NotificationResult 通知发送结果 +type NotificationResult struct { + UserUID uuid.UUID `json:"user_uid"` + EventType EventType `json:"event_type"` + Method NotificationMethod `json:"method"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` + ProviderResponse string `json:"provider_response,omitempty"` + SentAt time.Time `json:"sent_at"` +} + +// NotificationProvider 通知提供者接口(简化版) +type NotificationProvider interface { + Send(ctx context.Context, message *NotificationMessage) (*NotificationResult, error) + GetName() string + IsAvailable() bool + GetSupportedMethods() []NotificationMethod +} + +// UserContactProvider 用户联系方式提供者接口 +type UserContactProvider interface { + GetUserContact(ctx context.Context, userUID uuid.UUID) (*types.NotificationRecipient, error) + SetUserContact(userUID uuid.UUID, recipient *types.NotificationRecipient) + RemoveUserContact(userUID uuid.UUID) +} + +// NotificationContentGenerator 通知内容生成器接口 +type NotificationContentGenerator interface { + GenerateContent( + event *NotificationEvent, + method NotificationMethod, + ) (title, content, templateID string, err error) +} + +// ProviderConfig 通知提供者配置 +type ProviderConfig struct { + // VMS配置 + VMSAPIKey string `json:"vms_api_key"` + VMSAPISecret string `json:"vms_api_secret"` + VMSEndpoint string `json:"vms_endpoint"` + VMSTemplates map[EventType]string `json:"vms_templates"` // 按事件类型配置模板ID + VMSNumberPool string `json:"vms_number_pool"` + VMSDefaultTemplate string `json:"vms_default_template"` // 默认模板ID + + // Email配置 + SMTPHost string `json:"smtp_host"` + SMTPPort int `json:"smtp_port"` + SMTPUsername string `json:"smtp_username"` + SMTPPassword string `json:"smtp_password"` + FromEmail string `json:"from_email"` + FromName string `json:"from_name"` + EmailTemplates map[EventType]string `json:"email_templates"` // 按事件类型配置邮件模板 + EmailDefaultTemplate string `json:"email_default_template"` // 默认邮件模板 + + // SMS配置 + SMSAccessKeyID string `json:"sms_access_key_id"` + SMSAccessKeySecret string `json:"sms_access_key_secret"` + SMSEndpoint string `json:"sms_endpoint"` + SMSSignName string `json:"sms_sign_name"` + SMSTemplates map[EventType]string `json:"sms_templates"` // 按事件类型配置短信模板 + SMSDefaultTemplate string `json:"sms_default_template"` // 默认短信模板 + + // 通用配置 + IsEnabled bool `json:"is_enabled"` + MaxRetries int `json:"max_retries"` + RetryDelay time.Duration `json:"retry_delay"` + Timeout time.Duration `json:"timeout"` + ExtraConfig map[string]any `json:"extra_config,omitempty"` +} + +func ParseConfigsWithJSON(cfgStr string) (map[NotificationMethod]ProviderConfig, error) { + var configs map[NotificationMethod]ProviderConfig + err := json.Unmarshal([]byte(cfgStr), &configs) + if err != nil { + return nil, fmt.Errorf("failed to parse config json: %w", err) + } + return configs, nil +} + +// GetVMSTemplateID 根据事件类型获取对应的模板ID +func (c *ProviderConfig) GetVMSTemplateID(eventType EventType) string { + if c.VMSTemplates != nil { + if templateID, exists := c.VMSTemplates[eventType]; exists && templateID != "" { + return templateID + } + } + return c.VMSDefaultTemplate +} + +// GetSMSTemplateCode 根据事件类型获取对应的短信模板代码 +func (c *ProviderConfig) GetSMSTemplateCode(eventType EventType) string { + if c.SMSTemplates != nil { + if templateCode, exists := c.SMSTemplates[eventType]; exists && templateCode != "" { + return templateCode + } + } + return c.SMSDefaultTemplate +} + +func (c *ProviderConfig) GenerateEmailContent(event *NotificationEvent) (string, error) { + emailData, err := generateEmailContent(event) + if err != nil { + return "", err + } + if emailData.PlanDetails != nil { + fmt.Printf("emailData: %+v\n", emailData.PlanDetails.Features) + } + // 构建HTML内容 + tmpl, err := template.New("email"). + Parse(c.GetEmailTemplate(EventTypeUniversalWorkspaceSubscriptionEvent)) + if err != nil { + return "", fmt.Errorf("failed to parse email template: %w", err) + } + var renderedContent strings.Builder + if err := tmpl.Execute(&renderedContent, emailData); err != nil { + return "", fmt.Errorf("failed to render email template: %w", err) + } + return renderedContent.String(), nil +} + +// GetEmailTemplate 根据事件类型获取对应的邮件模板 +func (c *ProviderConfig) GetEmailTemplate(eventType EventType) string { + if c.EmailTemplates != nil { + if template, exists := c.EmailTemplates[eventType]; exists && template != "" { + return template + } + } + return WorkspaceSubscriptionEventEmailRenderTmpl +} + +// DebtEventData 债务事件数据 +type DebtEventData struct { + Type EventType `json:"-"` + LastStatus types.DebtStatusType `json:"last_status"` + CurrentStatus types.DebtStatusType `json:"current_status"` + DebtDays int `json:"debt_days,omitempty"` +} + +type WorkspaceSubscriptionDebtEventData struct { + Type EventType `json:"-"` + RegionDomain string `json:"region_domain"` + WorkspaceName string `json:"workspace_name"` + PlanName string `json:"plan_name"` + ExpirationDate string `json:"expiration_date"` + LastStatus types.SubscriptionStatus `json:"last_status"` + CurrentStatus types.SubscriptionStatus `json:"current_status"` + DebtDays int `json:"debt_days,omitempty"` +} + +// WorkspaceSubscriptionEventData 订阅事件数据 +type WorkspaceSubscriptionEventData struct { + Type EventType `json:"type"` + RegionDomain string `json:"region_domain"` + ExpirationDate string `json:"expiration_date"` + WorkspaceName string `json:"workspace_name"` + Domain string `json:"domain,omitempty"` + Operator types.SubscriptionOperator `json:"operator,omitempty"` + Status types.SubscriptionStatus `json:"status,omitempty"` + PayStatus types.SubscriptionPayStatus `json:"pay_status,omitempty"` + PayMethod types.PaymentMethod `json:"pay_method,omitempty"` + Features []string `json:"features,omitempty"` + DaysRemaining int `json:"days_remaining,omitempty"` + OldPlanName string `json:"old_plan_name,omitempty"` + NewPlanName string `json:"new_plan_name,omitempty"` + Amount float64 `json:"amount,omitempty"` + NextPayDate string `json:"next_pay_date,omitempty"` + ErrorReason string `json:"error_reason,omitempty"` +} + +// WorkspaceSubscriptionTrafficEventData 流量事件数据 +type WorkspaceSubscriptionTrafficEventData struct { + RegionDomain string `json:"region_domain"` + PlanName string `json:"plan_name,omitempty"` + Type EventType `json:"-"` + Status types.WorkspaceTrafficStatus `json:"status,omitempty"` + ExpirationDate string `json:"expiration_date"` + UsagePercent int `json:"used_percentage,omitempty"` + TotalBytes int64 `json:"total_bytes,omitempty"` + UsedBytes int64 `json:"used_bytes,omitempty"` + Workspace string `json:"workspace_name,omitempty"` + Features []string `json:"features,omitempty"` +} + +// CustomEventData 自定义事件数据 +type CustomEventData struct { + Type EventType `json:"-"` + Title string `json:"title"` + Content string `json:"content"` + ExtraData map[string]any `json:"extra_data,omitempty"` +} diff --git a/controllers/pkg/utils/email.go b/controllers/pkg/utils/email.go new file mode 100644 index 000000000000..c9712226e39e --- /dev/null +++ b/controllers/pkg/utils/email.go @@ -0,0 +1,456 @@ +package utils + +import ( + "strconv" + "time" + + "github.com/go-gomail/gomail" + "github.com/labring/sealos/controllers/pkg/types" +) + +type SMTPConfig struct { + ServerHost string + ServerPort int + Username string + FromEmail string + Passwd string + EmailTitle string +} + +func (c *SMTPConfig) SendEmail(emailBody, to string) error { + m := gomail.NewMessage() + m.SetHeader("To", to) + m.SetAddressHeader("From", c.FromEmail, c.EmailTitle) + m.SetHeader("Subject", c.EmailTitle) + m.SetBody("text/html", emailBody) + d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd) + return d.DialAndSend(m) +} + +func (c *SMTPConfig) SendEmailWithSubject(subject, emailBody, to string) error { + m := gomail.NewMessage() + m.SetHeader("To", to) + m.SetAddressHeader("From", c.FromEmail, c.EmailTitle) + m.SetHeader("Subject", subject) + m.SetBody("text/html", emailBody) + d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.Username, c.Passwd) + return d.DialAndSend(m) +} + +const ( + EnvSMTPHost = "SMTP_HOST" + EnvSMTPPort = "SMTP_PORT" + EnvSMTPFrom = "SMTP_FROM" + EnvSMTPUser = "SMTP_USER" + EnvSMTPPassword = "SMTP_PASSWORD" + EnvSMTPTitle = "SMTP_TITLE" + + EnvPaySuccessEmailTmpl = "PAY_SUCCESS_EMAIL_TMPL" + EnvPayFailedEmailTmpl = "PAY_FAILED_EMAIL_TMPL" + EnvSubSuccessEmailTmpl = "SUB_SUCCESS_EMAIL_TMPL" + EnvSubFailedEmailTmpl = "SUB_FAILED_EMAIL_TMPL" +) + +type EmailRenderBuilder interface { + Build() map[string]any + GetType() string + SetUserInfo(userInfo *types.UserInfo) + GetSubject() string +} + +type EmailPayRender struct { + Type string + userInfo *types.UserInfo + Domain string + TopUpAmount int64 + AccountBalance int64 +} + +func (e *EmailPayRender) Build() map[string]any { + return map[string]any{ + "FirstName": e.userInfo.FirstName, + "LastName": e.userInfo.LastName, + "Domain": e.Domain, + "TopUpAmount": strconv.FormatInt(e.TopUpAmount, 10), + "AccountBalance": strconv.FormatInt(e.AccountBalance, 10), + } +} + +func (e *EmailPayRender) GetType() string { + return e.Type +} + +func (e *EmailPayRender) GetSubject() string { + return "Top-Up Successful" +} + +func (e *EmailPayRender) SetUserInfo(userInfo *types.UserInfo) { + e.userInfo = userInfo +} + +func (e *EmailSubRender) Build() map[string]any { + build := map[string]any{ + "FirstName": e.userInfo.FirstName, + "LastName": e.userInfo.LastName, + "Domain": e.Domain, + "SubscriptionPlanName": e.SubscriptionPlanName, + "StartDate": e.StartDate.Format(time.DateOnly), + "EndDate": e.EndDate.Format(time.DateOnly), + } + switch e.SubscriptionPlanName { + case "Hobby": + build["SubscriptionFeatures"] = []string{ + "Includes $5 credits", + "16 vCPU / 32GiB RAM", + "Unlimited disk & traffic within plan", + "Multiple regions", + "3 workspaces / region", + "5 seats / workspace", + } + case "Pro": + build["SubscriptionFeatures"] = []string{ + "Includes $20 credits", + "128 vCPU / 256GiB RAM", + "Unlimited disk & traffic within plan", + "Multiple regions", + "Multiple workspace / region", + "Multiple seat / workspace", + } + } + return build +} + +type EmailSubRender struct { + Type string + Operator types.SubscriptionOperator + + userInfo types.UserInfo + Domain string + + SubscriptionPlanName string + StartDate time.Time + EndDate time.Time +} + +func (e *EmailSubRender) GetType() string { + return e.Type +} + +func (e *EmailSubRender) SetUserInfo(userInfo *types.UserInfo) { + e.userInfo = *userInfo +} + +func (e *EmailSubRender) GetSubject() string { + switch e.Operator { + case types.SubscriptionTransactionTypeUpgraded: + return "Your Subscription Has Been Successfully Updated" + case types.SubscriptionTransactionTypeDowngraded: + return "Your Subscription Has Been Successfully Downgraded" + case types.SubscriptionTransactionTypeCanceled: + return "Your Subscription Has Been Successfully Canceled" + case types.SubscriptionTransactionTypeRenewed: + return "Your Subscription Has Been Successfully Renewed" + default: + return "Your Subscription Has Been Successfully Activated" + } +} + +type EmailDebtRender struct { + Type string + CurrentStatus types.DebtStatusType + + userName string + Domain string + Language string // 语言: "zh" 或 "en" + GraceReason []string + Balance int64 // 当前余额 +} + +type DebtGraceReason string + +const ( + GraceReasonNoBalance DebtGraceReason = "insufficient balance" + GraceReasonSubExpired DebtGraceReason = "subscription expired" +) + +func (e *EmailDebtRender) GetType() string { + return e.Type +} + +func (e *EmailDebtRender) SetUserName(userName string) { + e.userName = userName +} + +func (e *EmailDebtRender) SetLanguage(language string) { + e.Language = language +} + +func (e *EmailDebtRender) SetBalance(balance int64) { + e.Balance = balance +} + +func (e *EmailDebtRender) GetSubject() string { + if e.Language == "zh" { + return e.getSubjectZH() + } + return e.getSubjectEN() +} + +func (e *EmailDebtRender) getSubjectEN() string { + if types.ContainDebtStatus(types.DebtStates, e.CurrentStatus) { + if e.CurrentStatus == types.FinalDeletionPeriod { + return "Important: Your Resources Have Been Deleted" + } + return "Important: Your Account Has Entered Grace Period" + } + return "Low Account Balance Reminder" +} + +func (e *EmailDebtRender) getSubjectZH() string { + if types.ContainDebtStatus(types.DebtStates, e.CurrentStatus) { + if e.CurrentStatus == types.FinalDeletionPeriod { + return "重要提醒:您的资源已被删除" + } + return "重要提醒:您的账户已进入宽限期" + } + return "账户余额不足提醒" +} + +func (e *EmailDebtRender) Build() map[string]any { + balanceInUnits := float64(e.Balance) / 1000000.0 + + build := map[string]any{ + "Type": e.Type, + "FirstName": e.userName, // 使用 userName 作为 FirstName + "LastName": "", // 留空 + "Domain": e.Domain, + "GraceReason": e.GraceReason, + "Language": e.Language, + "IsCNY": e.Language == "zh", + "Balance": e.Balance, // 原始值(兼容旧代码) + "BalanceInUnits": balanceInUnits, // 格式化后的值 + } + if e.Type == "CriticalBalancePeriod" { + build["CreditsAvailable"] = "1" + } + if e.Type == "LowBalancePeriod" { + build["CreditsAvailable"] = "5" + } + return build +} + +// DebtEmailTemplateEN 标准英文债务邮件模板 +const DebtEmailTemplateEN = ` + + + + + Sealos - Account Balance Notification + + + + + + +
+ + + + +
+ + + + +
+
+ Sealos Logo +
+ Sealos +
+

+ Application-Centric Intelligent Cloud Operating System +

+
+ + + + + +
+

+ {{if .FirstName}}Hi {{.FirstName}},{{else}}Hi,{{end}} +

+ + {{if eq .Type "LowBalancePeriod"}} +

+ Your account balance is running low. You have ${{printf "%.2f" .BalanceInUnits}} USD in credits remaining. +

+

+ To avoid any service disruption, please consider topping up your account soon. Your resources will continue to function normally, but we recommend maintaining a sufficient balance. +

+ {{end}} + + {{if eq .Type "CriticalBalancePeriod"}} +

+ ⚠️ Critical: Your account balance is critically low.
+ You have ${{printf "%.2f" .BalanceInUnits}} USD in credits remaining. +

+

+ Please top up your account as soon as possible to prevent service suspension. Your resources may be affected if the balance reaches zero. +

+ {{end}} + + {{if or (eq .Type "DebtPeriod") (eq .Type "DebtDeletionPeriod") (eq .Type "FinalDeletionPeriod")}} +

+ ⚠️ Important: Your account has entered the grace period.
+ {{range .GraceReason}} + - {{.}}
+ {{end}} +

+ {{end}} + + {{if eq .Type "DebtPeriod"}} +

+ Your services have been suspended. To restore your account and prevent data loss, please top up your account immediately. You have 7 days from today before your resources will be permanently deleted. +

+ {{end}} + + {{if eq .Type "DebtDeletionPeriod"}} +

+ This is a final reminder. Your resources will be permanently deleted within 7 days. Please take immediate action to recover your account and prevent permanent data loss. +

+ {{end}} + + {{if eq .Type "FinalDeletionPeriod"}} +

+ Your resources have been permanently deleted due to prolonged account inactivity and insufficient balance. We're sorry to see you go. If you wish to use Sealos services again in the future, you can start a new subscription at any time. +

+ {{end}} + + + + +
+
+ +` + +// DebtEmailTemplateZH 标准中文债务邮件模板 +const DebtEmailTemplateZH = ` + + + + + Sealos - 账户余额通知 + + + + + + +
+ + + + +
+ + + + +
+
+ Sealos Logo +
+ Sealos +
+

+ 以应用为中心的云操作系统 +

+
+ + + + + +
+

+ {{if .FirstName}}您好 {{.FirstName}},{{else}}您好,{{end}} +

+ + {{if eq .Type "LowBalancePeriod"}} +

+ 您的账户余额不足。您当前的余额为 {{printf "%.2f" .BalanceInUnits}} 元。 +

+

+ 为避免影响您的正常使用,请及时充值。您的资源目前可以正常使用,但我们建议保持充足的余额以确保服务稳定。 +

+ {{end}} + + {{if eq .Type "CriticalBalancePeriod"}} +

+ ⚠️ 紧急提醒:您的账户余额即将耗尽。
+ 您当前的余额为 {{printf "%.2f" .BalanceInUnits}} 元。 +

+

+ 请尽快充值以防止服务暂停。如果余额归零,您的资源可能会受到影响。 +

+ {{end}} + + {{if or (eq .Type "DebtPeriod") (eq .Type "DebtDeletionPeriod") (eq .Type "FinalDeletionPeriod")}} +

+ ⚠️ 重要提醒:您的账户已进入宽限期。
+ {{range .GraceReason}} + - {{.}}
+ {{end}} +

+ {{end}} + + {{if eq .Type "DebtPeriod"}} +

+ 您的服务已被暂停。要恢复账户并防止数据丢失,请立即充值。从今天起,您有 7 天 的时间来恢复账户,之后您的资源将被永久删除。 +

+ {{end}} + + {{if eq .Type "DebtDeletionPeriod"}} +

+ 这是最后提醒。您的资源将在 7 天内 被永久删除。请立即采取措施恢复您的账户,防止永久性数据丢失。 +

+ {{end}} + + {{if eq .Type "FinalDeletionPeriod"}} +

+ 由于账户长期处于非活跃状态且余额不足,您的资源已被永久删除。很遗憾未能继续为您服务。如果您将来希望再次使用 Sealos 服务,可以随时开始新的订阅。 +

+ {{end}} + + + + +
+
+ +` + +// GetDebtEmailTemplate 根据语言返回对应的邮件模板 +func GetDebtEmailTemplate(language string) string { + if language == "zh" { + return DebtEmailTemplateZH + } + return DebtEmailTemplateEN +} diff --git a/controllers/pkg/utils/env/env.go b/controllers/pkg/utils/env/env.go new file mode 100644 index 000000000000..e7a450030951 --- /dev/null +++ b/controllers/pkg/utils/env/env.go @@ -0,0 +1,69 @@ +// Copyright © 2023 sealos. +// +// 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 env + +import ( + "fmt" + "os" + "strconv" + "time" +) + +func GetEnvWithDefault(key, defaultValue string) string { + if value, ok := os.LookupEnv(key); ok && value != "" { + return value + } + return defaultValue +} + +func GetBoolWithDefault(key string, defaultValue bool) bool { + if env, ok := os.LookupEnv(key); ok && env != "" { + if value, err := strconv.ParseBool(env); err == nil { + return value + } + } + return defaultValue +} + +func GetInt64EnvWithDefault(key string, defaultValue int64) int64 { + if env, ok := os.LookupEnv(key); ok && env != "" { + if value, err := strconv.ParseInt(env, 10, 64); err == nil { + return value + } + } + return defaultValue +} + +func GetIntEnvWithDefault(key string, defaultValue int) int { + return int(GetInt64EnvWithDefault(key, int64(defaultValue))) +} + +func GetDurationEnvWithDefault(key string, defaultValue time.Duration) time.Duration { + if env, ok := os.LookupEnv(key); ok && env != "" { + if value, err := time.ParseDuration(env); err == nil { + return value + } + } + return defaultValue +} + +func CheckEnvSetting(keys []string) error { + for _, key := range keys { + if val, ok := os.LookupEnv(key); !ok || val == "" { + return fmt.Errorf("env %s not set", key) + } + } + return nil +} diff --git a/controllers/pkg/utils/flags/compression.go b/controllers/pkg/utils/flags/compression.go new file mode 100644 index 000000000000..28be3aa83ba6 --- /dev/null +++ b/controllers/pkg/utils/flags/compression.go @@ -0,0 +1,73 @@ +// Copyright © 2023 sealos. +// +// 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 flags + +import ( + "fmt" + "strings" + + "github.com/containers/storage/pkg/archive" +) + +type Compression int + +const ( + Disable Compression = iota + Uncompressed + Bzip2 + Gzip + Xz + Zstd +) + +func (c *Compression) String() string { + switch *c { + case Disable: + return "disable" + case Uncompressed: + return "tar" + case Bzip2: + return "bzip2" + case Gzip: + return "gzip" + case Xz: + return "xz" + case Zstd: + return "zstd" + } + return "unknown" +} + +func (c *Compression) Compression() archive.Compression { + return archive.Compression(int(*c) - 1) +} + +func (c *Compression) Set(s string) error { + switch strings.ToLower(s) { + case "gz", "gzip": + *c = Gzip + case "zst", "zstd": + *c = Zstd + case "tar", "uncompressed": + *c = Uncompressed + case "", "disable": + *c = Disable + default: + return fmt.Errorf("unknown compression algorithm %s", s) + } + return nil +} + +func (c *Compression) Type() string { return "compressionAlgorithm" } diff --git a/controllers/pkg/utils/flags/flags.go b/controllers/pkg/utils/flags/flags.go new file mode 100644 index 000000000000..79816f46f32d --- /dev/null +++ b/controllers/pkg/utils/flags/flags.go @@ -0,0 +1,48 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 flags + +import ( + "os" + "strings" + + "github.com/labring/sealos/controllers/pkg/utils/logger" + "github.com/spf13/pflag" +) + +// PrintFlags logs the flags in the flagset +func PrintFlags(flags *pflag.FlagSet) { + flags.VisitAll(func(flag *pflag.Flag) { + logger.Debug("FLAG: --%s=%q", flag.Name, flag.Value) + }) +} + +// SetFlagsFromEnv set value of flag if not changed but has env key +func SetFlagsFromEnv(prefix string, flags *pflag.FlagSet) { + if prefix != "" && !strings.HasSuffix(prefix, "_") { + prefix += "_" + } + flags.VisitAll(func(flag *pflag.Flag) { + if flag.Changed { + return + } + envVar := strings.ToUpper(strings.ReplaceAll(prefix+flag.Name, "-", "_")) + if v := os.Getenv(envVar); v != "" { + _ = flags.Set(flag.Name, v) + } + }) +} diff --git a/controllers/pkg/utils/jwt.go b/controllers/pkg/utils/jwt.go new file mode 100644 index 000000000000..865991020862 --- /dev/null +++ b/controllers/pkg/utils/jwt.go @@ -0,0 +1,147 @@ +package utils + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" +) + +type JWTManager struct { + secretKey []byte + tokenDuration time.Duration +} + +type UserClaims struct { + jwt.StandardClaims `json:",inline"` + JwtUser `json:",inline"` +} + +type JwtUser struct { + Requester string `json:"requester,omitempty"` + UserUID uuid.UUID `json:"userUid,omitempty"` + UserCrUID string `json:"userCrUid,omitempty"` + UserCrName string `json:"userCrName,omitempty"` + RegionUID string `json:"regionUid,omitempty"` + UserID string `json:"userId,omitempty"` + WorkspaceID string `json:"workspaceId,omitempty"` + WorkspaceUID string `json:"workspaceUid,omitempty"` +} + +const ( + AdminJWTRequester = "sealos-admin" + AdminJWTIssuer = "sealos-admin" + AdminJWTAudience = "sealos-account-admin" +) + +func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager { + return &JWTManager{[]byte(secretKey), tokenDuration} +} + +func (manager *JWTManager) GenerateToken(user JwtUser) (string, error) { + claims := UserClaims{ + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(manager.tokenDuration).Unix(), + }, + JwtUser: user, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(manager.secretKey) +} + +func (manager *JWTManager) VerifyToken(tokenString string) (*UserClaims, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &UserClaims{}, + func(token *jwt.Token) (any, error) { + _, ok := token.Method.(*jwt.SigningMethodHMAC) + if !ok { + return nil, errors.New("unexpected token signing method") + } + + return manager.secretKey, nil + }, + ) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + claims, ok := token.Claims.(*UserClaims) + if !ok { + return nil, errors.New("invalid token claims") + } + + return claims, nil +} + +func (manager *JWTManager) ParseUser(token string) (*JwtUser, error) { + claims, err := manager.VerifyToken(token) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + return &claims.JwtUser, nil +} + +func (manager *JWTManager) GenerateAdminToken(user JwtUser) (string, error) { + now := time.Now() + claims := UserClaims{ + StandardClaims: jwt.StandardClaims{ + ExpiresAt: now.Add(manager.tokenDuration).Unix(), + IssuedAt: now.Unix(), + Issuer: AdminJWTIssuer, + Audience: AdminJWTAudience, + }, + JwtUser: user, + } + claims.JwtUser.Requester = AdminJWTRequester + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(manager.secretKey) +} + +func (manager *JWTManager) VerifyAdminToken(tokenString string) (*UserClaims, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &UserClaims{}, + func(token *jwt.Token) (any, error) { + if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() { + return nil, errors.New("unexpected token signing method") + } + + return manager.secretKey, nil + }, + ) + if err != nil { + return nil, fmt.Errorf("invalid admin token: %w", err) + } + + claims, ok := token.Claims.(*UserClaims) + if !ok { + return nil, errors.New("invalid admin token claims") + } + if !claims.VerifyExpiresAt(time.Now().Unix(), true) { + return nil, errors.New("admin token expiration is required") + } + if !claims.VerifyIssuer(AdminJWTIssuer, true) { + return nil, errors.New("invalid admin token issuer") + } + if !claims.VerifyAudience(AdminJWTAudience, true) { + return nil, errors.New("invalid admin token audience") + } + if claims.JwtUser.Requester != AdminJWTRequester { + return nil, errors.New("user is not admin") + } + + return claims, nil +} + +func (manager *JWTManager) ParseAdminUser(token string) (*JwtUser, error) { + claims, err := manager.VerifyAdminToken(token) + if err != nil { + return nil, err + } + return &claims.JwtUser, nil +} diff --git a/controllers/pkg/utils/jwt_test.go b/controllers/pkg/utils/jwt_test.go new file mode 100644 index 000000000000..eb35c30b0bf3 --- /dev/null +++ b/controllers/pkg/utils/jwt_test.go @@ -0,0 +1,111 @@ +package utils + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt" +) + +func TestAdminTokenClaims(t *testing.T) { + manager := NewJWTManager("admin-secret", time.Hour) + token, err := manager.GenerateAdminToken(JwtUser{Requester: "ordinary-user"}) + if err != nil { + t.Fatalf("GenerateAdminToken() error = %v", err) + } + + user, err := manager.ParseAdminUser(token) + if err != nil { + t.Fatalf("ParseAdminUser() error = %v", err) + } + if user.Requester != AdminJWTRequester { + t.Fatalf("ParseAdminUser() requester = %q, want %q", user.Requester, AdminJWTRequester) + } +} + +func TestVerifyAdminTokenRejectsInvalidClaims(t *testing.T) { + manager := NewJWTManager("admin-secret", time.Hour) + now := time.Now().Unix() + + tests := []struct { + name string + method jwt.SigningMethod + claims jwt.StandardClaims + secret string + }{ + { + name: "ordinary claims", + claims: jwt.StandardClaims{ + ExpiresAt: now + 3600, + }, + secret: "admin-secret", + }, + { + name: "wrong issuer", + claims: jwt.StandardClaims{ + ExpiresAt: now + 3600, + Issuer: "account-api", + Audience: AdminJWTAudience, + }, + secret: "admin-secret", + }, + { + name: "wrong audience", + claims: jwt.StandardClaims{ + ExpiresAt: now + 3600, + Issuer: AdminJWTIssuer, + Audience: "account-api", + }, + secret: "admin-secret", + }, + { + name: "expired", + claims: jwt.StandardClaims{ + ExpiresAt: now - 1, + Issuer: AdminJWTIssuer, + Audience: AdminJWTAudience, + }, + secret: "admin-secret", + }, + { + name: "wrong secret", + claims: jwt.StandardClaims{ + ExpiresAt: now + 3600, + Issuer: AdminJWTIssuer, + Audience: AdminJWTAudience, + }, + secret: "api-secret", + }, + { + name: "wrong algorithm", + method: jwt.SigningMethodHS512, + claims: jwt.StandardClaims{ + ExpiresAt: now + 3600, + Issuer: AdminJWTIssuer, + Audience: AdminJWTAudience, + }, + secret: "admin-secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + method := tt.method + if method == nil { + method = jwt.SigningMethodHS256 + } + claims := UserClaims{ + StandardClaims: tt.claims, + JwtUser: JwtUser{Requester: AdminJWTRequester}, + } + token, err := jwt.NewWithClaims(method, claims).SignedString([]byte(tt.secret)) + if err != nil { + t.Fatalf("SignedString() error = %v", err) + } + + if _, err = manager.VerifyAdminToken(token); err == nil { + t.Fatal("VerifyAdminToken() error = nil, want error") + } + }) + } +} diff --git a/controllers/pkg/utils/label/constant.go b/controllers/pkg/utils/label/constant.go new file mode 100644 index 000000000000..172e9a4aa553 --- /dev/null +++ b/controllers/pkg/utils/label/constant.go @@ -0,0 +1,31 @@ +// Copyright © 2024 sealos. +// +// 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 label + +type AppKey = string + +// @see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels +const ( + AppName AppKey = "app.kubernetes.io/name" + AppInstance AppKey = "app.kubernetes.io/instance" + AppVersion AppKey = "app.kubernetes.io/version" + AppComponent AppKey = "app.kubernetes.io/component" + AppPartOf AppKey = "app.kubernetes.io/part-of" + AppManagedBy AppKey = "app.kubernetes.io/managed-by" +) + +const ( + DefaultManagedBy = "sealos" +) diff --git a/controllers/pkg/utils/label/recommend.go b/controllers/pkg/utils/label/recommend.go new file mode 100644 index 000000000000..e4291c18732c --- /dev/null +++ b/controllers/pkg/utils/label/recommend.go @@ -0,0 +1,53 @@ +// Copyright © 2024 sealos. +// +// 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 label + +type Recommended struct { + Name string + Instance string + Version string + Component string + PartOf string + ManagedBy string +} + +func (r *Recommended) Labels() map[string]string { + ret := map[string]string{} + + if r.Name != "" { + ret[AppName] = r.Name + } + if r.Instance != "" { + ret[AppInstance] = r.Instance + } + if r.Version != "" { + ret[AppVersion] = r.Version + } + if r.Component != "" { + ret[AppComponent] = r.Component + } + if r.PartOf != "" { + ret[AppPartOf] = r.PartOf + } + if r.ManagedBy != "" { + ret[AppManagedBy] = r.ManagedBy + } + + return ret +} + +func RecommendedLabels(r *Recommended) map[string]string { + return r.Labels() +} diff --git a/controllers/pkg/utils/lock/descributed_lock.go b/controllers/pkg/utils/lock/descributed_lock.go new file mode 100644 index 000000000000..3967011226bc --- /dev/null +++ b/controllers/pkg/utils/lock/descributed_lock.go @@ -0,0 +1,180 @@ +package dlock + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "gorm.io/gorm" +) + +var ( + ErrLockNotAcquired = errors.New("lock not acquired") + ErrLockNotHeld = errors.New("lock not held by this instance") +) + +type DistributedLock struct { + db *gorm.DB + lockName string + holderID string + stopRenew chan struct{} + once sync.Once +} + +func NewDistributedLock(db *gorm.DB, lockName, holderID string) *DistributedLock { + return &DistributedLock{ + db: db, + lockName: lockName, + holderID: holderID, + stopRenew: make(chan struct{}), + once: sync.Once{}, + } +} + +// TryLock acquisition method +func (dl *DistributedLock) TryLock(ctx context.Context, ttl time.Duration) error { + expiresAt := time.Now().UTC().Add(ttl) + + err := dl.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Exec(` + CREATE TABLE IF NOT EXISTS distributed_locks ( + lock_name TEXT PRIMARY KEY, + holder_id TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + version INT NOT NULL DEFAULT 1 + ) + `).Error + if err != nil { + return fmt.Errorf("failed to create table: %w", err) + } + + // try to get the current lock state first + var currentLock struct { + HolderID string + ExpiresAt time.Time + } + + err = tx.Raw(` + SELECT holder_id, expires_at + FROM distributed_locks + WHERE lock_name = ? FOR UPDATE + `, dl.lockName).Scan(¤tLock).Error + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + // determines whether the lock can be obtained + if currentLock.HolderID != "" && + currentLock.ExpiresAt.After(time.Now().UTC()) && + currentLock.HolderID != dl.holderID { + return ErrLockNotAcquired + } + + // use upsert atomic operations + result := tx.Exec(` + INSERT INTO distributed_locks (lock_name, holder_id, expires_at, version) + VALUES (?, ?, ?, 1) + ON CONFLICT (lock_name) DO UPDATE + SET + holder_id = excluded.holder_id, + expires_at = excluded.expires_at, + version = distributed_locks.version + 1 + WHERE distributed_locks.expires_at <= now() OR distributed_locks.holder_id = excluded.holder_id + `, dl.lockName, dl.holderID, expiresAt) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return ErrLockNotAcquired + } + + return nil + }) + if err != nil { + return err + } + + go dl.renewLock(ttl) + return nil +} + +func (dl *DistributedLock) renewLock(ttl time.Duration) { + ticker := time.NewTicker(ttl / 2) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + expiresAt := time.Now().UTC().Add(ttl) + + err := dl.db.Transaction(func(tx *gorm.DB) error { + result := tx.Exec(` + UPDATE distributed_locks + SET expires_at = ?, version = version + 1 + WHERE lock_name = ? AND holder_id = ? + `, expiresAt, dl.lockName, dl.holderID) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return ErrLockNotHeld + } + + return nil + }) + if err != nil { + // Failed to renew the lock. The lock may have been acquired by another instance + close(dl.stopRenew) + return + } + + case <-dl.stopRenew: + return + } + } +} + +func (dl *DistributedLock) Unlock() error { + dl.once.Do(func() { + close(dl.stopRenew) + }) + + return dl.db.Transaction(func(tx *gorm.DB) error { + result := tx.Exec(` + DELETE FROM distributed_locks + WHERE lock_name = ? AND holder_id = ? + `, dl.lockName, dl.holderID) + + if result.Error != nil { + return result.Error + } + // + // if result.RowsAffected == 0 { + // return ErrLockNotHeld + //} + + return nil + }) +} + +func (dl *DistributedLock) IsHeld(ctx context.Context) (bool, error) { + var count int64 + err := dl.db.WithContext(ctx).Model(&struct { + LockName string `gorm:"column:lock_name"` + }{}). + Table("distributed_locks"). + Where("lock_name = ? AND holder_id = ? AND expires_at > now()", dl.lockName, dl.holderID). + Count(&count).Error + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/controllers/pkg/utils/lock/descributed_lock_test.go b/controllers/pkg/utils/lock/descributed_lock_test.go new file mode 100644 index 000000000000..2a0c2f07672e --- /dev/null +++ b/controllers/pkg/utils/lock/descributed_lock_test.go @@ -0,0 +1,330 @@ +package dlock + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// setupTestDB 创建测试数据库连接 +func setupTestDB(t *testing.T) *gorm.DB { + t.Helper() + // TODO need to set up a real test database + dsn := os.Getenv("TEST_DB_URI") + if dsn == "" { + t.Skip("requires TEST_DB_URI") + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + require.NoError(t, err) + + // 确保表存在 + err = db.Exec(` + CREATE TABLE IF NOT EXISTS distributed_locks ( + lock_name TEXT PRIMARY KEY, + holder_id TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + version INT NOT NULL DEFAULT 1 + ) + `).Error + require.NoError(t, err) + + // 清空测试数据 + err = db.Exec("DELETE FROM distributed_locks").Error + require.NoError(t, err) + + return db +} + +func checkAssert(t *testing.T, ok bool) { + t.Helper() + if !ok { + t.Fatalf( + "[%s] Test failed: %v", + time.Now().UTC().Format(time.RFC3339), + errors.New("test failed"), + ) + } +} + +const ( + instanceID = "instance-1" +) + +func TestLockAcquireAndRelease(t *testing.T) { + db := setupTestDB(t) + lockName := "test-lock" + holderID := instanceID + + lock := NewDistributedLock(db, lockName, holderID) + + // 测试获取锁 + err := lock.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 验证锁确实被持有 + held, err := lock.IsHeld(context.Background()) + checkAssert(t, assert.NoError(t, err)) + checkAssert(t, assert.True(t, held)) + + // 测试释放锁 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) + + // 验证锁已释放 + held, err = lock.IsHeld(context.Background()) + checkAssert(t, assert.NoError(t, err)) + checkAssert(t, assert.False(t, held)) +} + +func TestLockMutualExclusion(t *testing.T) { + db := setupTestDB(t) + lockName := "mutex-lock" + + // 第一个实例获取锁 + lock1 := NewDistributedLock(db, lockName, instanceID) + err := lock1.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 第二个实例尝试获取相同的锁 + lock2 := NewDistributedLock(db, lockName, "instance-2") + err = lock2.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.Error(t, err)) + checkAssert(t, assert.True(t, errors.Is(err, ErrLockNotAcquired))) + + // 第一个实例释放锁 + err = lock1.Unlock() + checkAssert(t, assert.NoError(t, err)) + + // 现在第二个实例应该能获取锁 + err = lock2.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 清理 + err = lock2.Unlock() + checkAssert(t, assert.NoError(t, err)) +} + +func TestConcurrentLockAcquisition(t *testing.T) { + db := setupTestDB(t) + lockName := "concurrent-lock" + numClients := 10 + var wg sync.WaitGroup + successCh := make(chan bool, numClients) + + barrier := make(chan struct{}) // 添加并发屏障 + + for i := range numClients { + wg.Add(1) + go func(instanceID int) { + defer wg.Done() + holderID := fmt.Sprintf("instance-%d", instanceID) + lock := NewDistributedLock(db, lockName, holderID) + + <-barrier // 等待所有goroutine就绪 + + err := lock.TryLock(context.Background(), 5*time.Second) + if err == nil { + successCh <- true + fmt.Printf("Instance %d acquired the lock\n", instanceID) + time.Sleep(100 * time.Millisecond) // 模拟工作 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) + } else { + fmt.Printf("Instance %d failed to acquire the lock: %v\n", instanceID, err) + checkAssert(t, assert.True(t, errors.Is(err, ErrLockNotAcquired))) + } + }(i) + } + + close(barrier) // 同时释放所有goroutine + wg.Wait() + close(successCh) + + // 验证只有一个成功获取锁 + successCount := 0 + for range successCh { + successCount++ + } + checkAssert(t, assert.Equal(t, 1, successCount)) +} + +func TestLockRenewal(t *testing.T) { + db := setupTestDB(t) + lockName := "renewal-lock" + holderID := instanceID + + lock := NewDistributedLock(db, lockName, holderID) + + // 获取锁,TTL很短 + err := lock.TryLock(context.Background(), 1*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 等待超过初始TTL,但续约应该保持锁 + time.Sleep(2 * time.Second) + + // 验证锁仍然被持有 + held, err := lock.IsHeld(context.Background()) + checkAssert(t, assert.NoError(t, err)) + checkAssert(t, assert.True(t, held)) + + // 停止续约 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) + + // 验证锁已释放 + held, err = lock.IsHeld(context.Background()) + checkAssert(t, assert.NoError(t, err)) + checkAssert(t, assert.False(t, held)) +} + +func TestLockExpiration(t *testing.T) { + db := setupTestDB(t) + lockName := "expiring-lock" + + // 第一个实例获取锁,TTL很短 + lock1 := NewDistributedLock(db, lockName, instanceID) + err := lock1.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + fmt.Printf("%s Instance 1 acquired the lock\n", time.Now().UTC()) + // 等待锁过期 + time.Sleep(11 * time.Second) + + fmt.Printf("%s Instance 1 lock expired\n", time.Now().UTC()) + + // 第二个实例仍然不能获取锁,因为第一个实例持有锁后台不断续约 + lock2 := NewDistributedLock(db, lockName, "instance-2") + err = lock2.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.Error(t, err)) + + // 清理 + err = lock2.Unlock() + checkAssert(t, assert.NoError(t, err)) +} + +func TestDoubleUnlock(t *testing.T) { + db := setupTestDB(t) + lockName := "double-unlock-lock" + holderID := instanceID + + lock := NewDistributedLock(db, lockName, holderID) + + // 获取锁 + err := lock.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 第一次释放 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) + + // 第二次释放也成功 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) +} + +func TestContextCancellation(t *testing.T) { + db := setupTestDB(t) + lockName := "ctx-cancel-lock" + holderID := instanceID + + // 先让另一个实例持有锁 + otherLock := NewDistributedLock(db, lockName, "instance-2") + err := otherLock.TryLock(context.Background(), 10*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 创建可取消的上下文 + ctx, cancel := context.WithCancel(context.Background()) + + // 在新的goroutine中尝试获取锁 + var wg sync.WaitGroup + wg.Add(1) + var acquireErr error + + go func() { + defer wg.Done() + lock := NewDistributedLock(db, lockName, holderID) + acquireErr = lock.TryLock(ctx, 10*time.Second) + }() + + // 等待一会儿然后取消上下文 + time.Sleep(1 * time.Millisecond) + cancel() + + wg.Wait() + + // 验证获取被取消 + checkAssert(t, assert.Error(t, acquireErr)) + checkAssert(t, assert.True(t, errors.Is(acquireErr, context.Canceled))) + + // 清理 + err = otherLock.Unlock() + checkAssert(t, assert.NoError(t, err)) +} + +func TestLongRunningTaskWithLock(t *testing.T) { + db := setupTestDB(t) + lockName := "long-task-lock" + holderID := instanceID + + lock := NewDistributedLock(db, lockName, holderID) + + // 获取锁,TTL较短以测试续约 + err := lock.TryLock(context.Background(), 2*time.Second) + checkAssert(t, assert.NoError(t, err)) + + // 模拟长时间运行任务 + done := make(chan struct{}) + go func() { + for range 10 { + held, err := lock.IsHeld(context.Background()) + checkAssert(t, assert.NoError(t, err)) + if !held { + t.Log("锁丢失,任务中止") + return + } + time.Sleep(1 * time.Second) + } + close(done) + }() + + // 等待任务完成或超时 + select { + case <-done: + t.Log("任务成功完成") + case <-time.After(11 * time.Second): + t.Fatal("任务超时") + } + + // 清理 + err = lock.Unlock() + checkAssert(t, assert.NoError(t, err)) +} + +func TestAll(t *testing.T) { + // setupTestDB(t) + + TestLockAcquireAndRelease(t) + + TestLockMutualExclusion(t) + + TestConcurrentLockAcquisition(t) + + TestLockRenewal(t) + + TestLockExpiration(t) + + TestDoubleUnlock(t) + + TestContextCancellation(t) + + TestLongRunningTaskWithLock(t) +} diff --git a/controllers/pkg/utils/logger/feishu.go b/controllers/pkg/utils/logger/feishu.go new file mode 100644 index 000000000000..16f9e9797af0 --- /dev/null +++ b/controllers/pkg/utils/logger/feishu.go @@ -0,0 +1,273 @@ +package logger + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "github.com/sirupsen/logrus" +) + +/* +func main() { + // Example usage: + // Replace with your Feishu Webhook URL, e.g., "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx" + webhookURL := "" // Set your webhook here + // Set minimum level for Feishu alerts (e.g., WARN means WARN, ERROR, FATAL will send alerts) + // Set component name for the system component + logger := NewLogger(nil, webhookURL, WARN, "PaymentService") + + logger.Debugln("This is a debug message (stdout only with default WARN level).") + logger.Infoln("This is an info message (stdout only with default WARN level).") + logger.Warnln("This is a warning (orange card in Feishu).") + logger.Errorln("This is an error (red card in Feishu).") + logger.Errorf("Error with format: %s and %d", "foo", 123) + // logger.Fatalln("This is a fatal error (red card in Feishu, then exit).") +} +*/ + +// Level defines the log levels. +type Level int + +const ( + DEBUG Level = iota + INFO + WARN + ERROR + FATAL +) + +// String converts Level to string representation. +func (l Level) String() string { + switch l { + case DEBUG: + return "DEBUG" + case INFO: + return "INFO" + case WARN: + return "WARN" + case ERROR: + return "ERROR" + case FATAL: + return "FATAL" + default: + return "UNKNOWN" + } +} + +// LarkCardMessage represents the structure for a Feishu Interactive Card message. +type LarkCardMessage struct { + MsgType string `json:"msg_type"` + Card struct { + Header struct { + Template string `json:"template"` + Title struct { + Content string `json:"content"` + Tag string `json:"tag"` + } `json:"title"` + } `json:"header"` + Elements []struct { + Tag string `json:"tag"` + Content string `json:"content"` + } `json:"elements"` + } `json:"card"` +} + +// Logger is a custom logger that outputs to stdout and sends styled alerts to Feishu based on level. +type Logger struct { + stdLogger *logrus.Logger + webhook string + minLevel Level + component string // System component name +} + +// NewLogger creates a new Logger instance. +// - stdOutput: the output for standard logs (default os.Stdout if nil) +// - webhook: the Feishu Webhook URL for alerts (empty string to disable) +// - minLevel: the minimum log level for sending Feishu alerts +// - component: the system component name to include in Feishu alerts +func NewFeishuLogger(_ *os.File, webhook string, minLevel Level, component string) *Logger { + // if stdOutput == nil { + // stdOutput = os.Stdout + //} + if component == "" { + component = "Unknown" + } + logger := &Logger{ + stdLogger: logrus.New(), + webhook: webhook, + minLevel: minLevel, + component: component, + } + logger.Infof( + "starting Feishu logger for component: %s with minLevel: %s", + component, + minLevel.String(), + ) + return logger +} + +// sendAlert sends a styled card message to Feishu Webhook based on log level. +func (l *Logger) sendAlert(level Level, message string) error { + if l.webhook == "" || level < l.minLevel { + return nil // No webhook configured or level below threshold + } + + // Determine card style based on log level + var templateColor string + switch level { + case DEBUG: + templateColor = "blue" + case INFO: + templateColor = "green" + case WARN: + templateColor = "orange" + case ERROR, FATAL: + templateColor = "red" + default: + templateColor = "blue" + } + + // Construct Feishu Interactive Card + msg := LarkCardMessage{ + MsgType: "interactive", + } + msg.Card.Header.Template = templateColor + msg.Card.Header.Title.Content = fmt.Sprintf("[%s] %s Alert", l.component, level.String()) + msg.Card.Header.Title.Tag = "plain_text" + msg.Card.Elements = []struct { + Tag string `json:"tag"` + Content string `json:"content"` + }{ + { + Tag: "markdown", + Content: fmt.Sprintf( + "**Message**: %s\n**Time**: %s", + message, + time.Now().Format(time.RFC3339), + ), + }, + } + + jsonData, err := json.Marshal(msg) + if err != nil { + return err + } + + resp, err := http.Post(l.webhook, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("feishu webhook returned status: %d", resp.StatusCode) + } + + return nil +} + +// Debugf logs a debug-level message to stdout. +func (l *Logger) Debugf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.stdLogger.Printf("[DEBUG] %s", msg) + if err := l.sendAlert(DEBUG, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Printf logs an info-level message to stdout (alias for Infof). +func (l *Logger) Printf(format string, v ...any) { + l.Infof(format, v...) +} + +// Infof logs an info-level message to stdout. +func (l *Logger) Infof(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.stdLogger.Printf("[INFO] %s", msg) + if err := l.sendAlert(INFO, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Warnf logs a warn-level message to stdout and sends a Feishu alert if level >= minLevel. +func (l *Logger) Warnf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.stdLogger.Printf("[WARN] %s", msg) + if err := l.sendAlert(WARN, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Errorf logs an error-level message to stdout and sends a Feishu alert if level >= minLevel. +func (l *Logger) Errorf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.stdLogger.Printf("[ERROR] %s", msg) + if err := l.sendAlert(ERROR, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Fatalf logs a fatal error to stdout, sends an alert if level >= minLevel, and exits. +func (l *Logger) Fatalf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.stdLogger.Printf("[FATAL] %s", msg) + if err := l.sendAlert(FATAL, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } + os.Exit(1) +} + +// Debugln logs a debug-level message to stdout. +func (l *Logger) Debugln(v ...any) { + msg := fmt.Sprintln(v...) + l.stdLogger.Printf("[DEBUG] %s", msg) + if err := l.sendAlert(DEBUG, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Println logs an info-level message to stdout (alias for Infoln). +func (l *Logger) Println(v ...any) { + l.Infoln(v...) +} + +// Infoln logs an info-level message to stdout. +func (l *Logger) Infoln(v ...any) { + msg := fmt.Sprintln(v...) + l.stdLogger.Printf("[INFO] %s", msg) + if err := l.sendAlert(INFO, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Warnln logs a warn-level message to stdout and sends a Feishu alert if level >= minLevel. +func (l *Logger) Warnln(v ...any) { + msg := fmt.Sprintln(v...) + l.stdLogger.Printf("[WARN] %s", msg) + if err := l.sendAlert(WARN, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Errorln logs an error-level message to stdout and sends a Feishu alert if level >= minLevel. +func (l *Logger) Errorln(v ...any) { + msg := fmt.Sprintln(v...) + l.stdLogger.Printf("[ERROR] %s", msg) + if err := l.sendAlert(ERROR, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } +} + +// Fatalln logs a fatal error to stdout, sends an alert if level >= minLevel, and exits. +func (l *Logger) Fatalln(v ...any) { + msg := fmt.Sprintln(v...) + l.stdLogger.Printf("[FATAL] %s", msg) + if err := l.sendAlert(FATAL, msg); err != nil { + l.stdLogger.Printf("[ERROR] Failed to send Feishu alert: %v", err) + } + os.Exit(1) +} diff --git a/controllers/pkg/utils/logger/logger.go b/controllers/pkg/utils/logger/logger.go new file mode 100644 index 000000000000..b8018578c36d --- /dev/null +++ b/controllers/pkg/utils/logger/logger.go @@ -0,0 +1,180 @@ +// Copyright © 2022 sealos. +// +// 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 logger + +import ( + "fmt" + "os" + "strings" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var defaultLogger *zap.Logger + +// init default logger with only console output info above +func init() { + zc := zapcore.NewTee(newConsoleCore(zap.InfoLevel)) + defaultLogger = zap.New(zc) +} + +// CfgConsoleLogger config for console logs +// cfg donot support concurrent calls (as any package should init cfg at startup once) +func CfgConsoleLogger(debugMode, showPath bool) { + level, zos := genConfigs(debugMode, showPath) + + zc := zapcore.NewTee(newConsoleCore(level)) + + defaultLogger = zap.New(zc, zos...) +} + +// TODO: export more file configs +// CfgConsoleAndFileLogger config for both console and file logs +// cfg donot support concurrent calls (as any package should init cfg at startup once) +func CfgConsoleAndFileLogger(debugMode bool, logDir, name string, showPath bool) { + level, zos := genConfigs(debugMode, showPath) + + filename := fmt.Sprintf("%s/%s.log", logDir, name) + + zc := zapcore.NewTee(newConsoleCore(level), newFileCore(filename, level)) + + defaultLogger = zap.New(zc, zos...) +} + +func genConfigs(debugMode, showPath bool) (zapcore.LevelEnabler, []zap.Option) { + level := zapcore.InfoLevel + if debugMode { + level = zapcore.DebugLevel + } + + zos := []zap.Option{ + // zap.AddStacktrace(zapcore.WarnLevel), + } + if showPath { + // skip self wrapper + zos = append(zos, zap.AddCaller(), zap.AddCallerSkip(2)) + } + + return level, zos +} + +func newConsoleCore(le zapcore.LevelEnabler) zapcore.Core { + consoleLogger := zapcore.Lock(os.Stdout) + + zec := zap.NewProductionEncoderConfig() + zec.EncodeLevel = zapcore.LowercaseColorLevelEncoder + zec.EncodeTime = zapcore.ISO8601TimeEncoder + zec.EncodeTime = shortTimeEncoder + // zec.EncodeTime = zapcore.ISO8601TimeEncoder + zec.ConsoleSeparator = " " + + consoleEncoder := zapcore.NewConsoleEncoder(zec) + + return zapcore.NewCore(consoleEncoder, consoleLogger, le) +} + +func newFileCore(filename string, le zapcore.LevelEnabler) zapcore.Core { + // TODO: export more rotate configs + fileLogger := zapcore.AddSync(&lumberjack.Logger{ + Filename: filename, + MaxSize: 10, // megabytes per file + }) + + zec := zap.NewProductionEncoderConfig() + zec.EncodeTime = zapcore.ISO8601TimeEncoder + + fileEncoder := zapcore.NewJSONEncoder(zec) + return zapcore.NewCore(fileEncoder, fileLogger, le) +} + +const shortTimeLayout = "2006-01-02T15:04:05" + +func shortTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(shortTimeLayout)) +} + +// IsDebugMode check DebugLevel enabled +func IsDebugMode() bool { + return defaultLogger.Core().Enabled(zapcore.DebugLevel) +} + +// Fatal logs a message at emergency level and exit. +func Fatal(f any, v ...any) { + defaultLogger.Sugar().Fatalf(formatLog(zapcore.FatalLevel, f, v...)) +} + +// Panic logs a message at emergency level and exit. +func Panic(f any, v ...any) { + defaultLogger.Sugar().Panicf(formatLog(zapcore.PanicLevel, f, v...)) +} + +// Error logs a message at error level. +func Error(f any, v ...any) { + defaultLogger.Sugar().Errorf(formatLog(zapcore.ErrorLevel, f, v...)) +} + +// Warn logs a message at warning level. +func Warn(f any, v ...any) { + defaultLogger.Sugar().Warnf(formatLog(zapcore.WarnLevel, f, v...)) +} + +// Info logs a message at info level. +func Info(f any, v ...any) { + defaultLogger.Sugar().Infof(formatLog(zapcore.InfoLevel, f, v...)) +} + +// Debug logs a message at debug level. +func Debug(f any, v ...any) { + defaultLogger.Sugar().Debugf(formatLog(zapcore.DebugLevel, f, v...)) +} + +func formatLog(l zapcore.Level, f any, v ...any) string { + var msg string + switch f := f.(type) { + case string: + msg = f + if len(v) == 0 { + return appendColor(l, msg) + } + if !strings.Contains(msg, "%") || strings.Contains(msg, "%%") { + // do not contain format char + msg += strings.Repeat(" %v", len(v)) + } + default: + msg = fmt.Sprint(f) + if len(v) == 0 { + return appendColor(l, msg) + } + msg += strings.Repeat(" %v", len(v)) + } + return appendColor(l, fmt.Sprintf(msg, v...)) +} + +func appendColor(l zapcore.Level, s string) string { + // default all red + c := uint8(31) + switch l { + case zapcore.DebugLevel: + c = uint8(35) // Magenta + case zapcore.InfoLevel: + c = uint8(34) // Blue + case zapcore.WarnLevel: + c = uint8(33) // Yellow + } + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) +} diff --git a/controllers/pkg/utils/logger/logger_test.go b/controllers/pkg/utils/logger/logger_test.go new file mode 100644 index 000000000000..0c6ac5c4ae4b --- /dev/null +++ b/controllers/pkg/utils/logger/logger_test.go @@ -0,0 +1,68 @@ +// Copyright © 2022 sealos. +// +// 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 logger + +import ( + "errors" + "os" + "os/exec" + "testing" +) + +func TestInfoLog(t *testing.T) { + CfgConsoleLogger(false, false) + + Info("can see me") + Debug("cannot see me") + + logDir := t.TempDir() + CfgConsoleAndFileLogger(true, logDir, "log_test.log", true) + + Info("can see me") + Debug("cannot see me") + Warn("this is warn") + Error("this is error: %s", errors.New("this is error")) + Error("info %% is dead", errors.New("this is error"), 2) + Error(errors.New("this is error")) + Error(errors.New("this is error"), "more error") + + if IsDebugMode() == false { + t.Error("not in debug mode") + } +} + +func TestFatalLog(t *testing.T) { + if os.Getenv("LOG_FATAL") == "1" { + Fatal("this is fatal") + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestFatalLog") // #nosec G204 + cmd.Env = append(os.Environ(), "LOG_FATAL=1") + err := cmd.Run() + e := &exec.ExitError{} + if errors.As(err, &e) { + return + } + t.Fatalf("process ran with err %v, want exit status 1", err) +} + +func TestPanicLog(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + Panic("this panics") +} diff --git a/controllers/pkg/utils/maps/concurrent_map.go b/controllers/pkg/utils/maps/concurrent_map.go new file mode 100644 index 000000000000..be9d63cece20 --- /dev/null +++ b/controllers/pkg/utils/maps/concurrent_map.go @@ -0,0 +1,125 @@ +// Copyright © 2025 sealos. +// +// 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 maps + +import "sync" + +type ConcurrentMap struct { + mu sync.RWMutex + m map[string]any +} + +func NewConcurrentMap() *ConcurrentMap { + return &ConcurrentMap{ + m: make(map[string]any), + } +} + +func (cm *ConcurrentMap) Set(key string, value any) { + cm.mu.Lock() + defer cm.mu.Unlock() + cm.m[key] = value +} + +func (cm *ConcurrentMap) Get(key string) (any, bool) { + cm.mu.RLock() + defer cm.mu.RUnlock() + val, ok := cm.m[key] + return val, ok +} + +func (cm *ConcurrentMap) GetAllKey() []string { + cm.mu.RLock() + defer cm.mu.RUnlock() + + keys := make([]string, 0, len(cm.m)) + for k := range cm.m { + keys = append(keys, k) + } + return keys +} + +func (cm *ConcurrentMap) Delete(key string) { + cm.mu.Lock() + defer cm.mu.Unlock() + delete(cm.m, key) +} + +func (cm *ConcurrentMap) DeleteAll() { + cm.mu.Lock() + defer cm.mu.Unlock() + cm.m = make(map[string]any) +} + +func (cm *ConcurrentMap) Len() int { + cm.mu.RLock() + defer cm.mu.RUnlock() + return len(cm.m) +} + +type ConcurrentNullValueMap struct { + mu sync.RWMutex + m map[string]struct{} +} + +func NewConcurrentNullValueMap() *ConcurrentNullValueMap { + return &ConcurrentNullValueMap{ + m: make(map[string]struct{}), + } +} + +func (cm *ConcurrentNullValueMap) Set(keys ...string) { + cm.mu.Lock() + defer cm.mu.Unlock() + for _, key := range keys { + cm.m[key] = struct{}{} + } +} + +func (cm *ConcurrentNullValueMap) Get(key string) (struct{}, bool) { + cm.mu.RLock() + defer cm.mu.RUnlock() + val, ok := cm.m[key] + return val, ok +} + +func (cm *ConcurrentNullValueMap) GetAllKey() []string { + cm.mu.RLock() + defer cm.mu.RUnlock() + + keys := make([]string, 0, len(cm.m)) + for k := range cm.m { + keys = append(keys, k) + } + return keys +} + +func (cm *ConcurrentNullValueMap) Delete(key string) { + cm.mu.Lock() + defer cm.mu.Unlock() + delete(cm.m, key) +} + +func (cm *ConcurrentNullValueMap) DeleteAll() { + cm.mu.Lock() + defer cm.mu.Unlock() + cm.m = make(map[string]struct{}) +} + +func (cm *ConcurrentNullValueMap) Len() int { + cm.mu.RLock() + defer cm.mu.RUnlock() + return len(cm.m) +} diff --git a/controllers/pkg/utils/maps/ttl_map.go b/controllers/pkg/utils/maps/ttl_map.go new file mode 100644 index 000000000000..45a8a8225918 --- /dev/null +++ b/controllers/pkg/utils/maps/ttl_map.go @@ -0,0 +1,61 @@ +package maps + +import ( + "sync" + "time" +) + +type item[T any] struct { + value T + expiresAt int64 +} + +type TTLMap[T any] struct { + m map[string]*item[T] + l sync.RWMutex + ttl time.Duration +} + +func New[T any](maxTTL int) (m *TTLMap[T]) { + m = &TTLMap[T]{ + m: make(map[string]*item[T]), + ttl: time.Duration(maxTTL) * time.Second, + } + go func() { + for now := range time.Tick(2 * time.Second) { + m.l.Lock() + for k, v := range m.m { + if now.UnixNano() >= v.expiresAt { + delete(m.m, k) + } + } + m.l.Unlock() + } + }() + return m +} + +func (m *TTLMap[T]) Len() int { + m.l.RLock() + defer m.l.RUnlock() + return len(m.m) +} + +func (m *TTLMap[T]) Put(k string, v T) { + m.l.Lock() + defer m.l.Unlock() + m.m[k] = &item[T]{ + value: v, + expiresAt: time.Now().Add(m.ttl).UnixNano(), + } +} + +func (m *TTLMap[T]) Get(k string) (v T, ok bool) { + m.l.RLock() + defer m.l.RUnlock() + it, ok := m.m[k] + if !ok || time.Now().UnixNano() >= it.expiresAt { + return v, false + } + return it.value, true +} diff --git a/controllers/pkg/utils/maps/ttl_map_test.go b/controllers/pkg/utils/maps/ttl_map_test.go new file mode 100644 index 000000000000..a6b12674346f --- /dev/null +++ b/controllers/pkg/utils/maps/ttl_map_test.go @@ -0,0 +1,181 @@ +package maps + +import ( + "sync" + "testing" + "time" +) + +func TestNew(t *testing.T) { + // Test that New creates a non-nil TTLMap with initialized map + m := New[string](60) + if m == nil { + t.Fatal("New() returned nil") + } + if m.m == nil { + t.Fatal("New() did not initialize map") + } + if len(m.m) != 0 { + t.Errorf("New() initialized map with non-zero length: %d", len(m.m)) + } +} + +func TestPutAndGet(t *testing.T) { + m := New[string](60) + + // Test putting and getting a value + m.Put("key1", "value1") + if v, _ := m.Get("key1"); v != "value1" { + t.Errorf("Get(key1) = %v; want value1", v) + } + + // Test getting non-existent key + if v, _ := m.Get("key2"); v != "" { + t.Errorf("Get(key2) = %v; want nil", v) + } + + // Test updating existing key + m.Put("key1", "value2") + if v, _ := m.Get("key1"); v != "value2" { + t.Errorf("Get(key1) = %v; want value2", v) + } +} + +func TestLen(t *testing.T) { + m := New[string](60) + + // Test initial length + if l := m.Len(); l != 0 { + t.Errorf("Len() = %d; want 0", l) + } + + // Test length after adding items + m.Put("key1", "value1") + m.Put("key2", "value2") + if l := m.Len(); l != 2 { + t.Errorf("Len() = %d; want 2", l) + } +} + +func TestTTLExpiration(t *testing.T) { + m := New[string](1) // 1 second TTL, large initial capacity + var wg sync.WaitGroup + const numOps = 500 // Number of concurrent operations + + // Concurrent writes + for i := range numOps { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := string(rune('a'+(i%26))) + string(rune(i)) + m.Put(key, "value"+string(rune(i))) + }(i) + } + + // Wait for all writes to complete + wg.Wait() + + // Verify some values are present immediately + for i := range 10 { // Check a subset to avoid long test duration + key := string(rune('a'+(i%26))) + string(rune(i)) + if v, _ := m.Get(key); v == "" { + t.Errorf("Get(%s) = nil; want non-nil value", key) + } + } + + // Concurrent reads during TTL period + for i := range numOps { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := string(rune('a'+(i%26))) + string(rune(i)) + m.Get(key) + }(i) + } + + // Wait less than TTL so entries remain available. + time.Sleep(500 * time.Millisecond) + wg.Wait() + + // Verify entries remain before their fixed expiration time. + if l := m.Len(); l < numOps/2 { + t.Errorf("Len() = %d; want at least %d after partial expiration", l, numOps/2) + } + + // Wait for full expiration (1 second TTL + buffer) + time.Sleep(1500 * time.Millisecond) + + // Concurrent reads after expiration + for i := range numOps { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := string(rune('a'+(i%26))) + string(rune(i)) + if v, _ := m.Get(key); v != "" { + t.Errorf("Get(%s) = %v; want nil after expiration", key, v) + } + }(i) + } + + wg.Wait() + + // Verify length is 0 after expiration + if l := m.Len(); l != 0 { + t.Errorf("Len() = %d; want 0 after expiration", l) + } +} + +func TestConcurrentAccess(t *testing.T) { + m := New[int](60) + var wg sync.WaitGroup + + // Number of concurrent operations + const numOps = 100 + + // Concurrent writes + for i := range numOps { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := string(rune('a' + i%26)) + m.Put(key, i) + }(i) + } + + // Concurrent reads + for i := range numOps { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := string(rune('a' + i%26)) + m.Get(key) + }(i) + } + + wg.Wait() + + // Verify some values + for i := range 26 { + key := string(rune('a' + i)) + if v, _ := m.Get(key); v == 0 { + t.Errorf("Get(%s) = nil; want non-nil value", key) + } + } +} + +func TestGetDoesNotExtendTTL(t *testing.T) { + m := New[string](1) + m.Put("key1", "value1") + + for i := range 2 { + time.Sleep(400 * time.Millisecond) + if value, ok := m.Get("key1"); !ok || value != "value1" { + t.Fatalf("Get(key1) = (%q, %v), want (value1, true) at iteration %d", value, ok, i) + } + } + + time.Sleep(400 * time.Millisecond) + if value, ok := m.Get("key1"); ok { + t.Fatalf("Get(key1) = (%q, true), want expired entry", value) + } +} diff --git a/controllers/pkg/utils/rate/rate_limiter.go b/controllers/pkg/utils/rate/rate_limiter.go new file mode 100644 index 000000000000..207cf8e6437f --- /dev/null +++ b/controllers/pkg/utils/rate/rate_limiter.go @@ -0,0 +1,69 @@ +// Copyright © 2023 sealos. +// +// 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 rate + +import ( + "flag" + "time" +) + +const ( + defaultMinRetryDelay = 5 * time.Millisecond + defaultMaxRetryDelay = 1000 * time.Second + defaultQPS = float64(10.0) + defaultBurst = 100 + flagMinRetryDelay = "min-retry-delay" + flagMaxRetryDelay = "max-retry-delay" + flagQPS = "default-qps" + flagBurst = "default-burst" +) + +// LimiterOptions used on reconcilers. +type LimiterOptions struct { + MinRetryDelay time.Duration + QPS float64 + Burst int + MaxRetryDelay time.Duration +} + +func (o *LimiterOptions) BindFlags(fs *flag.FlagSet) { + fs.DurationVar( + &o.MinRetryDelay, + flagMinRetryDelay, + defaultMinRetryDelay, + "The minimum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.DurationVar( + &o.MaxRetryDelay, + flagMaxRetryDelay, + defaultMaxRetryDelay, + "The maximum amount of time for which an object being reconciled will have to wait before a retry.", + ) + fs.Float64Var(&o.QPS, flagQPS, defaultQPS, "The maximum number of batches per second to allow.") + fs.IntVar( + &o.Burst, + flagBurst, + defaultBurst, + "The maximum number of batches to allow in a short period of time.", + ) +} + +// func GetRateLimiter(opts LimiterOptions) ratelimiter.RateLimiter { +// return workqueue.NewMaxOfRateLimiter( +// workqueue.NewItemExponentialFailureRateLimiter(opts.MinRetryDelay, opts.MaxRetryDelay), +// // 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item) +// &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(opts.QPS), opts.Burst)}, +// ) +//} diff --git a/controllers/pkg/utils/retry/retry.go b/controllers/pkg/utils/retry/retry.go new file mode 100644 index 000000000000..b225f8d96473 --- /dev/null +++ b/controllers/pkg/utils/retry/retry.go @@ -0,0 +1,33 @@ +// Copyright © 2021 sealos. +// +// 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 retry + +import ( + "fmt" + "time" +) + +func Retry(tryTimes int, trySleepTime time.Duration, action func() error) error { + var err error + for i := range tryTimes { + err = action() + if err == nil { + return nil + } + + time.Sleep(trySleepTime * time.Duration(2*i+1)) + } + return fmt.Errorf("retry action timeout: %w", err) +} diff --git a/controllers/pkg/utils/retry/retry_create_or_update.go b/controllers/pkg/utils/retry/retry_create_or_update.go new file mode 100644 index 000000000000..910975bfbe61 --- /dev/null +++ b/controllers/pkg/utils/retry/retry_create_or_update.go @@ -0,0 +1,107 @@ +// Copyright © 2023 sealos. +// +// 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 retry + +import ( + "context" + "errors" + "time" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// mutate wraps a MutateFn and applies validation to its result. +func mutate(f MutateFn, key client.ObjectKey, obj client.Object) error { + if err := f(); err != nil { + return err + } + if newKey := client.ObjectKeyFromObject(obj); key != newKey { + return errors.New("mutateFn cannot mutate object name and/or object namespace") + } + return nil +} + +type MutateFn func() error + +func CreateOrUpdate( + ctx context.Context, + c client.Client, + obj client.Object, + f MutateFn, + tryTimes int, + trySleepTime time.Duration, +) (OperationResult, error) { + var result OperationResult + err := Retry(tryTimes, trySleepTime, func() error { + key := client.ObjectKeyFromObject(obj) + if err := c.Get(ctx, key, obj); err != nil { + if !apierrors.IsNotFound(err) { + result = OperationResultNone + return err + } + if err := mutate(f, key, obj); err != nil { + result = OperationResultNone + return err + } + if err := c.Create(ctx, obj); err != nil { + result = OperationResultNone + return err + } + result = OperationResultCreated + return nil + } + + existing := obj.DeepCopyObject() + if err := mutate(f, key, obj); err != nil { + result = OperationResultNone + return err + } + + if equality.Semantic.DeepEqual(existing, obj) { + result = OperationResultNone + return nil + } + + if err := c.Update(ctx, obj); err != nil { + result = OperationResultNone + return err + } + result = OperationResultUpdated + + return nil + }) + if err != nil { + return OperationResultNone, err + } + return result, nil +} + +// OperationResult is the action result of a CreateOrUpdate call. +type OperationResult string + +const ( // They should complete the sentence "Deployment default/foo has been ..." + // OperationResultNone means that the resource has not been changed. + OperationResultNone OperationResult = "unchanged" + // OperationResultCreated means that a new resource is created. + OperationResultCreated OperationResult = "created" + // OperationResultUpdated means that an existing resource is updated. + OperationResultUpdated OperationResult = "updated" + // OperationResultUpdatedStatus means that an existing resource and its status is updated. + OperationResultUpdatedStatus OperationResult = "updatedStatus" + // OperationResultUpdatedStatusOnly means that only an existing status is updated. + OperationResultUpdatedStatusOnly OperationResult = "updatedStatusOnly" +) diff --git a/controllers/resources/.dockerignore b/controllers/resources/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/resources/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/resources/.gitignore b/controllers/resources/.gitignore new file mode 100644 index 000000000000..c0a7a54cac5a --- /dev/null +++ b/controllers/resources/.gitignore @@ -0,0 +1,25 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ diff --git a/controllers/resources/Dockerfile b/controllers/resources/Dockerfile new file mode 100644 index 000000000000..4c5ab5f5d187 --- /dev/null +++ b/controllers/resources/Dockerfile @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-resources-$TARGETARCH /manager +ENTRYPOINT ["/manager"] diff --git a/controllers/resources/Makefile b/controllers/resources/Makefile new file mode 100644 index 000000000000..fa0f4c09ba10 --- /dev/null +++ b/controllers/resources/Makefile @@ -0,0 +1,139 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-resources-controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.23 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: ## Run tests. + go test -race ./... -count=1 + +##@ Build + +.PHONY: build +build: ## Build manager binary. + LD_FLAGS="-s -w" && CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: build + mv bin/manager bin/controller-resources-${TARGETARCH} + docker build -t $(IMG) . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: docker-build + docker push $(IMG) + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: pre-deploy +pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v3.8.7 +CONTROLLER_TOOLS_VERSION ?= v0.8.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/resources/PROJECT b/controllers/resources/PROJECT new file mode 100644 index 000000000000..6189761b97a5 --- /dev/null +++ b/controllers/resources/PROJECT @@ -0,0 +1,22 @@ +domain: sealos.io +layout: +- go.kubebuilder.io/v3 +projectName: resources +repo: github.com/labring/sealos +resources: +- controller: true + domain: sealos.io + group: resources + kind: Metering + version: v1alpha1 +- controller: true + domain: sealos.io + group: resources + kind: Monitor + version: v1alpha1 +- controller: true + domain: sealos.io + group: resources + kind: Billing + version: v1alpha1 +version: "3" diff --git a/controllers/resources/README.md b/controllers/resources/README.md new file mode 100644 index 000000000000..fbe222063829 --- /dev/null +++ b/controllers/resources/README.md @@ -0,0 +1,94 @@ +# resources +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/resources:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/resources:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller to the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) +which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**NOTE:** Run `make --help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2023 sealos. + +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. + diff --git a/controllers/resources/config/default/kustomization.yaml b/controllers/resources/config/default/kustomization.yaml new file mode 100644 index 000000000000..3afa168e5626 --- /dev/null +++ b/controllers/resources/config/default/kustomization.yaml @@ -0,0 +1,88 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: resources-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: resources- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +#- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/controllers/resources/config/default/manager_auth_proxy_patch.yaml b/controllers/resources/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..0251da0ea9b3 --- /dev/null +++ b/controllers/resources/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,54 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.11.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/controllers/resources/config/default/manager_config_patch.yaml b/controllers/resources/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..ca4883fc5ce5 --- /dev/null +++ b/controllers/resources/config/default/manager_config_patch.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/controllers/resources/config/manager/controller_manager_config.yaml b/controllers/resources/config/manager/controller_manager_config.yaml new file mode 100644 index 000000000000..fd68e304ee2f --- /dev/null +++ b/controllers/resources/config/manager/controller_manager_config.yaml @@ -0,0 +1,25 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: a63686c3.sealos.io diff --git a/controllers/resources/config/manager/kustomization.yaml b/controllers/resources/config/manager/kustomization.yaml new file mode 100644 index 000000000000..6f28fccfa712 --- /dev/null +++ b/controllers/resources/config/manager/kustomization.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-resources-controller + newTag: latest diff --git a/controllers/resources/config/manager/manager.yaml b/controllers/resources/config/manager/manager.yaml new file mode 100644 index 000000000000..c7a14bee3162 --- /dev/null +++ b/controllers/resources/config/manager/manager.yaml @@ -0,0 +1,79 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + containers: + - command: + - /manager + env: + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: mongo-secret + key: MONGO_URI + image: ghcr.io/labring/sealos-resources-controller:latest + imagePullPolicy: Always + name: manager + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 1000m + memory: 1280Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/resources/config/prometheus/kustomization.yaml b/controllers/resources/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/resources/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/resources/config/prometheus/monitor.yaml b/controllers/resources/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..32401c3d7fdc --- /dev/null +++ b/controllers/resources/config/prometheus/monitor.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..0e421adaba5d --- /dev/null +++ b/controllers/resources/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/resources/config/rbac/auth_proxy_role.yaml b/controllers/resources/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..7539c6cd1e86 --- /dev/null +++ b/controllers/resources/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,31 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/resources/config/rbac/auth_proxy_role_binding.yaml b/controllers/resources/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..84c16ebd8d6a --- /dev/null +++ b/controllers/resources/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/resources/config/rbac/auth_proxy_service.yaml b/controllers/resources/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..70e47bf67833 --- /dev/null +++ b/controllers/resources/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,29 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/resources/config/rbac/kustomization.yaml b/controllers/resources/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/resources/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/resources/config/rbac/leader_election_role.yaml b/controllers/resources/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..7ccba03c9765 --- /dev/null +++ b/controllers/resources/config/rbac/leader_election_role.yaml @@ -0,0 +1,51 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/resources/config/rbac/leader_election_role_binding.yaml b/controllers/resources/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..493b75ed3993 --- /dev/null +++ b/controllers/resources/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/resources/config/rbac/role.yaml b/controllers/resources/config/rbac/role.yaml new file mode 100644 index 000000000000..64d2d82e7304 --- /dev/null +++ b/controllers/resources/config/rbac/role.yaml @@ -0,0 +1,101 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - resourcequotas + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - resourcequotas/status + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services/status + verbs: + - get + - list + - watch +- apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - get + - list + - watch +- apiGroups: + - app.sealos.io + resources: + - instances/status + verbs: + - get + - list + - watch diff --git a/controllers/resources/config/rbac/role_binding.yaml b/controllers/resources/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..749383ffe1ec --- /dev/null +++ b/controllers/resources/config/rbac/role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/resources/config/rbac/service_account.yaml b/controllers/resources/config/rbac/service_account.yaml new file mode 100644 index 000000000000..b8ca78e010b4 --- /dev/null +++ b/controllers/resources/config/rbac/service_account.yaml @@ -0,0 +1,19 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/controllers/resources/controllers/cache/cache.go b/controllers/resources/controllers/cache/cache.go new file mode 100644 index 000000000000..d0a38a57e1f2 --- /dev/null +++ b/controllers/resources/controllers/cache/cache.go @@ -0,0 +1,358 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "context" + "fmt" + + kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + appv1 "github.com/labring/sealos/controllers/app/api/v1" + "github.com/labring/sealos/controllers/pkg/gpu" + "github.com/labring/sealos/controllers/pkg/resources" + accounttypes "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/label" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + gpuAliasConfigKey = "alias" + gpuInfoConfigKey = "gpu" + backupRepositoryLabelKey = "dataprotection.kubeblocks.io/backup-repo-name" + networkStatusAnnotationKey = "network.sealos.io/status" + originalNodePortLabelKey = "network.sealos.io/original-nodeport" + persistentVolumeClaimPhaseKey = "status.phase" + backupPhaseKey = "status.phase" + serviceTypeKey = "spec.type" +) + +// Options retains only the Kubernetes fields used by resource monitoring. +func Options() ctrlcache.Options { + return ctrlcache.Options{ + ReaderFailOnMissingInformer: true, + DefaultTransform: ctrlcache.TransformStripManagedFields(), + ByObject: map[client.Object]ctrlcache.ByObject{ + &corev1.Namespace{}: { + Transform: transformNamespace, + }, + &corev1.Pod{}: { + Transform: transformPod, + }, + &corev1.PersistentVolumeClaim{}: { + Transform: transformPersistentVolumeClaim, + }, + &kbv1alpha1.Backup{}: { + Transform: transformBackup, + }, + &corev1.Service{}: { + Transform: transformService, + }, + &appv1.Instance{}: { + Transform: transformInstance, + }, + &corev1.ConfigMap{}: { + Namespaces: map[string]ctrlcache.Config{ + gpu.NodeInfoConfigmapNamespace: { + FieldSelector: fields.OneTermEqualSelector( + "metadata.name", + gpu.NodeInfoConfigmapName, + ), + }, + }, + Transform: transformGPUConfigMap, + }, + }, + } +} + +// UncachedObjects keeps general controller reads and all writes on complete API objects. +func UncachedObjects() []client.Object { + return []client.Object{ + &corev1.Namespace{}, + &corev1.Pod{}, + &corev1.PersistentVolumeClaim{}, + &kbv1alpha1.Backup{}, + &corev1.Service{}, + &appv1.Instance{}, + &corev1.ConfigMap{}, + } +} + +// SetupInformers registers projected informers that are read only by the periodic monitor. +func SetupInformers(mgr ctrl.Manager) error { + instanceMetadata := &metav1.PartialObjectMetadata{} + instanceMetadata.SetGroupVersionKind(appv1.GroupVersion.WithKind("Instance")) + objects := []client.Object{ + &corev1.Namespace{}, + &corev1.Pod{}, + &corev1.ConfigMap{}, + instanceMetadata, + } + + for _, object := range objects { + if _, err := mgr.GetCache().GetInformer(context.Background(), object); err != nil { + return fmt.Errorf("register informer for %T: %w", object, err) + } + } + return nil +} + +func transformNamespace(obj any) (any, error) { + ns, ok := obj.(*corev1.Namespace) + if !ok { + return obj, nil + } + + metadata := projectObjectMeta(ns.ObjectMeta) + metadata.Labels = copyMapValues(ns.Labels, userv1.UserLabelOwnerKey) + metadata.Annotations = copyMapValues( + ns.Annotations, + accounttypes.DebtNamespaceAnnoStatusKey, + accounttypes.WorkspaceSubscriptionStatusAnnoKey, + networkStatusAnnotationKey, + ) + return &corev1.Namespace{ + TypeMeta: ns.TypeMeta, + ObjectMeta: metadata, + }, nil +} + +func transformPod(obj any) (any, error) { + pod, ok := obj.(*corev1.Pod) + if !ok { + return obj, nil + } + + containers := make([]corev1.Container, 0, len(pod.Spec.Containers)) + for i := range pod.Spec.Containers { + container := &pod.Spec.Containers[i] + projected := corev1.Container{ + Name: container.Name, + Resources: corev1.ResourceRequirements{ + Limits: copyResourceList(container.Resources.Limits), + Requests: copyResourceList(container.Resources.Requests), + }, + } + if container.Name == "acmesolver" { + projected.Args = append([]string(nil), container.Args...) + } + containers = append(containers, projected) + } + + metadata := projectObjectMeta(pod.ObjectMeta) + metadata.Labels = resourceLabels(pod.Labels) + projected := &corev1.Pod{ + TypeMeta: pod.TypeMeta, + ObjectMeta: metadata, + Spec: corev1.PodSpec{ + NodeName: pod.Spec.NodeName, + Containers: containers, + }, + Status: corev1.PodStatus{ + Phase: pod.Status.Phase, + }, + } + if pod.Status.StartTime != nil { + projected.Status.StartTime = pod.Status.StartTime.DeepCopy() + } + return projected, nil +} + +func transformPersistentVolumeClaim(obj any) (any, error) { + pvc, ok := obj.(*corev1.PersistentVolumeClaim) + if !ok { + return obj, nil + } + + metadata := projectObjectMeta(pvc.ObjectMeta) + metadata.Labels = resourceLabels(pvc.Labels) + if len(pvc.OwnerReferences) > 0 { + metadata.OwnerReferences = []metav1.OwnerReference{{ + Kind: pvc.OwnerReferences[0].Kind, + }} + } + requests := corev1.ResourceList{} + if storage, ok := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; ok { + requests[corev1.ResourceStorage] = storage.DeepCopy() + } + return &corev1.PersistentVolumeClaim{ + TypeMeta: pvc.TypeMeta, + ObjectMeta: metadata, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.VolumeResourceRequirements{Requests: requests}, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: pvc.Status.Phase, + }, + }, nil +} + +func transformBackup(obj any) (any, error) { + backup, ok := obj.(*kbv1alpha1.Backup) + if !ok { + return obj, nil + } + + metadata := projectObjectMeta(backup.ObjectMeta) + metadata.Labels = resourceLabels(backup.Labels) + return &kbv1alpha1.Backup{ + TypeMeta: backup.TypeMeta, + ObjectMeta: metadata, + Status: kbv1alpha1.BackupStatus{ + Phase: backup.Status.Phase, + TotalSize: backup.Status.TotalSize, + }, + }, nil +} + +func transformService(obj any) (any, error) { + service, ok := obj.(*corev1.Service) + if !ok { + return obj, nil + } + + ports := make([]corev1.ServicePort, len(service.Spec.Ports)) + for i := range service.Spec.Ports { + ports[i].NodePort = service.Spec.Ports[i].NodePort + } + metadata := projectObjectMeta(service.ObjectMeta) + metadata.Labels = resourceLabels(service.Labels) + if value, ok := service.Labels[originalNodePortLabelKey]; ok { + if metadata.Labels == nil { + metadata.Labels = make(map[string]string) + } + metadata.Labels[originalNodePortLabelKey] = value + } + return &corev1.Service{ + TypeMeta: service.TypeMeta, + ObjectMeta: metadata, + Spec: corev1.ServiceSpec{ + Type: service.Spec.Type, + Ports: ports, + }, + }, nil +} + +func transformInstance(obj any) (any, error) { + switch instance := obj.(type) { + case *appv1.Instance: + metadata := projectObjectMeta(instance.ObjectMeta) + metadata.Labels = copyMapValues(instance.Labels, resources.AppStoreDeployLabelKey) + return &appv1.Instance{ + TypeMeta: instance.TypeMeta, + ObjectMeta: metadata, + }, nil + case *metav1.PartialObjectMetadata: + metadata := projectObjectMeta(instance.ObjectMeta) + metadata.Labels = copyMapValues(instance.Labels, resources.AppStoreDeployLabelKey) + return &metav1.PartialObjectMetadata{ + TypeMeta: instance.TypeMeta, + ObjectMeta: metadata, + }, nil + default: + return obj, nil + } +} + +func transformGPUConfigMap(obj any) (any, error) { + configMap, ok := obj.(*corev1.ConfigMap) + if !ok { + return obj, nil + } + + return &corev1.ConfigMap{ + TypeMeta: configMap.TypeMeta, + ObjectMeta: projectObjectMeta(configMap.ObjectMeta), + Data: copyMapValues( + configMap.Data, + gpuAliasConfigKey, + gpuInfoConfigKey, + ), + }, nil +} + +func resourceLabels(source map[string]string) map[string]string { + return copyMapValues( + source, + resources.DBPodLabelInstanceKey, + resources.DBPodLabelComponentNameKey, + resources.TerminalIDLabelKey, + label.AppManagedBy, + label.AppPartOf, + label.AppName, + resources.AppLabelKey, + resources.AppDeployLabelKey, + resources.JobNameLabelKey, + resources.ACMEChallengeKey, + backupRepositoryLabelKey, + resources.InstanceLabelKey, + ) +} + +func copyResourceList(source corev1.ResourceList) corev1.ResourceList { + if len(source) == 0 { + return nil + } + result := make(corev1.ResourceList, len(source)) + for name, quantity := range source { + result[name] = quantity.DeepCopy() + } + return result +} + +func projectObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + out := metav1.ObjectMeta{ + Name: in.Name, + Namespace: in.Namespace, + UID: in.UID, + ResourceVersion: in.ResourceVersion, + Generation: in.Generation, + CreationTimestamp: in.CreationTimestamp, + } + if in.DeletionTimestamp != nil { + out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy() + } + if in.DeletionGracePeriodSeconds != nil { + gracePeriod := *in.DeletionGracePeriodSeconds + out.DeletionGracePeriodSeconds = &gracePeriod + } + return out +} + +func copyMapValues(source map[string]string, keys ...string) map[string]string { + var result map[string]string + for _, key := range keys { + if value, ok := source[key]; ok { + if result == nil { + result = make(map[string]string) + } + result[key] = value + } + } + return result +} + +// Index field names shared with the monitor reconciler. +const ( + PersistentVolumeClaimPhaseKey = persistentVolumeClaimPhaseKey + BackupPhaseKey = backupPhaseKey + ServiceTypeKey = serviceTypeKey +) diff --git a/controllers/resources/controllers/cache/cache_test.go b/controllers/resources/controllers/cache/cache_test.go new file mode 100644 index 000000000000..968e3fb869e8 --- /dev/null +++ b/controllers/resources/controllers/cache/cache_test.go @@ -0,0 +1,419 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "reflect" + "testing" + + kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + appv1 "github.com/labring/sealos/controllers/app/api/v1" + "github.com/labring/sealos/controllers/pkg/gpu" + "github.com/labring/sealos/controllers/pkg/resources" + accounttypes "github.com/labring/sealos/controllers/pkg/types" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestOptionsRegistersOnlyMonitorObjects(t *testing.T) { + options := Options() + if !options.ReaderFailOnMissingInformer { + t.Fatal("missing informer reads are allowed") + } + if options.DefaultTransform == nil { + t.Fatal("default managed fields transform is nil") + } + + required := []client.Object{ + &corev1.Namespace{}, + &corev1.Pod{}, + &corev1.PersistentVolumeClaim{}, + &kbv1alpha1.Backup{}, + &corev1.Service{}, + &appv1.Instance{}, + &corev1.ConfigMap{}, + } + if len(options.ByObject) != len(required) { + t.Fatalf("cache object count = %d, want %d", len(options.ByObject), len(required)) + } + for _, expected := range required { + found := false + for obj, byObject := range options.ByObject { + if reflect.TypeOf(obj) != reflect.TypeOf(expected) { + continue + } + found = true + if byObject.Transform == nil { + t.Fatalf("%T transform is nil", expected) + } + } + if !found { + t.Fatalf("%T cache options not found", expected) + } + } +} + +func TestUncachedObjectsProtectProjectedWrites(t *testing.T) { + disabled := UncachedObjects() + if len(disabled) != len(Options().ByObject) { + t.Fatalf("uncached object count = %d, want %d", len(disabled), len(Options().ByObject)) + } + for cached := range Options().ByObject { + found := false + for _, object := range disabled { + if reflect.TypeOf(cached) == reflect.TypeOf(object) { + found = true + break + } + } + if !found { + t.Fatalf("projected %T is not disabled on the general client", cached) + } + } +} + +func TestOptionsLimitsGPUConfigMapCache(t *testing.T) { + options := Options() + for obj, byObject := range options.ByObject { + if _, ok := obj.(*corev1.ConfigMap); !ok { + continue + } + if len(byObject.Namespaces) != 1 { + t.Fatalf("configmap cache namespaces = %d, want 1", len(byObject.Namespaces)) + } + config, ok := byObject.Namespaces[gpu.NodeInfoConfigmapNamespace] + if !ok { + t.Fatalf("configmap cache does not include %q", gpu.NodeInfoConfigmapNamespace) + } + if got, want := config.FieldSelector.String(), "metadata.name="+gpu.NodeInfoConfigmapName; got != want { + t.Fatalf("configmap field selector = %q, want %q", got, want) + } + return + } + t.Fatal("configmap cache options not found") +} + +func TestTransformNamespaceKeepsMonitorSelectionFields(t *testing.T) { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-user-a", + ResourceVersion: "42", + Labels: map[string]string{ + userv1.UserLabelOwnerKey: "user-a", + "unused": "large-value", + }, + Annotations: map[string]string{ + accounttypes.DebtNamespaceAnnoStatusKey: accounttypes.SuspendDebtNamespaceAnnoStatus, + accounttypes.WorkspaceSubscriptionStatusAnnoKey: "active", + networkStatusAnnotationKey: "Suspend", + "unused": "large-value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.NamespaceSpec{Finalizers: []corev1.FinalizerName{"kubernetes"}}, + } + + transformed, err := transformNamespace(ns) + if err != nil { + t.Fatalf("transform namespace: %v", err) + } + got, ok := transformed.(*corev1.Namespace) + if !ok { + t.Fatalf("transformed type = %T, want *v1.Namespace", transformed) + } + if got.Name != ns.Name || got.ResourceVersion != ns.ResourceVersion { + t.Fatalf("required identity metadata was not retained: %#v", got.ObjectMeta) + } + if !reflect.DeepEqual(got.Labels, map[string]string{userv1.UserLabelOwnerKey: "user-a"}) { + t.Fatalf("namespace labels = %#v", got.Labels) + } + if !reflect.DeepEqual(got.Annotations, map[string]string{ + accounttypes.DebtNamespaceAnnoStatusKey: accounttypes.SuspendDebtNamespaceAnnoStatus, + accounttypes.WorkspaceSubscriptionStatusAnnoKey: "active", + networkStatusAnnotationKey: "Suspend", + }) { + t.Fatalf("namespace annotations = %#v", got.Annotations) + } + if len(got.ManagedFields) != 0 || len(got.Spec.Finalizers) != 0 { + t.Fatalf("unused namespace payload was retained: %#v", got) + } +} + +func TestTransformPodKeepsResourceAccountingFields(t *testing.T) { + startTime := metav1.Now() + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-a-0", + Namespace: "ns-user-a", + Labels: map[string]string{ + resources.AppLabelKey: "app-a", + "unused": "large-value", + }, + Annotations: map[string]string{"unused": "large-value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.PodSpec{ + NodeName: "node-a", + Containers: []corev1.Container{ + { + Name: "app", + Image: "large-image-name", + Args: []string{"unused", "arguments"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + Claims: []corev1.ResourceClaim{{Name: "unused-claim"}}, + }, + }, + { + Name: "acmesolver", + Args: []string{"--domain=example.test"}, + }, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + StartTime: &startTime, + PodIP: "10.0.0.1", + }, + } + + transformed, err := transformPod(pod) + if err != nil { + t.Fatalf("transform pod: %v", err) + } + got, ok := transformed.(*corev1.Pod) + if !ok { + t.Fatalf("transformed type = %T, want *v1.Pod", transformed) + } + if got.Spec.NodeName != pod.Spec.NodeName || got.Status.Phase != pod.Status.Phase || + got.Status.StartTime == nil || !got.Status.StartTime.Equal(pod.Status.StartTime) { + t.Fatalf("pod scheduling fields were not retained: %#v", got) + } + if got.Spec.Containers[0].Resources.Requests.Cpu().String() != "500m" || + got.Spec.Containers[0].Resources.Limits.Memory().String() != "1Gi" { + t.Fatalf("pod resources were not retained: %#v", got.Spec.Containers[0].Resources) + } + if len(got.Spec.Containers[0].Args) != 0 || got.Spec.Containers[0].Image != "" || + len(got.Spec.Containers[0].Resources.Claims) != 0 || + !reflect.DeepEqual(got.Spec.Containers[1].Args, []string{"--domain=example.test"}) { + t.Fatalf("pod container projection is incorrect: %#v", got.Spec.Containers) + } + if got.Status.PodIP != "" || len(got.Annotations) != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("unused pod payload was retained: %#v", got) + } + if named := resources.NewResourceNamed(got); named.Name() != "app-a" { + t.Fatalf("resource name = %q, want app-a", named.Name()) + } +} + +func TestTransformPersistentVolumeClaimKeepsIndexAndStorage(t *testing.T) { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "data-app-a", + Namespace: "ns-user-a", + Labels: map[string]string{resources.AppLabelKey: "app-a", "unused": "value"}, + OwnerReferences: []metav1.OwnerReference{ + {Kind: "StatefulSet", Name: "app-a", UID: types.UID("owner-a")}, + {Kind: "Other", Name: "unused"}, + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "large-volume-name", + Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("20Gi"), + corev1.ResourceCPU: resource.MustParse("1"), + }}, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + Capacity: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("20Gi")}, + }, + } + + transformed, err := transformPersistentVolumeClaim(pvc) + if err != nil { + t.Fatalf("transform pvc: %v", err) + } + got, ok := transformed.(*corev1.PersistentVolumeClaim) + if !ok { + t.Fatalf("transformed type = %T, want *v1.PersistentVolumeClaim", transformed) + } + if got.Status.Phase != corev1.ClaimBound || + got.Spec.Resources.Requests.Storage().String() != "20Gi" { + t.Fatalf("pvc accounting fields were not retained: %#v", got) + } + if len(got.OwnerReferences) != 1 || got.OwnerReferences[0].Kind != "StatefulSet" || + got.OwnerReferences[0].Name != "" { + t.Fatalf("pvc owner projection = %#v", got.OwnerReferences) + } + if got.Spec.VolumeName != "" || len(got.Status.Capacity) != 0 || + got.Spec.Resources.Requests.Cpu().Sign() != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("unused pvc payload was retained: %#v", got) + } +} + +func TestTransformBackupAndServiceKeepAccountingFields(t *testing.T) { + backup := &kbv1alpha1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-a", + Labels: map[string]string{ + resources.InstanceLabelKey: "database-a", + "unused": "value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Status: kbv1alpha1.BackupStatus{ + Phase: kbv1alpha1.BackupPhaseCompleted, + TotalSize: "2Gi", + FailureReason: "unused-large-value", + }, + } + transformedBackup, err := transformBackup(backup) + if err != nil { + t.Fatalf("transform backup: %v", err) + } + gotBackup, ok := transformedBackup.(*kbv1alpha1.Backup) + if !ok { + t.Fatalf("transformed type = %T, want *v1alpha1.Backup", transformedBackup) + } + if gotBackup.Status.Phase != backup.Status.Phase || gotBackup.Status.TotalSize != "2Gi" || + gotBackup.Status.FailureReason != "" || len(gotBackup.ManagedFields) != 0 { + t.Fatalf("backup projection is incorrect: %#v", gotBackup) + } + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-a", + Labels: map[string]string{ + resources.AppLabelKey: "app-a", + originalNodePortLabelKey: "true", + "unused": "value", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + ClusterIP: "10.96.0.1", + Selector: map[string]string{"large": "selector"}, + Ports: []corev1.ServicePort{ + {Name: "http", Port: 80, NodePort: 30080}, + {Name: "https", Port: 443, NodePort: 30443}, + }, + }, + } + transformedService, err := transformService(service) + if err != nil { + t.Fatalf("transform service: %v", err) + } + gotService, ok := transformedService.(*corev1.Service) + if !ok { + t.Fatalf("transformed type = %T, want *v1.Service", transformedService) + } + if gotService.Spec.Type != corev1.ServiceTypeNodePort || + gotService.Spec.Ports[0].NodePort != 30080 || gotService.Spec.Ports[1].NodePort != 30443 { + t.Fatalf("service accounting fields were not retained: %#v", gotService.Spec) + } + if gotService.Labels[originalNodePortLabelKey] != "true" { + t.Fatalf("service network label was not retained: %#v", gotService.Labels) + } + if gotService.Spec.ClusterIP != "" || len(gotService.Spec.Selector) != 0 || + gotService.Spec.Ports[0].Name != "" || gotService.Spec.Ports[0].Port != 0 || + len(gotService.ManagedFields) != 0 { + t.Fatalf("unused service payload was retained: %#v", gotService) + } +} + +func TestTransformInstanceAndGPUConfigMap(t *testing.T) { + instance := &appv1.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "instance-a", + Labels: map[string]string{ + resources.AppStoreDeployLabelKey: "store-a", + "unused": "value", + }, + Annotations: map[string]string{"unused": "value"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + } + transformedInstance, err := transformInstance(instance) + if err != nil { + t.Fatalf("transform instance: %v", err) + } + gotInstance, ok := transformedInstance.(*appv1.Instance) + if !ok { + t.Fatalf("transformed type = %T, want *v1.Instance", transformedInstance) + } + if !reflect.DeepEqual(gotInstance.Labels, map[string]string{ + resources.AppStoreDeployLabelKey: "store-a", + }) || len(gotInstance.Annotations) != 0 || len(gotInstance.ManagedFields) != 0 { + t.Fatalf("instance projection is incorrect: %#v", gotInstance) + } + instanceMetadata := &metav1.PartialObjectMetadata{ + ObjectMeta: instance.ObjectMeta, + } + instanceMetadata.SetGroupVersionKind(appv1.GroupVersion.WithKind("Instance")) + transformedMetadata, err := transformInstance(instanceMetadata) + if err != nil { + t.Fatalf("transform instance metadata: %v", err) + } + gotMetadata, ok := transformedMetadata.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformedMetadata) + } + if gotMetadata.GroupVersionKind() != instanceMetadata.GroupVersionKind() || + !reflect.DeepEqual(gotMetadata.Labels, gotInstance.Labels) || + len(gotMetadata.Annotations) != 0 || len(gotMetadata.ManagedFields) != 0 { + t.Fatalf("instance metadata projection is incorrect: %#v", gotMetadata) + } + + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: gpu.NodeInfoConfigmapName, + Namespace: gpu.NodeInfoConfigmapNamespace, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + ResourceVersion: "42", + }, + Data: map[string]string{ + gpuAliasConfigKey: "alias-data", + gpuInfoConfigKey: "gpu-data", + "unused": "large-value", + }, + BinaryData: map[string][]byte{"unused": []byte("large-value")}, + } + transformedConfigMap, err := transformGPUConfigMap(configMap) + if err != nil { + t.Fatalf("transform configmap: %v", err) + } + gotConfigMap, ok := transformedConfigMap.(*corev1.ConfigMap) + if !ok { + t.Fatalf("transformed type = %T, want *v1.ConfigMap", transformedConfigMap) + } + if !reflect.DeepEqual(gotConfigMap.Data, map[string]string{ + gpuAliasConfigKey: "alias-data", + gpuInfoConfigKey: "gpu-data", + }) || len(gotConfigMap.BinaryData) != 0 || len(gotConfigMap.ManagedFields) != 0 { + t.Fatalf("configmap projection is incorrect: %#v", gotConfigMap) + } +} diff --git a/controllers/resources/controllers/monitor_controller.go b/controllers/resources/controllers/monitor_controller.go new file mode 100644 index 000000000000..548c2b46c970 --- /dev/null +++ b/controllers/resources/controllers/monitor_controller.go @@ -0,0 +1,1092 @@ +/* +Copyright 2023 sealos. + +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 controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "strings" + "sync" + "time" + + kbv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/go-logr/logr" + appv1 "github.com/labring/sealos/controllers/app/api/v1" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/gpu" + objstorage "github.com/labring/sealos/controllers/pkg/objectstorage" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/pkg/utils/logger" + "github.com/labring/sealos/controllers/pkg/utils/retry" + resourcecache "github.com/labring/sealos/controllers/resources/controllers/cache" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + "github.com/minio/minio-go/v7" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MonitorReconciler reconciles a Monitor object +type MonitorReconciler struct { + client.Client + cache client.Reader + logr.Logger + Interval time.Duration + Scheme *runtime.Scheme + stopCh chan struct{} + wg sync.WaitGroup + periodicReconcile time.Duration + gpuAliasCard map[string]corev1.ResourceName + gpuNodeAlias map[string]string + gpuMutex sync.RWMutex + DBClient database.Interface + TrafficClient database.Interface + Properties *resources.PropertyTypeLS + PromURL string + lastObjectMetrics objstorage.Metrics + currentObjectMetrics objstorage.Metrics + ObjStorageClient *minio.Client + ObjStorageMetricsClient *objstorage.MetricsClient + ObjStorageUserBackupSize map[string]int64 + ObjectStorageInstance string +} + +type quantity struct { + *resource.Quantity + detail string +} + +type gpuAliasResource struct { + Card string `json:"card"` +} + +type gpuAliasConfig struct { + Resource gpuAliasResource `json:"resource"` +} + +type gpuNodeConfig struct { + Ref string `json:"gpu.ref"` + Product string `json:"gpu.product"` +} + +const ( + PrometheusURL = "PROM_URL" + ObjectStorageInstance = "OBJECT_STORAGE_INSTANCE" + ConcurrentLimit = "CONCURRENT_LIMIT" + envEphemeralStorageChargeThreshold = "EPHEMERAL_STORAGE_CHARGE_THRESHOLD" +) + +const ( + gpuAliasConfigKey = "alias" + gpuInfoConfigKey = "gpu" +) + +var ( + concurrentLimit = int64(DefaultConcurrencyLimit) + ephemeralStorageChargeThreshold = resource.MustParse( + env.GetEnvWithDefault(envEphemeralStorageChargeThreshold, "10Gi"), + ) +) + +const ( + DefaultConcurrencyLimit = 1000 +) + +//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=resourcequotas/status,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=services/status,verbs=get;list;watch +//+kubebuilder:rbac:groups=app.sealos.io,resources=instances,verbs=get;list;watch +//+kubebuilder:rbac:groups=app.sealos.io,resources=instances/status,verbs=get;list;watch +//+kubebuilder:rbac:groups=dataprotection.apecloud.io,resources=backups,verbs=get;list;watch +//+kubebuilder:rbac:groups=dataprotection.apecloud.io,resources=backups/status,verbs=get;list;watch + +func NewMonitorReconciler(mgr ctrl.Manager) (*MonitorReconciler, error) { + r := &MonitorReconciler{ + Client: mgr.GetClient(), + cache: mgr.GetCache(), + Logger: ctrl.Log.WithName("controllers").WithName("Monitor"), + stopCh: make(chan struct{}), + periodicReconcile: 1 * time.Minute, + PromURL: os.Getenv(PrometheusURL), + ObjectStorageInstance: os.Getenv(ObjectStorageInstance), + gpuAliasCard: make(map[string]corev1.ResourceName), + gpuNodeAlias: make(map[string]string), + } + concurrentLimit = env.GetInt64EnvWithDefault(ConcurrentLimit, DefaultConcurrencyLimit) + var err error + err = retry.Retry(2, 1*time.Second, func() error { + if err = r.refreshGPUConfig(context.Background()); err != nil { + return fmt.Errorf("failed to refresh gpu config: %w", err) + } + return nil + }) + if err != nil { + fmt.Printf("INFO: refresh gpu config: %v, will retry in next reconcile", err) + } + r.gpuMutex.RLock() + aliasCount := len(r.gpuAliasCard) + nodeCount := len(r.gpuNodeAlias) + r.gpuMutex.RUnlock() + r.Info("refresh gpu config", "aliasCount", aliasCount, "nodeCount", nodeCount) + return r, nil +} + +func InitIndexField(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer(). + IndexField(context.Background(), &corev1.PersistentVolumeClaim{}, resourcecache.PersistentVolumeClaimPhaseKey, func(rawObj client.Object) []string { + pvc, ok := rawObj.(*corev1.PersistentVolumeClaim) + if !ok { + return nil + } + return []string{string(pvc.Status.Phase)} + }); err != nil { + return err + } + if err := mgr.GetFieldIndexer(). + IndexField(context.Background(), &kbv1alpha1.Backup{}, resourcecache.BackupPhaseKey, func(rawObj client.Object) []string { + backup, ok := rawObj.(*kbv1alpha1.Backup) + if !ok { + return nil + } + return []string{string(backup.Status.Phase)} + }); err != nil { + return err + } + return mgr.GetFieldIndexer(). + IndexField(context.Background(), &corev1.Service{}, resourcecache.ServiceTypeKey, func(rawObj client.Object) []string { + svc, ok := rawObj.(*corev1.Service) + if !ok { + return nil + } + return []string{string(svc.Spec.Type)} + }) +} + +func (r *MonitorReconciler) StartReconciler(ctx context.Context) error { + r.startPeriodicReconcile() + if r.TrafficClient != nil || r.ObjStorageClient != nil { + r.startMonitorTraffic() + } + <-ctx.Done() + r.stopPeriodicReconcile() + return nil +} + +func (r *MonitorReconciler) startPeriodicReconcile() { + r.wg.Add(1) + go func() { + defer r.wg.Done() + waitNextMinute() + ticker := time.NewTicker(r.periodicReconcile) + for { + select { + case <-ticker.C: + r.enqueueNamespacesForReconcile() + if err := r.refreshGPUConfig(context.Background()); err != nil { + r.Error(err, "refresh gpu config failed") + } + case <-r.stopCh: + ticker.Stop() + return + } + } + }() +} + +func (r *MonitorReconciler) getNamespaceList() (*corev1.NamespaceList, error) { + namespaceList := &corev1.NamespaceList{} + req, err := labels.NewRequirement(userv1.UserLabelOwnerKey, selection.Exists, nil) + if err != nil { + return nil, fmt.Errorf("failed to create label requirement: %w", err) + } + return namespaceList, r.cache.List(context.Background(), namespaceList, &client.ListOptions{ + LabelSelector: labels.NewSelector().Add(*req), + }) +} + +func waitNextMinute() { + waitTime := time.Until(time.Now().Truncate(time.Minute).Add(1 * time.Minute)) + if waitTime > 0 { + logger.Info("wait for first reconcile", "waitTime", waitTime) + time.Sleep(waitTime) + } +} + +func waitNextHour() { + waitTime := time.Until(time.Now().Truncate(time.Hour).Add(1 * time.Hour)) + if waitTime > 0 { + logger.Info("wait for first reconcile", "waitTime", waitTime) + time.Sleep(waitTime) + } +} + +func (r *MonitorReconciler) startMonitorTraffic() { + r.wg.Add(1) + go func() { + defer r.wg.Done() + startTime, endTime := time.Now(). + UTC(), + time.Now(). + Truncate(time.Hour). + Add(1*time.Hour). + UTC() + waitNextHour() + ticker := time.NewTicker(1 * time.Hour) + if err := r.MonitorTrafficUsed(startTime, endTime); err != nil { + r.Error(err, "failed to monitor pod traffic used") + } + for { + select { + case <-ticker.C: + startTime, endTime = endTime, endTime.Add(1*time.Hour) + if err := r.MonitorTrafficUsed(startTime, endTime); err != nil { + r.Error(err, "failed to monitor pod traffic used") + break + } + case <-r.stopCh: + ticker.Stop() + return + } + } + }() +} + +func (r *MonitorReconciler) stopPeriodicReconcile() { + close(r.stopCh) + r.wg.Wait() +} + +func (r *MonitorReconciler) enqueueNamespacesForReconcile() { + r.Info("enqueue namespaces for reconcile", "time", time.Now().Format(time.RFC3339)) + + namespaceList, err := r.getNamespaceList() + if err != nil { + r.Error(err, "failed to list namespaces") + return + } + + filterNormalNamespace(namespaceList) + + r.processNamespaceList(namespaceList) +} + +func filterNormalNamespace(namespaceList *corev1.NamespaceList) { + items := make([]corev1.Namespace, 0, len(namespaceList.Items)) + for i := range namespaceList.Items { + debtStatus := "" + anno := namespaceList.Items[i].Annotations + if anno != nil { + debtStatus = anno[types.DebtNamespaceAnnoStatusKey] + } + if debtStatus == types.SuspendDebtNamespaceAnnoStatus || + debtStatus == types.SuspendCompletedDebtNamespaceAnnoStatus || + debtStatus == types.FinalDeletionDebtNamespaceAnnoStatus || + debtStatus == types.FinalDeletionCompletedDebtNamespaceAnnoStatus { + continue + } + items = append(items, namespaceList.Items[i]) + } + namespaceList.Items = items + logger.Info( + "filter normal namespace", + "namespaceList len", + len(namespaceList.Items), + "time", + time.Now().Format(time.RFC3339), + ) +} + +func (r *MonitorReconciler) processNamespaceList(namespaceList *corev1.NamespaceList) { + logger.Info( + "start processNamespaceList", + "namespaceList len", + len(namespaceList.Items), + "time", + time.Now().Format(time.RFC3339), + ) + if len(namespaceList.Items) == 0 { + r.Error(errors.New("no namespace to process"), "") + return + } + if err := r.preMonitorResourceUsage(); err != nil { + r.Error(err, "failed to pre monitor resource usage") + } + sem := semaphore.NewWeighted(concurrentLimit) + wg := sync.WaitGroup{} + wg.Add(len(namespaceList.Items)) + for i := range namespaceList.Items { + go func(namespace *corev1.Namespace) { + defer wg.Done() + if err := sem.Acquire(context.Background(), 1); err != nil { + fmt.Printf("Failed to acquire semaphore: %v\n", err) + return + } + defer sem.Release(1) + if err := r.monitorResourceUsage(namespace); err != nil { + r.Error(err, "monitor pod resource", "namespace", namespace.Name) + } + }(&namespaceList.Items[i]) + } + wg.Wait() + if err := r.monitorObjectStorageTraffic(); err != nil { + r.Error(err, "failed to monitor object storage traffic") + } + logger.Info("end processNamespaceList", "time", time.Now().Format(time.DateTime)) +} + +func (r *MonitorReconciler) preMonitorResourceUsage() error { + if r.ObjStorageMetricsClient != nil { + metrics, err := objstorage.QueryUserUsageAndTraffic(r.ObjStorageMetricsClient) + if err != nil { + r.lastObjectMetrics = r.currentObjectMetrics + return fmt.Errorf("failed to query object storage metrics: %w", err) + } + if r.currentObjectMetrics != nil { + r.lastObjectMetrics = r.currentObjectMetrics + } else { + latestObjTrafficSentMetrics := make(objstorage.Metrics) + startTime, endTime := time.Now().UTC().Add(-time.Hour), time.Now().UTC() + traffic, err := r.DBClient.GetAllLatestObjTraffic(startTime, endTime) + if err != nil { + return fmt.Errorf("failed to get all latest object storage traffic: %w", err) + } + for i := range traffic { + user := traffic[i].User + bucket := traffic[i].Bucket + if _, ok := metrics[user]; !ok { + continue + } + + if traffic[i].Time.Before(time.Now().Add(-time.Hour)) { + continue + } + + if _, ok := latestObjTrafficSentMetrics[user]; !ok { + latestObjTrafficSentMetrics[user] = objstorage.MetricData{ + Sent: make(map[string]int64), + } + } + + latestObjTrafficSentMetrics[user].Sent[bucket] = traffic[i].TotalSent + } + r.lastObjectMetrics = latestObjTrafficSentMetrics + } + r.currentObjectMetrics = metrics + logger.Info( + "success query object storage usage and traffic metrics", + "time", + time.Now().Format(time.DateTime), + ) + } + return nil +} + +func (r *MonitorReconciler) monitorResourceUsage(namespace *corev1.Namespace) error { + timeStamp := time.Now().UTC() + resUsed := map[string]map[corev1.ResourceName]*quantity{} + resNamed := make(map[string]*resources.ResourceNamed) + instances, err := r.getInstances(namespace.Name) + if err != nil { + return fmt.Errorf("failed to get instances: %w", err) + } + if err := r.monitorPodResourceUsage(namespace.Name, resUsed, resNamed, instances); err != nil { + return fmt.Errorf("failed to monitor pod resource usage: %w", err) + } + + if err := r.monitorPVCResourceUsage(namespace.Name, resUsed, resNamed, instances); err != nil { + return fmt.Errorf("failed to monitor PVC resource usage: %w", err) + } + + if err := r.monitorDatabaseBackupUsage(namespace.Name, resUsed, resNamed); err != nil { + return fmt.Errorf("failed to monitor backup resource usage: %w", err) + } + + if err := r.monitorServiceResourceUsage( + namespace.Name, + resUsed, + resNamed, + instances, + ); err != nil { + return fmt.Errorf("failed to monitor service resource usage: %w", err) + } + + r.monitorObjectStorageUsage(namespace.Name, resUsed, resNamed) + + monitors := make([]*resources.Monitor, 0, len(resUsed)) + + for name, podResource := range resUsed { + isEmpty, used := r.getResourceUsed(podResource) + if isEmpty { + continue + } + monitors = append(monitors, &resources.Monitor{ + Category: namespace.Name, + Used: used, + Time: timeStamp, + Type: resNamed[name].Type(), + Name: resNamed[name].Name(), + ParentType: resNamed[name].ParentType(), + ParentName: resNamed[name].ParentName(), + }) + } + return r.DBClient.InsertMonitor(context.Background(), monitors...) +} + +func (r *MonitorReconciler) getInstances(namespace string) (map[string]struct{}, error) { + instances := make(map[string]struct{}) + insList := metav1.PartialObjectMetadataList{} + insList.SetGroupVersionKind(appv1.GroupVersion.WithKind("InstanceList")) + if err := r.cache.List( + context.Background(), + &insList, + client.InNamespace(namespace), + ); err != nil { + return nil, fmt.Errorf("failed to list instances: %w", err) + } + for i := range insList.Items { + name := insList.Items[i].Labels[resources.AppStoreDeployLabelKey] + if name == "" { + name = insList.Items[i].Name + } + instances[name] = struct{}{} + } + return instances, nil +} + +func (r *MonitorReconciler) monitorPodResourceUsage( + namespace string, + resUsed map[string]map[corev1.ResourceName]*quantity, + resNamed map[string]*resources.ResourceNamed, + instances map[string]struct{}, +) error { + podList := &corev1.PodList{} + if err := r.cache.List(context.Background(), podList, &client.ListOptions{ + Namespace: namespace, + }); err != nil { + return fmt.Errorf("failed to list pods: %w", err) + } + + knownCardResources := r.getGPUCardResources() + for i := range podList.Items { + pod := &podList.Items[i] + if pod.Spec.NodeName == "" || + pod.Status.Phase == corev1.PodSucceeded && + time.Since(pod.Status.StartTime.Time) > 1*time.Minute { + continue + } + podResNamed := resources.NewResourceNamed(pod) + podResNamed.SetInstanceParent(instances) + resNamed[podResNamed.String()] = podResNamed + if resUsed[podResNamed.String()] == nil { + resUsed[podResNamed.String()] = initResources() + } + usesGPU := podUsesGPU(pod, knownCardResources) + var aliasKey string + var cardResource corev1.ResourceName + if usesGPU { + var err error + aliasKey, cardResource, err = r.getGPUConfigForNode(pod.Spec.NodeName) + if err != nil { + r.Error( + err, + "get gpu config failed", + "pod", + pod.Name, + "namespace", + pod.Namespace, + "node", + pod.Spec.NodeName, + ) + } + } + podEphemeralStorage := resource.NewQuantity(0, resource.BinarySI) + // skip pods that do not start for more than 1 minute + skip := pod.Status.Phase != corev1.PodRunning && + (pod.Status.StartTime == nil || time.Since(pod.Status.StartTime.Time) > 1*time.Minute) + for _, container := range pod.Spec.Containers { + // gpu only use limit and not ignore pod pending status + if usesGPU && cardResource != "" { + if gpuRequest, ok := container.Resources.Limits[cardResource]; ok { + if err := r.getGPUResourceUsage( + pod, + aliasKey, + gpuRequest, + resUsed[podResNamed.String()], + ); err != nil { + r.Error(err, "get gpu resource usage failed", "pod", pod.Name) + } + } + } + if skip { + continue + } + if cpuRequest, ok := container.Resources.Limits[corev1.ResourceCPU]; ok { + resUsed[podResNamed.String()][corev1.ResourceCPU].Add(cpuRequest) + } else { + resUsed[podResNamed.String()][corev1.ResourceCPU].Add( + container.Resources.Requests[corev1.ResourceCPU], + ) + } + if memoryRequest, ok := container.Resources.Limits[corev1.ResourceMemory]; ok { + resUsed[podResNamed.String()][corev1.ResourceMemory].Add(memoryRequest) + } else { + resUsed[podResNamed.String()][corev1.ResourceMemory].Add( + container.Resources.Requests[corev1.ResourceMemory], + ) + } + if ephemeralRequest, ok := container.Resources.Limits[corev1.ResourceEphemeralStorage]; ok { + podEphemeralStorage.Add(ephemeralRequest) + } else { + podEphemeralStorage.Add( + container.Resources.Requests[corev1.ResourceEphemeralStorage], + ) + } + } + if !skip && podEphemeralStorage.Cmp(ephemeralStorageChargeThreshold) == 1 { + podEphemeralStorage.Sub(ephemeralStorageChargeThreshold) + resUsed[podResNamed.String()][corev1.ResourceStorage].Add(*podEphemeralStorage) + } + } + return nil +} + +func (r *MonitorReconciler) monitorPVCResourceUsage( + namespace string, + resUsed map[string]map[corev1.ResourceName]*quantity, + resNamed map[string]*resources.ResourceNamed, + instances map[string]struct{}, +) error { + pvcList := &corev1.PersistentVolumeClaimList{} + if err := r.cache.List(context.Background(), pvcList, &client.ListOptions{ + Namespace: namespace, + FieldSelector: fields.OneTermEqualSelector( + resourcecache.PersistentVolumeClaimPhaseKey, + string(corev1.ClaimBound), + ), + }); err != nil { + return fmt.Errorf("failed to list pvc: %w", err) + } + for i := range pvcList.Items { + pvc := &pvcList.Items[i] + if len(pvc.OwnerReferences) > 0 && pvc.OwnerReferences[0].Kind == "BackupRepo" { + continue + } + pvcRes := resources.NewResourceNamed(pvc) + pvcRes.SetInstanceParent(instances) + if resUsed[pvcRes.String()] == nil { + resNamed[pvcRes.String()] = pvcRes + resUsed[pvcRes.String()] = initResources() + } + resUsed[pvcRes.String()][corev1.ResourceStorage].Add( + pvc.Spec.Resources.Requests[corev1.ResourceStorage], + ) + } + return nil +} + +func (r *MonitorReconciler) monitorDatabaseBackupUsage( + namespace string, + resUsed map[string]map[corev1.ResourceName]*quantity, + resNamed map[string]*resources.ResourceNamed, +) error { + backupList := &kbv1alpha1.BackupList{} + if err := r.cache.List(context.Background(), backupList, &client.ListOptions{ + Namespace: namespace, + FieldSelector: fields.OneTermEqualSelector( + resourcecache.BackupPhaseKey, + string(kbv1alpha1.BackupPhaseCompleted), + ), + }); err != nil { + return fmt.Errorf("failed to list backup: %w", err) + } + if len(backupList.Items) == 0 { + return nil + } + for i := range backupList.Items { + backup := &backupList.Items[i] + backupRes := resources.NewResourceNamed(backup) + // fmt.Printf("backup name: %v, backup size: %v, backupRes: %s \n", backupList.Items[i].Name, backupList.Items[i].Status.TotalSize, backupRes.String()) + if resUsed[backupRes.String()] == nil { + resNamed[backupRes.String()] = backupRes + resUsed[backupRes.String()] = initResources() + } + resUsed[backupRes.String()][corev1.ResourceStorage].Add( + resource.MustParse(backup.Status.TotalSize), + ) + } + return nil +} + +// instance is the app instance name +func (r *MonitorReconciler) monitorServiceResourceUsage( + namespace string, + resUsed map[string]map[corev1.ResourceName]*quantity, + resNamed map[string]*resources.ResourceNamed, + instances map[string]struct{}, +) error { + svcList := &corev1.ServiceList{} + if err := r.cache.List(context.Background(), svcList, &client.ListOptions{ + Namespace: namespace, + FieldSelector: fields.OneTermEqualSelector( + resourcecache.ServiceTypeKey, + string(corev1.ServiceTypeNodePort), + ), + }); err != nil { + return fmt.Errorf("failed to list svc: %w", err) + } + for i := range svcList.Items { + svc := &svcList.Items[i] + if len(svc.Spec.Ports) == 0 { + continue + } + port := make(map[int32]struct{}) + for _, svcPort := range svc.Spec.Ports { + port[svcPort.NodePort] = struct{}{} + } + svcRes := resources.NewResourceNamed(svc) + svcRes.SetInstanceParent(instances) + if resUsed[svcRes.String()] == nil { + resNamed[svcRes.String()] = svcRes + resUsed[svcRes.String()] = initResources() + } + // nodeport 1:1000, the measurement is quantity 1000 + resUsed[svcRes.String()][corev1.ResourceServicesNodePorts].Add( + *resource.NewQuantity(int64(1000*len(port)), resource.BinarySI), + ) + } + return nil +} + +func (r *MonitorReconciler) getResourceUsed( + podResource map[corev1.ResourceName]*quantity, +) (bool, map[uint8]int64) { + used := map[uint8]int64{} + isEmpty := true + for i := range podResource { + if podResource[i].MilliValue() == 0 { + continue + } + isEmpty = false + if pType, ok := r.Properties.StringMap[i.String()]; ok { + used[pType.Enum] = int64( + math.Ceil(float64(podResource[i].MilliValue()) / float64(pType.Unit.MilliValue())), + ) + continue + } + r.Error(errors.New("not found resource type"), "", "resource", i.String()) + } + return isEmpty, used +} + +func (r *MonitorReconciler) monitorObjectStorageUsage( + namespace string, + resMap map[string]map[corev1.ResourceName]*quantity, + namedMap map[string]*resources.ResourceNamed, +) { + username := config.GetUserNameByNamespace(namespace) + if r.currentObjectMetrics == nil || r.currentObjectMetrics[username].Usage == nil { + return + } + for bucket, usage := range r.currentObjectMetrics[username].Usage { + if bucket == "" || usage <= 0 { + continue + } + objStorageNamed := resources.NewObjStorageResourceNamed(bucket) + namedMap[objStorageNamed.String()] = objStorageNamed + if _, ok := resMap[objStorageNamed.String()]; !ok { + resMap[objStorageNamed.String()] = initResources() + } + resMap[objStorageNamed.String()][corev1.ResourceStorage].Add( + *resource.NewQuantity(usage, resource.BinarySI), + ) + } +} + +func (r *MonitorReconciler) monitorObjectStorageTraffic() error { + if r.currentObjectMetrics == nil { + return nil + } + var objTraffic []*types.ObjectStorageTraffic + now := time.Now().UTC() + for user, metric := range r.currentObjectMetrics { + if len(metric.Sent) == 0 { + continue + } + for bucket, m := range metric.Sent { + sent := int64(0) + if r.lastObjectMetrics != nil && r.lastObjectMetrics[user].Sent != nil { + if _, ok := r.lastObjectMetrics[user].Sent[bucket]; ok { + if m == -1 { + r.currentObjectMetrics[user].Sent[bucket] = r.lastObjectMetrics[user].Sent[bucket] + m = r.lastObjectMetrics[user].Sent[bucket] + } else { + ss := m - r.lastObjectMetrics[user].Sent[bucket] + if ss > 0 { + sent = ss + } + } + } + } + objTraffic = append(objTraffic, &types.ObjectStorageTraffic{ + Time: now, + User: user, + Bucket: bucket, + TotalSent: m, + Sent: sent, + }) + } + } + if len(objTraffic) != 0 { + if err := r.DBClient.SaveObjTraffic(objTraffic...); err != nil { + return fmt.Errorf("failed to save object storage traffic: %w", err) + } + } + return nil +} + +func (r *MonitorReconciler) MonitorTrafficUsed(startTime, endTime time.Time) error { + logger.Info( + "start getTrafficUsed", + "startTime", + startTime.Format(time.RFC3339), + "endTime", + endTime.Format(time.RFC3339), + ) + execTime := time.Now().UTC() + if r.TrafficClient != nil { + if err := r.monitorPodTrafficUsed(startTime, endTime); err != nil { + r.Error(err, "failed to monitor pod traffic used") + } + } + if r.ObjStorageClient != nil { + if err := r.monitorObjectStorageTrafficUsed(startTime, endTime); err != nil { + r.Error(err, "failed to monitor object storage traffic used") + } + } + r.Info( + "success to monitor pod traffic used", + "startTime", + startTime.Format(time.RFC3339), + "endTime", + endTime.Format(time.RFC3339), + "execTime", + time.Since(execTime).String(), + ) + return nil +} + +func (r *MonitorReconciler) monitorObjectStorageTrafficUsed(startTime, endTime time.Time) error { + buckets, err := r.DBClient.GetTimeObjBucketBucket(startTime, endTime) + if err != nil { + return fmt.Errorf("failed to get object storage buckets: %w", err) + } + r.Info("object storage buckets", "buckets len", len(buckets)) + wg, _ := errgroup.WithContext(context.Background()) + wg.SetLimit(10) + for i := range buckets { + bucket := buckets[i] + if !strings.Contains(bucket, "-") { + continue + } + wg.Go(func() error { + return r.handlerObjectStorageTrafficUsed(startTime, endTime, bucket) + }) + } + return wg.Wait() +} + +func (r *MonitorReconciler) handlerObjectStorageTrafficUsed( + startTime, endTime time.Time, + bucket string, +) error { + bytes, err := r.DBClient.HandlerTimeObjBucketSentTraffic(startTime, endTime, bucket) + if err != nil { + return fmt.Errorf("failed to get object storage flow: %w", err) + } + // Because the obtained traffic includes traffic communicating with the controller, filter out traffic smaller than 1 MB + if bytes < 1024*1024 { + return nil + } + unit := r.Properties.StringMap[resources.ResourceNetwork].Unit + used := int64( + math.Ceil( + float64( + resource.NewQuantity(bytes, resource.BinarySI).MilliValue(), + ) / float64( + unit.MilliValue(), + ), + ), + ) + + namespace := "ns-" + strings.SplitN(bucket, "-", 2)[0] + ro := resources.Monitor{ + Category: namespace, + Name: bucket, + Used: map[uint8]int64{r.Properties.StringMap[resources.ResourceNetwork].Enum: used}, + Time: endTime.Add(-1 * time.Minute), + Type: resources.AppType[resources.ObjectStorage], + } + r.Info("object storage traffic used", "monitor", ro) + err = r.DBClient.InsertMonitor(context.Background(), &ro) + if err != nil { + return fmt.Errorf("failed to insert monitor: %w", err) + } + return nil +} + +func (r *MonitorReconciler) monitorPodTrafficUsed(startTime, endTime time.Time) error { + monitors, err := r.DBClient.GetDistinctMonitorCombinations(startTime, endTime) + if err != nil { + return fmt.Errorf("failed to get distinct monitor combinations: %w", err) + } + r.Info("distinct monitor combinations", "monitors len", len(monitors)) + wg, _ := errgroup.WithContext(context.Background()) + wg.SetLimit(100) + for i := range monitors { + monitor := monitors[i] + wg.Go(func() error { + return r.handlerTrafficUsed(startTime, endTime, monitor) + }) + } + return wg.Wait() +} + +func (r *MonitorReconciler) handlerTrafficUsed( + startTime, endTime time.Time, + monitor resources.Monitor, +) error { + bytes, err := r.TrafficClient.GetTrafficSentBytes( + startTime, + endTime, + monitor.Category, + monitor.Type, + monitor.Name, + ) + if err != nil { + return fmt.Errorf("failed to get traffic sent bytes: %w", err) + } + unit := r.Properties.StringMap[resources.ResourceNetwork].Unit + used := int64( + math.Ceil( + float64( + resource.NewQuantity(bytes, resource.BinarySI).MilliValue(), + ) / float64( + unit.MilliValue(), + ), + ), + ) + if used == 0 { + return nil + } + // logger.Info("traffic used ", "monitor", monitor, "used", used, "unit", unit, "bytes", bytes) + ro := resources.Monitor{ + Category: monitor.Category, + Name: monitor.Name, + Used: map[uint8]int64{r.Properties.StringMap[resources.ResourceNetwork].Enum: used}, + Time: endTime.Add(-1 * time.Minute), + Type: monitor.Type, + } + err = r.DBClient.InsertMonitor(context.Background(), &ro) + if err != nil { + return fmt.Errorf("failed to insert monitor: %w", err) + } + return nil +} + +func (r *MonitorReconciler) refreshGPUConfig(ctx context.Context) error { + configmap := &corev1.ConfigMap{} + if err := r.cache.Get(ctx, client.ObjectKey{ + Namespace: gpu.NodeInfoConfigmapNamespace, + Name: gpu.NodeInfoConfigmapName, + }, configmap); err != nil { + return err + } + aliasRaw := strings.TrimSpace(configmap.Data[gpuAliasConfigKey]) + gpuRaw := strings.TrimSpace(configmap.Data[gpuInfoConfigKey]) + if aliasRaw == "" || gpuRaw == "" { + return fmt.Errorf( + "gpu configmap %s/%s missing data", + gpu.NodeInfoConfigmapNamespace, + gpu.NodeInfoConfigmapName, + ) + } + aliasMap := make(map[string]gpuAliasConfig) + if err := json.Unmarshal([]byte(aliasRaw), &aliasMap); err != nil { + return fmt.Errorf("unmarshal gpu alias failed: %w", err) + } + gpuMap := make(map[string]gpuNodeConfig) + if err := json.Unmarshal([]byte(gpuRaw), &gpuMap); err != nil { + return fmt.Errorf("unmarshal gpu info failed: %w", err) + } + aliasCard := make(map[string]corev1.ResourceName, len(aliasMap)) + for aliasKey, alias := range aliasMap { + if alias.Resource.Card == "" { + continue + } + aliasCard[aliasKey] = corev1.ResourceName(alias.Resource.Card) + } + nodeAlias := make(map[string]string, len(gpuMap)) + for nodeName, info := range gpuMap { + ref := strings.TrimSpace(info.Ref) + if ref == "" { + ref = strings.TrimSpace(info.Product) + } + if ref == "" { + continue + } + nodeAlias[nodeName] = ref + } + r.gpuMutex.Lock() + r.gpuAliasCard = aliasCard + r.gpuNodeAlias = nodeAlias + r.gpuMutex.Unlock() + return nil +} + +func (r *MonitorReconciler) getGPUConfigForNode( + nodeName string, +) (string, corev1.ResourceName, error) { + r.gpuMutex.RLock() + aliasKey, ok := r.gpuNodeAlias[nodeName] + cardResource := r.gpuAliasCard[aliasKey] + r.gpuMutex.RUnlock() + if ok && cardResource != "" { + return aliasKey, cardResource, nil + } + if err := r.refreshGPUConfig(context.Background()); err != nil { + return "", "", err + } + r.gpuMutex.RLock() + aliasKey, ok = r.gpuNodeAlias[nodeName] + cardResource = r.gpuAliasCard[aliasKey] + r.gpuMutex.RUnlock() + if !ok || cardResource == "" { + return "", "", fmt.Errorf("node %s not found gpu config", nodeName) + } + return aliasKey, cardResource, nil +} + +func (r *MonitorReconciler) getGPUCardResources() []corev1.ResourceName { + r.gpuMutex.RLock() + defer r.gpuMutex.RUnlock() + cardResources := make([]corev1.ResourceName, 0, len(r.gpuAliasCard)) + for _, card := range r.gpuAliasCard { + if card == "" { + continue + } + cardResources = append(cardResources, card) + } + return cardResources +} + +func podUsesGPU(pod *corev1.Pod, cardResources []corev1.ResourceName) bool { + if len(cardResources) == 0 { + return false + } + for _, container := range pod.Spec.Containers { + for _, card := range cardResources { + if _, ok := container.Resources.Limits[card]; ok { + return true + } + } + } + return false +} + +func (r *MonitorReconciler) getGPUResourceUsage( + pod *corev1.Pod, + aliasKey string, + gpuReq resource.Quantity, + rs map[corev1.ResourceName]*quantity, +) (err error) { + if aliasKey == "" { + return errors.New("gpu alias is empty") + } + gpuResource := resources.NewGpuResource(aliasKey) + if _, ok := rs[gpuResource]; !ok { + rs[gpuResource] = initGpuResources() + } + logger.Info( + "gpu request", + "pod", + pod.Name, + "namespace", + pod.Namespace, + "gpu req", + gpuReq.String(), + "node", + pod.Spec.NodeName, + "gpu alias", + aliasKey, + ) + rs[gpuResource].Add(gpuReq) + return nil +} + +func initResources() (rs map[corev1.ResourceName]*quantity) { + rs = make(map[corev1.ResourceName]*quantity) + rs[resources.ResourceGPU] = initGpuResources() + rs[corev1.ResourceCPU] = &quantity{ + Quantity: resource.NewQuantity(0, resource.DecimalSI), + detail: "", + } + rs[corev1.ResourceMemory] = &quantity{ + Quantity: resource.NewQuantity(0, resource.BinarySI), + detail: "", + } + rs[corev1.ResourceStorage] = &quantity{ + Quantity: resource.NewQuantity(0, resource.BinarySI), + detail: "", + } + rs[resources.ResourceNetwork] = &quantity{ + Quantity: resource.NewQuantity(0, resource.BinarySI), + detail: "", + } + rs[corev1.ResourceServicesNodePorts] = &quantity{ + Quantity: resource.NewQuantity(0, resource.DecimalSI), + detail: "", + } + return rs +} + +func initGpuResources() *quantity { + return &quantity{Quantity: resource.NewQuantity(0, resource.DecimalSI), detail: ""} +} + +func (r *MonitorReconciler) DropMonitorCollectionOlder() error { + return r.DBClient.DropMonitorCollectionsOlderThan(30) +} diff --git a/controllers/resources/controllers/monitor_controller_test.go b/controllers/resources/controllers/monitor_controller_test.go new file mode 100644 index 000000000000..59ca1fac3c03 --- /dev/null +++ b/controllers/resources/controllers/monitor_controller_test.go @@ -0,0 +1,15 @@ +// Copyright © 2023 sealos. +// +// 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 controllers diff --git a/controllers/resources/controllers/network_controller.go b/controllers/resources/controllers/network_controller.go new file mode 100644 index 000000000000..fb0beede7a19 --- /dev/null +++ b/controllers/resources/controllers/network_controller.go @@ -0,0 +1,485 @@ +/* +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 controllers + +import ( + "context" + stderrors "errors" + "fmt" + "reflect" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// NetworkReconciler reconciles Namespace, Ingress, and Service objects to manage network traffic +type NetworkReconciler struct { + Client client.Client + Log logr.Logger +} + +const ( + NetworkStatusAnnoKey = "network.sealos.io/status" + NetworkSuspend = "Suspend" + NetworkResume = "Resume" + NetworkResumeCompleted = "ResumeCompleted" + NodePortLabelKey = "network.sealos.io/original-nodeport" + IngressClassKey = "kubernetes.io/ingress.class" + + Disable = "disable" + True = "true" +) + +//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;update;patch + +func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := r.Log.WithValues("Namespace", req.Namespace, "Name", req.NamespacedName) + + logger.Info("Reconciling Network") + // Fetch the namespace + ns := corev1.Namespace{} + keyObj := client.ObjectKey{Name: req.Namespace} + if req.Namespace == "" && req.Name != "" { + keyObj = client.ObjectKey{Name: req.Name} + } + if err := r.Client.Get(ctx, keyObj, &ns); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Skip if namespace is terminating + if ns.Status.Phase == corev1.NamespaceTerminating { + logger.Info("namespace is terminating") + return ctrl.Result{}, nil + } + + // Check network status annotation + networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey] + if !ok { + logger.Info("no network status annotation found") + return ctrl.Result{}, nil + } + + logger.Info("network status", "status", networkStatus) + + // Skip completed state + if networkStatus == NetworkResumeCompleted { + logger.Info("skipping completed network status") + return ctrl.Result{}, nil + } + + switch networkStatus { + case NetworkSuspend: + // If NamespacedName.Namespace is empty, then req is the namespace itself, and req.namespacedname.name is the Name of the namespace + if req.Namespace == "" { + // Handle namespace suspension + if err := r.suspendNetworkResources(ctx, req.Name); err != nil { + logger.Error(err, "failed to suspend network resources") + return ctrl.Result{}, err + } + break + } + if err := r.handleResource(ctx, req.NamespacedName, ns); err != nil { + logger.Error(err, "failed to handle resource") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + case NetworkResume: + namespace := req.Namespace + if req.Namespace == "" { + namespace = req.Name + } + // Handle namespace resumption + if err := r.resumeNetworkResources(ctx, namespace); err != nil { + logger.Error(err, "failed to resume network resources") + return ctrl.Result{}, err + } + // Update namespace status + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[NetworkStatusAnnoKey] = NetworkResumeCompleted + if err := r.Client.Update(ctx, &ns); err != nil { + logger.Error(err, "failed to update namespace network status to ResumeCompleted") + return ctrl.Result{}, err + } + default: + logger.Error(stderrors.New("unknown network status"), "", "status", networkStatus) + } + + return ctrl.Result{}, nil +} + +func (r *NetworkReconciler) handleResource( + ctx context.Context, + key client.ObjectKey, + ns corev1.Namespace, +) error { + // Only process resources in suspended namespaces + networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey] + if !ok || networkStatus != NetworkSuspend { + return nil + } + + // Try fetching as Ingress + ingress := networkingv1.Ingress{} + if err := r.Client.Get(ctx, key, &ingress); err == nil { + if ingress.Annotations == nil { + ingress.Annotations = make(map[string]string) + } + if ingress.Annotations[IngressClassKey] != Disable { + ingress.Annotations[IngressClassKey] = Disable + if err := r.Client.Update(ctx, &ingress); err != nil { + return fmt.Errorf("failed to suspend ingress %s: %w", key.Name, err) + } + r.Log.V(1).Info("Suspended ingress", "name", key.Name) + } + return nil + } else if !errors.IsNotFound(err) { + return fmt.Errorf("failed to get ingress %s: %w", key.Name, err) + } + + // Try fetching as Service + svc := corev1.Service{} + if err := r.Client.Get(ctx, key, &svc); err == nil { + if svc.Spec.Type == corev1.ServiceTypeNodePort && + (svc.Labels == nil || svc.Labels[NodePortLabelKey] != True) { + if svc.Labels == nil { + svc.Labels = make(map[string]string) + } + svc.Labels[NodePortLabelKey] = True + svc.Spec.Type = corev1.ServiceTypeClusterIP + if err := r.Client.Update(ctx, &svc); err != nil { + return fmt.Errorf("failed to suspend service %s: %w", key.Name, err) + } + r.Log.V(1).Info("Suspended service", "name", key.Name) + } + return nil + } else if !errors.IsNotFound(err) { + return fmt.Errorf("failed to get service %s: %w", key.Name, err) + } + + return nil +} + +func (r *NetworkReconciler) suspendNetworkResources(ctx context.Context, namespace string) error { + // Suspend Ingresses + ingressList := networkingv1.IngressList{} + if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list ingresses in namespace %s: %w", namespace, err) + } + for _, ingress := range ingressList.Items { + if ingress.Annotations == nil { + ingress.Annotations = make(map[string]string) + } + if ingress.Annotations[IngressClassKey] != Disable { + ingress.Annotations[IngressClassKey] = Disable + if err := r.Client.Update(ctx, &ingress); err != nil { + return fmt.Errorf("failed to suspend ingress %s: %w", ingress.Name, err) + } + r.Log.V(1).Info("Suspended ingress", "name", ingress.Name) + } + } + + // Suspend NodePort Services + serviceList := corev1.ServiceList{} + if err := r.Client.List(ctx, &serviceList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list services in namespace %s: %w", namespace, err) + } + for _, svc := range serviceList.Items { + if svc.Spec.Type != corev1.ServiceTypeNodePort { + continue + } + if svc.Labels == nil { + svc.Labels = make(map[string]string) + } + svc.Labels[NodePortLabelKey] = True + svc.Spec.Type = corev1.ServiceTypeClusterIP + if err := r.Client.Update(ctx, &svc); err != nil { + return fmt.Errorf("failed to suspend service %s: %w", svc.Name, err) + } + r.Log.V(1).Info("Suspended service", "name", svc.Name) + } + + return nil +} + +func (r *NetworkReconciler) resumeNetworkResources(ctx context.Context, namespace string) error { + // Resume Ingresses + ingressList := networkingv1.IngressList{} + if err := r.Client.List(ctx, &ingressList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list ingresses in namespace %s: %w", namespace, err) + } + for _, ingress := range ingressList.Items { + if ingress.Annotations == nil || ingress.Annotations[IngressClassKey] != Disable { + continue + } + ingress.Annotations[IngressClassKey] = "nginx" + if err := r.Client.Update(ctx, &ingress); err != nil { + return fmt.Errorf("failed to resume ingress %s: %w", ingress.Name, err) + } + r.Log.V(1).Info("Resumed ingress", "name", ingress.Name) + } + + // Resume NodePort Services + serviceList := corev1.ServiceList{} + if err := r.Client.List(ctx, &serviceList, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("failed to list services in namespace %s: %w", namespace, err) + } + for _, svc := range serviceList.Items { + if svc.Labels == nil || svc.Labels[NodePortLabelKey] != True { + continue + } + svc.Spec.Type = corev1.ServiceTypeNodePort + delete(svc.Labels, NodePortLabelKey) + if err := r.Client.Update(ctx, &svc); err != nil { + return fmt.Errorf("failed to resume service %s: %w", svc.Name, err) + } + r.Log.V(1).Info("Resumed service", "name", svc.Name) + } + + return nil +} + +// SuspendedNamespaceHandler enqueues requests for Ingress and Service objects only in suspended namespaces +type SuspendedNamespaceHandler struct { + Client client.Client + Logger logr.Logger +} + +func (e *SuspendedNamespaceHandler) Create( + ctx context.Context, + evt event.TypedCreateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], +) { + if isNil(evt.Object) { + e.Logger.Error(nil, "CreateEvent received with no metadata", "event", evt) + return + } + + ns := corev1.Namespace{} + if err := e.Client.Get( + ctx, + types.NamespacedName{Name: evt.Object.GetNamespace()}, + &ns, + ); err != nil { + e.Logger.Error(err, "failed to get namespace", "namespace", evt.Object.GetNamespace()) + return + } + + networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey] + if !ok || networkStatus != NetworkSuspend { + return + } + + item := reconcile.Request{NamespacedName: types.NamespacedName{ + Name: evt.Object.GetName(), + Namespace: evt.Object.GetNamespace(), + }} + q.Add(item) +} + +func (e *SuspendedNamespaceHandler) Update( + ctx context.Context, + evt event.TypedUpdateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], +) { + switch { + case !isNil(evt.ObjectNew): + ns := corev1.Namespace{} + if err := e.Client.Get( + ctx, + types.NamespacedName{Name: evt.ObjectNew.GetNamespace()}, + &ns, + ); err != nil { + e.Logger.Error( + err, + "failed to get namespace", + "namespace", + evt.ObjectNew.GetNamespace(), + ) + return + } + + networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey] + if !ok || networkStatus != NetworkSuspend { + return + } + + item := reconcile.Request{NamespacedName: types.NamespacedName{ + Name: evt.ObjectNew.GetName(), + Namespace: evt.ObjectNew.GetNamespace(), + }} + q.Add(item) + case !isNil(evt.ObjectOld): + ns := corev1.Namespace{} + if err := e.Client.Get( + ctx, + types.NamespacedName{Name: evt.ObjectOld.GetNamespace()}, + &ns, + ); err != nil { + e.Logger.Error( + err, + "failed to get namespace", + "namespace", + evt.ObjectOld.GetNamespace(), + ) + return + } + + networkStatus, ok := ns.Annotations[NetworkStatusAnnoKey] + if !ok || networkStatus != NetworkSuspend { + return + } + + item := reconcile.Request{NamespacedName: types.NamespacedName{ + Name: evt.ObjectOld.GetName(), + Namespace: evt.ObjectOld.GetNamespace(), + }} + q.Add(item) + default: + e.Logger.Error(nil, "UpdateEvent received with no metadata", "event", evt) + } +} + +func (e *SuspendedNamespaceHandler) Delete( + ctx context.Context, + evt event.TypedDeleteEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], +) { + // No action needed for delete events +} + +func (e *SuspendedNamespaceHandler) Generic( + ctx context.Context, + evt event.TypedGenericEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], +) { + // No action needed for generic events +} + +func isNil(arg any) bool { + if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Pointer || + v.Kind() == reflect.Interface || + v.Kind() == reflect.Slice || + v.Kind() == reflect.Map || + v.Kind() == reflect.Chan || + v.Kind() == reflect.Func) && v.IsNil()) { + return true + } + return false +} + +func (r *NetworkReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Log = ctrl.Log.WithName("controllers").WithName("Network") + r.Client = mgr.GetClient() + suspendedHandler := &SuspendedNamespaceHandler{Client: r.Client, Logger: r.Log} + + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Namespace{}, builder.WithPredicates(NetworkAnnotationPredicate{})). + Watches( + &networkingv1.Ingress{}, + suspendedHandler, + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + newIngress, ok := e.ObjectNew.(*networkingv1.Ingress) + if !ok { + return false + } + return newIngress.Annotations != nil && + newIngress.Annotations[IngressClassKey] != Disable + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + Watches( + &corev1.Service{}, + suspendedHandler, + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + svc, ok := e.Object.(*corev1.Service) + if !ok { + return false + } + return svc.Spec.Type == corev1.ServiceTypeNodePort + }, + UpdateFunc: func(e event.UpdateEvent) bool { + newSvc, ok := e.ObjectNew.(*corev1.Service) + if !ok { + return false + } + return newSvc.Spec.Type == corev1.ServiceTypeNodePort && + (newSvc.Labels == nil || newSvc.Labels[NodePortLabelKey] != True) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + Complete(r) +} + +// NetworkAnnotationPredicate filters namespace events based on network status annotation changes +type NetworkAnnotationPredicate struct { + predicate.Funcs +} + +func (NetworkAnnotationPredicate) Create(e event.CreateEvent) bool { + networkStatus, ok := e.Object.GetAnnotations()[NetworkStatusAnnoKey] + return ok && networkStatus != NetworkResumeCompleted +} + +func (NetworkAnnotationPredicate) Update(e event.UpdateEvent) bool { + oldObj, ok1 := e.ObjectOld.(*corev1.Namespace) + newObj, ok2 := e.ObjectNew.(*corev1.Namespace) + if !ok1 || !ok2 || newObj.Annotations == nil { + return false + } + oldStatus := oldObj.Annotations[NetworkStatusAnnoKey] + newStatus := newObj.Annotations[NetworkStatusAnnoKey] + return oldStatus != newStatus && newStatus != NetworkResumeCompleted +} + +func (NetworkAnnotationPredicate) Delete(e event.DeleteEvent) bool { + return false +} + +func (NetworkAnnotationPredicate) Generic(e event.GenericEvent) bool { + return false +} diff --git a/controllers/resources/controllers/quota_controller.go b/controllers/resources/controllers/quota_controller.go new file mode 100644 index 000000000000..2c44f6d61bdb --- /dev/null +++ b/controllers/resources/controllers/quota_controller.go @@ -0,0 +1,339 @@ +package controllers + +import ( + "context" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-logr/logr" + "github.com/labring/sealos/controllers/pkg/types" + "github.com/labring/sealos/controllers/pkg/utils/env" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// NamespaceQuotaReconciler reconciles namespace events and adjusts quotas +type NamespaceQuotaReconciler struct { + client.Client + Logger logr.Logger + Scheme *runtime.Scheme + limitExpansionCycle time.Duration + Recorder record.EventRecorder + namespaceLocks map[string]*sync.Mutex +} + +// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;patch +// +kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch;create;update;patch + +// Reconcile handles namespace events +func (r *NamespaceQuotaReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + evt := &corev1.Event{} + if err := r.Get(ctx, req.NamespacedName, evt); err == nil { + if strings.Contains(evt.Message, "exceeded quota") && + (evt.Reason == "FailedCreate" || evt.Reason == "Devbox is exceeded quota") { + // lock + if r.namespaceLocks[evt.Namespace] == nil { + r.namespaceLocks[evt.Namespace] = &sync.Mutex{} + } + // Try to acquire the lock + if r.namespaceLocks[evt.Namespace].TryLock() { + defer r.namespaceLocks[evt.Namespace].Unlock() + } else { + r.Logger.Info("Namespace is already being processed", "namespace", evt.Namespace) + return ctrl.Result{}, nil + } + if err := r.handleQuotaExceeded(ctx, evt); err != nil { + r.Logger.Error( + err, + "failed to handle quota exceeded", + "namespace", + evt.Namespace, + "event", + evt.Message, + ) + return ctrl.Result{RequeueAfter: 5 * time.Minute}, err + } + } + } else if client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// handleQuotaExceeded increases quota by 50% if not already increased +func (r *NamespaceQuotaReconciler) handleQuotaExceeded( + ctx context.Context, + evt *corev1.Event, +) error { + ns := evt.Namespace + quotas := &corev1.ResourceQuotaList{} + if err := r.List(ctx, quotas, client.InNamespace(ns)); err != nil { + return fmt.Errorf("failed to list ResourceQuotas: %w", err) + } + if len(quotas.Items) == 0 || len(quotas.Items) > 1 { + return nil + } + + quota := quotas.Items[0] + newQuota := quota.DeepCopy() + + // Check if quota was recently updated + if quota.Labels != nil { + if lastUpdate, exists := quota.Labels["last-quota-update"]; exists { + lastUpdateTime, err := time.Parse("2006-01-02-15-04-05", lastUpdate) + if err == nil && time.Since(lastUpdateTime) < r.limitExpansionCycle { + r.Logger.Info("Quota was recently updated, skipping update", "namespace", ns) + return nil + } + } + } + + if updateRequired := AdjustQuota(newQuota); updateRequired { + // Add/update label for last update time + if newQuota.Labels == nil { + newQuota.Labels = make(map[string]string) + } + newQuota.Labels["last-quota-update"] = time.Now().UTC().Format("2006-01-02-15-04-05") + + numStr, ok := newQuota.Labels["auto-adapt-quota-num"] + if !ok { + numStr = "0" + } + num, err := strconv.Atoi(numStr) + if err != nil { + r.Logger.Error(err, "failed to parse quota number", "namespace", ns) + num = 0 + } + newQuota.Labels["auto-adapt-quota-num"] = strconv.Itoa(num + 1) + if err := r.Update(ctx, newQuota); err != nil { + return fmt.Errorf("failed to update ResourceQuota %s: %w", newQuota.Name, err) + } + r.Logger.Info( + "Quota updated", + "namespace", + ns, + "history count", + num, + "newQuota", + newQuota.Spec.Hard, + ) + r.Recorder.Event( + evt, + corev1.EventTypeNormal, + "QuotaAdjusted", + fmt.Sprintf("Increased quota by 50%% due to event msg: '%s'", evt.Message), + ) + } + + return nil +} + +// getResourceUsage retrieves the used quantity for a given resource from the ResourceQuota status. +func getResourceUsage( + resourceName corev1.ResourceName, + status corev1.ResourceQuotaStatus, +) (resource.Quantity, error) { + usedQuantity, exists := status.Used[resourceName] + if !exists { + return resource.Quantity{}, fmt.Errorf("resource %s not found in status", resourceName) + } + return usedQuantity, nil +} + +func AdjustQuota(quota *corev1.ResourceQuota) bool { + updateRequired := false + + // Define base and upper limits for each resource + limits := map[corev1.ResourceName]struct { + baseLimit resource.Quantity + upperLimit resource.Quantity + }{ + corev1.ResourceLimitsCPU: { + baseLimit: resource.MustParse("64"), + upperLimit: resource.MustParse("200"), + }, + corev1.ResourceRequestsCPU: { + baseLimit: resource.MustParse("64"), + upperLimit: resource.MustParse("200"), + }, + corev1.ResourceLimitsMemory: { + baseLimit: resource.MustParse("256Gi"), + upperLimit: resource.MustParse("1024Gi"), + }, + corev1.ResourceRequestsMemory: { + baseLimit: resource.MustParse("256Gi"), + upperLimit: resource.MustParse("1024Gi"), + }, + corev1.ResourceRequestsStorage: { + baseLimit: resource.MustParse("300Gi"), + upperLimit: resource.MustParse("800Gi"), + }, + corev1.ResourceServicesNodePorts: { + baseLimit: resource.MustParse("50"), + upperLimit: resource.MustParse("200"), + }, + } + + for resourceName, quantity := range quota.Spec.Hard { + if resourceName == corev1.ResourceLimitsCPU || resourceName == corev1.ResourceRequestsCPU || + resourceName == corev1.ResourceLimitsMemory || resourceName == corev1.ResourceRequestsMemory || + resourceName == corev1.ResourceRequestsStorage || resourceName == corev1.ResourceServicesNodePorts { + limit, exists := limits[resourceName] + if !exists { + continue + } + + // Case 1: Below base limit, double the quota + if quantity.Cmp(limit.baseLimit) < 0 { + newQuantity := resource.MustParse( + fmt.Sprintf("%.0f", float64(quantity.Value())*2), + ) + quota.Spec.Hard[resourceName] = resource.MustParse( + formatQuantity(newQuantity, resourceName), + ) + updateRequired = true + continue + } + + // Case 3: Between base and upper limit, check usage ratio + usedQuantity, err := getResourceUsage(resourceName, quota.Status) + if err != nil { + continue + } + + // Calculate usage ratio + usageRatio := float64(usedQuantity.Value()) / float64(quantity.Value()) + + // Expand quota by 1.5x if usage is above 50% + if usageRatio > 0.5 { + newQuantity := resource.MustParse( + fmt.Sprintf("%.0f", float64(quantity.Value())*1.5), + ) + // Check if new quantity exceeds upper limit + if quantity.Cmp(limit.upperLimit) >= 0 { + newQuantity = resource.MustParse( + fmt.Sprintf("%.0f", float64(quantity.Value())*1.3), + ) + } + // Ensure new quantity does not exceed upper limit + if newQuantity.Cmp(limit.upperLimit) > 0 { + newQuantity = limit.upperLimit + } + quota.Spec.Hard[resourceName] = resource.MustParse( + formatQuantity(newQuantity, resourceName), + ) + updateRequired = true + } + } + } + + return updateRequired +} + +func formatQuantity(quantity resource.Quantity, resourceName corev1.ResourceName) string { + switch resourceName { + case corev1.ResourceLimitsCPU, corev1.ResourceRequestsCPU: + // Use cores if >= 1000m, otherwise use milliCPU + if quantity.MilliValue() >= 1000 { + return fmt.Sprintf("%.2f", float64(quantity.MilliValue())/1000) + } + return quantity.String() + case corev1.ResourceLimitsMemory, corev1.ResourceRequestsMemory: + // Use GiB if >= 1Gi, otherwise use MiB or bytes + if quantity.Value() >= 1<<30 { // 1 GiB + return fmt.Sprintf("%.0fGi", float64(quantity.Value())/(1<<30)) + } else if quantity.Value() >= 1<<20 { // 1 MiB + return fmt.Sprintf("%.0fMi", float64(quantity.Value())/(1<<20)) + } + return quantity.String() + case corev1.ResourceRequestsStorage, corev1.ResourceLimitsEphemeralStorage: + // Use GiB if >= 1Gi, otherwise use MiB + if quantity.Value() >= 1<<30 { // 1 GiB + return fmt.Sprintf("%.0fGi", float64(quantity.Value())/(1<<30)) + } else if quantity.Value() >= 1<<20 { // 1 MiB + return fmt.Sprintf("%.0fMi", float64(quantity.Value())/(1<<20)) + } + return quantity.String() + default: + return quantity.String() + } +} + +// SetupWithManager sets up the controller with the Manager +func (r *NamespaceQuotaReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Logger = ctrl.Log.WithName("namespace-quota-controller") + r.namespaceLocks = make(map[string]*sync.Mutex) + r.limitExpansionCycle = env.GetDurationEnvWithDefault( + "LIMIT_QUOTA_EXPANSION_CYCLE", + 24*time.Hour, + ) + + checkEventPredicate := func(obj client.Object) bool { + eventObj, ok := obj.(*corev1.Event) + if !ok || + (eventObj.Reason != "FailedCreate" && eventObj.Reason != "Devbox is exceeded quota") || + !strings.Contains(eventObj.Message, "exceeded quota") || + strings.Contains(eventObj.Message, "debt-limit0") { + return false + } + + // Get the namespace of the Event + nsName := obj.GetNamespace() + if !strings.HasPrefix(nsName, "ns-") { + return false + } + + // Fetch the namespace object to check annotations + var ns corev1.Namespace + if err := mgr.GetClient(). + Get(context.Background(), client.ObjectKey{Name: nsName}, &ns); err != nil { + r.Logger.Error(err, "Failed to fetch namespace", "namespace", nsName) + return false + } + + annos := ns.GetAnnotations() + if annos != nil { + if status, ok := annos[types.DebtNamespaceAnnoStatusKey]; ok && + status != types.NormalDebtNamespaceAnnoStatus { + return false + } + if _, ok := annos[types.WorkspaceSubscriptionStatusAnnoKey]; ok { + return false + } + } + return true + } + // Predicate for filtering Events based on their namespace and reason + eventPredicate := predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return checkEventPredicate(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return checkEventPredicate(e.ObjectNew) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false // Don't reconcile on delete + }, + GenericFunc: func(e event.GenericEvent) bool { + return checkEventPredicate(e.Object) + }, + } + + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Event{}, builder.WithPredicates(eventPredicate)). + Complete(r) +} diff --git a/controllers/resources/deploy/Kubefile b/controllers/resources/deploy/Kubefile new file mode 100644 index 000000000000..d08056377d44 --- /dev/null +++ b/controllers/resources/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY resources-controller-entrypoint.sh resources-controller-entrypoint.sh + +CMD ["bash resources-controller-entrypoint.sh"] diff --git a/controllers/resources/deploy/README.md b/controllers/resources/deploy/README.md new file mode 100644 index 000000000000..560b7f0b8e61 --- /dev/null +++ b/controllers/resources/deploy/README.md @@ -0,0 +1,86 @@ +# resources-controller + +## 说明 +sealos run 镜像时会在目标节点执行 Kubefile,本镜像通过 Helm 安装/升级 resources controller。 + +## 必填参数 + +**无**(默认自动从集群 ConfigMap 读取 MongoDB 配置) + +## 默认配置读取规则 + +当未指定 `MONGO_URI` 环境变量时,系统将按以下顺序自动读取配置: + +1. **主数据库**:从 `sealos-system` 命名空间下的 `sealos-config` ConfigMap 读取 `databaseMongodbURI` 字段 +2. **流量数据库**:从 `sealos-system` 命名空间下的 `nm-agent-config` ConfigMap 读取 `MONGO_URI` 字段,若不存在则使用主数据库配置 + +> 如需禁用自动配置,可设置 `RESOURCES_ENV_AUTO_CONFIG_ENABLED=false` + +## 如何运行 + +```shell +# 最简配置(自动读取集群 ConfigMap 中的 MongoDB 配置) +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest +``` + +> 默认使用 mongodb 作为存储,sealos-resources 为数据库名 + +## 可选参数 + +- MONGO_URI: MongoDB 连接 URI(包含用户名和密码),若不指定则自动从 ConfigMap 读取 +- TRAFFIC_MONGO_URI: 流量统计 MongoDB 连接 URI,默认与 MONGO_URI 相同 +- RESOURCES_TRAFFICS_SERVICE_CONNECT_ADDRESS: 流量服务连接地址 +- RELEASE_NAMESPACE: Helm 安装命名空间,默认 `resources-system` +- RELEASE_NAME: Helm release 名称,默认 `resources` +- HELM_OPTS: 透传 Helm 参数 +- CHART_PATH: Helm chart 路径,默认 `./charts/resources-controller` +- RESOURCES_ENV_AUTO_CONFIG_ENABLED: 是否自动从 ConfigMap 读取配置,默认 `true` +- RESOURCES_BACKUP_ENABLED: 是否启用备份,默认 `true` + +## 示例 + +```shell +# 1. 最简配置(使用集群默认 MongoDB) +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest + +# 2. 指定自定义 MongoDB +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \ + --env MONGO_URI="mongodb://user:pass@host:27017/resources?authSource=admin" + +# 3. 自定义命名空间和流量 MongoDB +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \ + --env MONGO_URI="mongodb://user:pass@mongo1:27017/resources?authSource=admin" \ + --env TRAFFIC_MONGO_URI="mongodb://user:pass@mongo2:27017/traffic?authSource=admin" \ + --env RELEASE_NAMESPACE="my-resources" + +# 4. 使用 HELM_OPTS 自定义资源配置 +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \ + --env MONGO_URI="mongodb://user:pass@host:27017/resources?authSource=admin" \ + --env HELM_OPTS="--set resources.limits.cpu=2000m --set resources.limits.memory=2048Mi" + +# 5. 完整配置示例 +sealos run ghcr.io/labring/sealos-cloud-resources-controller:latest \ + --env MONGO_URI="mongodb://admin:password123@10.0.0.1:27017/resources?authSource=admin" \ + --env TRAFFIC_MONGO_URI="mongodb://admin:password123@10.0.0.1:27017/traffic?authSource=admin" \ + --env RELEASE_NAMESPACE="production" \ + --env RESOURCES_BACKUP_ENABLED="false" \ + --env HELM_OPTS="--set replicaCount=2" +``` + +## Helm Chart 可配置参数 + +可通过 `HELM_OPTS` 传递以下参数: + +- `replicaCount`: 副本数,默认 `1` +- `image`: 容器镜像,默认 `ghcr.io/labring/sealos-resources-controller:latest` +- `imagePullPolicy`: 镜像拉取策略,默认 `Always` +- `secret.name`: Secret 名称,默认 `mongo-secret` +- `secret.mongoURI`: MongoDB 连接 URI +- `secret.trafficMongoURI`: 流量 MongoDB 连接 URI +- `secret.trafficsServiceConnectAddress`: 流量服务连接地址 +- `metrics.enabled`: 是否启用 metrics,默认 `false` +- `resources.limits.cpu/memory`: CPU/内存限制 +- `resources.requests.cpu/memory`: CPU/内存请求 +- `nodeSelector`: 节点选择器 +- `tolerations`: 容忍度配置 +- `affinity`: 亲和性配置 diff --git a/controllers/resources/deploy/charts/resources-controller/Chart.yaml b/controllers/resources/deploy/charts/resources-controller/Chart.yaml new file mode 100644 index 000000000000..0385a392de60 --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: resources +description: Helm chart for the sealos resources controller +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml b/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml new file mode 100644 index 000000000000..18ddcd967a9e --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/resources-controller-values.yaml @@ -0,0 +1,20 @@ +# Custom values for resources controller helm chart. +# This file contains user-customizable configurations. + +replicaCount: 1 + +resources: + limits: + cpu: 1000m + memory: 1280Mi + requests: + cpu: 10m + memory: 64Mi + +configmap: + # User custom configurations + # Note: These values can be overridden by auto-configured values from sealos-config ConfigMap + enableAutoResourceQuota: "false" + concurrentLimit: "1000" + ephemeralStorageChargeThreshold: "10Gi" + limitQuotaExpansionCycle: "24h" diff --git a/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl b/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl new file mode 100644 index 000000000000..a49424b0c0cf --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "resources.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "resources.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "resources.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "resources.labels" -}} +helm.sh/chart: {{ include "resources.chart" . }} +{{ include "resources.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "resources.selectorLabels" -}} +app.kubernetes.io/name: {{ include "resources.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "resources.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "resources.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml b/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml new file mode 100644 index 000000000000..71cf83f1d8d3 --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/cert.yaml @@ -0,0 +1,27 @@ +{{- if .Values.metrics.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + {{- include "resources.labels" . | nindent 4 }} + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + {{- include "resources.labels" . | nindent 4 }} + name: metrics-certs +spec: + privateKey: + rotationPolicy: Always + dnsNames: + - {{ include "resources.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc + - {{ include "resources.fullname" . }}-controller-manager-metrics-service.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: {{ .Values.metrics.secretName }} +{{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml b/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml new file mode 100644 index 000000000000..a474dfbdecef --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/configmap.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.configmap.name }} + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} +data: + {{- $data := dict + "MONGO_URI" .Values.configmap.mongoURI + "TRAFFIC_MONGO_URI" .Values.configmap.trafficMongoURI + "TRAFFICS_SERVICE_CONNECT_ADDRESS" .Values.configmap.trafficsServiceConnectAddress + "MINIO_ENDPOINT" .Values.configmap.minioEndpoint + "MINIO_AK" .Values.configmap.minioAK + "MINIO_SK" .Values.configmap.minioSK + "MINIO_METRICS_ADDR" .Values.configmap.minioMetricsAddr + "MINIO_METRICS_SECURE" .Values.configmap.minioMetricsSecure + "PROM_URL" .Values.configmap.promURL + "OBJECT_STORAGE_INSTANCE" .Values.configmap.objectStorageInstance + "ENABLE_AUTO_RESOURCE_QUOTA" .Values.configmap.enableAutoResourceQuota + "CONCURRENT_LIMIT" .Values.configmap.concurrentLimit + "EPHEMERAL_STORAGE_CHARGE_THRESHOLD" .Values.configmap.ephemeralStorageChargeThreshold + "LIMIT_QUOTA_EXPANSION_CYCLE" .Values.configmap.limitQuotaExpansionCycle + -}} + {{- if eq (default "overwrite" .Values.configmapMergeStrategy) "preserve" }} + {{- $existing := (lookup "v1" "ConfigMap" .Release.Namespace .Values.configmap.name) }} + {{- if $existing }} + {{- range $k, $v := $existing.data }} + {{- $_ := set $data $k $v }} + {{- end }} + {{- end }} + {{- end }} + {{- range $k, $v := $data }} + {{ $k }}: {{ $v | quote }} + {{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml b/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml new file mode 100644 index 000000000000..af217d624c7b --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/deployment.yaml @@ -0,0 +1,121 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "resources.fullname" . }}-controller-manager + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + checksum/resources-config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "resources.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: manager + command: + - /manager + args: + - --health-probe-bind-address=:8081 + - --leader-elect + {{- if .Values.metrics.enabled }} + - --metrics-secure=true + - --metrics-bind-address=:8443 + {{- else }} + - --metrics-bind-address=127.0.0.1:8080 + {{- end }} + envFrom: + - configMapRef: + name: {{ .Values.configmap.name }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + {{- if .Values.metrics.enabled }} + - containerPort: 8443 + name: https + protocol: TCP + {{- end }} + image: "{{ .Values.image }}" + imagePullPolicy: {{ .Values.imagePullPolicy }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics-certs + mountPath: {{ .Values.metrics.certPath }} + readOnly: true + {{- end }} + terminationGracePeriodSeconds: 10 + affinity: + {{- if .Values.affinity }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: control-plane + operator: In + values: + - controller-manager + topologyKey: kubernetes.io/hostname + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if .Values.metrics.enabled }} + - name: metrics-certs + secret: + secretName: {{ .Values.metrics.secretName }} + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml b/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml new file mode 100644 index 000000000000..efc9366aad5c --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/rbac.yaml @@ -0,0 +1,263 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "resources.fullname" . }}-leader-election-role + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "resources.fullname" . }}-manager-role + labels: + {{- include "resources.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - update + - patch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - get + - list + - watch + - create + - update + - patch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - resourcequotas + verbs: + - get + - list + - watch + - create + - update + - patch + - apiGroups: + - "" + resources: + - resourcequotas/status + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - update + - patch + - apiGroups: + - "" + resources: + - services/status + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + - configmaps + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - app.sealos.io + resources: + - instances + verbs: + - get + - list + - watch + - apiGroups: + - app.sealos.io + resources: + - instances/status + verbs: + - get + - list + - watch + - apiGroups: + - dataprotection.kubeblocks.io + resources: + - backups + verbs: + - get + - list + - watch + - apiGroups: + - dataprotection.kubeblocks.io + resources: + - backups/status + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "resources.fullname" . }}-metrics-reader + labels: + {{- include "resources.labels" . | nindent 4 }} +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "resources.fullname" . }}-proxy-role + labels: + {{- include "resources.labels" . | nindent 4 }} +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "resources.fullname" . }}-leader-election-rolebinding + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "resources.fullname" . }}-leader-election-role +subjects: + - kind: ServiceAccount + name: {{ include "resources.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "resources.fullname" . }}-manager-rolebinding + labels: + {{- include "resources.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "resources.fullname" . }}-manager-role +subjects: + - kind: ServiceAccount + name: {{ include "resources.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "resources.fullname" . }}-proxy-rolebinding + labels: + {{- include "resources.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "resources.fullname" . }}-proxy-role +subjects: + - kind: ServiceAccount + name: {{ include "resources.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/service.yaml b/controllers/resources/deploy/charts/resources-controller/templates/service.yaml new file mode 100644 index 000000000000..52287ca44bfa --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "resources.fullname" . }}-controller-manager-metrics-service + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} +spec: + ports: + - name: https + port: {{ .Values.service.metrics.port }} + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +{{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml b/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml new file mode 100644 index 000000000000..789c2346e15b --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/templates/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "resources.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "resources.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/controllers/resources/deploy/charts/resources-controller/values.yaml b/controllers/resources/deploy/charts/resources-controller/values.yaml new file mode 100644 index 000000000000..18d84d125952 --- /dev/null +++ b/controllers/resources/deploy/charts/resources-controller/values.yaml @@ -0,0 +1,111 @@ +# Default values for resources controller helm chart. + +replicaCount: 1 + +image: ghcr.io/labring/sealos-resources-controller:latest +imagePullPolicy: Always + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "resources" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "resources-controller-manager" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + +service: + metrics: + port: 8443 + +resources: + limits: + cpu: 1000m + memory: 1280Mi + requests: + cpu: 10m + memory: 64Mi + +# ============================================================================ +# Auto-configured values (from sealos-system ConfigMaps) +# ============================================================================ +# The following configmap values are automatically fetched from ConfigMaps +# by the entrypoint script and will override any values set here. +# These are provided as reference defaults. +# +# To override these auto-configured values, use HELM_OPTIONS or modify the +# ConfigMaps directly. +# ============================================================================ +# Sources: +# - sealos-system/sealos-config ConfigMap: databaseMongodbURI +# - sealos-system/nm-agent-config ConfigMap: MONGO_URI (for trafficMongoURI) +# - sealos-system/objectstorage-config ConfigMap: MINIO_ROOT_USER, MINIO_ROOT_PASSWORD +# ============================================================================ + +configmap: + name: "resources-config" + # Database URIs (auto-configured from sealos-config) + mongoURI: "mongodb://mongo:27017/resources" # Auto-fetched from sealos-config.databaseMongodbURI + trafficMongoURI: "mongodb://mongo:27017/traffic" # Auto-fetched from nm-agent-config.MONGO_URI or sealos-config + trafficsServiceConnectAddress: "" + + # MinIO configuration (auto-configured from objectstorage-config) + minioEndpoint: "object-storage.objectstorage-system.svc:80" + minioAK: "" # Auto-fetched from objectstorage-config.MINIO_ROOT_USER + minioSK: "" # Auto-fetched from objectstorage-config.MINIO_ROOT_PASSWORD + minioMetricsAddr: "object-storage.objectstorage-system.svc:80" + minioMetricsSecure: "false" + + # Monitoring configuration + promURL: "http://vmselect-vm-stack-victoria-metrics-k8s-stack.vm.svc:8481/select/0/prometheus/" + objectStorageInstance: "object-storage.objectstorage-system.svc:80" + + # Resource quota configuration + enableAutoResourceQuota: "false" + concurrentLimit: "1000" + ephemeralStorageChargeThreshold: "10Gi" + limitQuotaExpansionCycle: "24h" + +# End of auto-configured values +# ============================================================================ + +extraEnv: [] + +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +metrics: + enabled: false + certPath: /tmp/k8s-metrics-server/metrics-certs + secretName: metrics-server-cert + +volumes: [] +volumeMounts: [] + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/controllers/resources/deploy/resources-controller-entrypoint.sh b/controllers/resources/deploy/resources-controller-entrypoint.sh new file mode 100644 index 000000000000..562b1a611384 --- /dev/null +++ b/controllers/resources/deploy/resources-controller-entrypoint.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e + +HELM_OPTS=${HELM_OPTS:-""} +RELEASE_NAME=${RELEASE_NAME:-"resources-controller"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"resources-system"} +CHART_PATH=${CHART_PATH:-"./charts/resources-controller"} +RESOURCES_ENV_MERGE_STRATEGY=${RESOURCES_ENV_MERGE_STRATEGY:-"overwrite"} +RESOURCES_BACKUP_ENABLED=${RESOURCES_BACKUP_ENABLED:-"true"} +RESOURCES_BACKUP_DIR=${RESOURCES_BACKUP_DIR:-"/tmp/sealos-backup/resources-controller"} + +adopt_namespaced_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl -n "${RELEASE_NAMESPACE}" annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +adopt_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +get_cm_value() { + local namespace="$1" + local name="$2" + local key="$3" + kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true +} + +backup_ns_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" -o yaml >> "${RESOURCES_BACKUP_FILE}" + printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}" + fi +} + +backup_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl get "${kind}" "${name}" -o yaml >> "${RESOURCES_BACKUP_FILE}" + printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}" + fi +} + +backup_resources_resources() { + if [ "${RESOURCES_BACKUP_ENABLED}" != "true" ]; then + return + fi + local ts + ts=$(date +%Y%m%d%H%M%S) + mkdir -p "${RESOURCES_BACKUP_DIR}" + RESOURCES_BACKUP_FILE="${RESOURCES_BACKUP_DIR}/update-${ts}.yaml" + : > "${RESOURCES_BACKUP_FILE}" + + backup_cluster_resource clusterrole resources-manager-role + backup_cluster_resource clusterrole resources-metrics-reader + backup_cluster_resource clusterrole resources-proxy-role + backup_cluster_resource clusterrolebinding resources-manager-rolebinding + backup_cluster_resource clusterrolebinding resources-proxy-rolebinding + + if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + kubectl get namespace "${RELEASE_NAMESPACE}" -o yaml >> "${RESOURCES_BACKUP_FILE}" + printf "\n---\n" >> "${RESOURCES_BACKUP_FILE}" + fi + backup_ns_resource configmap resources-manager-config + backup_ns_resource configmap resources-config + backup_ns_resource service resources-controller-manager-metrics-service + backup_ns_resource deployment resources-controller-manager + backup_ns_resource serviceaccount resources-controller-manager + backup_ns_resource role resources-leader-election-role + backup_ns_resource rolebinding resources-leader-election-rolebinding + backup_ns_resource issuer selfsigned-issuer + backup_ns_resource certificate metrics-certs +} + +# 执行备份 +backup_resources_resources + +HELM_SET_ARGS=() + +AUTO_CONFIG_HELM_OPTS="" + +MONGODB_URI=$(get_cm_value sealos-system sealos-config databaseMongodbURI) +MINIO_USER=$(get_cm_value sealos-system objectstorage-config MINIO_ROOT_USER) +MINIO_PASSWORD=$(get_cm_value sealos-system objectstorage-config MINIO_ROOT_PASSWORD) + +TRAFFIC_MONGO=$(get_cm_value sealos-system nm-agent-config MONGO_URI) +if [ -z "${TRAFFIC_MONGO}" ] && [ -n "${MONGODB_URI}" ]; then + TRAFFIC_MONGO="${MONGODB_URI}" +fi + +[ -n "${MONGODB_URI}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.mongoURI=${MONGODB_URI}" +[ -n "${TRAFFIC_MONGO}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.trafficMongoURI=${TRAFFIC_MONGO}" +[ -n "${MINIO_USER}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.minioAK=${MINIO_USER}" +[ -n "${MINIO_PASSWORD}" ] && AUTO_CONFIG_HELM_OPTS="${AUTO_CONFIG_HELM_OPTS} --set-string configmap.minioSK=${MINIO_PASSWORD}" + +if ! helm status "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + if kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + kubectl label namespace "${RELEASE_NAMESPACE}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate namespace "${RELEASE_NAMESPACE}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi + + adopt_namespaced_resource configmap resources-manager-config + adopt_namespaced_resource configmap resources-config + adopt_namespaced_resource service resources-controller-manager-metrics-service + adopt_namespaced_resource deployment resources-controller-manager + adopt_namespaced_resource serviceaccount resources-controller-manager + adopt_namespaced_resource role resources-leader-election-role + adopt_namespaced_resource rolebinding resources-leader-election-rolebinding + adopt_namespaced_resource issuer selfsigned-issuer + adopt_namespaced_resource certificate metrics-certs + + adopt_cluster_resource clusterrole resources-manager-role + adopt_cluster_resource clusterrole resources-metrics-reader + adopt_cluster_resource clusterrole resources-proxy-role + adopt_cluster_resource clusterrolebinding resources-manager-rolebinding + adopt_cluster_resource clusterrolebinding resources-proxy-rolebinding +fi + +if [ -n "${RESOURCES_ENV_MERGE_STRATEGY}" ]; then + HELM_SET_ARGS+=(--set-string "configmapMergeStrategy=${RESOURCES_ENV_MERGE_STRATEGY}") +fi + +# Prepare values files +SERVICE_NAME="resources-controller" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +# merge all helm_opts +# 1. AUTO_CONFIG_HELM_OPTS (Configuration automatically obtained from ConfigMap) +# 2. HELM_SET_ARGS (parameters set internally in the script) +# 3. HELM_OPTS (the parameter passed by the user via --env, with the highest priority, can override the previous configuration) +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + ${AUTO_CONFIG_HELM_OPTS} \ + "${HELM_SET_ARGS[@]}" \ + ${HELM_OPTS} diff --git a/controllers/resources/go.mod b/controllers/resources/go.mod new file mode 100644 index 000000000000..ccae1e073ac7 --- /dev/null +++ b/controllers/resources/go.mod @@ -0,0 +1,118 @@ +module github.com/labring/sealos/controllers/resources + +go 1.24.0 + +replace ( + k8s.io/api => k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.32.1 + k8s.io/client-go => k8s.io/client-go v0.32.1 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/apecloud/kubeblocks v0.9.3 + github.com/go-logr/logr v1.4.2 + github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089 + github.com/labring/sealos/controllers/pkg v0.0.0-20240715064441-d1193f70675b + github.com/labring/sealos/controllers/user v0.0.0 + github.com/minio/minio-go/v7 v7.0.64 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.36.1 + golang.org/x/sync v0.8.0 + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v12.0.0+incompatible + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matoous/go-nanoid/v2 v2.0.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.6.6 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/prom2json v1.3.3 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.mongodb.org/mongo-driver v1.12.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.4 // indirect + gorm.io/gorm v1.25.5 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + github.com/labring/sealos/controllers/pkg => ../pkg + github.com/labring/sealos/controllers/user => ../user +) diff --git a/controllers/resources/go.sum b/controllers/resources/go.sum new file mode 100644 index 000000000000..e199fc05e8ad --- /dev/null +++ b/controllers/resources/go.sum @@ -0,0 +1,340 @@ +github.com/apecloud/kubeblocks v0.9.3 h1:06hUB4oVZdHfkFg/wez6LJahiJCE+vp5DDyrcXHqCZc= +github.com/apecloud/kubeblocks v0.9.3/go.mod h1:uC7CHg8mTEEhYeJSyiZI1tX2Ep7rpq5qumcJyTIqlCg= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089 h1:UVeQQ61npBKmrT7r6GrLLmCMUSgAY3c5/fFEAjVN+II= +github.com/labring/sealos/controllers/app v0.0.0-20240807070504-eb838607f089/go.mod h1:F/fdFEzWKs0mDmXWDxcaAVPv2SKfl/DXH4X4pimApHE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q= +github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/resources/hack/boilerplate.go.txt b/controllers/resources/hack/boilerplate.go.txt new file mode 100644 index 000000000000..4b31f4ca244e --- /dev/null +++ b/controllers/resources/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2023 sealos. + +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. +*/ \ No newline at end of file diff --git a/controllers/resources/main.go b/controllers/resources/main.go new file mode 100644 index 000000000000..40cd9712cc02 --- /dev/null +++ b/controllers/resources/main.go @@ -0,0 +1,295 @@ +/* +Copyright 2023 sealos. + +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 ( + "context" + "errors" + "flag" + "fmt" + "os" + "time" + + "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + appv1 "github.com/labring/sealos/controllers/app/api/v1" + "github.com/labring/sealos/controllers/pkg/database" + "github.com/labring/sealos/controllers/pkg/database/mongo" + "github.com/labring/sealos/controllers/pkg/objectstorage" + "github.com/labring/sealos/controllers/pkg/resources" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/labring/sealos/controllers/resources/controllers" + resourcecache "github.com/labring/sealos/controllers/resources/controllers/cache" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(appv1.AddToScheme(scheme)) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Cache: resourcecache.Options(), + Client: client.Options{Cache: &client.CacheOptions{ + DisableFor: resourcecache.UncachedObjects(), + }}, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "a63686c3.sealos.io", + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + 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 = resourcecache.SetupInformers(mgr); err != nil { + setupLog.Error(err, "failed to set up resource cache informers") + os.Exit(1) + } + err = controllers.InitIndexField(mgr) + if err != nil { + setupLog.Error(err, "failed to init index field") + os.Exit(1) + } + // if env.GetBoolWithDefault("ENABLE_AUTO_RESOURCE_QUOTA", false) { + // if err = (&controllers.NamespaceQuotaReconciler{ + // Client: mgr.GetClient(), + // Scheme: mgr.GetScheme(), + // Recorder: mgr.GetEventRecorderFor("namespace-quota-controller"), + // }).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "NamespaceQuota") + // os.Exit(1) + // } + //} + // if err = (&controllers.NetworkReconciler{}).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "Network") + // os.Exit(1) + //} + + managerCtx := ctrl.SetupSignalHandler() + go func() { + if err := mgr.Start(managerCtx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } + }() + // ReaderFailOnMissingInformer skips the per-read sync wait, so synchronize all + // explicitly registered informers before the monitor performs its first read. + if !mgr.GetCache().WaitForCacheSync(managerCtx) { + setupLog.Error( + errors.New("resource cache sync did not complete"), + "unable to start monitor", + ) + os.Exit(1) + } + + reconciler, err := controllers.NewMonitorReconciler(mgr) + if err != nil { + setupLog.Error(err, "failed to init monitor reconciler") + os.Exit(1) + } + reconciler.DBClient, err = mongo.NewMongoInterface( + context.Background(), + os.Getenv(database.MongoURI), + ) + if err != nil { + setupLog.Error(err, "failed to init db client") + os.Exit(1) + } + defer func() { + if err := reconciler.DBClient.Disconnect(context.Background()); err != nil { + setupLog.Error(err, "failed to disconnect db client") + } + }() + if trafficURI := os.Getenv(database.TrafficMongoURI); trafficURI != "" { + reconciler.TrafficClient, err = mongo.NewMongoInterface(context.Background(), trafficURI) + if err != nil { + setupLog.Error(err, "failed to init traffic db client") + os.Exit(1) + } + defer func() { + if err := reconciler.TrafficClient.Disconnect(context.Background()); err != nil { + setupLog.Error(err, "failed to disconnect traffic db client") + } + }() + } else { + setupLog.Info("traffic mongo uri not found, please check env: TRAFFIC_MONGO_URI") + } + + err = reconciler.DBClient.InitDefaultPropertyTypeLSWithDefaults() + if err != nil { + setupLog.Error(err, "failed to init property type with defaults") + os.Exit(1) + } + reconciler.Properties = resources.DefaultPropertyTypeLS + const ( + MinioEndpoint = "MINIO_ENDPOINT" + MinioAk = "MINIO_AK" + MinioSk = "MINIO_SK" + PromURL = "PROM_URL" + MinioMetricsAddr = "MINIO_METRICS_ADDR" + MinioMetricsAddrSecure = "MINIO_METRICS_SECURE" + ) + if endpoint, ak, sk, mAddr := os.Getenv( + MinioEndpoint, + ), os.Getenv( + MinioAk, + ), os.Getenv( + MinioSk, + ), os.Getenv( + MinioMetricsAddr, + ); endpoint != "" && + ak != "" && + sk != "" && + mAddr != "" { + reconciler.Info("init minio client") + if reconciler.ObjStorageClient, err = objectstorage.NewOSClient( + endpoint, + ak, + sk, + ); err != nil { + reconciler.Error(err, "failed to new minio client") + os.Exit(1) + } + _, err := reconciler.ObjStorageClient.ListBuckets(context.Background()) + if err != nil { + reconciler.Error(err, "failed to list minio buckets") + os.Exit(1) + } + if reconciler.PromURL = os.Getenv(PromURL); reconciler.PromURL == "" { + reconciler.Info("prometheus url not found, please check env: PROM_URL") + } + secure := env.GetBoolWithDefault(MinioMetricsAddrSecure, false) + reconciler.ObjStorageMetricsClient, err = objectstorage.NewMetricsClient( + mAddr, + ak, + sk, + secure, + ) + if err != nil { + reconciler.Error(err, "failed to new minio metrics client") + os.Exit(1) + } + reconciler.Info( + fmt.Sprintf( + "init minio client with info (endpoint %s, metrics addr %s, metrics addr secure %v) success", + endpoint, + mAddr, + secure, + ), + ) + } else { + reconciler.Info( + "minio info not found, please check env: MINIO_ENDPOINT, MINIO_AK, MINIO_SK, MINIO_METRICS_ADDR", + ) + } + err = reconciler.DBClient.CreateTTLTrafficTimeSeries() + if err != nil { + reconciler.Error(err, "failed to create ttl traffic time series") + } + // timer creates tomorrow's timing table in advance to ensure that tomorrow's table exists + // Execute immediately and then every 24 hours. + time.AfterFunc(time.Until(getNextMidnight()), func() { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + for { + err := reconciler.DBClient.CreateMonitorTimeSeriesIfNotExist( + time.Now().UTC().Add(24 * time.Hour), + ) + if err != nil { + reconciler.Error(err, "failed to create monitor time series") + } + if err := reconciler.DropMonitorCollectionOlder(); err != nil { + reconciler.Error(err, "failed to drop monitor collection") + } + <-ticker.C + } + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := reconciler.StartReconciler(ctx); err != nil { + setupLog.Error(err, "failed to start monitor reconciler") + os.Exit(1) + } +} + +// getNextMidnight returns the next midnight time from now +func getNextMidnight() time.Time { + now := time.Now().UTC() + midnight := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, time.UTC) + return midnight +} diff --git a/controllers/user/.dockerignore b/controllers/user/.dockerignore new file mode 100644 index 000000000000..8e6fac709b69 --- /dev/null +++ b/controllers/user/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +testbin/ diff --git a/controllers/user/.gitignore b/controllers/user/.gitignore new file mode 100644 index 000000000000..3ac36b37de4f --- /dev/null +++ b/controllers/user/.gitignore @@ -0,0 +1,27 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +testbin/* + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +deploy/registry + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +user.env diff --git a/controllers/user/Dockerfile b/controllers/user/Dockerfile new file mode 100644 index 000000000000..7faf10354053 --- /dev/null +++ b/controllers/user/Dockerfile @@ -0,0 +1,22 @@ +# Copyright © 2023 sealos. +# +# 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. + +FROM gcr.io/distroless/static:nonroot +ARG TARGETARCH + +WORKDIR / +USER 65532:65532 + +COPY bin/controller-user-$TARGETARCH /manager +ENTRYPOINT ["/manager"] diff --git a/controllers/user/Makefile b/controllers/user/Makefile new file mode 100644 index 000000000000..cbb35dac05ee --- /dev/null +++ b/controllers/user/Makefile @@ -0,0 +1,150 @@ + +# Image URL to use all building/pushing image targets +IMG ?= ghcr.io/labring/sealos-user-controller:latest +TARGETARCH ?= amd64 +LICENSE_KEY ?= +CONTROLLER_LICENSE_PKG=github.com/labring/sealos/controllers/pkg/license + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.25.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + cp -r config/crd/bases/* deploy/charts/user/crds/ + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: envtest ## Run tests. + @LD_FLAGS=""; \ + [ -n "$(LICENSE_KEY)" ] && LD_FLAGS="-X ${CONTROLLER_LICENSE_PKG}.encryptionKey=${LICENSE_KEY}"; \ + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -race -ldflags "$${LD_FLAGS}" ./... -count=1 + +##@ Build + +.PHONY: build +build: ## Build manager binary. + LD_FLAGS="-s -w"; \ + [ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE_PKG}.encryptionKey=${LICENSE_KEY}"; \ + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: build ## Build docker image with the manager. + mv bin/manager bin/controller-user-${TARGETARCH} + chmod +x bin/controller-user-${TARGETARCH} + docker build -t ${IMG} . --build-arg TARGETARCH=${TARGETARCH} + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: pre-deploy +pre-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > deploy/manifests/deploy.yaml + + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/controllers/user/PROJECT b/controllers/user/PROJECT new file mode 100644 index 000000000000..add8ee105376 --- /dev/null +++ b/controllers/user/PROJECT @@ -0,0 +1,45 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: sealos.io +layout: +- go.kubebuilder.io/v3 +projectName: user +repo: github.com/labring/sealos/controllers/user +resources: +- api: + crdVersion: v1 + controller: true + domain: sealos.io + group: user + kind: User + path: github.com/labring/sealos/controllers/user/api/v1 + version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: user + kind: Operationrequest + path: github.com/labring/sealos/controllers/user/api/v1 + version: v1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: user + kind: DeleteRequest + path: github.com/labring/sealos/controllers/user/api/v1 + version: v1 +version: "3" diff --git a/controllers/user/README.md b/controllers/user/README.md new file mode 100644 index 000000000000..37a31b23e63e --- /dev/null +++ b/controllers/user/README.md @@ -0,0 +1,475 @@ +# add user + +create user: + +```yaml +apiVersion: user.sealos.io/v1 +kind: User +metadata: + name: f8699ded-58d3-432b-a9ff-56568b57a38d +spec: + displayName: cuisongliu + csrExpirationSeconds: 1000000000 + kubeConfigRotateAt: "2026-01-19T14:55:00Z" + +``` + +show all resources: + +```yaml +apiVersion: v1 +items: +- apiVersion: user.sealos.io/v1 + kind: User + metadata: + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.finalizers + generation: 1 + name: f8699ded-58d3-432b-a9ff-56568b57a38d + resourceVersion: "81068" + uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07 + spec: + csrExpirationSeconds: 1000000000 + displayName: cuisongliu +- apiVersion: user.sealos.io/v1 + kind: UserGroup + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.finalizers + generation: 1 + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07 + resourceVersion: "81052" + uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9 +- apiVersion: user.sealos.io/v1 + kind: UserGroupBinding + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07 + resourceVersion: "81057" + uid: 4d0bf140-f31f-4413-a276-4810566eb873 + roleRef: user + subject: + kind: Namespace + name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d + userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +- apiVersion: user.sealos.io/v1 + kind: UserGroupBinding + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: UserGroup + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9 + resourceVersion: "82412" + uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538 + roleRef: user + subject: + apiGroup: user.sealos.io + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +kind: List +metadata: + resourceVersion: "" +``` + +# add userGroup +```yaml +apiVersion: user.sealos.io/v1 +kind: UserGroup +metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.finalizers + generation: 1 + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07 + resourceVersion: "81052" + uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9 +``` +# add user to userGroup +```yaml +apiVersion: user.sealos.io/v1 +kind: UserGroupBinding +metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: UserGroup + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9 + resourceVersion: "82412" + uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538 +roleRef: user +subject: + apiGroup: user.sealos.io + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d +userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +``` +# add namespace to userGroup + +```yaml +apiVersion: user.sealos.io/v1 +kind: UserGroupBinding +metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: dc25db9a-3c07-411a-9d0f-d77a943cfd07 + resourceVersion: "81057" + uid: 4d0bf140-f31f-4413-a276-4810566eb873 +roleRef: user +subject: + kind: Namespace + name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d +userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +``` + +# add manager to userGroup + +```yaml +apiVersion: user.sealos.io/v1 +kind: UserGroupBinding +metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-07T14:06:56Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: UserGroup + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + uid: 2e2a0e63-8510-4527-9cd5-b36085760ed9 + resourceVersion: "82412" + uid: be7d630b-5ecc-4fc8-8dff-9b25a5ea9538 +roleRef: manager +subject: + apiGroup: user.sealos.io + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d +userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +``` + + +# add webhook + +```go + +const ( + UserAnnotationOwnerKey = "user.sealos.io/creator" + UserAnnotationDisplayKey = "user.sealos.io/display-name" +) + +const ( + UgNameLabelKey = "user.sealos.io/usergroup.name" + UgRoleLabelKey = "user.sealos.io/usergroup.role" + UgBindingKindLabelKey = "user.sealos.io/usergroupbinding.kind" + UgBindingNameLabelKey = "user.sealos.io/usergroupbinding.name" +) +``` + +```yaml +apiVersion: v1 +items: +- apiVersion: user.sealos.io/v1 + kind: User + metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"user.sealos.io/v1","kind":"User","metadata":{"annotations":{},"name":"f8699ded-58d3-432b-a9ff-56568b57a38d"},"spec":{"csrExpirationSeconds":1000000000}} + user.sealos.io/display-name: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-14T14:52:02Z" + finalizers: + - sealos.io/user.finalizers + generation: 1 + name: f8699ded-58d3-432b-a9ff-56568b57a38d + resourceVersion: "416042" + uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f + spec: + csrExpirationSeconds: 1000000000 + status: + conditions: + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: user has been initialized + reason: Initialized + status: "True" + type: Initialized + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync kube config successfully + reason: Ready + status: "True" + type: KubeConfigSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync owner ug successfully + reason: Ready + status: "True" + type: OwnerUGSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync owner ug namespace binding successfully + reason: Ready + status: "True" + type: OwnerUGNamespaceBindingSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: User is available now + reason: Ready + status: "True" + type: Ready + kubeConfig: | + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeU1Ea3hOREExTlRRMU5Wb1lEekl4TWpJd09ESXhNRFUxTkRVMVdqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCjBMSndEbndvYzV2YittMkpadVplMnB3dkhWZHpyY21SRkdIaUVmQzg4L2hscjVDWllMSkZkNVc5WjRjUFU1VjMKY1NVNG8wL1J1bmNiclNpNmZrdTNFZ0lya1Z0UUkxUm5Ub3ExTUNFeXk1Q3ZvRlpETkMwSW5wRTY2Nng2S0xpagpRZTFyRXYrYmxZc3B4MnE4Tmhtek1zZWR3ZzZod1ErM2RoQ0U5ck9Vb3UwRHZwQ2VKVjAwazkweVRmZGNTN1c0CnJCbFZ1dUhtNVlVU1M3SEZubTBFMUZQd0RONXhNaG9vZmkyVGE3eTZPNzlBZTl5cDZDZlV4cFRra28vOGNwZUIKKzI3NzRNelYyZmJ4Yys0Ri9McWpRTms2VVpIOTlTT3gvTUZJLzFCZWVqeTA1OGdtQ2U3QUlkbEpCdnFQOWRXbwp0cEtlb1ZyN3Fkc0owSEhTeUhmaVNRSURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVlOW9SQmQ2UVBjNk1VNjcyQjAwOU9nR2VtVjB3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFEYkVDMk9oWkpiUzZldUZmTFNEdG11VlNoc3dFbDRlMFBsWWhnYTBKRHlQSDBUZQpCNmdPWVhZSndrZWxhTWxTUTFFY3ZVa3FHWlhqVUw4Qnl4YzJ0K0pxUDYzQ3ZtZHVzY0NHcWQzTDZTY21UQmZvCjhhbmpENmtoSDZBaHNPVnVsODFYcU9iV2Zxd0diTytGZFgvQi81TDB4bUQ0WUtnNE9iMzdlMWxXb2xJbEJZWmYKMEhqZGNMNjFyYytyWEloeGFPMmlzWHJiSmxzQ0hwTERoVkVKM2o0cjJWdkJFbjRMYWxlZmlYdVdrU3M4RmF0Nwo3N1hwSmhIblA5SHlma0Z1UERsRmpyN1FpcE9wRnJKUXBaeHhmSmREVEhFWUpRV3V0ZytqZjhVZzQweGg5ZS9GCnBNdDlNcTVKSGt3YWVPOEhpSmhaN2dxNjJBQTdseVp4TzBDbnhUYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + server: https://192.168.64.29:6443 + name: sealos + contexts: + - context: + cluster: sealos + user: f8699ded-58d3-432b-a9ff-56568b57a38d + name: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos + current-context: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos + kind: Config + preferences: {} + users: + - name: f8699ded-58d3-432b-a9ff-56568b57a38d + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lSQU9SY1E5Nnc5UHh6MjBTQXNOKzVoa1V3RFFZSktvWklodmNOQVFFTEJRQXcKRlRFVE1CRUdBMVVFQXhNS2EzVmlaWEp1WlhSbGN6QWdGdzB5TWpBNU1UUXhORFEzTURKYUdBOHlNRFUwTURVeQpNekUyTXpNME1sb3dMekV0TUNzR0ExVUVBeE1rWmpnMk9UbGtaV1F0TlRoa015MDBNekppTFdFNVptWXROVFkxCk5qaGlOVGRoTXpoa01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnB4VHVwNHUKR0xzeVQ1a1RBeldrZGlTSUtnY09OTTkxU3FLV25CM2t1ektaKzUvWkdiT1MrWElGV3IrbEdtbkZnMUY0Sng5Ywpvb3I0K09KTnBSWHdpNElkejgvRVllbGU0cmU1bjE3enlvUHFLWFBGemdjM1pOc2d0Ni9WQm5ENVBYaUlvc3pECnI0dC9RQWh3SFA0VjVyTkIrTUJHbjdtMzJaY1UxNTlFak5sOVJjQWhNZHJWKzAvRVZNN1ZGVThVVnhzZWV1c1QKQlBvTDJOanZwek1NY0VTeFpsY3lVS1lpNG5TamtXR3ZOSnZja1BDaTFHaUhUc1lZZE1qKzRFQTZmZnRKRC9IYwprMkVCWVVVQmlWeXRtVUNRSEZIWm5rU0Eva1FwUFpHVll0Y1VTaDB1bmY4cWwyM2k1RUVsL3M1eTRjSmI0SVdmCmY3anpaWk9uTmpBVlVRSURBUUFCbzFZd1ZEQU9CZ05WSFE4QkFmOEVCQU1DQmFBd0V3WURWUjBsQkF3d0NnWUkKS3dZQkJRVUhBd0l3REFZRFZSMFRBUUgvQkFJd0FEQWZCZ05WSFNNRUdEQVdnQlI3MmhFRjNwQTl6b3hUcnZZSApUVDA2QVo2WlhUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFGdHNUOFlaS1dNelEydUIzZVZqeVVaRXM5dEY4Cms3MXk4YVZIV2owcEVFSTgzUlpSUjNMbzg5V3l4a3p6c0N2NjZYSkdKVHRzWFlYdTVjL1FpZ3NHemhGRTB0WlAKeGRIc3Y4SzRVelZablhSc2Y5U3BWcmNTQnR6WHEveEw5bXJ4VCtLcklBTEEzYUxQTUpsRGs2bnNuNnlRc0J2egpvWHBJbWlkblVnQWNydmxMSFFSMTRaSHU3dEFaVmh5blFJTGNJRjhaUnpvYnhpclNPM0ZVUkhpMFZPaHRWMUpaCi93NzhwNVhWZ3NYT2ozUmliWm9qUkdUT2w1b0xlUHBaT0xueHZoYkNaMjBmV2FBaHliMlhNSzZiQmR6ckZUbWUKR2VPdGFUWEt2UTE3cXJZTjVKVTVKQ0JhaWFRbjEyZjA1c3E2U0NtUVJVRWxpVmQ1VERiNS9ydVpZUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBMnB4VHVwNHVHTHN5VDVrVEF6V2tkaVNJS2djT05NOTFTcUtXbkIza3V6S1orNS9aCkdiT1MrWElGV3IrbEdtbkZnMUY0Sng5Y29vcjQrT0pOcFJYd2k0SWR6OC9FWWVsZTRyZTVuMTd6eW9QcUtYUEYKemdjM1pOc2d0Ni9WQm5ENVBYaUlvc3pEcjR0L1FBaHdIUDRWNXJOQitNQkduN20zMlpjVTE1OUVqTmw5UmNBaApNZHJWKzAvRVZNN1ZGVThVVnhzZWV1c1RCUG9MMk5qdnB6TU1jRVN4WmxjeVVLWWk0blNqa1dHdk5KdmNrUENpCjFHaUhUc1lZZE1qKzRFQTZmZnRKRC9IY2syRUJZVVVCaVZ5dG1VQ1FIRkhabmtTQS9rUXBQWkdWWXRjVVNoMHUKbmY4cWwyM2k1RUVsL3M1eTRjSmI0SVdmZjdqelpaT25OakFWVVFJREFRQUJBb0lCQUVGYkpTa1FxMFZYaWNZRQpSSXZjR0t2OVpmdnltZ3V0emlvZkM1bm0wZ0FwTTZIbmV4ZFUrV2E3OE4vZGxiV2MwNkRiMVdrVTFqUCtDa3NXCkZSSjZ5YXhiblNBSXhIUmxPYW5jTlpGdzZMK3R5bTdVSHRubWpHZ0pudkZyanV0YkVSaVFsWURlYU01dGpRQ08KeFQzVnRLU0JDUU1lTUw4RjNmSWVXZXZRd01TK24wUU5TVnQ4R1lLblptVDVJaUQyU2FtcUFLQW1vRW9kc09GRwpDR3JoUm5jL2MydVJGQzhwdms0aFZCWURSTEV1VGhNU3gvY1JRbjhTdTJKaktCNWRndnZXVHVxL3NJSjQzOWwrCncxWUpSZTMreWlXVTF5Y3lCNm5aOG5hT0RRc1RpTnVudkpQd2tvYUF3U3RxZzVYTGtXTXBiUVFKVWo5ZktzaHgKVURpdThVRUNnWUVBOW4xTkE1d0M0clE1RnVQMHZxMXdZKzNsaitSSzZBT0ZVd085VkN0S2FXUGZqT1JsaWxQNwpmSTFpUUc2V2MxbG9kTHVDVjFQZm9icmFyZ3p0M0hJV213dlpnMVRLTldZTVNwTGMzMUhnN0FlMkU2bHZGNnloCmFweU9saDdIRU1NV3Aybm55d0FaSWdWM1EyUWdUNHVIY1lYU3ZpbCtqQmVWZUQ4NzlJM1dXVmtDZ1lFQTR3dW4KUk80QmwyTjByNXJhRXJoTVRjT2QwYXFxV29lYWpEMHhIaXVTRWlRTytqYU1HU2theWNURzQ4UGc3Vk1LaTVPNQpiN1dydVJ3NFVNVFRaK3ZGRTRURjdDR0lBcmJ2bEhPRExxaDRJeWQyY1BJcEtQc2xpZ1F3VDhKUkt0VmtoVXNoCnpPRTgreHgvVXBzQ1NpVU14S3Z0aGZFcW5IK0ZhR3NiYW85aEpMa0NnWUVBcDA2OXE2bUZ1K09nckl6bVgxYlMKWnNIZmhCL3RTRGE3bEhHQlhWUlZHZHdVclVtS01xNTJrVUJXWkcycjZYZlNrd21EbTBydkRKUU1Rdis3cEZvOApSZGx4TnRlaXVVUWZLNjhzQk5sSzFtc1ZoNXNHTVFlWU9Ra3pMMnFNckMvL2ZIdENQbVErcXYwU3lzZDN0d1o2ClZQQUU3OWF5Y3R5L00vT1grcE1iMzJFQ2dZQkRhcjVzVlUvYnFBaUo3QzA4eWNTK2dvdG1Lb3NTL3BrQmMxb1cKWDFHc1dWQ3BNM0UrTjFwZzl6RW5pSTVOYWc4RGl3WFJDZHpIeG4xaVhrOTMyQ1pZdVdBYjBZa1ZaVVNXakFZego0SXByeGRnQmlhWjNLTHZvMDMyVThWb3dvZFNMVDlmbGdpd2RWRVRxUG1UYnAweTNTV1hxRThaWFdmWS9IeTZ3CjRZb3k0UUtCZ0ZvYmJHdVBhQ0QwT044dmk3TnhGZStzbGNEWlBOTWgydXRZRWFwQnpsTkNDNFB2eXFuTjlTdDgKLzhJSmRVc0M3MVNJYU4zZWJWQmUzKzdieFk0MzVwN0hEVXlGdkRQbTJnOHUvelVPbHU5SzNjZXVQRExJcDVoOAp1MG9oaHFFZUN5cHRFcDFBM1hTNGFRaVlsb3NUTkxrenlKRXM5cnh0aWdHT3ZoOXZKVjRtCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + observedCSRExpirationSeconds: 1000000000 + observedGeneration: 1 + phase: Active +- apiVersion: user.sealos.io/v1 + kind: UserGroup + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + user.sealos.io/display-name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-14T14:52:02Z" + finalizers: + - sealos.io/user.group.finalizers + generation: 1 + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f + resourceVersion: "416046" + uid: c0b1faf8-3cc4-4817-bd28-e508eb3ab0f4 + status: + conditions: + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: user group has been initialized + reason: Initialized + status: "True" + type: Initialized + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync owner ug user binding successfully + reason: Ready + status: "True" + type: OwnerUGUserBindingSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: UserGroup is available now + reason: Ready + status: "True" + type: Ready + observedGeneration: 1 + phase: Active +- apiVersion: user.sealos.io/v1 + kind: UserGroupBinding + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-14T14:52:02Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + labels: + user.sealos.io/usergroup.name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + user.sealos.io/usergroup.role: user + user.sealos.io/usergroupbinding.kind: namespace + user.sealos.io/usergroupbinding.name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d + name: ugn-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + uid: 48a7d9e6-5d15-4b26-923c-667d7b8c429f + resourceVersion: "422287" + uid: 3ac62863-d9a1-4159-bc45-fe4c71d691de + roleRef: user + status: + conditions: + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: user group binding has been initialized + reason: Initialized + status: "True" + type: Initialized + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync ug namespace successfully + reason: Ready + status: "True" + type: UGNamespaceSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync ug namespace binding successfully + reason: Ready + status: "True" + type: UGNamespaceBindingSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: UserGroupBinding is available now + reason: Ready + status: "True" + type: Ready + observedGeneration: 1 + phase: Active + subject: + kind: Namespace + name: ns-f8699ded-58d3-432b-a9ff-56568b57a38d + userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +- apiVersion: user.sealos.io/v1 + kind: UserGroupBinding + metadata: + annotations: + user.sealos.io/creator: f8699ded-58d3-432b-a9ff-56568b57a38d + creationTimestamp: "2022-09-14T14:52:02Z" + finalizers: + - sealos.io/user.group.binding.finalizers + generation: 1 + labels: + user.sealos.io/usergroup.name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + user.sealos.io/usergroup.role: user + user.sealos.io/usergroupbinding.kind: user + user.sealos.io/usergroupbinding.name: f8699ded-58d3-432b-a9ff-56568b57a38d + name: ugu-f8699ded-58d3-432b-a9ff-56568b57a38d + ownerReferences: + - apiVersion: user.sealos.io/v1 + blockOwnerDeletion: true + controller: true + kind: UserGroup + name: ug-f8699ded-58d3-432b-a9ff-56568b57a38d + uid: c0b1faf8-3cc4-4817-bd28-e508eb3ab0f4 + resourceVersion: "419386" + uid: b6cbea8a-a248-4bb1-a9ae-30ba39db072f + roleRef: user + status: + conditions: + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: user group binding has been initialized + reason: Initialized + status: "True" + type: Initialized + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync ug user binding by owner successfully + reason: Ready + status: "True" + type: UGUserBindingSyncReadyByOwner + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync ug user binding successfully + reason: Ready + status: "True" + type: UGUserBindingSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: sync ug namespace binding successfully + reason: Ready + status: "True" + type: UGNamespaceBindingSyncReady + - lastHeartbeatTime: "2022-09-14T14:52:00Z" + lastTransitionTime: "2022-09-14T14:52:00Z" + message: UserGroupBinding is available now + reason: Ready + status: "True" + type: Ready + observedGeneration: 1 + phase: Active + subject: + apiGroup: user.sealos.io + kind: User + name: f8699ded-58d3-432b-a9ff-56568b57a38d + userGroupRef: ug-f8699ded-58d3-432b-a9ff-56568b57a38d +kind: List +metadata: + resourceVersion: "" +``` diff --git a/controllers/user/api/v1/deleterequest_types.go b/controllers/user/api/v1/deleterequest_types.go new file mode 100644 index 000000000000..7b4ca3dd1fb2 --- /dev/null +++ b/controllers/user/api/v1/deleterequest_types.go @@ -0,0 +1,61 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeleteRequestSpec defines the desired state of DeleteRequest +type DeleteRequestSpec struct { + User string `json:"user,omitempty"` +} + +// DeleteRequestStatus defines the observed state of DeleteRequest +type DeleteRequestStatus struct { + //+kubebuilder:validation:Enum=Pending;Processing;Completed;Failed + Phase RequestPhase `json:"phase,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +//+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user" +//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// DeleteRequest is the Schema for the deleterequests API +type DeleteRequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeleteRequestSpec `json:"spec,omitempty"` + Status DeleteRequestStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// DeleteRequestList contains a list of DeleteRequest +type DeleteRequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeleteRequest `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeleteRequest{}, &DeleteRequestList{}) +} diff --git a/controllers/user/api/v1/groupversion_info.go b/controllers/user/api/v1/groupversion_info.go new file mode 100644 index 000000000000..cfdf8b013608 --- /dev/null +++ b/controllers/user/api/v1/groupversion_info.go @@ -0,0 +1,72 @@ +/* +Copyright 2022 labring. + +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 v1 contains API Schema definitions for the user v1 API group +// +kubebuilder:object:generate=true +// +groupName=user.sealos.io +package v1 + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "user.sealos.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +const ( + // UserAnnotationCreatorKey refers to the user who created the resource + UserAnnotationCreatorKey = "user.sealos.io/creator" + // UserAnnotationOwnerKey refers to the user who owns the resource + UserAnnotationOwnerKey = "user.sealos.io/owner" + UserLabelOwnerKey = "user.sealos.io/owner" + UserAnnotationDisplayKey = "user.sealos.io/display-name" +) + +const ( + UgNameLabelKey = "user.sealos.io/usergroup.name" + UgRoleLabelKey = "user.sealos.io/usergroup.role" + UgBindingKindLabelKey = "user.sealos.io/usergroupbinding.kind" + UgBindingNameLabelKey = "user.sealos.io/usergroupbinding.name" +) + +func validateAnnotationKeyNotEmpty(meta metav1.ObjectMeta, key string) error { + if meta.Annotations[key] == "" { + return fmt.Errorf("annotation %s not allow empty", key) + } + return nil +} + +func initAnnotationAndLabels(meta metav1.ObjectMeta) metav1.ObjectMeta { + if meta.Annotations == nil { + meta.Annotations = make(map[string]string, 0) + } + if meta.Labels == nil { + meta.Labels = make(map[string]string, 0) + } + return meta +} diff --git a/controllers/user/api/v1/operationrequest_types.go b/controllers/user/api/v1/operationrequest_types.go new file mode 100644 index 000000000000..8b0d2adf2940 --- /dev/null +++ b/controllers/user/api/v1/operationrequest_types.go @@ -0,0 +1,89 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// OperationrequestSpec defines the desired state of Operationrequest +type OperationrequestSpec struct { + // Namespace is the workspace that needs to be operated. + Namespace string `json:"namespace,omitempty"` + User string `json:"user,omitempty"` + // +kubebuilder:validation:Enum=Owner;Manager;Developer + Role RoleType `json:"role,omitempty"` + // +kubebuilder:validation:Enum=Grant;Update;Deprive + Action ActionType `json:"action,omitempty"` +} + +type ActionType string + +const ( + Grant ActionType = "Grant" + Update ActionType = "Update" + Deprive ActionType = "Deprive" +) + +// OperationrequestStatus defines the observed state of Operationrequest +type OperationrequestStatus struct { + // Phase is the recently observed lifecycle phase of operationrequest. + //+kubebuilder:default:=Pending + //+kubebuilder:validation:Enum=Pending;Processing;Completed;Failed + Phase RequestPhase `json:"phase,omitempty"` +} + +type RequestPhase string + +// These are the valid phases of node. +const ( + RequestPending RequestPhase = "Pending" + RequestProcessing RequestPhase = "Processing" + RequestCompleted RequestPhase = "Completed" + RequestFailed RequestPhase = "Failed" +) + +//+kubebuilder:printcolumn:name="Action",type="string",JSONPath=".spec.action" +//+kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.namespace" +//+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user" +//+kubebuilder:printcolumn:name="Role",type="string",JSONPath=".spec.role" +//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Operationrequest is the Schema for the operation requests API +type Operationrequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OperationrequestSpec `json:"spec,omitempty"` + Status OperationrequestStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OperationrequestList contains a list of Operationrequest +type OperationrequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Operationrequest `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Operationrequest{}, &OperationrequestList{}) +} diff --git a/controllers/user/api/v1/operationrequest_webhook.go b/controllers/user/api/v1/operationrequest_webhook.go new file mode 100644 index 000000000000..dff8210dde4a --- /dev/null +++ b/controllers/user/api/v1/operationrequest_webhook.go @@ -0,0 +1,137 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "context" + "errors" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var operationrequestlog = logf.Log.WithName("operationrequest-resource") + +func (r *Operationrequest) SetupWebhookWithManager(mgr ctrl.Manager) error { + m := &ReqMutator{Client: mgr.GetClient()} + v := &ReqValidator{Client: mgr.GetClient()} + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithDefaulter(m). + WithValidator(v). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-user-sealos-io-v1-operationrequest,mutating=true,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=operationrequests,verbs=create;update,versions=v1,name=moperationrequest.kb.io,admissionReviewVersions=v1 +//+kubebuilder:object:generate=false + +type ReqMutator struct { + client.Client +} + +func (r ReqMutator) Default(_ context.Context, obj runtime.Object) error { + req, ok := obj.(*Operationrequest) + if !ok { + return errors.New("obj convert Operationrequest is error") + } + // mutate the request with an owner label + operationrequestlog.Info("mutate", "name", req.Name) + req.ObjectMeta = initAnnotationAndLabels(req.ObjectMeta) + req.Labels[UserLabelOwnerKey] = req.Spec.User + return nil +} + +//+kubebuilder:webhook:path=/validate-user-sealos-io-v1-operationrequest,mutating=false,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=operationrequests,verbs=create;update,versions=v1,name=voperationrequest.kb.io,admissionReviewVersions=v1 +//+kubebuilder:object:generate=false + +type ReqValidator struct { + client.Client +} + +func (r ReqValidator) ValidateCreate( + ctx context.Context, + obj runtime.Object, +) (admission.Warnings, error) { + req, ok := obj.(*Operationrequest) + if !ok { + message := "obj convert Operationrequest is error" + return admission.Warnings{message}, errors.New(message) + } + + // todo check request, _ := admission.RequestFromContext(ctx), request.UserInfo.Username if legal + + // list all requests in the same namespace with a same owner + var reqList OperationrequestList + err := r.List( + ctx, + &reqList, + client.InNamespace(req.Namespace), + client.MatchingLabels{UserLabelOwnerKey: req.Spec.User}, + ) + if client.IgnoreNotFound(err) != nil { + operationrequestlog.Error(err, "list operationrequest error") + return admission.Warnings{"list operationrequest error"}, err + } + + for _, item := range reqList.Items { + if item.Status.Phase != RequestCompleted { + operationrequestlog.Info( + "there is a request not completed, can not create new request", + "name", + item.Name, + "phase", + item.Status.Phase, + ) + message := "there is a request not completed, can not create new request" + return admission.Warnings{message}, errors.New(message) + } + } + return admission.Warnings{}, nil +} + +func (r ReqValidator) ValidateUpdate( + _ context.Context, + oldObj, newObj runtime.Object, +) (admission.Warnings, error) { + // todo check request, _ := admission.RequestFromContext(ctx), request.UserInfo.Username if legal + oldReq, ok := oldObj.(*Operationrequest) + if !ok { + message := "obj convert Operationrequest error" + return admission.Warnings{message}, errors.New(message) + } + newReq, ok := newObj.(*Operationrequest) + if !ok { + message := "obj convert Operationrequest error" + return admission.Warnings{message}, errors.New(message) + } + if oldReq.Spec != newReq.Spec { + message := "operation request spec do not support update" + return admission.Warnings{message}, errors.New(message) + } + return admission.Warnings{}, nil +} + +func (r ReqValidator) ValidateDelete( + _ context.Context, + _ runtime.Object, +) (admission.Warnings, error) { + return admission.Warnings{}, nil +} diff --git a/controllers/user/api/v1/user_types.go b/controllers/user/api/v1/user_types.go new file mode 100644 index 000000000000..78de9bc71899 --- /dev/null +++ b/controllers/user/api/v1/user_types.go @@ -0,0 +1,158 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "errors" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const DefaultCSRExpirationSeconds int32 = 1_000_000_000 + +// NormalizeCSRExpirationSeconds returns the effective expiration duration +// used when issuing credentials. Values below the minimum are preserved by +// the API object and only raised at the point where they are consumed. +func NormalizeCSRExpirationSeconds(value int32) int32 { + if value < DefaultCSRExpirationSeconds { + return DefaultCSRExpirationSeconds + } + return value +} + +// UserSpec defines the desired state of User +type UserSpec struct { + // expirationSeconds is the requested duration of validity of the issued + // kubeconfig credential. The issuer may issue a credential with a different + // validity duration so a client must check the issued credential to determine + // the actual duration. + // + // The minimum effective value for expirationSeconds is 1_000_000_000 seconds. + // + // +optional + //+kubebuilder:default:=1000000000 + CSRExpirationSeconds int32 `json:"csrExpirationSeconds,omitempty"` + // kubeConfigRotateAt 用于手动触发 kubeconfig 轮转。 + // 当字段被设置或更新时,controller 会重新请求 token 并重建 kubeconfig。 + // +optional + KubeConfigRotateAt *metav1.Time `json:"kubeConfigRotateAt,omitempty"` +} +type RoleType string + +const ( + OwnerRoleType RoleType = "Owner" + ManagerRoleType RoleType = "Manager" + DeveloperRoleType RoleType = "Developer" +) + +type UserPhase string + +// These are the valid phases of node. +const ( + UserPending UserPhase = "Pending" + UserUnknown UserPhase = "Unknown" + UserActive UserPhase = "Active" +) + +// UserStatus defines the observed state of User +type UserStatus struct { + // Phase is the recently observed lifecycle phase of user + //+kubebuilder:default:=Unknown + Phase UserPhase `json:"phase,omitempty"` + KubeConfig string `json:"kubeConfig"` + //+kubebuilder:default:=1000000000 + ObservedCSRExpirationSeconds int32 `json:"observedCSRExpirationSeconds,omitempty"` + // ObservedKubeConfigRotateAt 记录已处理的轮转请求时间戳。 + // +optional + ObservedKubeConfigRotateAt *metav1.Time `json:"observedKubeConfigRotateAt,omitempty"` + // KubeConfigRefreshAt is the next time the controller should refresh the + // kubeconfig token. It is persisted so a controller restart keeps the + // token refresh schedule without retaining the kubeconfig payload in cache. + // +optional + KubeConfigRefreshAt *metav1.Time `json:"kubeConfigRefreshAt,omitempty"` + // ObservedKubeConfigSecretUID identifies the Secret bound to the current + // kubeconfig token and detects same-name Secret recreation. + // +optional + ObservedKubeConfigSecretUID string `json:"observedKubeConfigSecretUID,omitempty"` + // The generation observed by the user controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // Conditions contains the different condition statuses for this user. + Conditions []Condition `json:"conditions,omitempty"` +} + +type ConditionType string + +const ( + Initialized ConditionType = "Initialized" + Ready ConditionType = "Ready" +) + +type Condition struct { + Type ConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=ConditionType"` + // Status is the status of the condition. One of True, False, Unknown. + Status v1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"` + // LastHeartbeatTime is the last time this condition was updated. + // +optional + LastHeartbeatTime metav1.Time `json:"lastHeartbeatTime,omitempty" protobuf:"bytes,3,opt,name=lastHeartbeatTime"` + // LastTransitionTime is the last time the condition changed from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"` + // Reason is a (brief) reason for the condition's last status change. + // +optional + Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"` + // Message is a human-readable message indicating details about the last status change. + // +optional + Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +// +kubebuilder:printcolumn:name="ExpirationSeconds",type="integer",JSONPath=".status.observedCSRExpirationSeconds" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// User is the Schema for the users API +type User struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UserSpec `json:"spec,omitempty"` + Status UserStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// UserList contains a list of User +type UserList struct { + metav1.TypeMeta ` json:",inline"` + metav1.ListMeta ` json:"metadata,omitempty"` + Items []User `json:"items"` +} + +func init() { + SchemeBuilder.Register(&User{}, &UserList{}) +} + +func (r *User) validateCSRExpirationSeconds() error { + if r.Spec.CSRExpirationSeconds == 0 { + return errors.New("csrExpirationSeconds is not allowed to be 0") + } + return nil +} diff --git a/controllers/user/api/v1/user_webhook.go b/controllers/user/api/v1/user_webhook.go new file mode 100644 index 000000000000..44563ce21a32 --- /dev/null +++ b/controllers/user/api/v1/user_webhook.go @@ -0,0 +1,162 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "context" + "errors" + "fmt" + + "github.com/labring/sealos/controllers/user/pkg/licensegate" + "github.com/labring/sealos/controllers/user/pkg/usercount" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var ( + userlog = logf.Log.WithName("user-webhook") + userWebhookCount *usercount.Counter +) + +const ( + licenseLimitErrorCode = 40301 + userCountLimitErrorCode = 40302 +) + +func buildLicenseLimitErrorMessage() string { + return fmt.Sprintf( + "{\"code\":%d,\"message\":\"license inactive: user limit reached\"}", + licenseLimitErrorCode, + ) +} + +func buildUserCountLimitErrorMessage() string { + return fmt.Sprintf( + "{\"code\":%d,\"message\":\"license active: user limit reached\"}", + userCountLimitErrorCode, + ) +} + +func (r *User) SetupWebhookWithManager( + mgr ctrl.Manager, + userCounter *usercount.Counter, +) error { + if userCounter == nil { + return errors.New("user webhook count cache is not initialized") + } + userWebhookCount = userCounter + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithDefaulter(r). + WithValidator(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-user-sealos-io-v1-user,mutating=true,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=users,verbs=create;update,versions=v1,name=muser.kb.io,admissionReviewVersions=v1 + +var _ webhook.CustomDefaulter = &User{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *User) Default(ctx context.Context, obj runtime.Object) error { + user, ok := obj.(*User) + if !ok { + return errors.New("obj convert User is error") + } + userlog.Info("default", "name", user.Name) + user.ObjectMeta = initAnnotationAndLabels(user.ObjectMeta) + if user.Spec.CSRExpirationSeconds == 0 { + user.Spec.CSRExpirationSeconds = DefaultCSRExpirationSeconds + } + if user.Annotations[UserAnnotationDisplayKey] == "" { + user.Annotations[UserAnnotationDisplayKey] = user.Name + } + if user.Annotations[UserAnnotationOwnerKey] == "" { + user.Annotations[UserAnnotationOwnerKey] = user.Name + } + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-user-sealos-io-v1-user,mutating=false,failurePolicy=fail,sideEffects=None,groups=user.sealos.io,resources=users,verbs=create;update,versions=v1,name=vuser.kb.io,admissionReviewVersions=v1,timeoutSeconds=30 + +var _ webhook.CustomValidator = &User{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *User) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + user, ok := obj.(*User) + if !ok { + return admission.Warnings{}, errors.New("obj convert User is error") + } + userlog.Info("validate create", "name", user.Name) + if err := user.validateCSRExpirationSeconds(); err != nil { + return admission.Warnings{}, err + } + if userWebhookCount == nil || !userWebhookCount.Initialized() { + return admission.Warnings{}, errors.New("user count cache is not initialized") + } + currentCount := userWebhookCount.Count() + if !licensegate.AllowNewUser(currentCount) { + message := buildLicenseLimitErrorMessage() + if licensegate.HasActiveLicense() { + message = buildUserCountLimitErrorMessage() + } + warnings := admission.Warnings{message} + return warnings, errors.New(message) + } + return admission.Warnings{}, validateAnnotationKeyNotEmpty( + user.ObjectMeta, + UserAnnotationDisplayKey, + ) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *User) ValidateUpdate( + ctx context.Context, + oldObj, newObj runtime.Object, +) (admission.Warnings, error) { + user, ok := newObj.(*User) + if !ok { + return admission.Warnings{}, errors.New("obj convert User is error") + } + userlog.Info("validate update", "name", user.Name) + if err := user.validateCSRExpirationSeconds(); err != nil { + return admission.Warnings{}, err + } + return admission.Warnings{}, validateAnnotationKeyNotEmpty( + user.ObjectMeta, + UserAnnotationDisplayKey, + ) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *User) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + user, ok := obj.(*User) + if !ok { + return admission.Warnings{}, errors.New("obj convert User is error") + } + userlog.Info("validate delete", "name", user.Name) + return admission.Warnings{}, validateAnnotationKeyNotEmpty( + user.ObjectMeta, + UserAnnotationDisplayKey, + ) +} diff --git a/controllers/user/api/v1/webhook_suite_test.go b/controllers/user/api/v1/webhook_suite_test.go new file mode 100644 index 000000000000..5d84f556c78f --- /dev/null +++ b/controllers/user/api/v1/webhook_suite_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2022 labring. + +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 v1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + "github.com/labring/sealos/controllers/user/pkg/usercount" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Webhook Suite", + []Reporter{}) +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + //nolint:fatcontext + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + LeaderElection: false, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + }) + Expect(err).NotTo(HaveOccurred()) + + userCounter := usercount.NewCounter() + userCounter.MarkInitialized() + err = (&User{}).SetupWebhookWithManager(mgr, userCounter) + Expect(err).NotTo(HaveOccurred()) + + err = (&Operationrequest{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf( + "%s:%d", + webhookInstallOptions.LocalServingHost, + webhookInstallOptions.LocalServingPort, + ) + Eventually(func() error { + conn, err := tls.DialWithDialer( + dialer, + "tcp", + addrPort, + &tls.Config{InsecureSkipVerify: true}, + ) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) +}, 60) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/controllers/user/api/v1/zz_generated.deepcopy.go b/controllers/user/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..bbc894530615 --- /dev/null +++ b/controllers/user/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,329 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2022 labring. + +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 v1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastHeartbeatTime.DeepCopyInto(&out.LastHeartbeatTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeleteRequest) DeepCopyInto(out *DeleteRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequest. +func (in *DeleteRequest) DeepCopy() *DeleteRequest { + if in == nil { + return nil + } + out := new(DeleteRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeleteRequest) 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 *DeleteRequestList) DeepCopyInto(out *DeleteRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeleteRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestList. +func (in *DeleteRequestList) DeepCopy() *DeleteRequestList { + if in == nil { + return nil + } + out := new(DeleteRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeleteRequestList) 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 *DeleteRequestSpec) DeepCopyInto(out *DeleteRequestSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestSpec. +func (in *DeleteRequestSpec) DeepCopy() *DeleteRequestSpec { + if in == nil { + return nil + } + out := new(DeleteRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeleteRequestStatus) DeepCopyInto(out *DeleteRequestStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeleteRequestStatus. +func (in *DeleteRequestStatus) DeepCopy() *DeleteRequestStatus { + if in == nil { + return nil + } + out := new(DeleteRequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Operationrequest) DeepCopyInto(out *Operationrequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operationrequest. +func (in *Operationrequest) DeepCopy() *Operationrequest { + if in == nil { + return nil + } + out := new(Operationrequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Operationrequest) 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 *OperationrequestList) DeepCopyInto(out *OperationrequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Operationrequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestList. +func (in *OperationrequestList) DeepCopy() *OperationrequestList { + if in == nil { + return nil + } + out := new(OperationrequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperationrequestList) 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 *OperationrequestSpec) DeepCopyInto(out *OperationrequestSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestSpec. +func (in *OperationrequestSpec) DeepCopy() *OperationrequestSpec { + if in == nil { + return nil + } + out := new(OperationrequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperationrequestStatus) DeepCopyInto(out *OperationrequestStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationrequestStatus. +func (in *OperationrequestStatus) DeepCopy() *OperationrequestStatus { + if in == nil { + return nil + } + out := new(OperationrequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User) DeepCopyInto(out *User) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *User) 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 *UserList) DeepCopyInto(out *UserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]User, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserList. +func (in *UserList) DeepCopy() *UserList { + if in == nil { + return nil + } + out := new(UserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UserList) 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 *UserSpec) DeepCopyInto(out *UserSpec) { + *out = *in + if in.KubeConfigRotateAt != nil { + in, out := &in.KubeConfigRotateAt, &out.KubeConfigRotateAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. +func (in *UserSpec) DeepCopy() *UserSpec { + if in == nil { + return nil + } + out := new(UserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserStatus) DeepCopyInto(out *UserStatus) { + *out = *in + if in.KubeConfigRefreshAt != nil { + in, out := &in.KubeConfigRefreshAt, &out.KubeConfigRefreshAt + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ObservedKubeConfigRotateAt != nil { + in, out := &in.ObservedKubeConfigRotateAt, &out.ObservedKubeConfigRotateAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStatus. +func (in *UserStatus) DeepCopy() *UserStatus { + if in == nil { + return nil + } + out := new(UserStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controllers/user/config/certmanager/certificate.yaml b/controllers/user/config/certmanager/certificate.yaml new file mode 100644 index 000000000000..54c78b15e6b3 --- /dev/null +++ b/controllers/user/config/certmanager/certificate.yaml @@ -0,0 +1,39 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/controllers/user/config/certmanager/kustomization.yaml b/controllers/user/config/certmanager/kustomization.yaml new file mode 100644 index 000000000000..27b9b3c07457 --- /dev/null +++ b/controllers/user/config/certmanager/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/controllers/user/config/certmanager/kustomizeconfig.yaml b/controllers/user/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 000000000000..a6f7e14c39fc --- /dev/null +++ b/controllers/user/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml new file mode 100644 index 000000000000..575311e72193 --- /dev/null +++ b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml @@ -0,0 +1,84 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: deleterequests.user.sealos.io +spec: + group: user.sealos.io + names: + kind: DeleteRequest + listKind: DeleteRequestList + plural: deleterequests + singular: deleterequest + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.user + name: User + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: DeleteRequest is the Schema for the deleterequests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DeleteRequestSpec defines the desired state of DeleteRequest + properties: + user: + type: string + type: object + status: + description: DeleteRequestStatus defines the observed state of DeleteRequest + properties: + phase: + enum: + - Pending + - Processing + - Completed + - Failed + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml new file mode 100644 index 000000000000..cfabdc44c850 --- /dev/null +++ b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml @@ -0,0 +1,110 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: operationrequests.user.sealos.io +spec: + group: user.sealos.io + names: + kind: Operationrequest + listKind: OperationrequestList + plural: operationrequests + singular: operationrequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.action + name: Action + type: string + - jsonPath: .spec.namespace + name: Namespace + type: string + - jsonPath: .spec.user + name: User + type: string + - jsonPath: .spec.role + name: Role + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: Operationrequest is the Schema for the operation requests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OperationrequestSpec defines the desired state of Operationrequest + properties: + action: + enum: + - Grant + - Update + - Deprive + type: string + namespace: + description: Namespace is the workspace that needs to be operated. + type: string + role: + enum: + - Owner + - Manager + - Developer + type: string + user: + type: string + type: object + status: + description: OperationrequestStatus defines the observed state of Operationrequest + properties: + phase: + default: Pending + description: Phase is the recently observed lifecycle phase of operationrequest. + enum: + - Pending + - Processing + - Completed + - Failed + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/config/crd/bases/user.sealos.io_users.yaml b/controllers/user/config/crd/bases/user.sealos.io_users.yaml new file mode 100644 index 000000000000..9699f7cac89e --- /dev/null +++ b/controllers/user/config/crd/bases/user.sealos.io_users.yaml @@ -0,0 +1,157 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: users.user.sealos.io +spec: + group: user.sealos.io + names: + kind: User + listKind: UserList + plural: users + singular: user + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.observedCSRExpirationSeconds + name: ExpirationSeconds + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: User is the Schema for the users API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: UserSpec defines the desired state of User + properties: + csrExpirationSeconds: + default: 1000000000 + description: |- + expirationSeconds is the requested duration of validity of the issued + kubeconfig credential. The issuer may issue a credential with a different + validity duration so a client must check the issued credential to determine + the actual duration. + + + The minimum effective value for expirationSeconds is 1_000_000_000 seconds. + format: int32 + type: integer + kubeConfigRotateAt: + description: kubeConfigRotateAt is a manual trigger for kubeconfig + rotation. When set/updated, controller will request a new token + and recreate kubeconfig. + format: date-time + type: string + type: object + status: + description: UserStatus defines the observed state of User + properties: + conditions: + description: Conditions contains the different condition statuses + for this user. + items: + properties: + lastHeartbeatTime: + description: LastHeartbeatTime is the last time this condition + was updated. + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time the condition + changed from one status to another. + format: date-time + type: string + message: + description: Message is a human-readable message indicating + details about the last status change. + type: string + reason: + description: Reason is a (brief) reason for the condition's + last status change. + type: string + status: + description: Status is the status of the condition. One of True, + False, Unknown. + type: string + type: + type: string + required: + - status + - type + type: object + type: array + kubeConfig: + type: string + observedCSRExpirationSeconds: + default: 1000000000 + format: int32 + type: integer + observedKubeConfigRotateAt: + description: ObservedKubeConfigRotateAt records the last processed + rotation request. + format: date-time + type: string + kubeConfigRefreshAt: + description: KubeConfigRefreshAt is the next time the controller + should refresh the kubeconfig token. + format: date-time + type: string + observedKubeConfigSecretUID: + description: ObservedKubeConfigSecretUID identifies the Secret bound + to the current kubeconfig token. + type: string + observedGeneration: + description: The generation observed by the user controller. + format: int64 + type: integer + phase: + default: Unknown + description: Phase is the recently observed lifecycle phase of user + type: string + required: + - kubeConfig + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/config/crd/kustomization.yaml b/controllers/user/config/crd/kustomization.yaml new file mode 100644 index 000000000000..8f613bf61efa --- /dev/null +++ b/controllers/user/config/crd/kustomization.yaml @@ -0,0 +1,51 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/user.sealos.io_users.yaml +- bases/user.sealos.io_operationrequests.yaml +- bases/user.sealos.io_deleterequests.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_users.yaml +#- patches/webhook_in_groups.yaml +#- patches/webhook_in_usergroups.yaml +#- patches/webhook_in_usergroupuserbindings.yaml +#- patches/webhook_in_usergroupnamespacebindings.yaml +#- patches/webhook_in_usergroupbindings.yaml +#- patches/webhook_in_operationrequests.yaml +#- patches/webhook_in_deleterequests.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_users.yaml +#- patches/cainjection_in_groups.yaml +#- patches/cainjection_in_usergroups.yaml +#- patches/cainjection_in_usergroupuserbindings.yaml +#- patches/cainjection_in_usergroupnamespacebindings.yaml +#- patches/cainjection_in_usergroupbindings.yaml +#- patches/cainjection_in_operationrequests.yaml +#- patches/cainjection_in_deleterequests.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/controllers/user/config/crd/kustomizeconfig.yaml b/controllers/user/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..9c99fc52fb20 --- /dev/null +++ b/controllers/user/config/crd/kustomizeconfig.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml b/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml new file mode 100644 index 000000000000..f44ebfb0d7ed --- /dev/null +++ b/controllers/user/config/crd/patches/cainjection_in_deleterequests.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: deleterequests.user.sealos.io diff --git a/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml b/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml new file mode 100644 index 000000000000..2ad5a01ae7e9 --- /dev/null +++ b/controllers/user/config/crd/patches/cainjection_in_operationrequests.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: operationrequests.user.sealos.io diff --git a/controllers/user/config/crd/patches/cainjection_in_users.yaml b/controllers/user/config/crd/patches/cainjection_in_users.yaml new file mode 100644 index 000000000000..7151c43f1dab --- /dev/null +++ b/controllers/user/config/crd/patches/cainjection_in_users.yaml @@ -0,0 +1,21 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: users.user.sealos.io diff --git a/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml b/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml new file mode 100644 index 000000000000..38178d15cd83 --- /dev/null +++ b/controllers/user/config/crd/patches/webhook_in_deleterequests.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: deleterequests.user.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml b/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml new file mode 100644 index 000000000000..6c60e8191110 --- /dev/null +++ b/controllers/user/config/crd/patches/webhook_in_operationrequests.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: operationrequests.user.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/user/config/crd/patches/webhook_in_users.yaml b/controllers/user/config/crd/patches/webhook_in_users.yaml new file mode 100644 index 000000000000..78136cab36af --- /dev/null +++ b/controllers/user/config/crd/patches/webhook_in_users.yaml @@ -0,0 +1,30 @@ +# Copyright © 2023 sealos. +# +# 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. + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: users.user.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/user/config/default/kustomization.yaml b/controllers/user/config/default/kustomization.yaml new file mode 100644 index 000000000000..e4a0af36b1c1 --- /dev/null +++ b/controllers/user/config/default/kustomization.yaml @@ -0,0 +1,88 @@ +# Copyright © 2023 sealos. +# +# 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. + +# Adds namespace to all resources. +namespace: user-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: user- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: + - ../crd + - ../rbac + - ../manager + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in + # crd/kustomization.yaml + - ../webhook + # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. + - ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: + # Protect the /metrics endpoint by putting it behind auth. + # If you want your controller-manager to expose the /metrics + # endpoint w/o any authn/z, please comment the following line. + - manager_auth_proxy_patch.yaml + + # Mount the controller config file for loading manager configurations + # through a ComponentConfig type + #- manager_config_patch.yaml + + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in + # crd/kustomization.yaml + - manager_webhook_patch.yaml + + # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. + # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. + # 'CERTMANAGER' needs to be enabled to use ca injection + - webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: + # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. + - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace + - name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + - name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace + - name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/controllers/user/config/default/manager_auth_proxy_patch.yaml b/controllers/user/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..56f9120ae7df --- /dev/null +++ b/controllers/user/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,63 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + imagePullPolicy: Always + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" + - "--kube-api-qps=5" + - "--kube-api-burst=10" + env: + - name: NAMESPACE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace diff --git a/controllers/user/config/default/manager_config_patch.yaml b/controllers/user/config/default/manager_config_patch.yaml new file mode 100644 index 000000000000..ca4883fc5ce5 --- /dev/null +++ b/controllers/user/config/default/manager_config_patch.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/controllers/user/config/default/manager_webhook_patch.yaml b/controllers/user/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000000..ade33aa9d5f0 --- /dev/null +++ b/controllers/user/config/default/manager_webhook_patch.yaml @@ -0,0 +1,37 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/controllers/user/config/default/webhookcainjection_patch.yaml b/controllers/user/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000000..b9ec7b20d96e --- /dev/null +++ b/controllers/user/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,29 @@ +# Copyright © 2023 sealos. +# +# 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. + +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/controllers/user/config/manager/controller_manager_config.yaml b/controllers/user/config/manager/controller_manager_config.yaml new file mode 100644 index 000000000000..d9a78f0c1df6 --- /dev/null +++ b/controllers/user/config/manager/controller_manager_config.yaml @@ -0,0 +1,35 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: 785548a1.sealos.io +# leaderElectionReleaseOnCancel defines if the leader should step down volume +# 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 diff --git a/controllers/user/config/manager/kustomization.yaml b/controllers/user/config/manager/kustomization.yaml new file mode 100644 index 000000000000..ed9883f73056 --- /dev/null +++ b/controllers/user/config/manager/kustomization.yaml @@ -0,0 +1,28 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manager.yaml +generatorOptions: + disableNameSuffixHash: true +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/labring/sealos-user-controller + newTag: latest diff --git a/controllers/user/config/manager/manager.yaml b/controllers/user/config/manager/manager.yaml new file mode 100644 index 000000000000..38eeb66fef83 --- /dev/null +++ b/controllers/user/config/manager/manager.yaml @@ -0,0 +1,86 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + imagePullPolicy: IfNotPresent + name: manager + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/controllers/user/config/prometheus/kustomization.yaml b/controllers/user/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..a32baf71b1b4 --- /dev/null +++ b/controllers/user/config/prometheus/kustomization.yaml @@ -0,0 +1,16 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- monitor.yaml diff --git a/controllers/user/config/prometheus/monitor.yaml b/controllers/user/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..32401c3d7fdc --- /dev/null +++ b/controllers/user/config/prometheus/monitor.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml b/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 000000000000..0e421adaba5d --- /dev/null +++ b/controllers/user/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,23 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/controllers/user/config/rbac/auth_proxy_role.yaml b/controllers/user/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..7539c6cd1e86 --- /dev/null +++ b/controllers/user/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,31 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/controllers/user/config/rbac/auth_proxy_role_binding.yaml b/controllers/user/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..84c16ebd8d6a --- /dev/null +++ b/controllers/user/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/user/config/rbac/auth_proxy_service.yaml b/controllers/user/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..70e47bf67833 --- /dev/null +++ b/controllers/user/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,29 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/controllers/user/config/rbac/deleterequest_editor_role.yaml b/controllers/user/config/rbac/deleterequest_editor_role.yaml new file mode 100644 index 000000000000..cdced0fa324a --- /dev/null +++ b/controllers/user/config/rbac/deleterequest_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit deleterequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: deleterequest-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: deleterequest-editor-role +rules: +- apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get diff --git a/controllers/user/config/rbac/deleterequest_viewer_role.yaml b/controllers/user/config/rbac/deleterequest_viewer_role.yaml new file mode 100644 index 000000000000..78e0cc001e38 --- /dev/null +++ b/controllers/user/config/rbac/deleterequest_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view deleterequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: deleterequest-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: deleterequest-viewer-role +rules: +- apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - get + - list + - watch +- apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get diff --git a/controllers/user/config/rbac/kustomization.yaml b/controllers/user/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..9ea536e0fd4f --- /dev/null +++ b/controllers/user/config/rbac/kustomization.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/controllers/user/config/rbac/leader_election_role.yaml b/controllers/user/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..7ccba03c9765 --- /dev/null +++ b/controllers/user/config/rbac/leader_election_role.yaml @@ -0,0 +1,51 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/controllers/user/config/rbac/leader_election_role_binding.yaml b/controllers/user/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..493b75ed3993 --- /dev/null +++ b/controllers/user/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/user/config/rbac/operationrequest_editor_role.yaml b/controllers/user/config/rbac/operationrequest_editor_role.yaml new file mode 100644 index 000000000000..629a0aafb9a5 --- /dev/null +++ b/controllers/user/config/rbac/operationrequest_editor_role.yaml @@ -0,0 +1,45 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit operationrequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: operationrequest-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: operationrequest-editor-role +rules: +- apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get diff --git a/controllers/user/config/rbac/operationrequest_viewer_role.yaml b/controllers/user/config/rbac/operationrequest_viewer_role.yaml new file mode 100644 index 000000000000..1cd005063f39 --- /dev/null +++ b/controllers/user/config/rbac/operationrequest_viewer_role.yaml @@ -0,0 +1,41 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view operationrequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: operationrequest-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: operationrequest-viewer-role +rules: +- apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - get + - list + - watch +- apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get diff --git a/controllers/user/config/rbac/role.yaml b/controllers/user/config/rbac/role.yaml new file mode 100644 index 000000000000..4ce169d6ed53 --- /dev/null +++ b/controllers/user/config/rbac/role.yaml @@ -0,0 +1,78 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - deleterequests/finalizers + verbs: + - update +- apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get + - patch + - update +- apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - operationrequests/finalizers + verbs: + - update +- apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get + - patch + - update diff --git a/controllers/user/config/rbac/role_binding.yaml b/controllers/user/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..749383ffe1ec --- /dev/null +++ b/controllers/user/config/rbac/role_binding.yaml @@ -0,0 +1,26 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/controllers/user/config/rbac/service_account.yaml b/controllers/user/config/rbac/service_account.yaml new file mode 100644 index 000000000000..b8ca78e010b4 --- /dev/null +++ b/controllers/user/config/rbac/service_account.yaml @@ -0,0 +1,19 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/controllers/user/config/rbac/user_editor_role.yaml b/controllers/user/config/rbac/user_editor_role.yaml new file mode 100644 index 000000000000..987c1ae7b559 --- /dev/null +++ b/controllers/user/config/rbac/user_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit users. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-editor-role +rules: +- apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - users/status + verbs: + - get diff --git a/controllers/user/config/rbac/user_viewer_role.yaml b/controllers/user/config/rbac/user_viewer_role.yaml new file mode 100644 index 000000000000..35b2a879e5ab --- /dev/null +++ b/controllers/user/config/rbac/user_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view users. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-viewer-role +rules: +- apiGroups: + - user.sealos.io + resources: + - users + verbs: + - get + - list + - watch +- apiGroups: + - user.sealos.io + resources: + - users/status + verbs: + - get diff --git a/controllers/user/config/rbac/usergroup_editor_role.yaml b/controllers/user/config/rbac/usergroup_editor_role.yaml new file mode 100644 index 000000000000..3d2696717c0f --- /dev/null +++ b/controllers/user/config/rbac/usergroup_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit usergroups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: usergroup-editor-role +rules: +- apiGroups: + - user.sealos.io + resources: + - usergroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - usergroups/status + verbs: + - get diff --git a/controllers/user/config/rbac/usergroup_viewer_role.yaml b/controllers/user/config/rbac/usergroup_viewer_role.yaml new file mode 100644 index 000000000000..79eed45d8eec --- /dev/null +++ b/controllers/user/config/rbac/usergroup_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view usergroups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: usergroup-viewer-role +rules: +- apiGroups: + - user.sealos.io + resources: + - usergroups + verbs: + - get + - list + - watch +- apiGroups: + - user.sealos.io + resources: + - usergroups/status + verbs: + - get diff --git a/controllers/user/config/rbac/usergroupbinding_editor_role.yaml b/controllers/user/config/rbac/usergroupbinding_editor_role.yaml new file mode 100644 index 000000000000..685ca3cd191d --- /dev/null +++ b/controllers/user/config/rbac/usergroupbinding_editor_role.yaml @@ -0,0 +1,38 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit usergroupbindings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: usergroupbinding-editor-role +rules: +- apiGroups: + - user.sealos.io + resources: + - usergroupbindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - usergroupbindings/status + verbs: + - get diff --git a/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml b/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml new file mode 100644 index 000000000000..91d90a1a681f --- /dev/null +++ b/controllers/user/config/rbac/usergroupbinding_viewer_role.yaml @@ -0,0 +1,34 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to view usergroupbindings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: usergroupbinding-viewer-role +rules: +- apiGroups: + - user.sealos.io + resources: + - usergroupbindings + verbs: + - get + - list + - watch +- apiGroups: + - user.sealos.io + resources: + - usergroupbindings/status + verbs: + - get diff --git a/controllers/user/config/samples/all.yaml b/controllers/user/config/samples/all.yaml new file mode 100644 index 000000000000..ebb84b35eadb --- /dev/null +++ b/controllers/user/config/samples/all.yaml @@ -0,0 +1,104 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: user.sealos.io/v1 +kind: User +metadata: + name: f8699ded-58d3-432b-a9ff-56568b57a38d +spec: + displayName: cuisongliu +--- +apiVersion: user.sealos.io/v1 +kind: UserGroup +metadata: + name: f8699ded-58d3-432b-a9ff-56568b57a38d +--- +apiVersion: user.sealos.io/v1 +kind: UserGroupBinding +metadata: + name: f8699ded-58d3-432b-a9ff-56568b57a38d +subject: + kind: User + name: "f8699ded-58d3-432b-a9ff-56568b57a38d" # Name is case sensitive + apiGroup: sealos.io/v1 +userGroupRef: f8699ded-58d3-432b-a9ff-56568b57a38d +roleRefs: + kind: ClusterRole + name: sealos-user-admin-role + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: user.sealos.io/v1 +kind: UserGroupBinding +metadata: + name: f8699ded-58d3-432b-a9ff-56568b57a38d +subject: + kind: Namespace + name: "f8699ded-58d3-432b-a9ff-56568b57a38d" +userGroupRef: f8699ded-58d3-432b-a9ff-56568b57a38d + +## 创建者 sealos-user-create-role权限 +## 管理员 不能创建管理员使用webhook校验操作 新增sealos-user-manager-role权限 +## 只有管理员和创建者才需要设置role其他不需要设置(默认给namespace设置cluster-admin的Role权限) +## 创建User默认创建UserGroup 和 2个UserGroupBinding (对user(直接是创建人的role)和对namespace) +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: sealos-user-create-role +rules: + - apiGroups: + - user.sealos.io + resources: + - 'usergroupbindings' + verbs: + - create + - delete + - deletecollection + - patch + - update + - apiGroups: + - user.sealos.io + resources: + - 'listusergroupsbyuser' + - 'listuserbyusergroup' + verbs: + - list + - get + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: sealos-user-manager-role +rules: + - apiGroups: + - user.sealos.io + resources: + - '*' + verbs: + - create + - delete + - deletecollection + - patch + - update + - apiGroups: + - user.sealos.io + resources: + - 'listusergroupsbyuser' + - 'listuserbyusergroup' + - 'listnamespacebyusergroup' + verbs: + - list + - get + - watch diff --git a/controllers/user/config/samples/app.log b/controllers/user/config/samples/app.log new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/controllers/user/config/samples/user_v1_deleterequest.yaml b/controllers/user/config/samples/user_v1_deleterequest.yaml new file mode 100644 index 000000000000..b538cb994932 --- /dev/null +++ b/controllers/user/config/samples/user_v1_deleterequest.yaml @@ -0,0 +1,20 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: user.sealos.io/v1 +kind: DeleteRequest +metadata: + name: deleterequest-sample +spec: + user: xxxxxxxx diff --git a/controllers/user/config/samples/user_v1_operationrequest.yaml b/controllers/user/config/samples/user_v1_operationrequest.yaml new file mode 100644 index 000000000000..048620d3ed79 --- /dev/null +++ b/controllers/user/config/samples/user_v1_operationrequest.yaml @@ -0,0 +1,33 @@ +# Copyright © 2023 sealos. +# +# 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. + +apiVersion: user.sealos.io/v1 +kind: Operationrequest +metadata: + name: request-grant-a-owns-b + namespace: ns-bbbb0001 +spec: + user: aaaa0001 + role: Owner + action: Grant +--- +apiVersion: user.sealos.io/v1 +kind: Operationrequest +metadata: + name: request-deprive-a-owns-b + namespace: ns-bbbb0001 +spec: + user: aaaa0001 + role: Owner + action: Deprive \ No newline at end of file diff --git a/controllers/user/config/samples/user_v1_user.yaml b/controllers/user/config/samples/user_v1_user.yaml new file mode 100644 index 000000000000..7129fdbc4ab0 --- /dev/null +++ b/controllers/user/config/samples/user_v1_user.yaml @@ -0,0 +1,32 @@ +# Copyright © 2023 sealos. +# +# 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. + +# user +apiVersion: user.sealos.io/v1 +kind: User +metadata: + annotations: + user.sealos.io/owner: aaaa0001 + name: aaaa0001 +spec: + csrExpirationSeconds: 1000000000 + kubeConfigRotateAt: "2026-01-19T14:55:00Z" +--- +# group +apiVersion: user.sealos.io/v1 +kind: User +metadata: + annotations: + user.sealos.io/owner: aaaa0001 + name: bbbb0001 diff --git a/controllers/user/config/webhook/kustomization.yaml b/controllers/user/config/webhook/kustomization.yaml new file mode 100644 index 000000000000..1bf9427c2424 --- /dev/null +++ b/controllers/user/config/webhook/kustomization.yaml @@ -0,0 +1,20 @@ +# Copyright © 2023 sealos. +# +# 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. + +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/controllers/user/config/webhook/kustomizeconfig.yaml b/controllers/user/config/webhook/kustomizeconfig.yaml new file mode 100644 index 000000000000..594f180c57f7 --- /dev/null +++ b/controllers/user/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,39 @@ +# Copyright © 2023 sealos. +# +# 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. + +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/controllers/user/config/webhook/manifests.yaml b/controllers/user/config/webhook/manifests.yaml new file mode 100644 index 000000000000..59d6e4ac5b24 --- /dev/null +++ b/controllers/user/config/webhook/manifests.yaml @@ -0,0 +1,107 @@ +# Copyright © 2023 sealos. +# +# 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. + +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + name: moperationrequest.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-user-sealos-io-v1-user + failurePolicy: Fail + name: muser.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + name: voperationrequest.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-user-sealos-io-v1-user + timeoutSeconds: 30 + failurePolicy: Fail + name: vuser.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None diff --git a/controllers/user/config/webhook/service.yaml b/controllers/user/config/webhook/service.yaml new file mode 100644 index 000000000000..03198124dd55 --- /dev/null +++ b/controllers/user/config/webhook/service.yaml @@ -0,0 +1,27 @@ +# Copyright © 2023 sealos. +# +# 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. + + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/controllers/user/controllers/adapt_rolebinding_controller.go b/controllers/user/controllers/adapt_rolebinding_controller.go new file mode 100644 index 000000000000..8a1934ed130a --- /dev/null +++ b/controllers/user/controllers/adapt_rolebinding_controller.go @@ -0,0 +1,133 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + + "github.com/go-logr/logr" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + 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/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// TODO This controller is used to adapt the old RoleBinding. only need to deploy the logic once for conversion and delete the controller in the future + +// AdaptRoleBindingReconciler reconciles a RoleBinding object, Old Role bindings are backward compatible and will be deleted in the future +type AdaptRoleBindingReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger +} + +func (r *AdaptRoleBindingReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + rolebinding := &v1.RoleBinding{} + if err := r.Get(ctx, req.NamespacedName, rolebinding); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // old rolebinding only has one subject + if len(rolebinding.Subjects) != 1 { + return ctrl.Result{}, nil + } + + if rolebinding.Subjects[0].Namespace != config.GetUserSystemNamespace() { + userName := rolebinding.GetAnnotations()[userAnnotationOwnerKey] + user := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: userName}, user); err != nil { + r.Logger.Error(err, "get user failed") + return ctrl.Result{}, err + } + appendSubject := rolebinding.Subjects[0].DeepCopy() + appendSubject.Namespace = config.GetUserSystemNamespace() + rolebinding.Subjects = append(rolebinding.Subjects, *appendSubject) + if err := r.Update(ctx, rolebinding); err != nil { + r.Logger.Error(err, "update rolebinding failed") + return ctrl.Result{}, err + } + if err := controllerutil.SetControllerReference(user, rolebinding, r.Scheme); err != nil { + r.Logger.Error(err, "set controller reference failed") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AdaptRoleBindingReconciler) SetupWithManager(mgr ctrl.Manager) error { + const controllerName = "adapt_rolebinding_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + r.Scheme = mgr.GetScheme() + r.Logger.V(1).Info("init reconcile AdaptRoleBinding controller") + return ctrl.NewControllerManagedBy(mgr). + For(&v1.RoleBinding{}, builder.WithPredicates(WorkspacePredicate{})). + Complete(r) +} + +type WorkspacePredicate struct{} + +func (WorkspacePredicate) Create(e event.CreateEvent) bool { + return isWorkspaceObject(e.Object) +} + +func (WorkspacePredicate) Delete(_ event.DeleteEvent) bool { + return false +} + +func (WorkspacePredicate) Update(_ event.UpdateEvent) bool { + return false +} + +func (WorkspacePredicate) Generic(_ event.GenericEvent) bool { + return false +} + +func isWorkspaceObject(obj client.Object) bool { + rolebinding, ok := obj.(*v1.RoleBinding) + if !ok { + return false + } + anno := obj.GetAnnotations() + if anno == nil { + return false + } + if anno["user.sealos.io/owner"] == "" { + return false + } + if len(obj.GetOwnerReferences()) > 0 { + return false + } + + for _, sub := range rolebinding.Subjects { + if sub.Namespace == config.GetUserSystemNamespace() { + return false + } + } + return true +} diff --git a/controllers/user/controllers/cache/cache.go b/controllers/user/controllers/cache/cache.go new file mode 100644 index 000000000000..f50a6fd512f5 --- /dev/null +++ b/controllers/user/controllers/cache/cache.go @@ -0,0 +1,566 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "strconv" + "strings" + "time" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + "github.com/labring/sealos/controllers/user/controllers/helper/hash" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/clientcmd" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RoleBindingSpecHashAnnotation stores the in-memory hash for a RoleBinding +// or ClusterRoleBinding projection. It is never written back to the API. +const RoleBindingSpecHashAnnotation = "user.sealos.io/cache-role-binding-spec-hash" + +type roleBindingSpec struct { + RoleRef rbacv1.RoleRef + Subjects []rbacv1.Subject +} + +// RoleBindingSpecHash returns the stable hash used by RBAC cache projections. +func RoleBindingSpecHash(roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) string { + return hash.HashToString(roleBindingSpec{RoleRef: roleRef, Subjects: subjects}) +} + +// Options keeps only explicitly registered informer data in memory. Large +// fields are removed when controllers only need a smaller object projection. +func Options(syncPeriod *time.Duration) ctrlcache.Options { + secretMetadata := &metav1.PartialObjectMetadata{} + secretMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) + return ctrlcache.Options{ + SyncPeriod: syncPeriod, + ReaderFailOnMissingInformer: true, + DefaultTransform: ctrlcache.TransformStripManagedFields(), + ByObject: map[client.Object]ctrlcache.ByObject{ + &corev1.Namespace{}: { + Transform: transformNamespace, + }, + &rbacv1.ClusterRoleBinding{}: { + Field: fields.OneTermEqualSelector( + "metadata.name", + config.AdminClusterRoleBindingName, + ), + // The admin binding is checked from cache during restart, including + // its subjects and role reference. + Transform: transformClusterRoleBinding, + }, + &licensev1.License{}: { + Transform: transformLicense, + }, + &userv1.User{}: { + Transform: transformUser, + }, + &userv1.DeleteRequest{}: { + Transform: transformDeleteRequest, + }, + secretMetadata: { + Namespaces: map[string]ctrlcache.Config{ + config.GetUserSystemNamespace(): {}, + }, + Transform: transformSecretMetadata, + }, + &corev1.ServiceAccount{}: { + Namespaces: map[string]ctrlcache.Config{ + config.GetUserSystemNamespace(): {}, + }, + Transform: transformServiceAccount, + }, + &userv1.Operationrequest{}: { + Namespaces: map[string]ctrlcache.Config{ + config.GetUserSystemNamespace(): {}, + }, + Transform: transformOperationrequest, + }, + &rbacv1.Role{}: { + Transform: transformRole, + }, + &rbacv1.RoleBinding{}: { + Transform: transformRoleBinding, + }, + }, + } +} + +// UncachedObjects returns objects whose reads require complete, current API data. +func UncachedObjects() []client.Object { + return []client.Object{ + &licensev1.License{}, + &userv1.DeleteRequest{}, + &userv1.Operationrequest{}, + &corev1.Namespace{}, + &corev1.Secret{}, + &corev1.ServiceAccount{}, + &rbacv1.ClusterRoleBinding{}, + &rbacv1.Role{}, + &rbacv1.RoleBinding{}, + } +} + +func transformUser(obj any) (any, error) { + user, ok := obj.(*userv1.User) + if !ok { + return obj, nil + } + + metadata := projectUserObjectMeta(user.ObjectMeta) + metadata.Finalizers = append([]string(nil), user.Finalizers...) + metadata.Annotations = copyMapValues( + user.Annotations, + userv1.UserAnnotationOwnerKey, + ) + metadata.Labels = copyMapValues( + user.Labels, + "user.sealos.io/status", + "user.sealos.io/type", + ) + status := projectUserStatus(user.Status) + if status.KubeConfigRefreshAt == nil { + status.KubeConfigRefreshAt = inferKubeConfigRefreshAt(user) + } + spec := user.Spec + if user.Spec.KubeConfigRotateAt != nil { + rotateAt := *user.Spec.KubeConfigRotateAt + spec.KubeConfigRotateAt = &rotateAt + } + return &userv1.User{ + TypeMeta: user.TypeMeta, + ObjectMeta: metadata, + Spec: spec, + Status: status, + }, nil +} + +// projectUserStatus copies the status fields used by reconciliation and drops +// the persisted kubeconfig before allocating a cache object. +func projectUserStatus(status userv1.UserStatus) userv1.UserStatus { + projected := userv1.UserStatus{ + Phase: status.Phase, + ObservedCSRExpirationSeconds: status.ObservedCSRExpirationSeconds, + ObservedKubeConfigSecretUID: status.ObservedKubeConfigSecretUID, + ObservedGeneration: status.ObservedGeneration, + } + if status.ObservedKubeConfigRotateAt != nil { + rotateAt := *status.ObservedKubeConfigRotateAt + projected.ObservedKubeConfigRotateAt = &rotateAt + } + if status.KubeConfigRefreshAt != nil { + refreshAt := *status.KubeConfigRefreshAt + projected.KubeConfigRefreshAt = &refreshAt + } + if len(status.Conditions) > 0 { + projected.Conditions = append([]userv1.Condition(nil), status.Conditions...) + } + return projected +} + +// inferKubeConfigRefreshAt derives the refresh point from legacy JWT-backed +// kubeconfigs while projecting the object. The payload is parsed and dropped; +// the cache never retains the credential itself. +func inferKubeConfigRefreshAt(user *userv1.User) *metav1.Time { + if user == nil || user.Status.KubeConfig == "" { + return nil + } + config, err := clientcmd.Load([]byte(user.Status.KubeConfig)) + if err != nil { + return nil + } + info, ok := config.AuthInfos[user.Name] + if !ok || info == nil { + return inferLegacyRefreshAt(user) + } + if info.Token != "" { + parts := strings.Split(info.Token, ".") + if len(parts) >= 2 { + if refreshAt := inferTokenRefreshAt(parts[1]); refreshAt != nil { + return refreshAt + } + } + } + if len(info.ClientCertificateData) == 0 { + return inferLegacyRefreshAt(user) + } + block, _ := pem.Decode(info.ClientCertificateData) + if block == nil { + return nil + } + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil + } + return refreshAtFromIssuedAt(certificate.NotBefore, certificate.NotAfter) +} + +func inferLegacyRefreshAt(user *userv1.User) *metav1.Time { + if user == nil { + return nil + } + issuedAt := user.Status.ObservedKubeConfigRotateAt + if issuedAt == nil || issuedAt.IsZero() { + issuedAt = &user.CreationTimestamp + } + expirationSeconds := user.Status.ObservedCSRExpirationSeconds + if expirationSeconds == 0 { + expirationSeconds = user.Spec.CSRExpirationSeconds + } + if expirationSeconds == 0 { + // Older User objects may predate both expiration fields. The legacy + // kubeconfig issuer used this same default when the spec was zero. + expirationSeconds = userv1.DefaultCSRExpirationSeconds + } + if issuedAt == nil || issuedAt.IsZero() { + return nil + } + expirationSeconds = userv1.NormalizeCSRExpirationSeconds(expirationSeconds) + return refreshAtFromIssuedAt( + issuedAt.Time, + issuedAt.Add(time.Duration(expirationSeconds)*time.Second), + ) +} + +func inferTokenRefreshAt(encodedPayload string) *metav1.Time { + payload, err := base64.RawURLEncoding.DecodeString(encodedPayload) + if err != nil { + payload, err = base64.URLEncoding.DecodeString(encodedPayload) + if err != nil { + return nil + } + } + claims := map[string]json.RawMessage{} + if err := json.Unmarshal(payload, &claims); err != nil { + return nil + } + var expiration json.Number + if err := json.Unmarshal(claims["exp"], &expiration); err != nil { + return nil + } + expirationUnix, err := strconv.ParseInt(string(expiration), 10, 64) + if err != nil { + return nil + } + expirationTime := time.Unix(expirationUnix, 0) + var issuedAt time.Time + // Kubernetes service-account tokens normally carry iat and nbf. Use nbf + // as a fallback so the inferred deadline remains stable across cache events. + for _, claimName := range []string{"iat", "nbf"} { + issued, ok := claims[claimName] + if !ok { + continue + } + var issuedNumber json.Number + if err := json.Unmarshal(issued, &issuedNumber); err != nil { + continue + } + issuedAtUnix, err := strconv.ParseInt(string(issuedNumber), 10, 64) + if err == nil { + issuedAt = time.Unix(issuedAtUnix, 0) + break + } + } + if !issuedAt.IsZero() && expirationTime.After(issuedAt) { + return refreshAtFromIssuedAt(issuedAt, expirationTime) + } + return refreshAtFromExpiration(expirationTime) +} + +func refreshAtFromExpiration(expiration time.Time) *metav1.Time { + refreshAt := time.Now().Add(time.Until(expiration) * 8 / 10) + return &metav1.Time{Time: refreshAt} +} + +func refreshAtFromIssuedAt(issuedAt, expiration time.Time) *metav1.Time { + return &metav1.Time{Time: issuedAt.Add(expiration.Sub(issuedAt) * 8 / 10)} +} + +func transformLicense(obj any) (any, error) { + metadata, ok := obj.(*metav1.PartialObjectMetadata) + if !ok { + return obj, nil + } + projected := projectEventObjectMeta(metadata.ObjectMeta) + projected.CreationTimestamp = metadata.CreationTimestamp + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projected, + }, nil +} + +func transformDeleteRequest(obj any) (any, error) { + metadata, ok := obj.(*metav1.PartialObjectMetadata) + if !ok { + return obj, nil + } + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta), + }, nil +} + +func transformOperationrequest(obj any) (any, error) { + metadata, ok := obj.(*metav1.PartialObjectMetadata) + if !ok { + return obj, nil + } + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta), + }, nil +} + +func transformServiceAccount(obj any) (any, error) { + serviceAccount, ok := obj.(*corev1.ServiceAccount) + if !ok { + return obj, nil + } + projected := &corev1.ServiceAccount{ + TypeMeta: serviceAccount.TypeMeta, + ObjectMeta: projectOwnerObjectMeta(serviceAccount.ObjectMeta), + } + if hasUserController(projected.OwnerReferences) && len(serviceAccount.Secrets) > 0 { + projected.Secrets = []corev1.ObjectReference{{Name: serviceAccount.Secrets[0].Name}} + } + return projected, nil +} + +func transformRole(obj any) (any, error) { + role, ok := obj.(*rbacv1.Role) + if !ok { + return obj, nil + } + projected := &rbacv1.Role{ + TypeMeta: role.TypeMeta, + ObjectMeta: projectOwnerObjectMeta(role.ObjectMeta), + } + if hasUserController(projected.OwnerReferences) { + if projected.Annotations == nil { + projected.Annotations = make(map[string]string, 1) + } + projected.Annotations[config.RoleRulesHashAnnotation] = hash.HashToString(role.Rules) + } + return projected, nil +} + +func transformRoleBinding(obj any) (any, error) { + roleBinding, ok := obj.(*rbacv1.RoleBinding) + if !ok { + return obj, nil + } + projected := &rbacv1.RoleBinding{ + TypeMeta: roleBinding.TypeMeta, + ObjectMeta: projectOwnerObjectMeta(roleBinding.ObjectMeta), + } + if hasUserController(projected.OwnerReferences) { + if projected.Annotations == nil { + projected.Annotations = make(map[string]string, 1) + } + projected.Annotations[RoleBindingSpecHashAnnotation] = RoleBindingSpecHash( + roleBinding.RoleRef, + roleBinding.Subjects, + ) + } else { + // The legacy RoleBinding adapter uses the owner annotation to identify + // workspace bindings before it performs its uncached read. + projected.Annotations = copyMapValues( + roleBinding.Annotations, + userv1.UserAnnotationCreatorKey, + userv1.UserAnnotationOwnerKey, + ) + } + return projected, nil +} + +func transformClusterRoleBinding(obj any) (any, error) { + roleBinding, ok := obj.(*rbacv1.ClusterRoleBinding) + if !ok { + return obj, nil + } + projected := &rbacv1.ClusterRoleBinding{ + TypeMeta: roleBinding.TypeMeta, + ObjectMeta: projectOwnerObjectMeta(roleBinding.ObjectMeta), + } + if hasUserController(projected.OwnerReferences) { + if projected.Annotations == nil { + projected.Annotations = make(map[string]string, 1) + } + projected.Annotations[RoleBindingSpecHashAnnotation] = RoleBindingSpecHash( + roleBinding.RoleRef, + roleBinding.Subjects, + ) + } + return projected, nil +} + +func hasUserController(references []metav1.OwnerReference) bool { + for _, reference := range references { + if reference.Controller != nil && *reference.Controller && + reference.APIVersion == userv1.GroupVersion.String() && reference.Kind == "User" { + return true + } + } + return false +} + +func transformNamespace(obj any) (any, error) { + metadata, ok := obj.(*metav1.PartialObjectMetadata) + if !ok { + return obj, nil + } + + projected := projectEventObjectMeta(metadata.ObjectMeta) + if !isUserNamespaceName(metadata.Name) { + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projected, + }, nil + } + projected.Annotations = copyMapValues( + metadata.Annotations, + userv1.UserAnnotationCreatorKey, + userv1.UserAnnotationOwnerKey, + ) + projected.Labels = copyMapValues( + metadata.Labels, + userv1.UserLabelOwnerKey, + ) + for key, value := range metadata.Labels { + if config.IsPodSecurityLabel(key) { + if projected.Labels == nil { + projected.Labels = make(map[string]string) + } + projected.Labels[key] = value + } + } + projected.OwnerReferences = projectControllerOwnerReferences(metadata.OwnerReferences) + return &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projected, + }, nil +} + +func projectOwnerObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + projected := projectEventObjectMeta(in) + projected.CreationTimestamp = in.CreationTimestamp + projected.OwnerReferences = projectControllerOwnerReferences(in.OwnerReferences) + if len(projected.OwnerReferences) == 0 { + return projected + } + projected.Annotations = copyMapValues( + in.Annotations, + userv1.UserAnnotationCreatorKey, + userv1.UserAnnotationOwnerKey, + ) + return projected +} + +func projectUserObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + out := metav1.ObjectMeta{ + Name: in.Name, + Namespace: in.Namespace, + UID: in.UID, + ResourceVersion: in.ResourceVersion, + Generation: in.Generation, + CreationTimestamp: in.CreationTimestamp, + } + if in.DeletionTimestamp != nil { + out.DeletionTimestamp = in.DeletionTimestamp.DeepCopy() + } + if in.DeletionGracePeriodSeconds != nil { + gracePeriod := *in.DeletionGracePeriodSeconds + out.DeletionGracePeriodSeconds = &gracePeriod + } + return out +} + +func projectEventObjectMeta(in metav1.ObjectMeta) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: in.Name, + Namespace: in.Namespace, + UID: in.UID, + ResourceVersion: in.ResourceVersion, + CreationTimestamp: in.CreationTimestamp, + } +} + +func projectControllerOwnerReferences(in []metav1.OwnerReference) []metav1.OwnerReference { + for _, reference := range in { + if reference.Controller == nil || !*reference.Controller || + reference.APIVersion != userv1.GroupVersion.String() || reference.Kind != "User" { + continue + } + controller := *reference.Controller + reference.Controller = &controller + if reference.BlockOwnerDeletion != nil { + blockOwnerDeletion := *reference.BlockOwnerDeletion + reference.BlockOwnerDeletion = &blockOwnerDeletion + } + return []metav1.OwnerReference{reference} + } + return nil +} + +func isUserNamespaceName(name string) bool { + return strings.HasPrefix(name, "ns-") && len(name) > len("ns-") +} + +func copyMapValues(source map[string]string, keys ...string) map[string]string { + count := 0 + for _, key := range keys { + if _, ok := source[key]; ok { + count++ + } + } + if count == 0 { + return nil + } + result := make(map[string]string, count) + for _, key := range keys { + if value, ok := source[key]; ok { + result[key] = value + } + } + return result +} + +func transformSecretMetadata(obj any) (any, error) { + metadata, ok := obj.(*metav1.PartialObjectMetadata) + if !ok { + return obj, nil + } + projected := &metav1.PartialObjectMetadata{ + TypeMeta: metadata.TypeMeta, + ObjectMeta: projectEventObjectMeta(metadata.ObjectMeta), + } + projected.CreationTimestamp = metadata.CreationTimestamp + projected.OwnerReferences = projectControllerOwnerReferences(metadata.OwnerReferences) + // Legacy token cleanup indexes all Secrets by this annotation, including + // Secrets that predate User owner references. + projected.Annotations = copyMapValues(metadata.Annotations, corev1.ServiceAccountNameKey) + return projected, nil +} diff --git a/controllers/user/controllers/cache/cache_test.go b/controllers/user/controllers/cache/cache_test.go new file mode 100644 index 000000000000..7a0dc757e933 --- /dev/null +++ b/controllers/user/controllers/cache/cache_test.go @@ -0,0 +1,728 @@ +// Copyright 2026 labring. +// +// 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 cache + +import ( + "encoding/base64" + "encoding/pem" + "reflect" + "strconv" + "testing" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + "github.com/labring/sealos/controllers/user/controllers/helper/hash" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func transformedAs[T any](t *testing.T, obj any) T { + t.Helper() + transformed, ok := obj.(T) + if !ok { + t.Fatalf("transformed object type = %T", obj) + } + return transformed +} + +func TestOptionsLimitsSecretCache(t *testing.T) { + syncPeriod := time.Hour + options := Options(&syncPeriod) + if options.SyncPeriod != &syncPeriod { + t.Fatal("sync period was not retained") + } + if !options.ReaderFailOnMissingInformer { + t.Fatal("missing informer reads are allowed") + } + if options.DefaultTransform == nil { + t.Fatal("default managed fields transform is nil") + } + + found := false + for obj, byObject := range options.ByObject { + if _, ok := obj.(*metav1.PartialObjectMetadata); !ok { + continue + } + found = true + if byObject.Transform == nil { + t.Fatal("secret transform is nil") + } + if len(byObject.Namespaces) != 1 { + t.Fatalf("secret cache namespaces = %d, want 1", len(byObject.Namespaces)) + } + if _, ok := byObject.Namespaces[config.GetUserSystemNamespace()]; !ok { + t.Fatal("user system namespace is not cached for secrets") + } + } + if !found { + t.Fatal("secret cache options not found") + } +} + +func TestOptionsLimitNamespacedMetadataCaches(t *testing.T) { + options := Options(nil) + for _, required := range []client.Object{ + &corev1.ServiceAccount{}, + &userv1.Operationrequest{}, + } { + found := false + for obj, byObject := range options.ByObject { + if reflect.TypeOf(obj) != reflect.TypeOf(required) { + continue + } + found = true + if len(byObject.Namespaces) != 1 { + t.Fatalf("%T cache namespaces = %d, want 1", required, len(byObject.Namespaces)) + } + if _, ok := byObject.Namespaces[config.GetUserSystemNamespace()]; !ok { + t.Fatalf("%T cache is not limited to the user system namespace", required) + } + } + if !found { + t.Fatalf("%T cache options not found", required) + } + } +} + +func TestOptionsLimitsClusterRoleBindingCacheToAdminBinding(t *testing.T) { + options := Options(nil) + var byObject ctrlcache.ByObject + for obj, candidate := range options.ByObject { + if reflect.TypeOf(obj) == reflect.TypeFor[*rbacv1.ClusterRoleBinding]() { + byObject = candidate + break + } + } + if byObject.Field == nil { + t.Fatal("cluster role binding cache options not found") + } + if byObject.Field.String() != "metadata.name="+config.AdminClusterRoleBindingName { + t.Fatalf("cluster role binding field selector = %v", byObject.Field) + } +} + +func TestTransformNamespaceKeepsSecurityAndOwnerMetadata(t *testing.T) { + namespace := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-user-a", + ResourceVersion: "42", + UID: "namespace-a", + CreationTimestamp: metav1.NewTime(time.Unix(100, 0)), + Labels: map[string]string{ + config.PodSecurityLabelPrefix + "enforce": "baseline", + userv1.UserLabelOwnerKey: "owner-a", + "unused.example/label": "unused", + }, + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: "user-a", + userv1.UserAnnotationOwnerKey: "owner-a", + "unused.example/annotation": "unused", + }, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + } + + transformed, err := transformNamespace(namespace) + if err != nil { + t.Fatalf("transform namespace: %v", err) + } + got, ok := transformed.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed) + } + wantLabels := map[string]string{ + config.PodSecurityLabelPrefix + "enforce": "baseline", + userv1.UserLabelOwnerKey: "owner-a", + } + if !reflect.DeepEqual(got.Labels, wantLabels) { + t.Fatalf("labels = %#v, want %#v", got.Labels, wantLabels) + } + wantAnnotations := map[string]string{ + userv1.UserAnnotationCreatorKey: "user-a", + userv1.UserAnnotationOwnerKey: "owner-a", + } + if !reflect.DeepEqual(got.Annotations, wantAnnotations) || len(got.ManagedFields) != 0 { + t.Fatalf("unused namespace metadata was retained: %#v", got.ObjectMeta) + } + if got.UID != namespace.UID || + !got.CreationTimestamp.Equal(&namespace.CreationTimestamp) { + t.Fatalf( + "required namespace event metadata was not retained: uid=%q creation=%v", + got.UID, + got.CreationTimestamp, + ) + } +} + +func TestTransformSecretMetadataKeepsOnlyIndexMetadata(t *testing.T) { + controller := true + metadata := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "token-a", + Namespace: config.GetUserSystemNamespace(), + ResourceVersion: "42", + CreationTimestamp: metav1.NewTime(time.Unix(100, 0)), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: "user-a", + "unused.example/key": "large-value", + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", Name: "user-a", UID: "user-a", Controller: &controller, + }}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + } + metadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) + + transformed, err := transformSecretMetadata(metadata) + if err != nil { + t.Fatalf("transform secret: %v", err) + } + got, ok := transformed.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed) + } + if got.Name != metadata.Name || + got.Namespace != metadata.Namespace || + got.ResourceVersion != "42" || + !got.CreationTimestamp.Equal(&metadata.CreationTimestamp) { + t.Fatalf("required metadata was not retained: %#v", got.ObjectMeta) + } + if got.Annotations[corev1.ServiceAccountNameKey] != "user-a" || len(got.Annotations) != 1 { + t.Fatalf("secret index annotations = %#v", got.Annotations) + } + if !reflect.DeepEqual(got.OwnerReferences, metadata.OwnerReferences) || + len(got.Labels) != 0 || len(got.ManagedFields) != 0 { + t.Fatalf("secret metadata projection = %#v", got.ObjectMeta) + } +} + +func TestTransformUserDropsOnlyLargeUnusedFields(t *testing.T) { + rotateAt := metav1.Now() + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-a", + ResourceVersion: "42", + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "owner-a", + "unused.example/annotation": "unused", + }, + Labels: map[string]string{ + "user.sealos.io/status": "active", + "user.sealos.io/type": "Group", + "unused.example/label": "unused", + }, + Finalizers: []string{"sealos.io/user.finalizers"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + }, + Spec: userv1.UserSpec{ + CSRExpirationSeconds: 600, + KubeConfigRotateAt: &rotateAt, + }, + Status: userv1.UserStatus{ + Phase: userv1.UserActive, + KubeConfig: "large-kubeconfig", + ObservedCSRExpirationSeconds: 600, + ObservedKubeConfigRotateAt: &rotateAt, + KubeConfigRefreshAt: &rotateAt, + ObservedKubeConfigSecretUID: "secret-a", + ObservedGeneration: 7, + Conditions: []userv1.Condition{{ + Type: userv1.Ready, + Status: corev1.ConditionTrue, + }}, + }, + } + + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform user: %v", err) + } + got, ok := transformed.(*userv1.User) + if !ok { + t.Fatalf("transformed type = %T, want *v1.User", transformed) + } + if got.Status.KubeConfig != "" { + t.Fatal("kubeconfig was retained") + } + if len(got.ManagedFields) != 0 { + t.Fatal("managed fields were retained") + } + wantAnnotations := map[string]string{userv1.UserAnnotationOwnerKey: "owner-a"} + wantLabels := map[string]string{ + "user.sealos.io/status": "active", + "user.sealos.io/type": "Group", + } + if got.Name != user.Name || got.ResourceVersion != user.ResourceVersion || + !reflect.DeepEqual(got.Annotations, wantAnnotations) || + !reflect.DeepEqual(got.Labels, wantLabels) || + !reflect.DeepEqual(got.Finalizers, user.Finalizers) || + !reflect.DeepEqual(got.Spec, user.Spec) { + t.Fatalf("required user fields were not retained: %#v", got) + } + wantStatus := user.Status.DeepCopy() + wantStatus.KubeConfig = "" + if !reflect.DeepEqual(&got.Status, wantStatus) { + t.Fatalf("status = %#v, want %#v", got.Status, *wantStatus) + } + if user.Status.KubeConfig == "" || len(user.ManagedFields) == 0 { + t.Fatal("transform mutated the source user") + } + got.Spec.KubeConfigRotateAt.Time = got.Spec.KubeConfigRotateAt.Add(time.Hour) + got.Status.Conditions[0].Message = "changed" + if user.Spec.KubeConfigRotateAt.Equal(got.Spec.KubeConfigRotateAt) { + t.Fatal("transform shared the kubeconfig rotate timestamp") + } + if user.Status.Conditions[0].Message == "changed" { + t.Fatal("transform shared the conditions slice") + } +} + +func TestTransformMetadataKeepsOnlyEventFields(t *testing.T) { + metadata := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "role-a", + Namespace: "ns-a", + ResourceVersion: "42", + CreationTimestamp: metav1.NewTime(time.Unix(100, 0)), + Annotations: map[string]string{"unused.example/key": "unused"}, + Labels: map[string]string{"unused.example/key": "unused"}, + Finalizers: []string{"unused.example/finalizer"}, + ManagedFields: []metav1.ManagedFieldsEntry{{Manager: "test"}}, + OwnerReferences: []metav1.OwnerReference{{Name: "user-a"}}, + }, + } + metadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + + transformed, err := transformLicense(metadata) + if err != nil { + t.Fatalf("transform license metadata: %v", err) + } + got, ok := transformed.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("transformed type = %T, want *metav1.PartialObjectMetadata", transformed) + } + if got.GroupVersionKind() != metadata.GroupVersionKind() || got.Name != metadata.Name || + got.Namespace != metadata.Namespace || got.ResourceVersion != metadata.ResourceVersion || + !got.CreationTimestamp.Equal(&metadata.CreationTimestamp) { + t.Fatalf("required event metadata was not retained: %#v", got) + } + if len(got.OwnerReferences) != 0 || + len(got.Annotations) != 0 || + len(got.Labels) != 0 || + len(got.Finalizers) != 0 || + len(got.ManagedFields) != 0 { + t.Fatalf("unused metadata was retained: %#v", got.ObjectMeta) + } + + for _, transform := range []func(any) (any, error){ + transformDeleteRequest, + transformOperationrequest, + } { + keyOnly, err := transform(metadata) + if err != nil { + t.Fatalf("transform request metadata: %v", err) + } + gotKey, ok := keyOnly.(*metav1.PartialObjectMetadata) + if !ok { + t.Fatalf("key metadata type = %T, want *metav1.PartialObjectMetadata", keyOnly) + } + if len(gotKey.OwnerReferences) != 0 || len(gotKey.Annotations) != 0 { + t.Fatalf("key-only ownership metadata was retained: %#v", gotKey.ObjectMeta) + } + if !gotKey.CreationTimestamp.Equal(&metadata.CreationTimestamp) { + t.Fatalf("key-only event creation timestamp = %v", gotKey.CreationTimestamp) + } + } +} + +func TestTransformOwnerObjectsKeepsReconcileFields(t *testing.T) { + controller := true + owner := metav1.OwnerReference{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", + Name: "alice", + UID: "user-a", + Controller: &controller, + } + annotations := map[string]string{ + userv1.UserAnnotationCreatorKey: "alice", + userv1.UserAnnotationOwnerKey: "owner-a", + "unused.example/key": "unused", + } + + role, err := transformRole(&rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Owner", + Namespace: "ns-alice", + ResourceVersion: "42", + Generation: 7, + CreationTimestamp: metav1.NewTime( + time.Unix(100, 0), + ), + Finalizers: []string{"unused.example/finalizer"}, + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{owner}, + }, + Rules: []rbacv1.PolicyRule{{ + APIGroups: []string{"*"}, + Resources: []string{"pods"}, + Verbs: []string{"get"}, + }}, + }) + if err != nil { + t.Fatalf("transform role: %v", err) + } + gotRole := transformedAs[*rbacv1.Role](t, role) + wantRoleHash := hash.HashToString([]rbacv1.PolicyRule{{ + APIGroups: []string{"*"}, Resources: []string{"pods"}, Verbs: []string{"get"}, + }}) + if len(gotRole.Rules) != 0 || + gotRole.Annotations[config.RoleRulesHashAnnotation] != wantRoleHash || + !reflect.DeepEqual(gotRole.OwnerReferences, []metav1.OwnerReference{owner}) || + len(gotRole.Annotations) != 3 { + t.Fatalf("role reconcile fields were not retained: %#v", gotRole) + } + if len(gotRole.Labels) != 0 || len(gotRole.ManagedFields) != 0 { + t.Fatalf("role unused metadata was retained: %#v", gotRole.ObjectMeta) + } + if gotRole.ResourceVersion != "42" || gotRole.Generation != 0 || + !gotRole.CreationTimestamp.Equal(&metav1.Time{Time: time.Unix(100, 0)}) || + len(gotRole.Finalizers) != 0 { + t.Fatalf("role non-reconcile metadata was retained: %#v", gotRole.ObjectMeta) + } + + roleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{owner}, + }, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "Owner"}, + Subjects: []rbacv1.Subject{ + {Kind: "ServiceAccount", Name: "alice", Namespace: config.GetUserSystemNamespace()}, + }, + }) + if err != nil { + t.Fatalf("transform role binding: %v", err) + } + gotRoleBinding := transformedAs[*rbacv1.RoleBinding](t, roleBinding) + if gotRoleBinding.RoleRef.Name != "" || len(gotRoleBinding.Subjects) != 0 || + gotRoleBinding.Annotations[RoleBindingSpecHashAnnotation] != RoleBindingSpecHash( + rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "Owner"}, + []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: "alice", + Namespace: config.GetUserSystemNamespace(), + }}, + ) || !reflect.DeepEqual(gotRoleBinding.OwnerReferences, []metav1.OwnerReference{owner}) { + t.Fatalf("role binding projection was not compact: %#v", gotRoleBinding) + } + + clusterRoleBinding, err := transformClusterRoleBinding(&rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{owner}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + Subjects: []rbacv1.Subject{ + {Kind: "ServiceAccount", Name: "alice", Namespace: config.GetUserSystemNamespace()}, + }, + }) + if err != nil { + t.Fatalf("transform cluster role binding: %v", err) + } + gotClusterRoleBinding := transformedAs[*rbacv1.ClusterRoleBinding](t, clusterRoleBinding) + if gotClusterRoleBinding.RoleRef.Name != "" || len(gotClusterRoleBinding.Subjects) != 0 || + gotClusterRoleBinding.Annotations[RoleBindingSpecHashAnnotation] != RoleBindingSpecHash( + rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-admin"}, + []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: "alice", + Namespace: config.GetUserSystemNamespace(), + }}, + ) { + t.Fatalf("cluster role binding projection was not compact: %#v", gotClusterRoleBinding) + } + + serviceAccount, err := transformServiceAccount(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + OwnerReferences: []metav1.OwnerReference{owner}, + }, + Secrets: []corev1.ObjectReference{{Name: "token-alice"}}, + }) + if err != nil { + t.Fatalf("transform service account: %v", err) + } + gotServiceAccount := transformedAs[*corev1.ServiceAccount](t, serviceAccount) + if !reflect.DeepEqual( + gotServiceAccount.Secrets, + []corev1.ObjectReference{{Name: "token-alice"}}, + ) { + t.Fatalf("service account secrets were not retained: %#v", gotServiceAccount.Secrets) + } +} + +func TestTransformOwnerObjectsDropsSpecForUnownedObjects(t *testing.T) { + role, err := transformRole(&rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: "unrelated"}, + Rules: []rbacv1.PolicyRule{ + {APIGroups: []string{"*"}, Resources: []string{"secrets"}, Verbs: []string{"*"}}, + }, + }) + if err != nil { + t.Fatalf("transform unrelated role: %v", err) + } + if got := transformedAs[*rbacv1.Role](t, role); len(got.Rules) != 0 { + t.Fatalf("unowned role rules were retained: %#v", got.Rules) + } + + roleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "unrelated"}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: "admin"}, + Subjects: []rbacv1.Subject{{Kind: "Group", Name: "admins"}}, + }) + if err != nil { + t.Fatalf("transform unrelated role binding: %v", err) + } + gotRoleBinding := transformedAs[*rbacv1.RoleBinding](t, roleBinding) + if gotRoleBinding.RoleRef.Name != "" || len(gotRoleBinding.Subjects) != 0 { + t.Fatalf("unowned role binding spec was retained: %#v", gotRoleBinding) + } + + legacyRoleBinding, err := transformRoleBinding(&rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "alice", + "unused.example/key": "unused", + }, + }, + Subjects: []rbacv1.Subject{{Kind: "User", Name: "alice"}}, + }) + if err != nil { + t.Fatalf("transform legacy role binding: %v", err) + } + gotLegacyRoleBinding := transformedAs[*rbacv1.RoleBinding](t, legacyRoleBinding) + if gotLegacyRoleBinding.Annotations[userv1.UserAnnotationOwnerKey] != "alice" || + len(gotLegacyRoleBinding.Annotations) != 1 || + len(gotLegacyRoleBinding.Subjects) != 0 { + t.Fatalf("legacy role binding projection lost adapter metadata: %#v", gotLegacyRoleBinding) + } +} + +func TestTransformUserInfersLegacyKubeConfigRefresh(t *testing.T) { + expiresAt := time.Now().Add(time.Hour).Unix() + payload := base64.RawURLEncoding.EncodeToString( + []byte(`{"exp":` + strconv.FormatInt(expiresAt, 10) + `}`), + ) + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "alice": {Token: "header." + payload + ".signature"}, + }}) + if err != nil { + t.Fatalf("write legacy kubeconfig: %v", err) + } + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{ + KubeConfig: string(data), + }} + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform legacy user: %v", err) + } + refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt + if refreshAt == nil || refreshAt.Time.Before(time.Now().Add(40*time.Minute)) || + refreshAt.After(time.Now().Add(50*time.Minute)) { + t.Fatalf("legacy refresh time = %v, want about 48 minutes from now", refreshAt) + } +} + +func TestTransformUserUsesTokenNotBeforeForStableRefresh(t *testing.T) { + issuedAt := time.Now().Add(-10 * time.Minute).Truncate(time.Second) + expiresAt := issuedAt.Add(time.Hour) + payload := base64.RawURLEncoding.EncodeToString( + []byte(`{"exp":` + strconv.FormatInt(expiresAt.Unix(), 10) + + `,"nbf":` + strconv.FormatInt(issuedAt.Unix(), 10) + `}`), + ) + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "alice": {Token: "header." + payload + ".signature"}, + }}) + if err != nil { + t.Fatalf("write legacy kubeconfig: %v", err) + } + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{ + KubeConfig: string(data), + }} + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform legacy user: %v", err) + } + refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt + want := metav1.NewTime(issuedAt.Add(48 * time.Minute)) + if refreshAt == nil || !refreshAt.Equal(&want) { + t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want) + } +} + +func TestTransformUserInfersLegacyRefreshFromObservedRotation(t *testing.T) { + rotation := metav1.NewTime(time.Now().Add(-time.Hour)) + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "alice": {Token: "legacy-token"}, + }}) + if err != nil { + t.Fatalf("write legacy kubeconfig: %v", err) + } + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Spec: userv1.UserSpec{ + CSRExpirationSeconds: 10 * 60, + }, Status: userv1.UserStatus{ + KubeConfig: string(data), + ObservedCSRExpirationSeconds: 10 * 60, + ObservedKubeConfigRotateAt: &rotation, + }} + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform legacy user: %v", err) + } + refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt + want := metav1.NewTime( + rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10), + ) + if refreshAt == nil || !refreshAt.Equal(&want) { + t.Fatalf("legacy fallback refresh time = %v, want %v", refreshAt, want) + } +} + +func TestTransformUserInfersLegacyRefreshWithDefaultExpiration(t *testing.T) { + rotation := metav1.NewTime(time.Now().Add(-time.Hour)) + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "alice": {Token: "legacy-token"}, + }}) + if err != nil { + t.Fatalf("write legacy kubeconfig: %v", err) + } + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{ + KubeConfig: string(data), + ObservedKubeConfigRotateAt: &rotation, + }} + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform legacy user: %v", err) + } + refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt + want := metav1.NewTime( + rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10), + ) + if refreshAt == nil || !refreshAt.Equal(&want) { + t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want) + } +} + +func TestTransformUserInfersLegacyRefreshWhenAuthInfoIsUnrecognized(t *testing.T) { + rotation := metav1.NewTime(time.Now().Add(-time.Hour)) + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "webhook-user": {}, + }}) + if err != nil { + t.Fatalf("write legacy kubeconfig: %v", err) + } + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Spec: userv1.UserSpec{ + CSRExpirationSeconds: 10 * 60, + }, Status: userv1.UserStatus{ + KubeConfig: string(data), + ObservedCSRExpirationSeconds: 10 * 60, + ObservedKubeConfigRotateAt: &rotation, + }} + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform legacy user: %v", err) + } + refreshAt := transformedAs[*userv1.User](t, transformed).Status.KubeConfigRefreshAt + want := metav1.NewTime( + rotation.Add(time.Duration(userv1.DefaultCSRExpirationSeconds) * time.Second * 8 / 10), + ) + if refreshAt == nil || !refreshAt.Equal(&want) { + t.Fatalf("legacy refresh time = %v, want %v", refreshAt, want) + } +} + +func TestTransformUserDoesNotInferRefreshFromMalformedKubeConfig(t *testing.T) { + rotation := metav1.NewTime(time.Now().Add(-time.Hour)) + for _, kubeConfig := range []string{ + "not: [valid", + string(mustWriteKubeConfig(t, &clientcmdapi.AuthInfo{ + ClientCertificateData: []byte("not a certificate"), + })), + string(mustWriteKubeConfig(t, &clientcmdapi.AuthInfo{ + ClientCertificateData: pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", Bytes: []byte("not certificate DER"), + }), + })), + } { + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "alice"}, + Status: userv1.UserStatus{ + KubeConfig: kubeConfig, + ObservedKubeConfigRotateAt: &rotation, + }, + } + transformed, err := transformUser(user) + if err != nil { + t.Fatalf("transform malformed user: %v", err) + } + transformedUser := transformedAs[*userv1.User](t, transformed) + if refreshAt := transformedUser.Status.KubeConfigRefreshAt; refreshAt != nil { + t.Fatalf("malformed kubeconfig refresh time = %v, want nil", refreshAt) + } + } +} + +func mustWriteKubeConfig(t *testing.T, authInfo *clientcmdapi.AuthInfo) []byte { + t.Helper() + data, err := clientcmd.Write(clientcmdapi.Config{AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "alice": authInfo, + }}) + if err != nil { + t.Fatalf("write kubeconfig: %v", err) + } + return data +} + +func TestUncachedObjects(t *testing.T) { + types := make(map[reflect.Type]struct{}) + for _, obj := range UncachedObjects() { + types[reflect.TypeOf(obj)] = struct{}{} + } + for _, required := range []client.Object{ + &corev1.Namespace{}, + &corev1.Secret{}, + &corev1.ServiceAccount{}, + } { + if _, ok := types[reflect.TypeOf(required)]; !ok { + t.Fatalf("%T reads are still cache-backed", required) + } + } + if _, ok := types[reflect.TypeFor[*userv1.User]()]; ok { + t.Fatal("user reads bypass the projected cache") + } +} diff --git a/controllers/user/controllers/deleterequest_controller.go b/controllers/user/controllers/deleterequest_controller.go new file mode 100644 index 000000000000..5b908cd23e29 --- /dev/null +++ b/controllers/user/controllers/deleterequest_controller.go @@ -0,0 +1,285 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// DeleteRequestReconciler reconciles a DeleteRequest object +type DeleteRequestReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger + Recorder record.EventRecorder + + // expirationTime is the time duration of the request is expired + expirationTime time.Duration + // retentionTime is the time duration of the request is retained after it is isCompleted + retentionTime time.Duration +} + +const DeleteRequestRequeueDuration time.Duration = 30 * time.Second + +//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=user.sealos.io,resources=deleterequests/finalizers,verbs=update + +func (r *DeleteRequestReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + deleteRequest := &userv1.DeleteRequest{} + if err := r.Get(ctx, req.NamespacedName, deleteRequest); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + return r.reconcile(ctx, deleteRequest) +} + +func (r *DeleteRequestReconciler) reconcile( + ctx context.Context, + request *userv1.DeleteRequest, +) (ctrl.Result, error) { + r.Logger.V(1). + Info("start reconcile deleterequest", "name", request.Name, "user", request.Spec.User) + // count the time cost of handling the request + startTime := time.Now() + defer func() { + r.Logger.V(1).Info("complete request handling", "handling cost time", time.Since(startTime)) + }() + + // delete OperationRequest first if its status is isCompleted and exist for retention time + if r.isRetained(request) { + r.Logger.Info("delete request", "name", request.Name) + if err := r.deleteRequest(ctx, request); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + // return early if its status is isCompleted and didn't exist for retention time + if r.isCompleted(request) { + r.Logger.Info("request is completed and requeue", "name", request.Name) + return ctrl.Result{RequeueAfter: DeleteRequestRequeueDuration}, nil + } + // change OperationRequest status to failed if it is expired + if r.isExpired(request) { + r.Logger.Info("request is expired, update status to failed", "name", request.Name) + if err := r.updateRequestStatus(ctx, request, userv1.RequestFailed); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // handle the request + + // get user + user := userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, &user); err != nil { + r.Logger.Error(err, "get user error", "name", request.Spec.User) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "GetUserError", + "get user %s error: %s", + request.Spec.User, + err.Error(), + ) + return ctrl.Result{}, err + } + + // delete user if it is not labeled deleted + if !isUserDeleted(user) && !isGroupUser(user) { + r.Logger.Info("user is not deleted or not a group user", "name", user.Name) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "UserNotDeleted", + "user %s is not deleted or not a group user", + user.Name, + ) + return ctrl.Result{RequeueAfter: DeleteRequestRequeueDuration}, nil + } + + // delete user + if err := r.Delete(ctx, &user); err != nil { + r.Logger.Error(err, "delete user error", "name", user.Name) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "DeleteUserError", + "delete user %s error: %s", + user.Name, + err.Error(), + ) + return ctrl.Result{}, err + } + + // get namespace + ns := corev1.Namespace{} + if err := r.Get( + ctx, + client.ObjectKey{Name: config.GetUsersNamespace(user.Name)}, + &ns, + ); err != nil { + r.Logger.Error(err, "get ns error", "name", ns.Name) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "GetNamespaceError", + "get namespace %s error: %s", + ns.Name, + err.Error(), + ) + return ctrl.Result{}, err + } + + // delete namespace + if err := r.Delete(ctx, &ns); err != nil { + r.Logger.Error(err, "delete ns error", "name", ns.Name) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "DeleteNamespaceError", + "delete namespace %s error: %s", + ns.Name, + err.Error(), + ) + return ctrl.Result{}, err + } + + // update Request status to completed + if err := r.updateRequestStatus(ctx, request, userv1.RequestCompleted); err != nil { + r.Logger.Error(err, "update request status error", "name", request.Name) + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "UpdateRequestStatusError", + "update request %s status error: %s", + request.Name, + err.Error(), + ) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// isRetained returns true if the request is isCompleted and exist for retention time +func (r *DeleteRequestReconciler) isRetained(request *userv1.DeleteRequest) bool { + if request.Status.Phase == userv1.RequestCompleted && + request.CreationTimestamp.Add(r.retentionTime).Before(time.Now()) { + return true + } + return false +} + +// isCompleted returns true if the request is isCompleted +func (r *DeleteRequestReconciler) isCompleted(request *userv1.DeleteRequest) bool { + return request.Status.Phase == userv1.RequestCompleted +} + +// isExpired returns true if the request is expired +func (r *DeleteRequestReconciler) isExpired(request *userv1.DeleteRequest) bool { + if request.Status.Phase != userv1.RequestCompleted && + request.CreationTimestamp.Add(r.expirationTime).Before(time.Now()) { + return true + } + return false +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeleteRequestReconciler) SetupWithManager(mgr ctrl.Manager) error { + const controllerName = "deleterequest_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + if r.Recorder == nil { + r.Recorder = mgr.GetEventRecorderFor(controllerName) + } + r.Scheme = mgr.GetScheme() + r.Logger.V(1).Info("init reconcile deleterequest controller") + r.expirationTime = time.Minute * 10 + r.retentionTime = time.Minute * 30 + return ctrl.NewControllerManagedBy(mgr). + For(&userv1.DeleteRequest{}, builder.OnlyMetadata). + Complete(r) +} + +func (r *DeleteRequestReconciler) deleteRequest( + ctx context.Context, + request *userv1.DeleteRequest, +) error { + r.Logger.V(1).Info("deleting OperationRequest", "request", request) + if err := r.Delete(ctx, request); client.IgnoreNotFound(err) != nil { + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "Failed to delete OperationRequest", + "Failed to delete OperationRequest %s/%s", + request.Namespace, + request.Name, + ) + r.Logger.Error(err, "Failed to delete OperationRequest", "request", request) + return fmt.Errorf("failed to delete OperationRequest %s: %w", request.Name, err) + } + r.Logger.V(1).Info("delete OperationRequest success") + return nil +} + +func (r *DeleteRequestReconciler) updateRequestStatus( + ctx context.Context, + request *userv1.DeleteRequest, + phase userv1.RequestPhase, +) error { + request.Status.Phase = phase + if err := r.Status().Update(ctx, request); err != nil { + r.Recorder.Eventf( + request, + corev1.EventTypeWarning, + "Failed to update OperationRequest status", + "Failed to update OperationRequest status %s/%s", + request.Namespace, + request.Name, + ) + r.Logger.V(1).Info("update OperationRequest status failed", "request", request) + return err + } + r.Logger.V(1).Info("update OperationRequest status success", "request", request) + return nil +} + +// isUserDeleted returns true if the user is deleted +func isUserDeleted(user userv1.User) bool { + return user.Labels["user.sealos.io/status"] == "Deleted" +} + +// isGroupUser returns true if the user is a group user +func isGroupUser(user userv1.User) bool { + return user.Labels["user.sealos.io/type"] == "Group" +} diff --git a/controllers/user/controllers/helper/config/config.go b/controllers/user/controllers/helper/config/config.go new file mode 100644 index 000000000000..db56519164da --- /dev/null +++ b/controllers/user/controllers/helper/config/config.go @@ -0,0 +1,47 @@ +// Copyright © 2024 sealos. +// +// 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 config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Global `yaml:"global"` + Feature `yaml:"feature"` +} + +type Global struct { + CloudAPIServerDomain string `yaml:"cloudAPIServerDomain"` + CloudAPIServerPort string `yaml:"cloudAPIServerPort"` +} + +type Feature struct { + AdminSkip bool `yaml:"adminSkip"` +} + +func LoadConfig(path string, target any) error { + configData, err := os.ReadFile(path) + if err != nil { + return err + } + err = yaml.Unmarshal(configData, target) + if err != nil { + return err + } + return nil +} diff --git a/controllers/user/controllers/helper/config/pod_security.go b/controllers/user/controllers/helper/config/pod_security.go new file mode 100644 index 000000000000..ace5b6d5fa26 --- /dev/null +++ b/controllers/user/controllers/helper/config/pod_security.go @@ -0,0 +1,38 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 config + +import "strings" + +const ( + PodSecurityVersion = "v1.25" + PodSecurityLabelPrefix = "pod-security.kubernetes.io/" +) + +func IsPodSecurityLabel(key string) bool { + return strings.HasPrefix(key, PodSecurityLabelPrefix) +} + +func SetPodSecurity(labels map[string]string) map[string]string { + labels[PodSecurityLabelPrefix+"enforce"] = "baseline" + labels[PodSecurityLabelPrefix+"enforce-version"] = PodSecurityVersion + labels[PodSecurityLabelPrefix+"audit"] = "restricted" + labels[PodSecurityLabelPrefix+"audit-version"] = PodSecurityVersion + labels[PodSecurityLabelPrefix+"warn"] = "restricted" + labels[PodSecurityLabelPrefix+"warn-version"] = PodSecurityVersion + return labels +} diff --git a/controllers/user/controllers/helper/config/rbac.go b/controllers/user/controllers/helper/config/rbac.go new file mode 100644 index 000000000000..69ad21f2a8b0 --- /dev/null +++ b/controllers/user/controllers/helper/config/rbac.go @@ -0,0 +1,92 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +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 config + +import ( + "os" + "strings" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +// AdminClusterRoleBindingName is the legacy cluster-admin binding for admin. +const AdminClusterRoleBindingName = "sealos-cloudadmin" + +// RoleRulesHashAnnotation is used only by the in-memory Role projection. +const RoleRulesHashAnnotation = "user.sealos.io/cache-role-rules-hash" + +func GetUserSystemNamespace() string { + return "user-system" +} + +func GetDefaultNamespace() string { + return os.Getenv("NAMESPACE_NAME") +} + +func GetUsersSubject(user string) []rbacv1.Subject { + return []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: user, + Namespace: GetUserSystemNamespace(), + }, + } +} + +func GetUserNameByNamespace(namespace string) string { + return strings.TrimPrefix(namespace, "ns-") +} + +func GetUsersNamespace(user string) string { + return "ns-" + user +} + +func GetGroupRoleBindingName(user string) string { + return "rb-" + user +} + +func GetUserRole(roleType userv1.RoleType) []rbacv1.PolicyRule { + switch roleType { + case userv1.OwnerRoleType: + return []rbacv1.PolicyRule{ + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + } + case userv1.ManagerRoleType: + return []rbacv1.PolicyRule{ + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + } + case userv1.DeveloperRoleType: + return []rbacv1.PolicyRule{ + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"list", "watch", "get"}, + }, + } + default: + return []rbacv1.PolicyRule{} + } +} diff --git a/controllers/user/controllers/helper/finalizer/finalizer.go b/controllers/user/controllers/helper/finalizer/finalizer.go new file mode 100644 index 000000000000..8399824040f7 --- /dev/null +++ b/controllers/user/controllers/helper/finalizer/finalizer.go @@ -0,0 +1,129 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 finalizer + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type Finalizer struct { + client client.Client + reader client.Reader + finalizerName string +} + +func (f *Finalizer) AddFinalizer(ctx context.Context, obj client.Object) (bool, error) { + var notDelete bool + if obj.GetDeletionTimestamp() == nil || obj.GetDeletionTimestamp().IsZero() { + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + notDelete = true + if controllerutil.ContainsFinalizer(obj, f.finalizerName) { + return notDelete, nil + } + controllerutil.AddFinalizer(obj, f.finalizerName) + if err := f.updateFinalizers( + ctx, + client.ObjectKeyFromObject(obj), + obj, + obj.GetFinalizers(), + ); err != nil { + return notDelete, err + } + } + return notDelete, nil +} + +func DefaultFunc(ctx context.Context, obj client.Object) error { + return nil +} + +func NewFinalizer(client client.Client, finalizerName string) *Finalizer { + return &Finalizer{ + client: client, + reader: client, + finalizerName: finalizerName, + } +} + +func (f *Finalizer) WithReader(reader client.Reader) *Finalizer { + f.reader = reader + return f +} + +func (f *Finalizer) RemoveFinalizer( + ctx context.Context, + obj client.Object, + fun func(ctx context.Context, obj client.Object) error, +) (bool, error) { + var deleteBool bool + if obj.GetDeletionTimestamp() != nil && !obj.GetDeletionTimestamp().IsZero() { + deleteBool = true + if controllerutil.ContainsFinalizer(obj, f.finalizerName) { + // our finalizer is present, so lets handle any external dependency + if err := fun(ctx, obj); err != nil { + return deleteBool, err + } + + controllerutil.RemoveFinalizer(obj, f.finalizerName) + if err := f.updateFinalizers( + ctx, + client.ObjectKeyFromObject(obj), + obj, + obj.GetFinalizers(), + ); err != nil { + return deleteBool, err + } + } + } + return deleteBool, nil +} + +func (f *Finalizer) updateFinalizers( + ctx context.Context, + objectKey client.ObjectKey, + obj client.Object, + finalizers []string, +) error { + gvk, err := f.client.GroupVersionKindFor(obj) + if err != nil { + return err + } + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + fetchObject := &unstructured.Unstructured{} + fetchObject.SetAPIVersion(gvk.GroupVersion().String()) + fetchObject.SetKind(gvk.Kind) + err := f.reader.Get(ctx, objectKey, fetchObject) + if err != nil { + // We log this error, but we continue and try to set the ownerRefs on the other resources. + return err + } + fetchObject.SetFinalizers(finalizers) + err = f.client.Update(ctx, fetchObject) + if err != nil { + // We log this error, but we continue and try to set the ownerRefs on the other resources. + return err + } + return nil + }) +} diff --git a/controllers/user/controllers/helper/finalizer/finalizer_test.go b/controllers/user/controllers/helper/finalizer/finalizer_test.go new file mode 100644 index 000000000000..bcd259de8255 --- /dev/null +++ b/controllers/user/controllers/helper/finalizer/finalizer_test.go @@ -0,0 +1,114 @@ +// Copyright 2026 labring. +// +// 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 finalizer + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func TestAddFinalizerResolvesMissingGVK(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + stored := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build() + + obj := &corev1.ConfigMap{} + key := client.ObjectKeyFromObject(stored) + if err := cli.Get(context.Background(), key, obj); err != nil { + t.Fatalf("get configmap: %v", err) + } + if !obj.GroupVersionKind().Empty() { + t.Fatalf("fetched object GVK = %s, want empty", obj.GroupVersionKind()) + } + + const finalizerName = "test.sealos.io/finalizer" + updated, err := NewFinalizer(cli, finalizerName).AddFinalizer(context.Background(), obj) + if err != nil { + t.Fatalf("add finalizer: %v", err) + } + if !updated { + t.Fatal("object was not handled") + } + + got := &corev1.ConfigMap{} + if err := cli.Get(context.Background(), key, got); err != nil { + t.Fatalf("get updated configmap: %v", err) + } + if !controllerutil.ContainsFinalizer(got, finalizerName) { + t.Fatalf("finalizers = %v, want %q", got.Finalizers, finalizerName) + } +} + +type updateCountingClient struct { + client.Client + updates int +} + +func (c *updateCountingClient) Update( + ctx context.Context, + obj client.Object, + opts ...client.UpdateOption, +) error { + c.updates++ + return c.Client.Update(ctx, obj, opts...) +} + +func TestAddFinalizerSkipsExistingFinalizerUpdate(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + const finalizerName = "test.sealos.io/finalizer" + stored := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{finalizerName}, + }, + } + baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build() + cli := &updateCountingClient{Client: baseClient} + + obj := &corev1.ConfigMap{} + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), obj); err != nil { + t.Fatalf("get configmap: %v", err) + } + handled, err := NewFinalizer(cli, finalizerName).AddFinalizer(context.Background(), obj) + if err != nil { + t.Fatalf("add finalizer: %v", err) + } + if !handled { + t.Fatal("object was not handled") + } + if cli.updates != 0 { + t.Fatalf("updates = %d, want 0", cli.updates) + } +} diff --git a/controllers/user/controllers/helper/hash/hash.go b/controllers/user/controllers/helper/hash/hash.go new file mode 100644 index 000000000000..9a5235d2a449 --- /dev/null +++ b/controllers/user/controllers/helper/hash/hash.go @@ -0,0 +1,60 @@ +// Copyright © 2022 The sealos 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. + +/* +import from "k8s.io/kubernetes/pkg/util/hash" +*/ + +package hash + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "hash" + + "github.com/davecgh/go-spew/spew" +) + +// DeepHashObject writes specified object to hash using the spew library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +func DeepHashObject(hasher hash.Hash, objectToWrite any) { + hasher.Reset() + printer := spew.ConfigState{ + Indent: " ", + SortKeys: true, + DisableMethods: true, + SpewKeys: true, + } + printer.Fprintf(hasher, "%#v", objectToWrite) +} + +// HashToString gen hash string base on actual values of the nested objects. +func HashToString(obj any) string { + hasher := sha256.New() + DeepHashObject(hasher, obj) + return hex.EncodeToString(hasher.Sum(nil)[0:]) +} + +func Hash(data any) string { + dataByte, err := json.Marshal(data) + if err != nil { + hasher := sha256.New() + DeepHashObject(hasher, data) + return hex.EncodeToString(hasher.Sum(nil)) + } + sum := sha256.Sum256(dataByte) + return hex.EncodeToString(sum[:]) +} diff --git a/controllers/user/controllers/helper/helper.go b/controllers/user/controllers/helper/helper.go new file mode 100644 index 000000000000..c907678c695e --- /dev/null +++ b/controllers/user/controllers/helper/helper.go @@ -0,0 +1,109 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 helper + +import ( + v1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func IsConditionTrue(conditions []v1.Condition, condition v1.Condition) bool { + for _, con := range conditions { + if con.Type == condition.Type && con.Status == condition.Status { + return true + } + } + return false +} + +func IsConditionsTrue(conditions []v1.Condition) bool { + if len(conditions) == 0 { + return false + } + for _, condition := range conditions { + if condition.Type == v1.Ready { + continue + } + if condition.Status != corev1.ConditionTrue { + return false + } + } + return true +} + +func GetCondition(conditions []v1.Condition, condition *v1.Condition) *v1.Condition { + for _, con := range conditions { + if con.Type == condition.Type { + return con.DeepCopy() + } + } + return condition.DeepCopy() +} + +func DiffCondition(condition1, condition2 *v1.Condition) bool { + if condition1.Reason != condition2.Reason || condition1.Status != condition2.Status || + condition1.Message != condition2.Message { + return true + } + return false +} + +// UpdateCondition updates condition in cluster conditions using giving condition +// adds condition if not existed +func UpdateCondition(conditions []v1.Condition, condition v1.Condition) []v1.Condition { + if conditions == nil { + conditions = make([]v1.Condition, 0) + } + hasCondition := false + for i, cond := range conditions { + if cond.Type == condition.Type { + hasCondition = true + if DiffCondition(conditions[i].DeepCopy(), condition.DeepCopy()) { + conditions[i] = condition + } + } + } + if !hasCondition { + conditions = append(conditions, condition) + } + return conditions +} + +func DeleteCondition(conditions []v1.Condition, conditionType v1.ConditionType) []v1.Condition { + for i, condition := range conditions { + if condition.Type != conditionType { + continue + } + filtered := make([]v1.Condition, 0, len(conditions)-1) + filtered = append(filtered, conditions[:i]...) + for _, remaining := range conditions[i+1:] { + if remaining.Type != conditionType { + filtered = append(filtered, remaining) + } + } + return filtered + } + return conditions +} + +func SetConditionError(condition *v1.Condition, reason string, err error) { + condition.LastHeartbeatTime = metav1.Now() + condition.Status = corev1.ConditionFalse + condition.Reason = reason + condition.Message = err.Error() +} diff --git a/controllers/user/controllers/helper/helper_test.go b/controllers/user/controllers/helper/helper_test.go new file mode 100644 index 000000000000..b9887ae0ed72 --- /dev/null +++ b/controllers/user/controllers/helper/helper_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 labring. +// +// 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 helper + +import ( + "testing" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + corev1 "k8s.io/api/core/v1" +) + +func TestGetConditionReturnsIndependentCopyWhenMissing(t *testing.T) { + t.Parallel() + desired := &userv1.Condition{Type: userv1.Ready, Status: corev1.ConditionTrue} + got := GetCondition(nil, desired) + got.Status = corev1.ConditionFalse + if desired.Status != corev1.ConditionTrue { + t.Fatal("missing condition result aliases the desired condition") + } +} + +func TestDeleteConditionKeepsOriginalSliceWhenUnchanged(t *testing.T) { + t.Parallel() + conditions := []userv1.Condition{{Type: userv1.Ready}} + got := DeleteCondition(conditions, userv1.Initialized) + if len(got) != len(conditions) || &got[0] != &conditions[0] { + t.Fatal("DeleteCondition allocated or changed a slice without a matching condition") + } +} + +func TestDeleteConditionRemovesAllMatchingConditions(t *testing.T) { + t.Parallel() + conditions := []userv1.Condition{ + {Type: userv1.Ready}, + {Type: userv1.Initialized}, + {Type: userv1.Initialized}, + } + got := DeleteCondition(conditions, userv1.Initialized) + if len(got) != 1 || got[0].Type != userv1.Ready { + t.Fatalf("remaining conditions = %#v, want only Ready", got) + } +} diff --git a/controllers/user/controllers/helper/kubeconfig/cert.go b/controllers/user/controllers/helper/kubeconfig/cert.go new file mode 100644 index 000000000000..004e5a18c323 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/cert.go @@ -0,0 +1,169 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math" + "math/big" + "net" + "time" + + confighelper "github.com/labring/sealos/controllers/user/controllers/helper/config" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// tryLoadKeyFromDisk tries to load the key from the disk and validates that it is valid +func tryLoadKeyFromDisk(pkiPath string) (crypto.Signer, error) { + // Parse the private key from a file + privKey, err := keyutil.PrivateKeyFromFile(pkiPath) + if err != nil { + return nil, fmt.Errorf("couldn't load the private key file %w", err) + } + + // Allow RSA and ECDSA formats only + var key crypto.Signer + switch k := privKey.(type) { + case *rsa.PrivateKey: + key = k + case *ecdsa.PrivateKey: + key = k + default: + return nil, fmt.Errorf("couldn't convert the private key file %w", err) + } + + return key, nil +} + +// encodeCertPEM returns PEM-endcoded certificate data +func encodeCertPEM(cert *x509.Certificate) []byte { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + return pem.EncodeToMemory(&block) +} + +func (c *CertConfig) Apply(config *rest.Config, _ client.Client) (*api.Config, error) { + // make sure cadata is loaded into config under incluster mode + if err := rest.LoadTLSFiles(config); err != nil { + return nil, err + } + certs, err := cert.ParseCertsPEM(config.CAData) + if err != nil { + return nil, fmt.Errorf("error reading by config: %s", err.Error()) + } + caCert := certs[0] + caKey, err := tryLoadKeyFromDisk(c.caKeyFile) + if err != nil { + return nil, fmt.Errorf("load ca key file failed %w", err) + } + clientCert, clientKey, err := newCertAndKey( + caCert, + caKey, + c.user, + c.groups, + c.dnsNames, + c.ipAddresses, + c.expirationSeconds, + ) + if err != nil { + return nil, fmt.Errorf("new client key failed %w", err) + } + encodedClientKey, err := keyutil.MarshalPrivateKeyToPEM(clientKey) + if err != nil { + return nil, fmt.Errorf("encode client key failed %w", err) + } + encodedClientCert := encodeCertPEM(clientCert) + ctx := fmt.Sprintf("%s@%s", c.user, c.clusterName) + return &api.Config{ + Clusters: map[string]*api.Cluster{ + c.clusterName: { + Server: GetKubernetesHost(config), + CertificateAuthorityData: encodeCertPEM(caCert), + }, + }, + Contexts: map[string]*api.Context{ + ctx: { + Cluster: c.clusterName, + AuthInfo: c.user, + Namespace: confighelper.GetUsersNamespace(c.user), + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + c.user: { + ClientCertificateData: encodedClientCert, + ClientKeyData: encodedClientKey, + }, + }, + CurrentContext: ctx, + }, nil +} + +func newCertAndKey( + caCert *x509.Certificate, + caKey crypto.Signer, + user string, + groups, dnsNames []string, + ipAddresses []net.IP, + expiration int32, +) (*x509.Certificate, crypto.Signer, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, fmt.Errorf("generate client key error %w", err) + } + serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + if err != nil { + return nil, nil, fmt.Errorf("rand serial error %w", err) + } + + noAfter := time.Now().Add(time.Second * time.Duration(expiration)) + + certTmpl := x509.Certificate{ + Subject: pkix.Name{ + CommonName: user, + Organization: groups, + }, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + SerialNumber: serial, + NotBefore: caCert.NotBefore, + NotAfter: noAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey) + if err != nil { + return nil, nil, fmt.Errorf("create cert failed %w", err) + } + cert, err := x509.ParseCertificate(certDERBytes) + if err != nil { + return nil, nil, fmt.Errorf("parse cert failed %w", err) + } + return cert, key, nil +} diff --git a/controllers/user/controllers/helper/kubeconfig/csr.go b/controllers/user/controllers/helper/kubeconfig/csr.go new file mode 100644 index 000000000000..c09cbcbabf33 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/csr.go @@ -0,0 +1,234 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "net" + "time" + + config2 "github.com/labring/sealos/controllers/user/controllers/helper/config" + csrv1 "k8s.io/api/certificates/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// newPrivateKey creates an RSA private key +func newPrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) { + if keyType == x509.ECDSA { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } + + return rsa.GenerateKey(rand.Reader, 2048) +} + +func (csr *CsrConfig) newSignedToCsrKey() (csrData, keyPEM []byte, err error) { + key, err := newPrivateKey(x509.RSA) + if err != nil { + return nil, nil, fmt.Errorf("new signed private failed %w", err) + } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("new signed private failed unexpected key type %T", key) + } + pk := x509.MarshalPKCS1PrivateKey(rsaKey) + keyPEM = pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", Bytes: pk, + }) + _, csrObj, err := csr.generateCSR(key) + if err != nil { + return nil, nil, fmt.Errorf("new signed csr failed %w", err) + } + csrData = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", Bytes: csrObj, + }) + if err != nil { + return nil, nil, fmt.Errorf("new signed csr failed %w", err) + } + + return csrData, keyPEM, err +} + +func (csr *CsrConfig) Apply(config *rest.Config, client client.Client) (*api.Config, error) { + csrKey, key, err := csr.newSignedToCsrKey() + if err != nil { + return nil, err + } + // make sure cadata is loaded into config under incluster mode + if err = rest.LoadTLSFiles(config); err != nil { + return nil, err + } + ca := config.CAData + csr.ctxCAKey = ca + csr.ctxTLSKey = key + csr.ctxTLSCsr = csrKey + if err = csr.updateCsr(config, client); err != nil { + return nil, err + } + ctx := fmt.Sprintf("%s@%s", csr.user, csr.clusterName) + return &api.Config{ + Clusters: map[string]*api.Cluster{ + csr.clusterName: { + Server: GetKubernetesHost(config), + CertificateAuthorityData: ca, + }, + }, + Contexts: map[string]*api.Context{ + ctx: { + Cluster: csr.clusterName, + AuthInfo: csr.user, + Namespace: config2.GetUsersNamespace(csr.user), + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + csr.user: { + ClientCertificateData: csr.ctxTLSCrt, + ClientKeyData: key, + }, + }, + CurrentContext: ctx, + }, nil +} + +func (csr *CsrConfig) updateCsr(config *rest.Config, cli client.Client) error { + var csrResource *csrv1.CertificateSigningRequest + if csr.csr != nil { + csrResource = csr.csr.DeepCopy() + } else { + csrName := "sealos-generater-" + csr.user + csrResource = &csrv1.CertificateSigningRequest{} + csrResource.Name = csrName + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + insertCSR := csrResource.DeepCopy() + insertCSR.ResourceVersion = "0" + insertCSR.Spec.Request = csr.ctxTLSCsr + insertCSR.Spec.SignerName = csrv1.KubeAPIServerClientSignerName + insertCSR.Spec.ExpirationSeconds = &csr.expirationSeconds + insertCSR.Spec.Groups = []string{"system:authenticated"} + insertCSR.Spec.Usages = []csrv1.KeyUsage{ + "digital signature", + "key encipherment", + "client auth", + } + + err := cli.Create(context.TODO(), insertCSR) + if err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + } + csrResource = insertCSR.DeepCopy() + return nil + }); err != nil { + return err + } + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + + csrResource.Status.Conditions = []csrv1.CertificateSigningRequestCondition{ + { + Type: csrv1.CertificateApproved, + Status: corev1.ConditionTrue, + Reason: "AutoApproved", + Message: "This CSR was approved by user certificate approve.", + }, + } + _, err = clientset.CertificatesV1(). + CertificateSigningRequests(). + UpdateApproval(context.TODO(), csrResource.Name, csrResource, v1.UpdateOptions{}) + if err != nil { + return err + } + + w, err := clientset.CertificatesV1(). + CertificateSigningRequests(). + Watch(context.TODO(), v1.ListOptions{FieldSelector: "metadata.name=" + csrResource.Name}) + if err != nil { + return err + } + start := time.Now() + for { + select { + case <-time.After(time.Second * 10): + return errors.NewBadRequest("The CSR is not ready.") + case event := <-w.ResultChan(): + if event.Type == watch.Modified || event.Type == watch.Added { + certificateSigningRequest, ok := event.Object.(*csrv1.CertificateSigningRequest) + if !ok { + return errors.NewBadRequest("unexpected csr watch type") + } + if certificateSigningRequest.Status.Certificate != nil { + csr.ctxTLSCrt = certificateSigningRequest.Status.Certificate + dis := time.Since(start).Milliseconds() + defaultLog.Info("The csr is ready", "using Milliseconds", dis) + return nil + } + } + } + } +} + +// generateCSR will generate a new *x509.CertificateRequest template to be used +// by issuers that utilise CSRs to obtain Certificates. +// The CSR will not be signed, and should be passed to either EncodeCSR or +// to the x509.CreateCertificateRequest function. +func (csr *CsrConfig) generateCSR(key crypto.Signer) (*x509.CertificateRequest, []byte, error) { + if len(csr.user) == 0 { + return nil, nil, errors.NewBadRequest("must specify a CommonName") + } + + dnsNames := make([]string, 0, len(csr.dnsNames)) + ips := make([]net.IP, 0, len(csr.ipAddresses)) + + dnsNames = append(dnsNames, csr.dnsNames...) + ips = append(ips, csr.ipAddresses...) + certTmpl := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: csr.user, + Organization: csr.groups, + }, + + DNSNames: dnsNames, + IPAddresses: ips, + } + certDERBytes, err := x509.CreateCertificateRequest(rand.Reader, &certTmpl, key) + if err != nil { + return nil, nil, err + } + r1, r3 := x509.ParseCertificateRequest(certDERBytes) + return r1, certDERBytes, r3 +} diff --git a/controllers/user/controllers/helper/kubeconfig/interface.go b/controllers/user/controllers/helper/kubeconfig/interface.go new file mode 100644 index 000000000000..3d036f8d0d6b --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/interface.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "context" + "net" + "os" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + csrv1 "k8s.io/api/certificates/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var defaultLog = ctrl.Log.WithName("kubeconfig") + +const defaultCSRExpirationSeconds int32 = userv1.DefaultCSRExpirationSeconds + +type Interface interface { + Apply(config *rest.Config, client client.Client) (*api.Config, error) +} + +type TokenRequestInterface interface { + Interface + ApplyWithTokenRequest( + ctx context.Context, + config *rest.Config, + client client.Client, + ) (*api.Config, metav1.Time, error) +} + +type CertConfig struct { + *DefaultConfig + caKeyFile string + groups []string + dnsNames []string + ipAddresses []net.IP +} + +type CsrConfig struct { + *DefaultConfig + groups []string + dnsNames []string + ipAddresses []net.IP + csr *csrv1.CertificateSigningRequest + ctxCAKey []byte + ctxTLSKey []byte + ctxTLSCrt []byte + ctxTLSCsr []byte +} +type ServiceAccountConfig struct { + *DefaultConfig + namespace string + secretName string + forceNewSecret bool + sa *v1.ServiceAccount +} + +type WebhookConfig struct { + *DefaultConfig + webhookURL string +} + +func GetKubernetesHost(config *rest.Config) string { + host, port := os.Getenv("SEALOS_CLOUD_APISERVER_HOST"), os.Getenv("SEALOS_CLOUD_APISERVER_PORT") + if len(host) != 0 && len(port) != 0 { + return "https://" + net.JoinHostPort(host, port) + } + host, port = os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") + if len(host) == 0 || len(port) == 0 { + return config.Host + } + return "https://" + net.JoinHostPort(host, port) +} + +type DefaultConfig struct { + user string + clusterName string // default is kubernetes + expirationSeconds int32 +} + +func NewConfig(user, clusterName string, expirationSeconds int32) *DefaultConfig { + if clusterName == "" { + clusterName = "sealos" + } + expirationSeconds = userv1.NormalizeCSRExpirationSeconds(expirationSeconds) + return &DefaultConfig{ + user: user, + clusterName: clusterName, + expirationSeconds: expirationSeconds, + } +} + +func (d *DefaultConfig) WithCertConfig( + caKeyFile string, + groups, dnsNames []string, + ipAddrs []net.IP, +) Interface { + return &CertConfig{ + DefaultConfig: d, + caKeyFile: caKeyFile, + groups: groups, + dnsNames: dnsNames, + ipAddresses: ipAddrs, + } +} + +func (d *DefaultConfig) WithCsrConfig( + groups, dnsNames []string, + ipAddrs []net.IP, + csr *csrv1.CertificateSigningRequest, +) Interface { + return &CsrConfig{ + DefaultConfig: d, + groups: groups, + dnsNames: dnsNames, + ipAddresses: ipAddrs, + csr: csr, + } +} + +func (d *DefaultConfig) WithServiceAccountConfig( + namespace string, + sa *v1.ServiceAccount, +) *ServiceAccountConfig { + if namespace == "" { + namespace = "default" + } + return &ServiceAccountConfig{ + DefaultConfig: d, + namespace: namespace, + sa: sa, + } +} + +func (sac *ServiceAccountConfig) WithForceNewSecret() *ServiceAccountConfig { + sac.forceNewSecret = true + return sac +} + +func (d *DefaultConfig) WithWebhookConfigConfig(webhookURL string) Interface { + return &WebhookConfig{ + DefaultConfig: d, + webhookURL: webhookURL, + } +} diff --git a/controllers/user/controllers/helper/kubeconfig/sa.go b/controllers/user/controllers/helper/kubeconfig/sa.go new file mode 100644 index 000000000000..b14f63876bd0 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/sa.go @@ -0,0 +1,287 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + config2 "github.com/labring/sealos/controllers/user/controllers/helper/config" + authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func (sac *ServiceAccountConfig) Apply( + config *rest.Config, + client client.Client, +) (*api.Config, error) { + cfg, _, err := sac.ApplyWithTokenRequest(context.Background(), config, client) + return cfg, err +} + +func (sac *ServiceAccountConfig) ApplyWithTokenRequest( + ctx context.Context, + config *rest.Config, + client client.Client, +) (*api.Config, metav1.Time, error) { + if err := sac.applyServiceAccount(ctx, config, client); err != nil { + return nil, metav1.Time{}, fmt.Errorf("failed to apply service account error: %w", err) + } + boundSecret, err := sac.applyBoundTokenSecret(ctx, client) + if err != nil { + return nil, metav1.Time{}, fmt.Errorf("failed to apply bound token secret: %w", err) + } + tokenRequest, err := sac.requestToken(ctx, config, boundSecret) + if err != nil { + return nil, metav1.Time{}, fmt.Errorf("failed to fetch token: %w", err) + } + cfg, err := sac.generatorKubeConfig(config, tokenRequest.Status.Token) + if err != nil { + return nil, metav1.Time{}, fmt.Errorf("failed to generate kube config: %w", err) + } + return cfg, tokenRequest.Status.ExpirationTimestamp, nil +} + +func (sac *ServiceAccountConfig) applyServiceAccount( + ctx context.Context, + _ *rest.Config, + client client.Client, +) error { + sa := sac.sa + if sa == nil { + sa = &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: sac.user, + Namespace: sac.namespace, + }, + } + } + if sa.Name == "" { + sa.Name = sac.user + } + if sa.Namespace == "" { + sa.Namespace = sac.namespace + } + _, err := controllerutil.CreateOrUpdate(ctx, client, sa, func() error { + if sac.forceNewSecret || len(sa.Secrets) == 0 { + sa.Secrets = []v1.ObjectReference{ + { + Name: sac.generateSecretName(), + }, + } + } + return nil + }) + if err != nil { + return err + } + sac.sa = sa + if len(sa.Secrets) > 0 { + sac.secretName = sa.Secrets[0].Name + } + return nil +} + +func (sac *ServiceAccountConfig) applyBoundTokenSecret( + ctx context.Context, + cli client.Client, +) (*v1.Secret, error) { + secretName := sac.secretName + if secretName == "" { + secretName = sac.generateSecretName() + } + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: sac.namespace, + }, + } + _, err := controllerutil.CreateOrUpdate(ctx, cli, secret, func() error { + // Secret type is immutable after creation. Keep the type of legacy + // bound secrets and use Opaque only for newly created secrets. + if secret.Type == "" { + secret.Type = v1.SecretTypeOpaque + } + if secret.Annotations == nil { + secret.Annotations = map[string]string{} + } + secret.Annotations[v1.ServiceAccountNameKey] = sac.user + if sac.sa != nil { + secret.OwnerReferences = append([]metav1.OwnerReference(nil), sac.sa.OwnerReferences...) + } + return nil + }) + if err != nil { + return nil, err + } + if secret.UID == "" { + return nil, fmt.Errorf( + "bound token secret %s/%s has empty uid", + secret.Namespace, + secret.Name, + ) + } + return secret, nil +} + +// CleanupLegacyBoundTokenSecrets removes stale bound token secrets for a user. +func CleanupLegacyBoundTokenSecrets( + ctx context.Context, + reader client.Reader, + writer client.Writer, + userName, keepSecretName string, +) error { + secrets := &metav1.PartialObjectMetadataList{} + secrets.SetGroupVersionKind(schema.GroupVersion{Version: "v1"}.WithKind("SecretList")) + if err := reader.List( + ctx, + secrets, + client.InNamespace(config2.GetUserSystemNamespace()), + client.MatchingFields{v1.ServiceAccountNameKey: userName}, + ); err != nil { + return fmt.Errorf("failed to list legacy bound token secrets: %w", err) + } + if keepSecretName == "" { + return errors.New("keep secret name is empty") + } + for i := range secrets.Items { + secret := &secrets.Items[i] + if secret.Name == "" || secret.Name == keepSecretName { + continue + } + if secret.Annotations == nil || secret.Annotations[v1.ServiceAccountNameKey] != userName { + continue + } + deleteTarget := &v1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name, Namespace: secret.Namespace, + }} + if err := writer.Delete(ctx, deleteTarget); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete legacy bound token secret %s: %w", secret.Name, err) + } + } + return nil +} + +func (sac *ServiceAccountConfig) requestToken( + ctx context.Context, + config *rest.Config, + boundSecret *v1.Secret, +) (*authenticationv1.TokenRequest, error) { + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + tokenRequest, err := clientset.CoreV1(). + ServiceAccounts(sac.namespace). + CreateToken(ctx, sac.user, &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: ptr.To(int64(sac.tokenRequestExpirationSeconds())), + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "Secret", + APIVersion: "v1", + Name: boundSecret.Name, + UID: boundSecret.UID, + }, + }, + }, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + if tokenRequest.Status.Token == "" { + return nil, fmt.Errorf( + "token request returned empty token for serviceaccount %s/%s", + sac.namespace, + sac.user, + ) + } + return tokenRequest, nil +} + +func (sac *ServiceAccountConfig) tokenRequestExpirationSeconds() int32 { + return userv1.NormalizeCSRExpirationSeconds(sac.expirationSeconds) +} + +func TokenSecretName(name string) string { + return "sealos-token-" + name +} + +func (sac *ServiceAccountConfig) generateSecretName() string { + if sac.secretName != "" { + return sac.secretName + } + if !sac.forceNewSecret && + sac.sa != nil && + len(sac.sa.Secrets) > 0 && + sac.sa.Secrets[0].Name != "" { + return sac.sa.Secrets[0].Name + } + return "sealos-token-" + sac.user + "-" + GetRandomString(5) +} + +func GetRandomString(n int) string { + randBytes := make([]byte, n/2) + if _, err := rand.Read(randBytes); err != nil { + return "" + } + return hex.EncodeToString(randBytes) +} + +func (sac *ServiceAccountConfig) generatorKubeConfig( + cfg *rest.Config, + token string, +) (*api.Config, error) { + // make sure cadata is loaded into config under incluster mode + if err := rest.LoadTLSFiles(cfg); err != nil { + return nil, err + } + ctx := fmt.Sprintf("%s@%s", sac.user, sac.clusterName) + config := &api.Config{ + Clusters: map[string]*api.Cluster{ + sac.clusterName: { + Server: GetKubernetesHost(cfg), + CertificateAuthorityData: cfg.CAData, + }, + }, + Contexts: map[string]*api.Context{ + ctx: { + Cluster: sac.clusterName, + AuthInfo: sac.user, + Namespace: config2.GetUsersNamespace(sac.user), + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + sac.user: { + Token: token, + }, + }, + CurrentContext: ctx, + } + return config, nil +} diff --git a/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go b/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go new file mode 100644 index 000000000000..a2b56c4265fc --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/sa_legacy_secret_test.go @@ -0,0 +1,279 @@ +/* +Copyright 2022 labring. + +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 kubeconfig + +import ( + "context" + "errors" + "testing" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + config2 "github.com/labring/sealos/controllers/user/controllers/helper/config" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type canceledContextClient struct { + client.Client +} + +func (c canceledContextClient) Get( + ctx context.Context, + key client.ObjectKey, + obj client.Object, + opts ...client.GetOption, +) error { + if err := ctx.Err(); err != nil { + return err + } + return errors.New("service account read did not receive the canceled context") +} + +func TestNewConfigNormalizesBelowMinimumExpiration(t *testing.T) { + t.Parallel() + + cfg := NewConfig("alice", "", 7_200) + if cfg.expirationSeconds != userv1.DefaultCSRExpirationSeconds { + t.Fatalf( + "config expiration = %d, want minimum %d", + cfg.expirationSeconds, + userv1.DefaultCSRExpirationSeconds, + ) + } + sac := cfg.WithServiceAccountConfig("user-system", nil) + if got := sac.tokenRequestExpirationSeconds(); got != userv1.DefaultCSRExpirationSeconds { + t.Fatalf( + "token request expiration = %d, want minimum %d", + got, + userv1.DefaultCSRExpirationSeconds, + ) + } +} + +func TestCleanupLegacyBoundTokenSecrets(t *testing.T) { + t.Parallel() + + const ( + userName = "alice" + currentSecret = "sealos-token-alice-new" + legacySecret = "sealos-token-alice-old" + duplicateSecret = "sealos-token-alice-dupe" + otherSecret = "sealos-token-bob-old" + ) + + current := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: currentSecret, + Namespace: config2.GetUserSystemNamespace(), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: userName, + }, + }, + Type: corev1.SecretTypeOpaque, + } + legacy := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: legacySecret, + Namespace: config2.GetUserSystemNamespace(), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: userName, + }, + }, + Type: corev1.SecretTypeServiceAccountToken, + } + duplicate := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: duplicateSecret, + Namespace: config2.GetUserSystemNamespace(), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: userName, + }, + }, + Type: corev1.SecretTypeOpaque, + } + other := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherSecret, + Namespace: config2.GetUserSystemNamespace(), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: "bob", + }, + }, + Type: corev1.SecretTypeServiceAccountToken, + } + + secretMetadata := &metav1.PartialObjectMetadata{} + secretMetadata.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) + cli := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(current, legacy, duplicate, other). + WithIndex(secretMetadata, corev1.ServiceAccountNameKey, func(obj client.Object) []string { + if obj.GetAnnotations() == nil { + return nil + } + value := obj.GetAnnotations()[corev1.ServiceAccountNameKey] + if value == "" { + return nil + } + return []string{value} + }). + Build() + + if err := CleanupLegacyBoundTokenSecrets( + context.Background(), + cli, + cli, + userName, + currentSecret, + ); err != nil { + t.Fatalf("cleanup legacy secrets: %v", err) + } + + var got corev1.Secret + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(current), &got); err != nil { + t.Fatalf("get current secret: %v", err) + } + if got.Type != corev1.SecretTypeOpaque { + t.Fatalf("current secret type = %s, want %s", got.Type, corev1.SecretTypeOpaque) + } + + if err := cli.Get( + context.Background(), + client.ObjectKeyFromObject(legacy), + &got, + ); !apierrors.IsNotFound(err) { + t.Fatalf("legacy secret err = %v, want not found", err) + } + if err := cli.Get( + context.Background(), + client.ObjectKeyFromObject(duplicate), + &got, + ); !apierrors.IsNotFound(err) { + t.Fatalf("duplicate secret err = %v, want not found", err) + } + + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(other), &got); err != nil { + t.Fatalf("get other secret: %v", err) + } +} + +func TestServiceAccountConfigWithForceNewSecret(t *testing.T) { + t.Parallel() + + const ( + userName = "alice" + oldSecret = "sealos-token-alice-old" + namespace = "user-system" + ) + + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: userName, + Namespace: namespace, + }, + Secrets: []corev1.ObjectReference{ + { + Name: oldSecret, + }, + }, + } + + cli := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(sa). + Build() + + cfg := &ServiceAccountConfig{ + DefaultConfig: &DefaultConfig{ + user: userName, + clusterName: "", + expirationSeconds: defaultCSRExpirationSeconds, + }, + namespace: namespace, + sa: sa, + forceNewSecret: true, + } + + if err := cfg.applyServiceAccount(context.Background(), nil, cli); err != nil { + t.Fatalf("apply service account: %v", err) + } + if len(cfg.sa.Secrets) != 1 { + t.Fatalf("secret count = %d, want 1", len(cfg.sa.Secrets)) + } + if cfg.sa.Secrets[0].Name == oldSecret { + t.Fatalf("secret name reused old secret %q", oldSecret) + } + if cfg.secretName != cfg.sa.Secrets[0].Name { + t.Fatalf( + "cached secret name %q != current secret name %q", + cfg.secretName, + cfg.sa.Secrets[0].Name, + ) + } +} + +func TestApplyServiceAccountPropagatesCanceledContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + cfg := &ServiceAccountConfig{ + DefaultConfig: &DefaultConfig{user: "alice"}, + namespace: "user-system", + } + err := cfg.applyServiceAccount(ctx, nil, canceledContextClient{}) + if !errors.Is(err, context.Canceled) { + t.Fatalf("apply service account error = %v, want context canceled", err) + } +} + +func TestApplyBoundTokenSecretPreservesExistingType(t *testing.T) { + t.Parallel() + + const ( + userName = "alice" + namespace = "user-system" + secretName = "sealos-token-alice" + ) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + UID: types.UID("secret-alice"), + }, + Type: corev1.SecretTypeServiceAccountToken, + } + cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(secret).Build() + cfg := &ServiceAccountConfig{ + DefaultConfig: &DefaultConfig{user: userName}, + namespace: namespace, + secretName: secretName, + } + + got, err := cfg.applyBoundTokenSecret(context.Background(), cli) + if err != nil { + t.Fatalf("apply bound token secret: %v", err) + } + if got.Type != corev1.SecretTypeServiceAccountToken { + t.Fatalf("secret type = %s, want %s", got.Type, corev1.SecretTypeServiceAccountToken) + } +} diff --git a/controllers/user/controllers/helper/kubeconfig/suite_test.go b/controllers/user/controllers/helper/kubeconfig/suite_test.go new file mode 100644 index 000000000000..fb0579ef7892 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/suite_test.go @@ -0,0 +1,234 @@ +/* +Copyright 2022 labring. + +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 kubeconfig + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + csrv1 "k8s.io/api/certificates/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestUtils(t *testing.T) { + if os.Getenv("RUN_KUBECONFIG_CLUSTER_TESTS") != "true" { + t.Skip("set RUN_KUBECONFIG_CLUSTER_TESTS=true to run kubeconfig cluster tests") + } + RegisterFailHandler(Fail) + + RunSpecs(t, "run helper suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + useExisting := true + if val := os.Getenv("USE_EXISTING_CLUSTER"); val != "" { + if parsed, err := strconv.ParseBool(val); err == nil { + useExisting = parsed + } + } + testEnv = &envtest.Environment{ + UseExistingCluster: &useExisting, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = csrv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = Describe("user kubeconfig ", func() { + Context("syncReNewConfig test", func() { + BeforeEach(func() { + clientSet, _ := kubernetes.NewForConfig(cfg) + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cuisongliu-rolebinding", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "user", + Name: "cuisongliu", + Namespace: "default", + APIGroup: "rbac.authorization.k8s.io", + }, + { + Kind: "ServiceAccount", + Name: "cuisongliu", + Namespace: "user-system", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "cluster-admin", + APIGroup: "rbac.authorization.k8s.io", + }, + } + + _, err := clientSet.RbacV1(). + RoleBindings("default"). + Create(context.TODO(), rb, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + AfterEach(func() { + clientSet, _ := kubernetes.NewForConfig(cfg) + _ = clientSet.RbacV1(). + RoleBindings("default"). + Delete(context.TODO(), "cuisongliu-rolebinding", metav1.DeleteOptions{}) + _ = os.RemoveAll("output") + }) + It("empty csr generate", func() { + defaultConfig := NewConfig("cuisongliu", "", 100000000) + gen := defaultConfig.WithCsrConfig( + []string{}, + []string{"apiserver.cluster.local"}, + nil, + nil, + ) + By("start to get kubeconfig") + config, err := gen.Apply(cfg, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + if info, ok := config.AuthInfos["cuisongliu"]; ok { + if info != nil { + cert, err := DecodeX509CertificateBytes(info.ClientCertificateData) + if err != nil { + Expect(err).ToNot(HaveOccurred()) + } + if cert.NotAfter.Before(time.Now()) { + config = nil + By(fmt.Sprintf("ClientCertificateData %s is expired", "cuisongliu")) + return + } + By("Cert NotAfter is " + cert.NotAfter.String()) + } + } + + kubeData, err := clientcmd.Write(*config) + Expect(err).ToNot(HaveOccurred()) + Expect(kubeData).NotTo(BeNil()) + By("start to write kubeconfig") + err = os.WriteFile("output", kubeData, 0o600) + Expect(err).ToNot(HaveOccurred()) + + newCfg, err := clientcmd.BuildConfigFromFlags("", "output") + Expect(err).ToNot(HaveOccurred()) + Expect(newCfg).NotTo(BeNil()) + newCfg.QPS = 1e6 + newCfg.Burst = 1e6 + clientSet, err := kubernetes.NewForConfig(newCfg) + Expect(err).ToNot(HaveOccurred()) + _, err = clientSet.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{}) + Expect(err).ToNot(HaveOccurred()) + _, err = clientSet.CoreV1(). + Pods("kube-system"). + List(context.TODO(), metav1.ListOptions{}) + errStatus := errors.ReasonForError(err) + Expect(err).To(HaveOccurred()) + Expect(errStatus).To(Equal(metav1.StatusReasonForbidden)) + }) + It("token generate", func() { + defaultConfig := NewConfig("cuisongliu", "", 100000000) + gen := defaultConfig.WithServiceAccountConfig("test", nil) + By("start to get kubeconfig") + config, err := gen.Apply(cfg, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + kubeData, err := clientcmd.Write(*config) + Expect(err).ToNot(HaveOccurred()) + Expect(kubeData).NotTo(BeNil()) + By("start to write kubeconfig") + err = os.WriteFile("output", kubeData, 0o600) + Expect(err).ToNot(HaveOccurred()) + + newCfg, err := clientcmd.BuildConfigFromFlags("", "output") + Expect(err).ToNot(HaveOccurred()) + Expect(newCfg).NotTo(BeNil()) + newCfg.QPS = 1e6 + newCfg.Burst = 1e6 + clientSet, err := kubernetes.NewForConfig(newCfg) + Expect(err).ToNot(HaveOccurred()) + _, err = clientSet.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{}) + Expect(err).ToNot(HaveOccurred()) + _, err = clientSet.CoreV1(). + Pods("kube-system"). + List(context.TODO(), metav1.ListOptions{}) + errStatus := errors.ReasonForError(err) + Expect(err).To(HaveOccurred()) + Expect(errStatus).To(Equal(metav1.StatusReasonForbidden)) + }) + It("webhook generate", func() { + defaultConfig := NewConfig("cuisongliu", "", 100000000) + gen := defaultConfig.WithWebhookConfigConfig("https://192.168.64.1:6443") + By("start to get kubeconfig") + config, err := gen.Apply(cfg, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + kubeData, err := clientcmd.Write(*config) + Expect(err).ToNot(HaveOccurred()) + Expect(kubeData).NotTo(BeNil()) + By("start to write kubeconfig") + err = os.WriteFile("output-webhook", kubeData, 0o600) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/controllers/user/controllers/helper/kubeconfig/tools.go b/controllers/user/controllers/helper/kubeconfig/tools.go new file mode 100644 index 000000000000..6cc75a9f8041 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/tools.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +// DecodeX509CertificateChainBytes will decode a PEM encoded x509 Certificate chain. +func DecodeX509CertificateChainBytes(certBytes []byte) ([]*x509.Certificate, error) { + certs := make([]*x509.Certificate, 0, 1) + + var block *pem.Block + + for { + // decode the tls certificate pem + block, certBytes = pem.Decode(certBytes) + if block == nil { + break + } + + // parse the tls certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing TLS certificate: %s", err.Error()) + } + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.New("error decoding certificate PEM block") + } + + return certs, nil +} + +// DecodeX509CertificateBytes will decode a PEM encoded x509 Certificate. +func DecodeX509CertificateBytes(certBytes []byte) (*x509.Certificate, error) { + certs, err := DecodeX509CertificateChainBytes(certBytes) + if err != nil { + return nil, err + } + + return certs[0], nil +} diff --git a/controllers/user/controllers/helper/kubeconfig/webhook.go b/controllers/user/controllers/helper/kubeconfig/webhook.go new file mode 100644 index 000000000000..feeb31f511f8 --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/webhook.go @@ -0,0 +1,48 @@ +/* +Copyright 2022 cuisongliu@qq.com. + +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 kubeconfig + +import ( + "fmt" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (c *WebhookConfig) Apply(_ *rest.Config, _ client.Client) (*api.Config, error) { + // make sure cadata is loaded into config under incluster mode + ctx := fmt.Sprintf("%s@%s", c.user, c.clusterName) + return &api.Config{ + Clusters: map[string]*api.Cluster{ + c.clusterName: { + Server: c.webhookURL, + InsecureSkipTLSVerify: true, + }, + }, + Contexts: map[string]*api.Context{ + ctx: { + Cluster: c.clusterName, + AuthInfo: "webhook-user", + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + "webhook-user": {}, + }, + CurrentContext: ctx, + }, nil +} diff --git a/controllers/user/controllers/helper/kubeconfig/webhook.md b/controllers/user/controllers/helper/kubeconfig/webhook.md new file mode 100644 index 000000000000..40225f5f4a2a --- /dev/null +++ b/controllers/user/controllers/helper/kubeconfig/webhook.md @@ -0,0 +1,228 @@ +## How to using webhook generate to webhook_config +set remote http webhook url +```go + +gen := NewGenerate(&Config{ + Webhook: true, + WebhookURL: "http://192.168.64.1:8080/auth", +}) +config, err := gen.KubeConfig(cfg, k8sClient) +kubeData, err := clientcmd.Write(*config) +err = os.WriteFile("output-webhook", kubeData, 0600) +``` + +Note that the address of this webhookURL should be specific to the path `/path` + +## Add webhook config to apiserver node +update apiserver config `/etc/kubernetes/manifests/kube-apiserver.yaml` +1. authorization-mode add Webhook mode +2. authorization-webhook-config-file set webhook config +3. add volume for apiserer config mount webhook config +```yaml +apiVersion: v1 +kind: Pod +metadata: + annotations: + kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 192.168.64.31:6443 + creationTimestamp: null + labels: + component: kube-apiserver + tier: control-plane + name: kube-apiserver + namespace: kube-system +spec: + containers: + - command: + - kube-apiserver + - --advertise-address=192.168.64.31 + - --allow-privileged=true + - --audit-log-format=json + - --audit-log-maxage=7 + - --audit-log-maxbackup=10 + - --audit-log-maxsize=200 + - --audit-log-path=/var/log/kubernetes/audit.log + - --audit-policy-file=/etc/kubernetes/audit-policy.yml + - --authorization-mode=Node,RBAC,Webhook + - --authorization-webhook-config-file=/root/.kube_webhook/config + - --client-ca-file=/etc/kubernetes/pki/ca.crt + - --enable-admission-plugins=NodeRestriction + - --enable-aggregator-routing=true + - --enable-bootstrap-token-auth=true + - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt + - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt + - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key + - --etcd-servers=https://192.168.64.31:2379 + - --feature-gates=TTLAfterFinished=true,EphemeralContainers=true + - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt + - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt + - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key + - --requestheader-allowed-names=front-proxy-client + - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt + - --requestheader-extra-headers-prefix=X-Remote-Extra- + - --requestheader-group-headers=X-Remote-Group + - --requestheader-username-headers=X-Remote-User + - --secure-port=6443 + - --service-account-issuer=https://kubernetes.default.svc.cluster.local + - --service-account-key-file=/etc/kubernetes/pki/sa.pub + - --service-account-signing-key-file=/etc/kubernetes/pki/sa.key + - --service-cluster-ip-range=10.96.0.0/22 + - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt + - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key + image: k8s.gcr.io/kube-apiserver:v1.23.10 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 8 + httpGet: + host: 192.168.64.31 + path: /livez + port: 6443 + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 15 + name: kube-apiserver + readinessProbe: + failureThreshold: 3 + httpGet: + host: 192.168.64.31 + path: /readyz + port: 6443 + scheme: HTTPS + periodSeconds: 1 + timeoutSeconds: 15 + resources: + requests: + cpu: 250m + startupProbe: + failureThreshold: 24 + httpGet: + host: 192.168.64.31 + path: /livez + port: 6443 + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 15 + volumeMounts: + - mountPath: /etc/kubernetes + name: audit + - mountPath: /var/log/kubernetes + name: audit-log + - mountPath: /etc/ssl/certs + name: ca-certs + readOnly: true + - mountPath: /etc/ca-certificates + name: etc-ca-certificates + readOnly: true + - mountPath: /etc/pki + name: etc-pki + readOnly: true + - mountPath: /etc/kubernetes/pki + name: k8s-certs + readOnly: true + - mountPath: /etc/localtime + name: localtime + readOnly: true + - mountPath: /usr/local/share/ca-certificates + name: usr-local-share-ca-certificates + readOnly: true + - mountPath: /usr/share/ca-certificates + name: usr-share-ca-certificates + readOnly: true + - mountPath: /root/.kube_webhook + name: webhook + readOnly: true + hostNetwork: true + priorityClassName: system-node-critical + securityContext: + seccompProfile: + type: RuntimeDefault + volumes: + - hostPath: + path: /etc/kubernetes + type: DirectoryOrCreate + name: audit + - hostPath: + path: /var/log/kubernetes + type: DirectoryOrCreate + name: audit-log + - hostPath: + path: /etc/ssl/certs + type: DirectoryOrCreate + name: ca-certs + - hostPath: + path: /etc/ca-certificates + type: DirectoryOrCreate + name: etc-ca-certificates + - hostPath: + path: /etc/pki + type: DirectoryOrCreate + name: etc-pki + - hostPath: + path: /etc/kubernetes/pki + type: DirectoryOrCreate + name: k8s-certs + - hostPath: + path: /etc/localtime + type: File + name: localtime + - hostPath: + path: /usr/local/share/ca-certificates + type: DirectoryOrCreate + name: usr-local-share-ca-certificates + - hostPath: + path: /usr/share/ca-certificates + type: DirectoryOrCreate + name: usr-share-ca-certificates + - hostPath: + path: /root/.kube_webhook + type: DirectoryOrCreate + name: webhook +status: {} +``` + +## coding http webhook server +```go +func HttpServer(port uint16) error { + r := gin.New() + gin.SetMode(gin.ReleaseMode) + r.Use(middlewares.GinLogger(), middlewares.GinRecovery(true)) + + r.POST("/auth", Auth) + gin.SetMode(gin.DebugMode) + //prefix := "/Users/cuisongliu/Workspaces/go/src/github.com/cuisongliu/rbac-blacklist/pki" + return r.Run(fmt.Sprintf("%s:%d", "0.0.0.0", port)) +} + +var serverLogger = ctrl.Log.WithName("serverLogger") + +func Auth(ctx *gin.Context) { + fmt.Println("auth in ...") + var review v1.SubjectAccessReview + err := ctx.BindJSON(&review) + //{"kind":"SubjectAccessReview","apiVersion":"authorization.k8s.io/v1beta1","metadata":{"creationTimestamp":null},"spec":{"resourceAttributes":{"namespace":"ns-cuisongliu","verb":"list","version":"v1","resource":"pods"},"user":"system:serviceaccount:default:cuisongliu","group":["system:serviceaccounts","system:serviceaccounts:default","system:authenticated"],"uid":"01575033-6a76-4426-88d0-3da94c6c3e03"},"status":{"allowed":false}} + //d, err := ctx.GetRawData() + fmt.Println("inff", "msg", string(review.Spec.User)) + if err != nil { + fmt.Println(err, "has error") + } + + //allow := true + //if review.Spec.ResourceAttributes.Resource == "configmaps" && review.Spec.ResourceAttributes.Name == "my-config" { + // allow = false + //} + r := &v1.SubjectAccessReview{ + TypeMeta: v12.TypeMeta{ + Kind: "SubjectAccessReview", + APIVersion: "authorization.k8s.io/v1beta1", + }, + Status: v1.SubjectAccessReviewStatus{Allowed: review.Status.Allowed, Denied: false}, + } + ctx.JSONP(200, r) +} + +``` + +The data obtained by the review structure is the data after the previous authentication logic (RBAC, ABAC) diff --git a/controllers/user/controllers/helper/ratelimiter/ratelimiter.go b/controllers/user/controllers/helper/ratelimiter/ratelimiter.go new file mode 100644 index 000000000000..6a0394fd8076 --- /dev/null +++ b/controllers/user/controllers/helper/ratelimiter/ratelimiter.go @@ -0,0 +1,95 @@ +// Copyright © 2023 sealos. +// +// 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 ratelimiter + +import ( + "flag" + "time" + + "golang.org/x/time/rate" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + defaultMinRetryDelay = 5 * time.Millisecond + defaultMaxRetryDelay = 1000 * time.Second + defaultQPS = float64(10.0) + defaultBurst = 100 + defaultConcurrent = 5 + flagMinRetryDelay = "min-retry-delay" + flagMaxRetryDelay = "max-retry-delay" + flagQPS = "default-qps" + flagBurst = "default-burst" + flagConcurrent = "default-concurrent" +) + +// RateLimiterOptions used on reconcilers. +type RateLimiterOptions struct { + minRetryDelay time.Duration + maxRetryDelay time.Duration + qps float64 + burst int + concurrent int +} + +func (o *RateLimiterOptions) BindFlags(fs *flag.FlagSet) { + fs.DurationVar( + &o.minRetryDelay, + flagMinRetryDelay, + defaultMinRetryDelay, + "Specifies the minimum delay time before retrying the reconciliation of an object. This delay provides a buffer to prevent rapid-fire retries.", + ) + fs.DurationVar( + &o.maxRetryDelay, + flagMaxRetryDelay, + defaultMaxRetryDelay, + "Specifies the maximum delay time before retrying the reconciliation of an object. This cap ensures that retry delays don't grow excessively long.", + ) + fs.Float64Var( + &o.qps, + flagQPS, + defaultQPS, + "Sets the maximum allowed quantity of process units (batches) that can be processed per second. This limit helps maintain a controlled processing rate.", + ) + fs.IntVar( + &o.burst, + flagBurst, + defaultBurst, + "Sets the maximum quantity of process units (batches) that can be processed in a burst. This limit helps control the processing rate during short periods of high activity.", + ) + flag.IntVar( + &o.concurrent, + flagConcurrent, + defaultConcurrent, + "The number of concurrent reconciles.", + ) +} + +func GetRateLimiter(opts RateLimiterOptions) workqueue.TypedRateLimiter[reconcile.Request] { + return workqueue.NewTypedMaxOfRateLimiter[reconcile.Request]( + workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request]( + opts.minRetryDelay, + opts.maxRetryDelay, + ), + &workqueue.TypedBucketRateLimiter[reconcile.Request]{ + Limiter: rate.NewLimiter(rate.Limit(opts.qps), opts.burst), + }, + ) +} + +func GetConcurrent(opts RateLimiterOptions) int { + return opts.concurrent +} diff --git a/controllers/user/controllers/helper/string_any.go b/controllers/user/controllers/helper/string_any.go new file mode 100644 index 000000000000..94ef8a11fc3c --- /dev/null +++ b/controllers/user/controllers/helper/string_any.go @@ -0,0 +1,210 @@ +/* +Copyright The Kubernetes 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 set-gen. DO NOT EDIT. + +package helper + +import ( + "sort" +) + +// sets.String is a set of strings, implemented via map[string]struct{} for minimal memory consumption. +type Any map[string]any + +// NewAny creates a Any from a list of values. +func NewAny(items map[string]any) Any { + ss := Any{} + ss.Insert(items) + return ss +} + +// Insert adds items to the set. +func (s Any) Insert(items map[string]any) Any { + if items == nil { + s = make(map[string]any, 0) + return s + } + for k, item := range items { + s[k] = item + } + return s +} + +func (s Any) InsertValue(key string, value any) Any { + s[key] = value + return s +} + +// Delete removes all items from the set. +func (s Any) Delete(items ...string) Any { + for _, item := range items { + delete(s, item) + } + return s +} + +// Has returns true if and only if item is contained in the set. +func (s Any) Has(item string) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s Any) HasAll(items ...string) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// HasAny returns true if any items are contained in the set. +func (s Any) HasAny(items ...string) bool { + for _, item := range items { + if s.Has(item) { + return true + } + } + return false +} + +// Difference returns a set of objects that are not in s2 +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s Any) Difference(s2 Any) Any { + result := NewAny(nil) + for key, v := range s { + if !s2.Has(key) { + result.InsertValue(key, v) + } + } + return result +} + +// Union returns a new set which includes items in either s1 or s2. +// For example: +// s1 = {a1, a2} +// s2 = {a3, a4} +// s1.Union(s2) = {a1, a2, a3, a4} +// s2.Union(s1) = {a1, a2, a3, a4} +func (s1 Any) Union(s2 Any) Any { + result := NewAny(nil) + for key, v := range s1 { + result.InsertValue(key, v) + } + for key, v := range s2 { + result.InsertValue(key, v) + } + return result +} + +// Intersection returns a new set which includes the item in BOTH s1 and s2 +// For example: +// s1 = {a1, a2} +// s2 = {a2, a3} +// s1.Intersection(s2) = {a2} +func (s1 Any) Intersection(s2 Any) Any { + var walk, other Any + result := NewAny(nil) + if s1.Len() < s2.Len() { + walk = s1 + other = s2 + } else { + walk = s2 + other = s1 + } + for key, v := range walk { + if other.Has(key) { + result.InsertValue(key, v) + } + } + return result +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s1 Any) IsSuperset(s2 Any) bool { + for item := range s2 { + if !s1.Has(item) { + return false + } + } + return true +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +// (In practice, this means same elements, order doesn't matter) +func (s1 Any) Equal(s2 Any) bool { + return len(s1) == len(s2) && s1.IsSuperset(s2) +} + +type sortableSliceOfString []string + +func (s sortableSliceOfString) Len() int { return len(s) } +func (s sortableSliceOfString) Less(i, j int) bool { return lessString(s[i], s[j]) } +func (s sortableSliceOfString) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// List returns the contents as a sorted string slice. +func (s Any) ListKey() []string { + res := make(sortableSliceOfString, 0, len(s)) + for key := range s { + res = append(res, key) + } + sort.Sort(res) + return []string(res) +} + +// List returns the contents as a sorted string slice. +func (s Any) ListValue() []any { + res := make([]any, 0, len(s)) + for _, v := range s { + res = append(res, v) + } + return []any(res) +} + +// UnsortedList returns the slice with contents in random order. +func (s Any) UnsortedList() []string { + res := make([]string, 0, len(s)) + for key := range s { + res = append(res, key) + } + return res +} + +// Returns a single element from the set. +func (s Any) PopAny() (string, bool) { + for key := range s { + s.Delete(key) + return key, true + } + var zeroValue string + return zeroValue, false +} + +// Len returns the size of the set. +func (s Any) Len() int { + return len(s) +} + +func lessString(lhs, rhs string) bool { + return lhs < rhs +} diff --git a/controllers/user/controllers/license_watcher.go b/controllers/user/controllers/license_watcher.go new file mode 100644 index 000000000000..8945904e1e95 --- /dev/null +++ b/controllers/user/controllers/license_watcher.go @@ -0,0 +1,59 @@ +// Copyright © 2026 sealos. +// +// 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 controllers + +import ( + "context" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + "github.com/labring/sealos/controllers/user/pkg/licensegate" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + ctrl "sigs.k8s.io/controller-runtime" +) + +func SetupLicenseGate(mgr ctrl.Manager) error { + logger := ctrl.Log.WithName("license-gate") + reader := mgr.GetAPIReader() + if err := licensegate.Refresh(context.Background(), reader); err != nil { + logger.Error(err, "initial license gate refresh failed") + } + licenseMetadata := &metav1.PartialObjectMetadata{} + licenseMetadata.SetGroupVersionKind(licensev1.GroupVersion.WithKind("License")) + informer, err := mgr.GetCache().GetInformer(context.Background(), licenseMetadata) + if err != nil { + return err + } + if _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + if err := licensegate.Refresh(context.Background(), reader); err != nil { + logger.Error(err, "license gate refresh failed on add") + } + }, + UpdateFunc: func(oldObj, newObj any) { + if err := licensegate.Refresh(context.Background(), reader); err != nil { + logger.Error(err, "license gate refresh failed on update") + } + }, + DeleteFunc: func(obj any) { + if err := licensegate.Refresh(context.Background(), reader); err != nil { + logger.Error(err, "license gate refresh failed on delete") + } + }, + }); err != nil { + return err + } + return nil +} diff --git a/controllers/user/controllers/migrate/migrate.go b/controllers/user/controllers/migrate/migrate.go new file mode 100644 index 000000000000..07f9abe42d88 --- /dev/null +++ b/controllers/user/controllers/migrate/migrate.go @@ -0,0 +1,59 @@ +/* +Copyright 2023 cuisongliu@qq.com. + +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 migrate + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var ( + UGBindingFinalizer = "sealos.io/user.group.binding.finalizers" + UGFinalizer = "sealos.io/user.group.finalizers" +) + +func RemoveFinalizer( + ctx context.Context, + cli client.Client, + obj client.Object, + oldFinalizer string, +) { + oldFinalizerName := oldFinalizer + if controllerutil.ContainsFinalizer(obj, oldFinalizerName) { + controllerutil.RemoveFinalizer(obj, oldFinalizerName) + } + _ = retry.RetryOnConflict(retry.DefaultRetry, func() error { + return cli.Update(ctx, obj) + }) +} + +func SetOwner(ctx context.Context, cli client.Client, obj, owner client.Object) { + _ = retry.RetryOnConflict(retry.DefaultRetry, func() error { + if owner == nil { + obj.SetOwnerReferences([]v1.OwnerReference{}) + return cli.Update(ctx, obj) + } + _, err := controllerutil.CreateOrUpdate(ctx, cli, obj, func() error { + return controllerutil.SetControllerReference(owner, obj, cli.Scheme()) + }) + return err + }) +} diff --git a/controllers/user/controllers/operationrequest_controller.go b/controllers/user/controllers/operationrequest_controller.go new file mode 100644 index 000000000000..0b683eb1b450 --- /dev/null +++ b/controllers/user/controllers/operationrequest_controller.go @@ -0,0 +1,483 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "fmt" + "maps" + "sync" + "time" + + "github.com/go-logr/logr" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + "k8s.io/utils/keymutex" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + controller "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// OperationReqRequeueDuration is the time interval to reconcile a OperationRequest if no error occurs +const OperationReqRequeueDuration time.Duration = 30 * time.Second + +// OperationReqReconciler reconciles a Operationrequest object +type OperationReqReconciler struct { + client.Client + + Logger logr.Logger + Scheme *runtime.Scheme + Recorder record.EventRecorder + locks keymutex.KeyMutex + lockOnce sync.Once + + // expirationTime is the time duration of the request is expired + expirationTime time.Duration + // retentionTime is the time duration of the request is retained after it is isCompleted + retentionTime time.Duration +} + +func (r *OperationReqReconciler) lockWorkspace(namespace string) func() { + r.lockOnce.Do(func() { + // A fixed lock set bounds memory while preserving concurrency across + // independent workspaces. + r.locks = keymutex.NewHashed(256) + }) + r.locks.LockKey(namespace) + return func() { + _ = r.locks.UnlockKey(namespace) + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OperationReqReconciler) SetupWithManager( + mgr ctrl.Manager, + opts ratelimiter.RateLimiterOptions, + expTime, retTime time.Duration, +) error { + const controllerName = "operationrequest_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + if r.Recorder == nil { + r.Recorder = mgr.GetEventRecorderFor(controllerName) + } + r.Scheme = mgr.GetScheme() + r.expirationTime = expTime + r.retentionTime = retTime + r.Logger.V(1).Info("init reconcile operationrequest controller") + return ctrl.NewControllerManagedBy(mgr). + For( + &userv1.Operationrequest{}, + builder.WithPredicates(namespaceOnlyPredicate(config.GetUserSystemNamespace())), + builder.OnlyMetadata, + ). + WithOptions(controller.Options{ + MaxConcurrentReconciles: ratelimiter.GetConcurrent(opts), + RateLimiter: ratelimiter.GetRateLimiter(opts), + }). + Complete(r) +} + +func namespaceOnlyPredicate(namespace string) predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return e.Object.GetNamespace() == namespace + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return e.Object.GetNamespace() == namespace + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return e.ObjectNew.GetNamespace() == namespace + }, + GenericFunc: func(e event.GenericEvent) bool { + return e.Object.GetNamespace() == namespace + }, + } +} + +// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/finalizers,verbs=update + +func (r *OperationReqReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + operationRequest := &userv1.Operationrequest{} + if err := r.Get(ctx, req.NamespacedName, operationRequest); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + return r.reconcile(ctx, operationRequest) +} + +func (r *OperationReqReconciler) reconcile( + ctx context.Context, + request *userv1.Operationrequest, +) (ctrl.Result, error) { + unlock := r.lockWorkspace(request.Spec.Namespace) + defer unlock() + r.Logger.V(1).Info("start reconcile controller operationRequest", getLog(request)...) + // count the time cost of handling the request + startTime := time.Now() + defer func() { + r.Logger.V(1). + Info("complete request handling", getLog(request, "create time", request.CreationTimestamp, "handling cost time", time.Since(startTime))...) + }() + + // delete OperationRequest first if its status is isCompleted and exist for retention time + if r.isRetained(request) { + r.Logger.V(1).Info("delete request", getLog(request)...) + if err := r.deleteRequest(ctx, request); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + // return early if its status is isCompleted and didn't exist for retention time + if r.isCompleted(request) { + r.Logger.V(1).Info("request is completed and requeue", getLog(request)...) + return ctrl.Result{RequeueAfter: OperationReqRequeueDuration}, nil + } + // change OperationRequest status to failed if it is expired + if r.isExpired(request) { + r.Logger.V(1).Info("request is expired, update status to failed", getLog(request)...) + if err := r.updateRequestStatus(ctx, request, userv1.RequestFailed); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // update OperationRequest status to processing + err := r.updateRequestStatus(ctx, request, userv1.RequestProcessing) + if err != nil { + return ctrl.Result{}, err + } + + // convert OperationRequest to RoleBinding + desiredRoleBinding := convertRequestToRoleBinding(request) + rolebinding := &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{ + Name: desiredRoleBinding.Name, + Namespace: desiredRoleBinding.Namespace, + }} + r.Logger.V(1).Info("convert OperationRequest to RoleBinding", + "rolebinding.name", desiredRoleBinding.Name, + "rolebinding.namespace", desiredRoleBinding.Namespace, + "rolebinding.subjects", desiredRoleBinding.Subjects, + "rolebinding.roleRef", desiredRoleBinding.RoleRef, + ) + + user := &userv1.User{} + if err := r.Get( + ctx, + client.ObjectKey{Name: config.GetUserNameByNamespace(request.Spec.Namespace)}, + user, + ); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to get user", + "Failed to get user %s", + request.Spec.User, + ) + return ctrl.Result{}, err + } + bindUser := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, bindUser); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to get bind user", + "Failed to get bind user %s", + request.Spec.User, + ) + return ctrl.Result{}, err + } + configureRoleBinding := func() error { + rolebinding.Annotations = maps.Clone(desiredRoleBinding.Annotations) + rolebinding.Labels = maps.Clone(desiredRoleBinding.Labels) + rolebinding.Subjects = append([]rbacv1.Subject(nil), desiredRoleBinding.Subjects...) + rolebinding.RoleRef = desiredRoleBinding.RoleRef + return ctrl.SetControllerReference(bindUser, rolebinding, r.Scheme) + } + + // handle OperationRequest, create or delete rolebinding + switch request.Spec.Action { + case userv1.Grant: + r.Recorder.Eventf( + request, + v1.EventTypeNormal, + "Grant", + "Grant role %s to user %s", + request.Spec.Role, + request.Spec.User, + ) + if _, err := ctrl.CreateOrUpdate( + ctx, + r.Client, + rolebinding, + configureRoleBinding, + ); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to create/update rolebinding", + "Failed to create rolebinding %s/%s", + rolebinding.Namespace, + rolebinding.Name, + ) + return ctrl.Result{}, err + } + if request.Spec.Role == userv1.OwnerRoleType { + // update user annotation + if err := r.patchUserOwner(ctx, user, request.Spec.User); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to update user", + "Failed to update user %s", + request.Spec.User, + ) + return ctrl.Result{}, err + } + } + case userv1.Deprive: + r.Recorder.Eventf( + request, + v1.EventTypeNormal, + "Deprive", + "Deprive role %s from user %s", + request.Spec.Role, + request.Spec.User, + ) + if err := r.Delete(ctx, rolebinding); client.IgnoreNotFound(err) != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to delete rolebinding", + "Failed to delete rolebinding %s/%s", + rolebinding.Namespace, + rolebinding.Name, + ) + return ctrl.Result{}, err + } + case userv1.Update: + r.Recorder.Eventf( + request, + v1.EventTypeNormal, + "Update", + "Update role %s to user %s", + request.Spec.Role, + request.Spec.User, + ) + if err := r.Delete(ctx, rolebinding); client.IgnoreNotFound(err) != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to delete rolebinding", + "Failed to delete rolebinding %s/%s", + rolebinding.Namespace, + rolebinding.Name, + ) + return ctrl.Result{}, err + } + if err = configureRoleBinding(); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to set owner reference", + "Failed to set owner reference for rolebinding %s/%s", + rolebinding.Namespace, + rolebinding.Name, + ) + return ctrl.Result{}, err + } + if err := r.Create(ctx, rolebinding); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to create rolebinding", + "Failed to create rolebinding %s/%s", + rolebinding.Namespace, + rolebinding.Name, + ) + return ctrl.Result{}, err + } + if request.Spec.Role == userv1.OwnerRoleType { + // update user annotation + if err := r.patchUserOwner(ctx, user, request.Spec.User); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to update user", + "Failed to update user %s", + request.Spec.User, + ) + return ctrl.Result{}, err + } + } + default: + return ctrl.Result{}, fmt.Errorf("invalid action %s", request.Spec.Action) + } + + // update OperationRequest status to completed + err = r.updateRequestStatus(ctx, request, userv1.RequestCompleted) + if err != nil { + return ctrl.Result{}, err + } + + r.Recorder.Eventf( + request, + v1.EventTypeNormal, + "Completed", + "Completed operation request %s/%s", + request.Spec.Namespace, + request.Name, + ) + return ctrl.Result{RequeueAfter: OperationReqRequeueDuration}, nil +} + +func (r *OperationReqReconciler) patchUserOwner( + ctx context.Context, + user *userv1.User, + owner string, +) error { + original := user.DeepCopy() + if user.Annotations == nil { + user.Annotations = make(map[string]string) + } + user.Annotations[userv1.UserAnnotationOwnerKey] = owner + return r.Patch(ctx, user, client.MergeFrom(original)) +} + +// isRetained returns true if the request is isCompleted and exist for retention time +func (r *OperationReqReconciler) isRetained(request *userv1.Operationrequest) bool { + if request.Status.Phase == userv1.RequestCompleted && + request.CreationTimestamp.Add(r.retentionTime).Before(time.Now()) { + return true + } + return false +} + +// isCompleted returns true if the request is isCompleted +func (r *OperationReqReconciler) isCompleted(request *userv1.Operationrequest) bool { + return request.Status.Phase == userv1.RequestCompleted +} + +// isExpired returns true if the request is expired +func (r *OperationReqReconciler) isExpired(request *userv1.Operationrequest) bool { + if request.Status.Phase != userv1.RequestCompleted && + request.CreationTimestamp.Add(r.expirationTime).Before(time.Now()) { + return true + } + return false +} + +func (r *OperationReqReconciler) deleteRequest( + ctx context.Context, + request *userv1.Operationrequest, +) error { + r.Logger.V(1).Info("deleting OperationRequest", "request", request) + if err := r.Delete(ctx, request); client.IgnoreNotFound(err) != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to delete OperationRequest", + "Failed to delete OperationRequest %s/%s", + request.Spec.Namespace, + request.Name, + ) + r.Logger.Error(err, "Failed to delete OperationRequest", getLog(request)...) + return fmt.Errorf( + "failed to delete OperationRequest %s/%s: %w", + request.Spec.Namespace, + request.Name, + err, + ) + } + r.Logger.V(1).Info("delete OperationRequest success", getLog(request)...) + return nil +} + +func (r *OperationReqReconciler) updateRequestStatus( + ctx context.Context, + request *userv1.Operationrequest, + phase userv1.RequestPhase, +) error { + request.Status.Phase = phase + if err := r.Status().Update(ctx, request); err != nil { + r.Recorder.Eventf( + request, + v1.EventTypeWarning, + "Failed to update OperationRequest status", + "Failed to update OperationRequest status %s/%s", + request.Spec.Namespace, + request.Name, + ) + r.Logger.V(1).Info("update OperationRequest status failed", getLog(request)...) + return err + } + r.Logger.V(1).Info("update OperationRequest status success", getLog(request)...) + return nil +} + +func convertRequestToRoleBinding(request *userv1.Operationrequest) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetGroupRoleBindingName(request.Spec.User), + Namespace: request.Spec.Namespace, + Annotations: map[string]string{ + userAnnotationOwnerKey: request.Spec.User, + }, + Labels: map[string]string{ + userLabelOwnerKey: request.Spec.User, + }, + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Name: request.Spec.User, + Namespace: config.GetUserSystemNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: string(request.Spec.Role), + APIGroup: rbacv1.GroupName, + }, + } +} + +func getLog(request *userv1.Operationrequest, kv ...any) []any { + return append([]any{ + "request.name", request.Name, + "request.Spec.Namespace", request.Spec.Namespace, + "request.user", request.Spec.User, + "request.role", request.Spec.Role, + "request.action", request.Spec.Action, + "request.phase", request.Status.Phase, + }, kv...) +} diff --git a/controllers/user/controllers/operationrequest_controller_test.go b/controllers/user/controllers/operationrequest_controller_test.go new file mode 100644 index 000000000000..f8a707162667 --- /dev/null +++ b/controllers/user/controllers/operationrequest_controller_test.go @@ -0,0 +1,186 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "testing" + "time" + + "github.com/go-logr/logr" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestOperationRequestWorkspaceLockSerializesSameWorkspace(t *testing.T) { + t.Parallel() + reconciler := &OperationReqReconciler{} + unlockFirst := reconciler.lockWorkspace("ns-workspace") + + attempting := make(chan struct{}) + acquired := make(chan func(), 1) + go func() { + close(attempting) + acquired <- reconciler.lockWorkspace("ns-workspace") + }() + <-attempting + + select { + case unlock := <-acquired: + unlock() + t.Fatal("same workspace lock was acquired concurrently") + case <-time.After(20 * time.Millisecond): + } + + unlockFirst() + select { + case unlock := <-acquired: + unlock() + case <-time.After(time.Second): + t.Fatal("waiting workspace lock was not released") + } +} + +func TestOperationRequestPersistsDesiredRoleBinding(t *testing.T) { + t.Parallel() + for _, action := range []userv1.ActionType{userv1.Grant, userv1.Update} { + t.Run(string(action), func(t *testing.T) { + t.Parallel() + testOperationRequestPersistsDesiredRoleBinding(t, action) + }) + } +} + +func testOperationRequestPersistsDesiredRoleBinding(t *testing.T, action userv1.ActionType) { + t.Helper() + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add User scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + + const ( + workspaceName = "workspace" + workspaceNamespace = "ns-workspace" + memberName = "member" + ) + workspace := &userv1.User{ObjectMeta: metav1.ObjectMeta{ + Name: workspaceName, + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: workspaceName, + }, + }} + member := &userv1.User{ObjectMeta: metav1.ObjectMeta{ + Name: memberName, + UID: types.UID("member-uid"), + }} + request := &userv1.Operationrequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "change-workspace-owner", + Namespace: config.GetUserSystemNamespace(), + CreationTimestamp: metav1.Now(), + }, + Spec: userv1.OperationrequestSpec{ + Namespace: workspaceNamespace, + User: memberName, + Role: userv1.OwnerRoleType, + Action: action, + }, + } + existing := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetGroupRoleBindingName(memberName), + Namespace: workspaceNamespace, + }, + Subjects: []rbacv1.Subject{{Kind: rbacv1.UserKind, Name: "stale"}}, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + }, + } + + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&userv1.Operationrequest{}). + WithObjects(workspace, member, request, existing). + Build() + storedRequest := &userv1.Operationrequest{} + requestKey := client.ObjectKeyFromObject(request) + if err := cli.Get(context.Background(), requestKey, storedRequest); err != nil { + t.Fatalf("get OperationRequest: %v", err) + } + reconciler := &OperationReqReconciler{ + Client: cli, + Logger: logr.Discard(), + Scheme: scheme, + Recorder: record.NewFakeRecorder(20), + expirationTime: time.Minute, + retentionTime: time.Minute, + } + if _, err := reconciler.reconcile(context.Background(), storedRequest); err != nil { + t.Fatalf("reconcile OperationRequest: %v", err) + } + + roleBinding := &rbacv1.RoleBinding{} + roleBindingKey := client.ObjectKey{ + Name: config.GetGroupRoleBindingName(memberName), + Namespace: workspaceNamespace, + } + if err := cli.Get(context.Background(), roleBindingKey, roleBinding); err != nil { + t.Fatalf("get RoleBinding: %v", err) + } + if roleBinding.RoleRef.Name != string(userv1.OwnerRoleType) || + len(roleBinding.Subjects) != 1 || roleBinding.Subjects[0].Name != memberName { + t.Fatalf("RoleBinding does not match request: %#v", roleBinding) + } + controller := metav1.GetControllerOf(roleBinding) + if controller == nil || controller.Name != memberName || controller.UID != member.UID { + t.Fatalf("RoleBinding controller = %#v, want member User", controller) + } + + updatedWorkspace := &userv1.User{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: workspaceName}, + updatedWorkspace, + ); err != nil { + t.Fatalf("get workspace User: %v", err) + } + if updatedWorkspace.Annotations[userv1.UserAnnotationOwnerKey] != memberName { + t.Fatalf( + "workspace owner = %q, want %q", + updatedWorkspace.Annotations[userv1.UserAnnotationOwnerKey], + memberName, + ) + } + + updatedRequest := &userv1.Operationrequest{} + if err := cli.Get(context.Background(), requestKey, updatedRequest); err != nil { + t.Fatalf("get completed OperationRequest: %v", err) + } + if updatedRequest.Status.Phase != userv1.RequestCompleted { + t.Fatalf("OperationRequest phase = %q, want Completed", updatedRequest.Status.Phase) + } +} diff --git a/controllers/user/controllers/suite_test.go b/controllers/user/controllers/suite_test.go new file mode 100644 index 000000000000..d2a58b34ec15 --- /dev/null +++ b/controllers/user/controllers/suite_test.go @@ -0,0 +1,191 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + v1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/kubeconfig" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + csrv1 "k8s.io/api/certificates/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "run controllers suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + useExisting := false + if val := os.Getenv("USE_EXISTING_CLUSTER"); val != "" { + if parsed, err := strconv.ParseBool(val); err == nil { + useExisting = parsed + } + } + testEnv = &envtest.Environment{ + UseExistingCluster: &useExisting, + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = csrv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = v1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = Describe("user kubeconfig ", func() { + Context("kubeconfig rotate trigger", func() { + It("should rotate when spec is set and status is empty", func() { + r := &UserReconciler{} + now := metav1.Now() + user := &v1.User{} + user.Spec.KubeConfigRotateAt = &now + Expect(r.shouldRotateKubeConfig(user)).To(BeTrue()) + }) + + It("should not rotate when spec equals status", func() { + r := &UserReconciler{} + now := metav1.Now() + user := &v1.User{} + user.Spec.KubeConfigRotateAt = &now + user.Status.ObservedKubeConfigRotateAt = &now + Expect(r.shouldRotateKubeConfig(user)).To(BeFalse()) + }) + + It("should rotate when spec differs from status", func() { + r := &UserReconciler{} + now := metav1.Now() + later := metav1.NewTime(now.Add(10 * time.Second)) + user := &v1.User{} + user.Spec.KubeConfigRotateAt = &later + user.Status.ObservedKubeConfigRotateAt = &now + Expect(r.shouldRotateKubeConfig(user)).To(BeTrue()) + }) + }) + + Context("syncReNewConfig test", func() { + AfterEach(func() { + }) + It("empty kubeconfig", func() { + usr := &v1.User{} + usr.Name = "cuisongliu" + cfg, event, err := syncReNewConfig(usr) + // Expect(err).NotTo(HaveOccurred()) + Expect(err).ToNot(HaveOccurred()) + Expect(event).To(BeNil()) + Expect(cfg).To(BeNil()) + }) + + It("new expired kubeconfig", func() { + usr := &v1.User{} + usr.Name = "f8699ded-58d3-432b-a9ff-56568b57a38d" + kubeConfig := `apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeU1EY3pNVEUwTkRJME1sb1lEekl4TWpJd056QTNNVFEwTWpReVdqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCnVzS3Z4U1RUNFdXUUhOM0VvZmtoT0kyV1FMMWs4UTJxUWRZMklNNTBDT3NycmVuTW5tMzhpSmJONEgwTlpmQkUKZ3lYNzEvNG81WmxmUEdWRFdjL01qNkJQQXAzcEYrdVZQdnFQc1pKNUdKa2pNbGMzdWRVNzkxcC9jRlc5dUcxSQp6U01HVVRyL0V4d2UxVVZWd3NkZTdVMVpUZGhOajdPbmJQd0F1dGJVVEVxdmtIWEFpSW11K0NLaFRnRjVRVTZwCkI0T2luRVk2dWFOMWtFbzFXSUJFODlockNmSzhCeXV5V1dNTFY1RktaZjg5Ry9XNmh3T0F3Qjc2bGlabC8zTEMKTEdLdS8xWnh3VUw4QW54TWxBRi9RVFprK0ZaSkloVVFHbWlreVRwdi8vYWNyeUhXSWxHSmRPeGl6T2tUSnUzKwppMUZPR2JIR0xCZ3lnejdyc0RYMTB3SURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVJbUZKVGtEMTc3OUhZVTRpNjlHbzlLczRXM0V3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFHb2FkSEUvbGg5ZnpGSUhoRUhJUTNQSEdaOVQ2K0NOL040cG5hcGcyUFY4U09XNwpOTmVTTXZpWmJxQk5VaG1kelNiaG5sTGllRWZHYlBjSm9BVThvdUN6bXFidXFzK3pDN3U1RnBrb0loYUJ1RHdSCm5ucmJXR1Z3cE93K1RvcjhmL1NyTGxxZVl3SFhneGprMGpmaEcwM1FRQUxvb3lBRWFEWjVPU0dyWHo0SWlkYTQKVHJwZmJZcjFvWE54UjllcFRkdDJIRTZoUlEyNEVUVXBjRVRqa01uSWtObWdLdnJHSndMUUF4b0d4QklqSUh4NApQZEdlOHdURit5V3I5WlNDWkNNZ0NNR2xuSVJtdXEyVzRQWklFdU8xUkZ3RDNCTVAyQmlsNEVWekhaZlpuUkVRClZRTDI0M08xangvb3gwaGlUOWRXRXBpbXZoMm1md0hNbzlSS2d4dz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + server: https://192.168.64.24:6443 + name: sealos +contexts: +- context: + cluster: sealos + namespace: ns-f8699ded-58d3-432b-a9ff-56568b57a38d + user: f8699ded-58d3-432b-a9ff-56568b57a38d + name: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos +current-context: f8699ded-58d3-432b-a9ff-56568b57a38d@sealos +kind: Config +preferences: {} +users: +- name: f8699ded-58d3-432b-a9ff-56568b57a38d + user: + token: test-token + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURTRENDQWpDZ0F3SUJBZ0lRYldZeDZSS3dOQ1M5L29oQTArMkZ4ekFOQmdrcWhraUc5dzBCQVFzRkFEQVYKTVJNd0VRWURWUVFERXdwcmRXSmxjbTVsZEdWek1CNFhEVEl5TURneU1EQTFNalkxT0ZvWERUSXlNRGd5TURBMgpNekUxT0Zvd0x6RXRNQ3NHQTFVRUF4TWtaamcyT1Rsa1pXUXROVGhrTXkwME16SmlMV0U1Wm1ZdE5UWTFOamhpCk5UZGhNemhrTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEzQmRFa0VFNy9tTjQKWllTUkIvSW5JSGpMeW9DNHZKeUUreHM2bkczdXc3VjROdm1ia0ROSU9uS2cvQW9uNE1TNWhHbHJ1S0VydnJWSQo0L0pjTnFNMXFxTTVWVWVMRXVjVzU3cXE3QkdDbmZQSW9ndEd1YmpZRTJpWXhhYU8ySFVuSHF0VWNUajZDbTd1Ck1PWVA2di9tMkRZWjNxTDdjYTRjc1MzcVp1aW5oTG5ML1hJQXMwZUg3SmRnQkJISW1aVUlrRW9ueUNvNzI5VGkKZkFEdWhiQjZ1REtiRmFsMlAwQzZ5a1ltU2VVNHVHaytXQ0pDeDF2Nkd5THIzWXo1cHh6bXd4ck5CUUF2d2hYTwpQMFczVUZ1Rnc5MGlwQTN1dXpaT09aakxpd2U1N1ZnT1ZxbFFpMGY4SFFmYWRBUU8vTnFVc1lTQTk5K2ZzSXFoClJWK3Z5RUtITndJREFRQUJvM293ZURBT0JnTlZIUThCQWY4RUJBTUNCYUF3RXdZRFZSMGxCQXd3Q2dZSUt3WUIKQlFVSEF3SXdEQVlEVlIwVEFRSC9CQUl3QURBZkJnTlZIU01FR0RBV2dCUWlZVWxPUVBYdnYwZGhUaUxyMGFqMApxemhiY1RBaUJnTlZIUkVFR3pBWmdoZGhjR2x6WlhKMlpYSXVZMngxYzNSbGNpNXNiMk5oYkRBTkJna3Foa2lHCjl3MEJBUXNGQUFPQ0FRRUFsK1BSN1NHVVZMdjQ0bC9mY1ducHJqcFZpanM2eGJpSm9mVGNSL0JaSnRGaWpTWjEKNlRJU0t1c3hjb2lzZE16M1dsVWsyREpPYlJVV2FKamY4VXRUdkZmR0Z3UEJHU1k5aGxtR0NENEpVeXJHWVBzawoyby9jTGVDVVRWQzJvd2FPOXVyRTdmOGR4eU4xMHliaGhaZ1BuM0xDUGtiL0hCTFNJWFNFcWxwd0NmeURaMXFHCkJMbkVpa2VuVnJXc1FHSVJ1Mk91WkJINjYyU1VjMkk5SHlHemorTm1UamRES1VHNnZHdkw3WGdJdlgreWhjTDMKcVBsbFR1aFlab0lZdkErS1p1MFdFQWoyQWkvVU5MU21IbktyQkF6bm1qeXVGY2JFZFpYZDFSVGQycytWd0FFSgoxMnVHbXhIank2WCsvb0NxY0d6ZXJYUVRDSmVGWi8wWTBVMFl4UT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBM0JkRWtFRTcvbU40WllTUkIvSW5JSGpMeW9DNHZKeUUreHM2bkczdXc3VjROdm1iCmtETklPbktnL0FvbjRNUzVoR2xydUtFcnZyVkk0L0pjTnFNMXFxTTVWVWVMRXVjVzU3cXE3QkdDbmZQSW9ndEcKdWJqWUUyaVl4YWFPMkhVbkhxdFVjVGo2Q203dU1PWVA2di9tMkRZWjNxTDdjYTRjc1MzcVp1aW5oTG5ML1hJQQpzMGVIN0pkZ0JCSEltWlVJa0VvbnlDbzcyOVRpZkFEdWhiQjZ1REtiRmFsMlAwQzZ5a1ltU2VVNHVHaytXQ0pDCngxdjZHeUxyM1l6NXB4em13eHJOQlFBdndoWE9QMFczVUZ1Rnc5MGlwQTN1dXpaT09aakxpd2U1N1ZnT1ZxbFEKaTBmOEhRZmFkQVFPL05xVXNZU0E5OStmc0lxaFJWK3Z5RUtITndJREFRQUJBb0lCQUdDVHZUeG53OEd2T0dlagpzbGlBUS9jVnlxZERzTXpqQ2Q5K1pVdTdXYWg3ZXhMMG1QUy83QlBwdFFha0ZqZWxXNXJrLzZYMjQxRStENHduCkQ1dlNKbnlJUDJKU2tJNXM1VG91U1U1cHFKMVRHbGt4QlluOXVLTmJwSjRtcGt3SnJHN2kwNTBUV1hVMmxFTFUKMFd0WGU1Nm9ydFZwUTFqeEJCb2pnZFpDaFMvUjMzM2dnUW9uRFI4NGx2aDc2cC9zM2x4MHFPbUE4K25nbURJdwpyUGdobzBuOFhVZFFaUDk0alYzVlREcklZNHFOZFlYbzRsODZqUUozTHhndkp4YzV2MUJuUk9MYXJ0N2FaR01iCnB6VzhsRXpKd2pkYlFiQ1hGRkc4bXFZTU42RkREaGl1YmRnb1h6dUd5RXNobSswRVk4clpOcWxhVzcvc0lzNjkKcUIxa0VqRUNnWUVBOFh3OWtjbjVBbGhEbnQ3SEg5bUszeUZ4TTU5Q3BRZVA0b0FPWXJuOFNZSkpvRTdzU1dvVwpXZmFmTmFBVnFQL3lCZFFnUTdSbFZYTlVSdHVibXN0MW5JKzFnbTJVQ0tEQ1VuWGR2Ylp1eHZiYnFSdlJXc0EyCmdTNG03eVBoR1hDb1lVMzhHQy9qcXB3UmFHMnVHWFp5T1M0TjVDN1lBUDMyMWpKNEp6cExzWHNDZ1lFQTZWSFUKUDlDS2VUUVFuV2RzdFRiVXJVYVZpVjhadGo3U0JkRWFtQnZWUnRoeGluU1QzaCtmOGxuaTY1czVXUzNvZUQycApYZW5oeXZwWDkvOFhrT2FtRXdzZVVHNFdCVk16Y1kzVm9CdDNlTVE2WGRaaHgwOWRRek5zZXhUMkN4bTZHTWpUCjV5cXR6TUtOTVBoOFB0cGlsUFNHbytKSXdRVkxaWkVnK24wNkhuVUNnWUVBbmttLzJlWTRFSzltYzZhM2cwc0gKV2tjRGVzRHo0RlRhbmE4dXZzd0djUEN6N2g2TmgzbkFlT3ZOWkVzd3AzeE5Xa0MzZldtcjJwMGtLdVljVXhUMgpYTTllUE1ZeStJelhrMUdyTFlWOWkzR1lmbnE3ZWU3d1N6RERXSkYrSlR6UlFpYnFEYmltVk5qRUdGMThkemhLCm11eHpNcFQ2Qlh4eTVlaGpGZU9DWmkwQ2dZRUFsR2RVL3FZUmZkaE0vU0ZzdHJMQ2dkaFVnd1QzWU1FQ2EycSsKWktQSGU0RnVicWVKNmczcGVZZ00ydGxubDc2b2o5cUFvTmlEb3N5cktYV2FzckxTVFdpVUJvcU8vU0lYcFpHVwpvSGozKzl1c1dFVmsraFlUOXd0OVk2aEllM1VJdG56K3M1bWs1SW1XcnVCT2Z0Zi9Qa2x1WGswdkEzN3ZueEc1CmpUb2J2b1VDZ1lFQTUvUDhBc1kzdjVVL24yUk5EWWczNEJPN1Awb3ZDVHo5V09TMmkybHBIcUxqMGR4V2RPUjcKU1VpNVN1UDBFK1RQODNwRGo3R3l0NVVVYmRaKys5RU4xMll2cHVxUmpIM3NMRFo3Qi8xN0tocWk3QVRIRExLcwpDalp4YmpQRlA5RUduUkxqbXJHaFd0YTlzaUN0YXhyb3ZlbVk4ME5rK2V1STBNQ1oyMklGZFdRPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +` + usr.Status.KubeConfig = kubeConfig + cfg, event, err := syncReNewConfig(usr) + eventStr := fmt.Sprintf("ClientCertificateData %s is expired", usr.Name) + Expect(err).ToNot(HaveOccurred()) + Expect(event).To(Equal(&eventStr)) + Expect(cfg).To(BeNil()) + }) + + It("new kubeconfig", func() { + user := &v1.User{} + user.Name = "cuisongliu" + defaultExpirationDuration := int32(100000000) + user.Spec.CSRExpirationSeconds = defaultExpirationDuration + defaultConfig := kubeconfig.NewConfig("cuisongliu", "", 100000000) + config, err := defaultConfig.WithServiceAccountConfig("default", nil). + Apply(cfg, k8sClient) + Expect(err).ToNot(HaveOccurred()) + kubeData, err := clientcmd.Write(*config) + Expect(err).ToNot(HaveOccurred()) + user.Status.KubeConfig = string(kubeData) + newCfg, event, err := syncReNewConfig(user) + Expect(err).ToNot(HaveOccurred()) + Expect(event).To(BeNil()) + Expect(newCfg).NotTo(BeNil()) + }) + }) +}) diff --git a/controllers/user/controllers/user_controller.go b/controllers/user/controllers/user_controller.go new file mode 100644 index 000000000000..2937a62f22e3 --- /dev/null +++ b/controllers/user/controllers/user_controller.go @@ -0,0 +1,1819 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "errors" + "fmt" + "maps" + "reflect" + "strings" + "sync" + "time" + + "github.com/go-logr/logr" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + usercache "github.com/labring/sealos/controllers/user/controllers/cache" + "github.com/labring/sealos/controllers/user/controllers/helper" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + "github.com/labring/sealos/controllers/user/controllers/helper/finalizer" + "github.com/labring/sealos/controllers/user/controllers/helper/hash" + "github.com/labring/sealos/controllers/user/controllers/helper/kubeconfig" + "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter" + "github.com/labring/sealos/controllers/user/pkg/licensegate" + "github.com/labring/sealos/controllers/user/pkg/usercount" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apiMeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + kubecontroller "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +const ( + userAnnotationCreatorKey = userv1.UserAnnotationCreatorKey + userAnnotationOwnerKey = userv1.UserAnnotationOwnerKey + userLabelOwnerKey = userv1.UserLabelOwnerKey + licenseLimitedCondition = userv1.ConditionType("LicenseLimited") + adminUserName = "admin" + adminClusterRoleBindingName = config.AdminClusterRoleBindingName + userFinalizerName = "sealos.io/user.finalizers" + namespaceSyncReadyCondition = userv1.ConditionType("NamespaceSyncReady") + serviceAccountReadyCondition = userv1.ConditionType("ServiceAccountSyncReady") + kubeConfigReadyCondition = userv1.ConditionType("KubeConfigSyncReady") + roleSyncReadyCondition = userv1.ConditionType("RoleSyncReady") + roleBindingReadyCondition = userv1.ConditionType("RoleBindingSyncReady") + clusterRoleBindingReadyCondition = userv1.ConditionType("ClusterRoleBindingSyncReady") +) + +// UserReconciler reconciles a User object +type UserReconciler struct { + Logger logr.Logger + Recorder record.EventRecorder + cache client.Reader + userCounter *usercount.Counter + config *rest.Config + *runtime.Scheme + client.Client + finalizer *finalizer.Finalizer + minRequeueDuration time.Duration + nextKubeConfigSync sync.Map + // EnableAdminClusterAdmin preserves the legacy cluster-admin binding for + // the admin user when explicitly enabled. It is disabled by default. + EnableAdminClusterAdmin bool + // EnableStrictNamespacePodSecurity applies Pod Security labels to every + // namespace whose name starts with ns-, including namespaces without a User. + // It is enabled by default by the controller entrypoint. + EnableStrictNamespacePodSecurity bool +} + +type userReconcileState struct { + serviceAccount *v1.ServiceAccount + tokenExpirationDeadline *metav1.Time + currentSecretName string + kubeConfigSyncAttempted bool + kubeConfigSynced bool + cleanupLegacySecrets bool + syncError error +} + +func (s *userReconcileState) recordSyncError(err error) { + if s == nil || err == nil { + return + } + s.syncError = errors.Join(s.syncError, err) +} + +// +kubebuilder:rbac:groups=*,resources=*,verbs=* +// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Logger.V(1).Info("start reconcile for users") + user := &userv1.User{} + if err := r.Get(ctx, req.NamespacedName, user); err != nil { + if apierrors.IsNotFound(err) { + r.nextKubeConfigSync.Delete(req.Name) + if req.Name == adminUserName { + if cleanupErr := r.cleanupDisabledAdminClusterRoleBinding(ctx); cleanupErr != nil { + return ctrl.Result{}, cleanupErr + } + } + } + if apierrors.IsNotFound(err) && r.EnableStrictNamespacePodSecurity { + if syncErr := r.syncOrphanNamespace( + ctx, + config.GetUsersNamespace(req.Name), + ); syncErr != nil { + return ctrl.Result{}, syncErr + } + } + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if ok, err := r.finalizer.RemoveFinalizer( + ctx, + user, + func(ctx context.Context, obj client.Object) error { + ns := &v1.Namespace{} + ns.Name = config.GetUsersNamespace(user.Name) + _ = r.Delete(ctx, ns) + if user.Name == adminUserName { + return r.cleanupDisabledAdminClusterRoleBinding(ctx) + } + return nil + }, + ); ok { + r.nextKubeConfigSync.Delete(user.Name) + return ctrl.Result{}, err + } + + if ok, err := r.finalizer.AddFinalizer(ctx, user); ok { + if err != nil { + return ctrl.Result{}, err + } + return r.reconcile(ctx, user) + } + return ctrl.Result{}, errors.New("reconcile error from Finalizer") +} + +type OwnerAnnotationChangedPredicate struct { + predicate.Funcs +} + +type DeletionTimestampChangedPredicate struct { + predicate.Funcs +} + +// NamespacePodSecurityPredicate reconciles ns-* namespaces when their +// Pod Security Admission labels or User ownership metadata changes. +type NamespacePodSecurityPredicate struct { + predicate.Funcs +} + +type AdminClusterRoleBindingPredicate struct { + predicate.Funcs +} + +// ignorePreStartCreatePredicate drops Create events for objects that already +// existed when this controller was configured. Existing Users re-evaluate the +// shared License and verify their children during startup. The manager starts +// each informer once; watch reconnects relist through the same informer. +// +// TODO: after upgrading controller-runtime to v0.22+, use +// event.CreateEvent.IsInInitialList and coalesce initial events for the same +// User with a bounded debounce before enqueueing one reconciliation request. +// When the minimum supported API server provides WatchList, validate its +// compatibility and fallback behavior before enabling client-go's +// KUBE_FEATURE_WatchListClient to reduce initial LIST peak memory. +type ignorePreStartCreatePredicate struct { + predicate.Funcs + startedAt time.Time +} + +func (p ignorePreStartCreatePredicate) Create(e event.CreateEvent) bool { + if e.Object == nil { + return false + } + createdAt := e.Object.GetCreationTimestamp().Time + if p.startedAt.IsZero() || createdAt.IsZero() { + return true + } + return !createdAt.Before(p.startedAt) +} + +func (AdminClusterRoleBindingPredicate) Create(e event.CreateEvent) bool { + return e.Object.GetName() == adminClusterRoleBindingName +} + +func (AdminClusterRoleBindingPredicate) Update(e event.UpdateEvent) bool { + return e.ObjectNew.GetName() == adminClusterRoleBindingName +} + +func (AdminClusterRoleBindingPredicate) Delete(e event.DeleteEvent) bool { + return e.Object.GetName() == adminClusterRoleBindingName +} + +func (AdminClusterRoleBindingPredicate) Generic(event.GenericEvent) bool { + return false +} + +func (NamespacePodSecurityPredicate) Create(e event.CreateEvent) bool { + return isUserNamespace(e.Object.GetName()) +} + +func (NamespacePodSecurityPredicate) Update(e event.UpdateEvent) bool { + if !isUserNamespace(e.ObjectNew.GetName()) { + return false + } + return namespaceMetadataChanged( + e.ObjectOld.GetAnnotations(), + e.ObjectNew.GetAnnotations(), + e.ObjectOld.GetLabels(), + e.ObjectNew.GetLabels(), + ) || e.ObjectOld.GetAnnotations()[userv1.UserAnnotationCreatorKey] != + e.ObjectNew.GetAnnotations()[userv1.UserAnnotationCreatorKey] || + !reflect.DeepEqual(e.ObjectOld.GetOwnerReferences(), e.ObjectNew.GetOwnerReferences()) +} + +func (NamespacePodSecurityPredicate) Delete(e event.DeleteEvent) bool { + return isUserNamespace(e.Object.GetName()) +} + +func (NamespacePodSecurityPredicate) Generic(event.GenericEvent) bool { + return false +} + +func podSecurityLabelsChanged(oldLabels, newLabels map[string]string) bool { + for key := range oldLabels { + if config.IsPodSecurityLabel(key) && oldLabels[key] != newLabels[key] { + return true + } + } + for key := range newLabels { + if config.IsPodSecurityLabel(key) && oldLabels[key] != newLabels[key] { + return true + } + } + return false +} + +func namespaceMetadataChanged( + oldAnnotations, newAnnotations, oldLabels, newLabels map[string]string, +) bool { + return podSecurityLabelsChanged(oldLabels, newLabels) || + oldAnnotations[userv1.UserAnnotationOwnerKey] != newAnnotations[userv1.UserAnnotationOwnerKey] || + oldLabels[userv1.UserLabelOwnerKey] != newLabels[userv1.UserLabelOwnerKey] +} + +func isUserNamespace(name string) bool { + return strings.HasPrefix(name, "ns-") && len(name) > len("ns-") +} + +func (r *UserReconciler) namespaceToUserRequests( + _ context.Context, + obj client.Object, +) []ctrl.Request { + if !isUserNamespace(obj.GetName()) { + return nil + } + return []ctrl.Request{{NamespacedName: client.ObjectKey{ + Name: config.GetUserNameByNamespace(obj.GetName()), + }}} +} + +func metadataValueMatches(values map[string]string, key, expected string) bool { + value, ok := values[key] + return ok && value == expected +} + +func controlledByUser(obj metav1.Object, user *userv1.User) bool { + for _, ref := range obj.GetOwnerReferences() { + if ref.Controller != nil && *ref.Controller && + ref.APIVersion == userv1.GroupVersion.String() && + ref.Kind == "User" && ref.Name == user.Name && ref.UID == user.UID { + return true + } + } + return false +} + +func userStatusNeedsSync(user *userv1.User) bool { + if user == nil { + return true + } + if user.Status.Phase != userv1.UserActive || + user.Status.ObservedGeneration != user.Generation || + !helper.IsConditionsTrue(user.Status.Conditions) || + !helper.IsConditionTrue(user.Status.Conditions, userv1.Condition{ + Type: userv1.Ready, + Status: v1.ConditionTrue, + }) { + return true + } + if !csrExpirationStatusMatches( + user.Spec.CSRExpirationSeconds, + user.Status.ObservedCSRExpirationSeconds, + ) { + return true + } + return user.Spec.KubeConfigRotateAt != nil && + (user.Status.ObservedKubeConfigRotateAt == nil || + !user.Spec.KubeConfigRotateAt.Equal(user.Status.ObservedKubeConfigRotateAt)) +} + +func csrExpirationStatusMatches(spec, observed int32) bool { + return userv1.NormalizeCSRExpirationSeconds(spec) == + userv1.NormalizeCSRExpirationSeconds(observed) +} + +func setObservedCSRExpirationSeconds(user *userv1.User) { + if user == nil { + return + } + user.Status.ObservedCSRExpirationSeconds = userv1.NormalizeCSRExpirationSeconds( + user.Spec.CSRExpirationSeconds, + ) +} + +func podSecurityLabelsNeedSync( + name string, + labels map[string]string, + enableAdminClusterAdmin bool, +) bool { + desired := desiredNamespaceLabels(name, cloneStringMap(labels), enableAdminClusterAdmin) + return podSecurityLabelsChanged(labels, desired) +} + +func cloneStringMap(source map[string]string) map[string]string { + if source == nil { + return make(map[string]string) + } + clone := make(map[string]string, len(source)) + maps.Copy(clone, source) + return clone +} + +func (r *UserReconciler) kubeConfigSyncDue(user *userv1.User) bool { + if user == nil { + return true + } + if value, ok := r.nextKubeConfigSync.Load(user.Name); ok { + deadline, valid := value.(time.Time) + return !valid || !deadline.After(time.Now()) + } + if user.Status.KubeConfigRefreshAt == nil { + return true + } + return !user.Status.KubeConfigRefreshAt.After(time.Now()) +} + +func namespaceMatchesUser( + namespace metav1.Object, + user *userv1.User, + enableAdminClusterAdmin bool, +) bool { + owner := user.Annotations[userv1.UserAnnotationOwnerKey] + securityLabelsNeedSync := podSecurityLabelsNeedSync( + namespace.GetName(), + namespace.GetLabels(), + enableAdminClusterAdmin, + ) + return metadataValueMatches( + namespace.GetAnnotations(), + userv1.UserAnnotationCreatorKey, + user.Name, + ) && + metadataValueMatches(namespace.GetAnnotations(), userv1.UserAnnotationOwnerKey, owner) && + metadataValueMatches(namespace.GetLabels(), userv1.UserLabelOwnerKey, owner) && + controlledByUser(namespace, user) && + !securityLabelsNeedSync +} + +func roleMatchesUser( + ctx context.Context, + reader client.Reader, + key client.ObjectKey, + roleType userv1.RoleType, + user *userv1.User, +) bool { + role := &rbacv1.Role{} + if err := reader.Get(ctx, key, role); err != nil { + return false + } + if !metadataMatchesUserResource(role, user) { + return false + } + if cachedRulesHash, ok := role.Annotations[config.RoleRulesHashAnnotation]; ok { + return cachedRulesHash == hash.HashToString(config.GetUserRole(roleType)) + } + return reflect.DeepEqual(role.Rules, config.GetUserRole(roleType)) +} + +func roleBindingMatchesUser(roleBinding *rbacv1.RoleBinding, user *userv1.User) bool { + if !metadataMatchesUserResource(roleBinding, user) { + return false + } + if cachedSpecHash, ok := roleBinding.Annotations[usercache.RoleBindingSpecHashAnnotation]; ok { + return cachedSpecHash == usercache.RoleBindingSpecHash( + rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + }, + config.GetUsersSubject(user.Name), + ) + } + return reflect.DeepEqual(roleBinding.Subjects, config.GetUsersSubject(user.Name)) && + reflect.DeepEqual(roleBinding.RoleRef, rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + }) +} + +func clusterRoleBindingMatchesUser(binding *rbacv1.ClusterRoleBinding, user *userv1.User) bool { + if !metadataMatchesUserResource(binding, user) { + return false + } + if cachedSpecHash, ok := binding.Annotations[usercache.RoleBindingSpecHashAnnotation]; ok { + return cachedSpecHash == usercache.RoleBindingSpecHash( + rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + config.GetUsersSubject(user.Name), + ) + } + return reflect.DeepEqual(binding.Subjects, config.GetUsersSubject(user.Name)) && + reflect.DeepEqual(binding.RoleRef, rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }) +} + +func metadataMatchesUserResource(metadata metav1.Object, user *userv1.User) bool { + owner := user.Annotations[userv1.UserAnnotationOwnerKey] + return metadataValueMatches( + metadata.GetAnnotations(), + userv1.UserAnnotationCreatorKey, + user.Name, + ) && + metadataValueMatches(metadata.GetAnnotations(), userv1.UserAnnotationOwnerKey, owner) && + controlledByUser(metadata, user) +} + +func (r *UserReconciler) syncOrphanNamespace(ctx context.Context, namespaceName string) error { + if !r.EnableStrictNamespacePodSecurity || !isUserNamespace(namespaceName) { + return nil + } + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + ns := &v1.Namespace{} + if err := r.Get(ctx, client.ObjectKey{Name: namespaceName}, ns); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error { + if ns.Labels == nil { + ns.Labels = make(map[string]string) + } + ns.Labels = config.SetPodSecurity(ns.Labels) + return nil + }); err != nil { + return fmt.Errorf( + "unable to apply Pod Security labels to orphan namespace %s: %w", + namespaceName, + err, + ) + } + return nil + }) +} + +func (r *UserReconciler) cleanupDisabledAdminClusterRoleBinding(ctx context.Context) error { + if r.EnableAdminClusterAdmin { + return nil + } + binding := &rbacv1.ClusterRoleBinding{} + binding.Name = adminClusterRoleBindingName + if err := r.Delete(ctx, binding); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("unable to remove disabled admin cluster role binding: %w", err) + } + return nil +} + +func (r *UserReconciler) adminClusterRoleBindingToUserRequests( + _ context.Context, + obj client.Object, +) []ctrl.Request { + if obj.GetName() != adminClusterRoleBindingName { + return nil + } + return []ctrl.Request{{NamespacedName: client.ObjectKey{Name: adminUserName}}} +} + +func desiredNamespaceLabels( + name string, + labels map[string]string, + enableAdminClusterAdmin bool, +) map[string]string { + if name == config.GetUsersNamespace(adminUserName) && enableAdminClusterAdmin { + for key := range labels { + if config.IsPodSecurityLabel(key) { + delete(labels, key) + } + } + return labels + } + return config.SetPodSecurity(labels) +} + +type adminPrivilegeMigration struct { + client client.Client + reader client.Reader + reconciler *UserReconciler + enableAdminClusterAdmin bool +} + +func (c *adminPrivilegeMigration) Start(ctx context.Context) error { + if !c.enableAdminClusterAdmin { + binding := &rbacv1.ClusterRoleBinding{} + binding.Name = adminClusterRoleBindingName + if err := c.client.Delete(ctx, binding); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("remove disabled admin cluster role binding: %w", err) + } + } + if c.reconciler == nil { + return nil + } + admin := &userv1.User{} + if c.reader == nil { + return errors.New("admin user privilege migration reader is nil") + } + if err := c.reader.Get(ctx, client.ObjectKey{Name: adminUserName}, admin); err != nil { + if apierrors.IsNotFound(err) || apiMeta.IsNoMatchError(err) { + return nil + } + return fmt.Errorf("get admin user for privilege migration: %w", err) + } + state := &userReconcileState{} + c.reconciler.syncNamespace(ctx, admin, state) + c.reconciler.syncClusterRoleBinding(ctx, admin, state) + if state.syncError != nil { + return fmt.Errorf("sync admin privileges: %w", state.syncError) + } + return nil +} + +func (c *adminPrivilegeMigration) NeedLeaderElection() bool { + return true +} + +func (OwnerAnnotationChangedPredicate) Update(e event.UpdateEvent) bool { + return e.ObjectOld.GetAnnotations()[userAnnotationOwnerKey] != + e.ObjectNew.GetAnnotations()[userAnnotationOwnerKey] +} + +func (DeletionTimestampChangedPredicate) Update(e event.UpdateEvent) bool { + return !reflect.DeepEqual( + e.ObjectOld.GetDeletionTimestamp(), + e.ObjectNew.GetDeletionTimestamp(), + ) +} + +// SetupWithManager sets up the controller with the Manager. +// The deprecated max-requeue-duration and restart-predicate-time arguments are +// retained for compatibility and ignored. +func (r *UserReconciler) SetupWithManager(mgr ctrl.Manager, opts ratelimiter.RateLimiterOptions, + minRequeueDuration, _, _ time.Duration, + userCounter *usercount.Counter, +) error { + controllerStartedAt := time.Now() + const controllerName = "user_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + if r.Recorder == nil { + r.Recorder = mgr.GetEventRecorderFor(controllerName) + } + if r.finalizer == nil { + r.finalizer = finalizer.NewFinalizer(r.Client, userFinalizerName). + WithReader(mgr.GetAPIReader()) + } + r.Scheme = mgr.GetScheme() + r.cache = mgr.GetCache() + r.userCounter = userCounter + r.config = mgr.GetConfig() + r.Logger.V(1).Info("init reconcile controller user") + r.minRequeueDuration = minRequeueDuration + + if err := mgr.Add(&adminPrivilegeMigration{ + client: r.Client, + reader: mgr.GetAPIReader(), + reconciler: r, + enableAdminClusterAdmin: r.EnableAdminClusterAdmin, + }); err != nil { + return fmt.Errorf("add admin privilege migration: %w", err) + } + + secretMetadata := &metav1.PartialObjectMetadata{} + secretMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret")) + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + secretMetadata, + v1.ServiceAccountNameKey, + func(rawObj client.Object) []string { + var annotations map[string]string + switch secret := rawObj.(type) { + case *v1.Secret: + annotations = secret.Annotations + case *metav1.PartialObjectMetadata: + annotations = secret.Annotations + } + if annotations == nil { + return nil + } + value := annotations[v1.ServiceAccountNameKey] + if value == "" { + return nil + } + return []string{value} + }, + ); err != nil { + return err + } + if err := registerStartupCacheInformers(mgr.GetCache()); err != nil { + return err + } + + ownerEventHandler := handler.EnqueueRequestForOwner( + r.Scheme, + r.RESTMapper(), + &userv1.User{}, + handler.OnlyControllerOwner(), + ) + ignorePreStartCreate := ignorePreStartCreatePredicate{startedAt: controllerStartedAt} + // Preserve the name derived from For(&userv1.User{}) for metrics and queue identity. + return ctrl.NewControllerManagedBy(mgr). + Named("user"). + Watches( + &userv1.User{}, + userEventHandler{}, + builder.WithPredicates(predicate.Or( + predicate.GenerationChangedPredicate{}, + OwnerAnnotationChangedPredicate{}, + DeletionTimestampChangedPredicate{}, + )), + ). + WatchesMetadata( + &v1.Namespace{}, + handler.EnqueueRequestsFromMapFunc(r.namespaceToUserRequests), + builder.WithPredicates(NamespacePodSecurityPredicate{}), + ). + Watches( + &rbacv1.ClusterRoleBinding{}, + handler.EnqueueRequestsFromMapFunc(r.adminClusterRoleBindingToUserRequests), + builder.WithPredicates(AdminClusterRoleBindingPredicate{}), + ). + Watches( + &licensev1.License{}, + handler.EnqueueRequestsFromMapFunc(r.licenseToUserRequests), + builder.OnlyMetadata, + builder.WithPredicates(ignorePreStartCreate), + ). + Watches( + &rbacv1.Role{}, + ownerEventHandler, + builder.WithPredicates(ignorePreStartCreate), + ). + Watches( + &rbacv1.RoleBinding{}, + ownerEventHandler, + builder.WithPredicates(ignorePreStartCreate), + ). + Watches( + &v1.ServiceAccount{}, + ownerEventHandler, + builder.WithPredicates(ignorePreStartCreate), + ). + WatchesMetadata( + &v1.Secret{}, + ownerEventHandler, + builder.WithPredicates(ignorePreStartCreate), + ). + WithOptions(kubecontroller.Options{ + MaxConcurrentReconciles: ratelimiter.GetConcurrent(opts), + RateLimiter: ratelimiter.GetRateLimiter(opts), + }). + Complete(r) +} + +func registerStartupCacheInformers(informers ctrlcache.Informers) error { + if informers == nil { + return errors.New("user controller cache is nil") + } + namespaceMetadata := &metav1.PartialObjectMetadata{} + namespaceMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace")) + secretMetadata := &metav1.PartialObjectMetadata{} + secretMetadata.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret")) + objects := []client.Object{ + namespaceMetadata, + &v1.ServiceAccount{}, + secretMetadata, + &rbacv1.Role{}, + &rbacv1.RoleBinding{}, + &rbacv1.ClusterRoleBinding{}, + } + for _, object := range objects { + if _, err := informers.GetInformer(context.Background(), object); err != nil { + return fmt.Errorf("register startup cache informer for %T: %w", object, err) + } + } + return nil +} + +func (r *UserReconciler) reconcile(ctx context.Context, obj client.Object) (ctrl.Result, error) { + r.Logger.V(1). + Info("update reconcile controller user", "request", client.ObjectKeyFromObject(obj)) + startTime := time.Now() + + user, ok := obj.(*userv1.User) + if !ok { + return ctrl.Result{}, errors.New("obj convert user is error") + } + + originalStatus := user.Status.DeepCopy() + blocked, err := r.handleLicenseLimit(ctx, user, originalStatus) + if err != nil { + return ctrl.Result{}, err + } + if blocked { + return ctrl.Result{RequeueAfter: r.minRequeueDuration}, nil + } + + defer func() { + r.Logger.V(1). + Info("finished reconcile", "user info", user.Name, "create time", user.CreationTimestamp, "reconcile cost time", time.Since(startTime)) + }() + + state := &userReconcileState{} + if userStatusNeedsSync(user) { + r.initStatus(ctx, user, state) + } + r.syncNamespaceIfNeeded(ctx, user, state) + r.syncServiceAccountIfNeeded(ctx, user, state) + r.syncKubeConfigIfNeeded(ctx, user, state) + r.syncRolesIfNeeded(ctx, user, state) + r.syncRoleBindingIfNeeded(ctx, user, state) + r.syncClusterRoleBindingIfNeeded(ctx, user, state) + r.syncFinalStatus(ctx, user, state) + if user.Status.Phase != userv1.UserUnknown { + user.Status.Phase = userv1.UserActive + } + if state.cleanupLegacySecrets { + // Best-effort migration cleanup for legacy service-account-token secrets. + if err := kubeconfig.CleanupLegacyBoundTokenSecrets( + ctx, + r.cache, + r.Client, + user.Name, + state.currentSecretName, + ); err != nil { + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "CleanupLegacyBoundTokenSecrets", + "Cleanup stale bound token secrets for %s is error: %v", + user.Name, + err, + ) + r.Logger.Error(err, "cleanup stale bound token secrets", "user", user.Name) + } + } + statusChanged := !reflect.DeepEqual(user.Status, *originalStatus) + if statusChanged && state.kubeConfigSynced { + // Normalize the observed duration only when this reconcile successfully + // refreshed kubeconfig and is about to persist its status. + setObservedCSRExpirationSeconds(user) + } + if !statusChanged { + requeueAfter := nextKubeConfigRequeueDuration(user, state) + if syncErr := r.finishKubeConfigSync(user, state); syncErr != nil { + return ctrl.Result{}, syncErr + } + if state.syncError != nil { + return ctrl.Result{}, state.syncError + } + return ctrl.Result{RequeueAfter: requeueAfter}, nil + } + if err = r.updateStatus(ctx, user, originalStatus); err != nil { + if state.kubeConfigSyncAttempted { + // The generated kubeconfig is not durable until the status patch succeeds. + r.nextKubeConfigSync.Store(user.Name, time.Now()) + } + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "SyncStatus", + "Sync status %s is error: %v", + user.Name, + err, + ) + return ctrl.Result{}, err + } + requeueAfter := nextKubeConfigRequeueDuration(user, state) + if syncErr := r.finishKubeConfigSync(user, state); syncErr != nil { + return ctrl.Result{}, syncErr + } + if state.syncError != nil { + return ctrl.Result{}, state.syncError + } + return ctrl.Result{RequeueAfter: requeueAfter}, nil +} + +func (r *UserReconciler) finishKubeConfigSync( + user *userv1.User, + state *userReconcileState, +) error { + if state == nil || user == nil { + return nil + } + if !state.kubeConfigSyncAttempted { + return nil + } + if !state.kubeConfigSynced { + // Keep the in-memory deadline due so the next workqueue retry does not + // get hidden by a future persisted refresh time. + r.nextKubeConfigSync.Store(user.Name, time.Now()) + return state.syncError + } + if user.Status.KubeConfigRefreshAt == nil || user.Status.KubeConfigRefreshAt.IsZero() { + // A successful refresh always persists this field. Clearing the entry + // keeps a malformed in-memory state from suppressing a retry. + r.nextKubeConfigSync.Delete(user.Name) + return nil + } + // The workqueue requeue interval is for ordinary drift checks. Kubeconfig + // refreshes must follow the persisted token deadline instead. + r.nextKubeConfigSync.Store(user.Name, user.Status.KubeConfigRefreshAt.Time) + return nil +} + +func (r *UserReconciler) initStatus(_ context.Context, user *userv1.User, _ *userReconcileState) { + initializedCondition := userv1.Condition{ + Type: userv1.Initialized, + Status: v1.ConditionTrue, + Reason: string(userv1.Initialized), + Message: "user has been initialized", + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + } + user.Status.Phase = userv1.UserPending + user.Status.ObservedGeneration = user.Generation + if !helper.IsConditionTrue(user.Status.Conditions, initializedCondition) { + user.Status.Conditions = helper.UpdateCondition( + user.Status.Conditions, + initializedCondition, + ) + } +} + +func (r *UserReconciler) markConditionReadyIfNeeded( + user *userv1.User, + conditionType userv1.ConditionType, +) { + for i := range user.Status.Conditions { + if user.Status.Conditions[i].Type != conditionType || + user.Status.Conditions[i].Status == v1.ConditionTrue { + continue + } + r.saveCondition(user, &userv1.Condition{ + Type: conditionType, + Status: v1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "cached resource matches desired state", + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + }) + return + } +} + +func (r *UserReconciler) syncNamespaceIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if r.cache != nil { + namespace := &metav1.PartialObjectMetadata{} + namespace.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace")) + if err := r.cache.Get( + ctx, + client.ObjectKey{Name: config.GetUsersNamespace(user.Name)}, + namespace, + ); err == nil && + namespaceMatchesUser(namespace, user, r.EnableAdminClusterAdmin) { + r.markConditionReadyIfNeeded(user, namespaceSyncReadyCondition) + return + } + } + r.syncNamespace(ctx, user, state) +} + +func (r *UserReconciler) syncServiceAccountIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if r.cache != nil { + serviceAccount := &v1.ServiceAccount{} + if err := r.cache.Get(ctx, client.ObjectKey{ + Name: user.Name, Namespace: config.GetUserSystemNamespace(), + }, serviceAccount); err == nil && metadataMatchesUserResource(serviceAccount, user) { + state.serviceAccount = serviceAccount + r.markConditionReadyIfNeeded(user, serviceAccountReadyCondition) + return + } + } + r.syncServiceAccount(ctx, user, state) +} + +func (r *UserReconciler) syncKubeConfigIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if r.cache != nil && !kubeConfigSyncFailed(user) && + !r.kubeConfigSyncDue(user) && + csrExpirationStatusMatches( + user.Spec.CSRExpirationSeconds, + user.Status.ObservedCSRExpirationSeconds, + ) && + user.Status.KubeConfigRefreshAt != nil && + !userKubeConfigNeedsRotation(user) && r.boundTokenSecretMatches(ctx, user, state) { + r.markConditionReadyIfNeeded(user, kubeConfigReadyCondition) + return + } + state.kubeConfigSyncAttempted = true + r.syncKubeConfig(ctx, user, state) +} + +func (r *UserReconciler) boundTokenSecretMatches( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) bool { + if state == nil || state.serviceAccount == nil || len(state.serviceAccount.Secrets) == 0 || + state.serviceAccount.Secrets[0].Name == "" { + return false + } + secret := &metav1.PartialObjectMetadata{} + secret.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Secret")) + if err := r.cache.Get(ctx, client.ObjectKey{ + Name: state.serviceAccount.Secrets[0].Name, Namespace: config.GetUserSystemNamespace(), + }, secret); err != nil { + return false + } + if secret.Annotations[v1.ServiceAccountNameKey] != user.Name || + !controlledByUser(secret, user) { + return false + } + if user.Status.ObservedKubeConfigSecretUID == "" { + // Legacy Users without an observed UID can keep using the current Secret. + // Persist the UID only after a successful kubeconfig refresh. + return true + } + return string(secret.UID) == user.Status.ObservedKubeConfigSecretUID +} + +func (r *UserReconciler) syncRolesIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + roleCondition := &userv1.Condition{ + Type: roleSyncReadyCondition, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync namespace role successfully", + } + previousCondition := helper.GetCondition(user.Status.Conditions, roleCondition).DeepCopy() + defer func() { + if helper.DiffCondition(previousCondition, roleCondition) { + r.saveCondition(user, roleCondition.DeepCopy()) + } + }() + for _, roleType := range []userv1.RoleType{ + userv1.OwnerRoleType, + userv1.ManagerRoleType, + userv1.DeveloperRoleType, + } { + healthy := false + if r.cache != nil { + healthy = roleMatchesUser(ctx, r.cache, client.ObjectKey{ + Name: string(roleType), Namespace: config.GetUsersNamespace(user.Name), + }, roleType, user) + } + if healthy { + continue + } + r.createRole(ctx, roleCondition, user, state, roleType) + } +} + +func (r *UserReconciler) syncRoleBindingIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if r.cache != nil { + roleBinding := &rbacv1.RoleBinding{} + if err := r.cache.Get(ctx, client.ObjectKey{ + Name: user.Name, Namespace: config.GetUsersNamespace(user.Name), + }, roleBinding); err == nil && roleBindingMatchesUser(roleBinding, user) { + r.markConditionReadyIfNeeded(user, roleBindingReadyCondition) + return + } + } + r.syncRoleBinding(ctx, user, state) +} + +func (r *UserReconciler) syncClusterRoleBindingIfNeeded( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if user.Name != adminUserName { + return + } + if r.cache != nil { + binding := &rbacv1.ClusterRoleBinding{} + err := r.cache.Get(ctx, client.ObjectKey{Name: adminClusterRoleBindingName}, binding) + healthy := (!r.EnableAdminClusterAdmin && apierrors.IsNotFound(err)) || + (r.EnableAdminClusterAdmin && err == nil && clusterRoleBindingMatchesUser(binding, user)) + if healthy { + r.markConditionReadyIfNeeded(user, clusterRoleBindingReadyCondition) + return + } + } + r.syncClusterRoleBinding(ctx, user, state) +} + +func (r *UserReconciler) syncNamespace( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + namespaceConditionType := userv1.ConditionType("NamespaceSyncReady") + nsCondition := &userv1.Condition{ + Type: namespaceConditionType, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync namespace successfully", + } + condition := helper.GetCondition(user.Status.Conditions, nsCondition) + defer func() { + if helper.DiffCondition(condition, nsCondition) { + r.saveCondition(user, nsCondition.DeepCopy()) + } + }() + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var change controllerutil.OperationResult + var err error + ns := &v1.Namespace{} + ns.Name = config.GetUsersNamespace(user.Name) + if err = r.Get(ctx, client.ObjectKeyFromObject(ns), ns); err != nil { + if !apierrors.IsNotFound(err) { + return err + } + } + var isCreated bool + if !ns.CreationTimestamp.IsZero() { + isCreated = true + r.Logger.V(1). + Info("define namespace User namespace is created", "isCreated", isCreated, "namespace", ns.Name) + } + if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error { + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + if ns.Labels == nil { + ns.Labels = make(map[string]string) + } + ns.Annotations[userAnnotationCreatorKey] = user.Name + ns.Annotations[userAnnotationOwnerKey] = user.Annotations[userAnnotationOwnerKey] + ns.Labels = desiredNamespaceLabels(ns.Name, ns.Labels, r.EnableAdminClusterAdmin) + // add label for namespace to filter + ns.Labels[userLabelOwnerKey] = user.Annotations[userAnnotationOwnerKey] + ns.SetOwnerReferences([]metav1.OwnerReference{}) + return controllerutil.SetControllerReference(user, ns, r.Scheme) + }); err != nil { + return fmt.Errorf("unable to create namespace by User: %w", err) + } + r.Logger.V(1).Info("create or update namespace by User", "OperationResult", change) + nsCondition.Message = fmt.Sprintf( + "sync namespace %s/%s successfully", + ns.Name, + ns.ResourceVersion, + ) + return nil + }); err != nil { + state.recordSyncError(err) + helper.SetConditionError(nsCondition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUser", + "Sync User namespace %s is error: %v", + user.Name, + err, + ) + } +} + +func (r *UserReconciler) createRole( + ctx context.Context, + condition *userv1.Condition, + user *userv1.User, + state *userReconcileState, + roleType userv1.RoleType, +) { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var change controllerutil.OperationResult + var err error + role := &rbacv1.Role{} + role.Name = string(roleType) + role.Namespace = config.GetUsersNamespace(user.Name) + role.Labels = map[string]string{} + if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, role, func() error { + role.Annotations = map[string]string{ + userAnnotationCreatorKey: user.Name, + userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey], + } + role.Rules = config.GetUserRole(roleType) + return controllerutil.SetControllerReference(user, role, r.Scheme) + }); err != nil { + return fmt.Errorf("unable to create namespace role by User: %w", err) + } + r.Logger.V(1).Info("create or update namespace role by User", "OperationResult", change) + condition.Message = fmt.Sprintf( + "sync namespace role %s/%s successfully", + role.Name, + role.ResourceVersion, + ) + return nil + }); err != nil { + state.recordSyncError(err) + helper.SetConditionError(condition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUserRole", + "Sync User namespace role %s is error: %v", + user.Name, + err, + ) + } +} + +func (r *UserReconciler) syncRoleBinding( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + roleBindingConditionType := userv1.ConditionType("RoleBindingSyncReady") + rbCondition := &userv1.Condition{ + Type: roleBindingConditionType, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync namespace role binding successfully", + } + condition := helper.GetCondition(user.Status.Conditions, rbCondition) + defer func() { + if helper.DiffCondition(condition, rbCondition) { + r.saveCondition(user, rbCondition.DeepCopy()) + } + }() + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var change controllerutil.OperationResult + var err error + roleBinding := &rbacv1.RoleBinding{} + roleBinding.Name = user.Name + roleBinding.Namespace = config.GetUsersNamespace(user.Name) + roleBinding.Labels = map[string]string{} + if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, roleBinding, func() error { + roleBinding.Annotations = map[string]string{ + userAnnotationCreatorKey: user.Name, + userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey], + } + roleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + } + roleBinding.Subjects = config.GetUsersSubject(user.Name) + return controllerutil.SetControllerReference(user, roleBinding, r.Scheme) + }); err != nil { + return fmt.Errorf("unable to create namespace role binding by User: %w", err) + } + r.Logger.V(1). + Info("create or update namespace role binding by User", "OperationResult", change) + rbCondition.Message = fmt.Sprintf( + "sync namespace role binding %s/%s successfully", + roleBinding.Name, + roleBinding.ResourceVersion, + ) + return nil + }); err != nil { + state.recordSyncError(err) + helper.SetConditionError(rbCondition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUserRoleBinding", + "Sync User namespace role binding %s is error: %v", + user.Name, + err, + ) + } +} + +func (r *UserReconciler) syncClusterRoleBinding( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + if user.Name != adminUserName { + return + } + roleBindingConditionType := userv1.ConditionType("ClusterRoleBindingSyncReady") + rbCondition := &userv1.Condition{ + Type: roleBindingConditionType, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync admin role binding successfully", + } + condition := helper.GetCondition(user.Status.Conditions, rbCondition) + defer func() { + if helper.DiffCondition(condition, rbCondition) { + r.saveCondition(user, rbCondition.DeepCopy()) + } + }() + if !r.EnableAdminClusterAdmin { + clusterRoleBinding := &rbacv1.ClusterRoleBinding{} + clusterRoleBinding.Name = adminClusterRoleBindingName + if err := r.Delete(ctx, clusterRoleBinding); err != nil && !apierrors.IsNotFound(err) { + err = fmt.Errorf("unable to remove disabled admin cluster role binding: %w", err) + state.recordSyncError(err) + helper.SetConditionError(rbCondition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUserClusterRoleBinding", + "Remove User admin cluster role binding %s is error: %v", + user.Name, + err, + ) + return + } + rbCondition.Message = "admin cluster role binding disabled" + return + } + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var change controllerutil.OperationResult + var err error + clusterRoleBinding := &rbacv1.ClusterRoleBinding{} + clusterRoleBinding.Name = adminClusterRoleBindingName + clusterRoleBinding.Labels = map[string]string{} + if change, err = controllerutil.CreateOrUpdate( + ctx, + r.Client, + clusterRoleBinding, + func() error { + clusterRoleBinding.Annotations = map[string]string{ + userAnnotationCreatorKey: user.Name, + userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey], + } + clusterRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + } + clusterRoleBinding.Subjects = config.GetUsersSubject(user.Name) + return controllerutil.SetControllerReference(user, clusterRoleBinding, r.Scheme) + }, + ); err != nil { + return fmt.Errorf( + "unable to create namespace admin cluster role binding by User: %w", + err, + ) + } + r.Logger.V(1). + Info("create or update namespace admin cluster role binding by User", "OperationResult", change) + rbCondition.Message = fmt.Sprintf( + "sync namespace admin cluster role binding %s/%s successfully", + clusterRoleBinding.Name, + clusterRoleBinding.ResourceVersion, + ) + return nil + }); err != nil { + state.recordSyncError(err) + helper.SetConditionError(rbCondition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUserClusterRoleBinding", + "Sync User admin cluster role binding %s is error: %v", + user.Name, + err, + ) + } +} + +func (r *UserReconciler) saveCondition(user *userv1.User, condition *userv1.Condition) { + user.Status.Conditions = helper.UpdateCondition(user.Status.Conditions, *condition) +} + +func (r *UserReconciler) syncServiceAccount( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + saConditionType := userv1.ConditionType("ServiceAccountSyncReady") + saCondition := &userv1.Condition{ + Type: saConditionType, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync namespace sa successfully", + } + condition := helper.GetCondition(user.Status.Conditions, saCondition) + defer func() { + if helper.DiffCondition(condition, saCondition) { + r.saveCondition(user, saCondition.DeepCopy()) + } + }() + state.serviceAccount = nil + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + var change controllerutil.OperationResult + var err error + sa := &v1.ServiceAccount{} + sa.Name = user.Name + sa.Namespace = config.GetUserSystemNamespace() + sa.Labels = map[string]string{} + + if err = r.Get(ctx, client.ObjectKey{ + Namespace: config.GetUserSystemNamespace(), + Name: user.Name, + }, sa); err != nil && !apierrors.IsNotFound(err) { + return err + } + + if change, err = controllerutil.CreateOrUpdate(ctx, r.Client, sa, func() error { + sa.Annotations = map[string]string{ + userAnnotationCreatorKey: user.Name, + userAnnotationOwnerKey: user.Annotations[userAnnotationOwnerKey], + } + return controllerutil.SetControllerReference(user, sa, r.Scheme) + }); err != nil { + return fmt.Errorf("unable to create namespace sa by User: %w", err) + } + r.Logger.V(1).Info("create or update namespace sa by User", "OperationResult", change) + saCondition.Message = fmt.Sprintf( + "sync namespace sa %s/%s successfully", + sa.Name, + sa.ResourceVersion, + ) + state.serviceAccount = sa + return nil + }); err != nil { + helper.SetConditionError(saCondition, "SyncUserError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncUserServiceAccount", + "Sync User namespace sa %s is error: %v", + user.Name, + err, + ) + } +} + +func (r *UserReconciler) syncKubeConfig( + ctx context.Context, + user *userv1.User, + state *userReconcileState, +) { + userCondition := &userv1.Condition{ + Type: kubeConfigReadyCondition, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "sync kube config successfully", + } + condition := helper.GetCondition(user.Status.Conditions, userCondition) + defer func() { + if helper.DiffCondition(condition, userCondition) { + r.saveCondition(user, userCondition.DeepCopy()) + } + }() + sa := state.serviceAccount + if sa == nil { + saErr := errors.New("serviceAccount not found") + state.recordSyncError(saErr) + helper.SetConditionError( + userCondition, + "SyncUserError", + saErr, + ) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Sync User namespace kubeconfig %s is error: %v", + user.Name, + saErr, + ) + return + } + // Keep an owned copy because kubeconfig generation may update metadata on it. + sa = sa.DeepCopy() + if r.shouldRotateKubeConfig(user) { + if err := r.deleteBoundTokenSecret(ctx, user); err != nil { + state.recordSyncError(err) + helper.SetConditionError(userCondition, "SyncKubeConfigError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Delete bound token secret %s is error: %v", + user.Name, + err, + ) + return + } + } + tokenRequestConfig := kubeconfig.NewConfig(user.Name, "", user.Spec.CSRExpirationSeconds). + WithServiceAccountConfig(config.GetUserSystemNamespace(), sa) + if r.shouldRotateKubeConfig(user) { + tokenRequestConfig = tokenRequestConfig.WithForceNewSecret() + } + apiConfig, tokenExpiresAt, err := tokenRequestConfig.ApplyWithTokenRequest( + ctx, + r.config, + r.Client, + ) + if err != nil { + state.recordSyncError(err) + helper.SetConditionError(userCondition, "SyncKubeConfigError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Sync KubeConfig apply %s is error: %v", + user.Name, + err, + ) + return + } + if apiConfig == nil { + configErr := errors.New("api.config is nil") + state.recordSyncError(configErr) + helper.SetConditionError( + userCondition, + "SyncKubeConfigError", + configErr, + ) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Sync KubeConfig apply %s is error: %v", + user.Name, + configErr, + ) + return + } + kubeData, err := clientcmd.Write(*apiConfig) + if err != nil { + state.recordSyncError(err) + helper.SetConditionError(userCondition, "OutputKubeConfigError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Output KubeConfig apply %s is error: %v", + user.Name, + err, + ) + return + } + keepSecretName := "" + if len(sa.Secrets) > 0 { + keepSecretName = sa.Secrets[0].Name + } + secretUID := "" + if keepSecretName != "" { + boundSecret := &v1.Secret{} + if err := r.Get(ctx, client.ObjectKey{ + Name: keepSecretName, Namespace: config.GetUserSystemNamespace(), + }, boundSecret); err != nil { + state.recordSyncError(err) + helper.SetConditionError(userCondition, "SyncKubeConfigError", err) + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "syncKubeConfig", + "Get bound token secret %s is error: %v", + user.Name, + err, + ) + return + } + secretUID = string(boundSecret.UID) + } + state.tokenExpirationDeadline = &tokenExpiresAt + refreshAt := metav1.NewTime(time.Now().Add(time.Until(tokenExpiresAt.Time) * 8 / 10)) + user.Status.KubeConfigRefreshAt = &refreshAt + user.Status.ObservedKubeConfigSecretUID = secretUID + if r.shouldRotateKubeConfig(user) { + user.Status.ObservedKubeConfigRotateAt = user.Spec.KubeConfigRotateAt + } + user.Status.KubeConfig = string(kubeData) + userCondition.Message = "renew sync kube config successfully hash " + hash.HashToString( + user.Status.KubeConfig, + ) + state.currentSecretName = keepSecretName + state.kubeConfigSynced = true + state.cleanupLegacySecrets = keepSecretName != "" +} + +func (r *UserReconciler) deleteBoundTokenSecret(ctx context.Context, user *userv1.User) error { + secretName := kubeconfig.TokenSecretName(user.Name) + sa := &v1.ServiceAccount{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: config.GetUserSystemNamespace(), + Name: user.Name, + }, sa); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get service account for bound token secret: %w", err) + } + } else if len(sa.Secrets) > 0 && sa.Secrets[0].Name != "" { + secretName = sa.Secrets[0].Name + } + + secret := &v1.Secret{} + secret.Name = secretName + secret.Namespace = config.GetUserSystemNamespace() + if err := r.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete bound token secret: %w", err) + } + return nil +} + +func syncReNewConfig(user *userv1.User) (*api.Config, *string, error) { + var apiConfig *api.Config + var err error + var event *string + if user.Status.KubeConfig != "" && + csrExpirationStatusMatches( + user.Spec.CSRExpirationSeconds, + user.Status.ObservedCSRExpirationSeconds, + ) { + apiConfig, err = clientcmd.Load([]byte(user.Status.KubeConfig)) + if err != nil { + return nil, nil, err + } + for _, ctx := range apiConfig.Contexts { + if ctx.Namespace == "" { + apiConfig = nil + ev := fmt.Sprintf("User %s Namespace is empty", user.Name) + event = &ev + return apiConfig, event, err + } + } + if info, ok := apiConfig.AuthInfos[user.Name]; ok { + if info != nil { + if info.Token == "" { + apiConfig = nil + ev := fmt.Sprintf("User %s Token is empty", user.Name) + event = &ev + return apiConfig, event, err + } + if info.ClientCertificateData == nil { + return apiConfig, event, err + } + cert, err := kubeconfig.DecodeX509CertificateBytes(info.ClientCertificateData) + if err != nil { + return nil, nil, err + } + if cert.NotAfter.Before(time.Now()) { + apiConfig = nil + ev := fmt.Sprintf("ClientCertificateData %s is expired", user.Name) + event = &ev + } + } + } + } + return apiConfig, event, err +} + +func (r *UserReconciler) syncFinalStatus( + _ context.Context, + user *userv1.User, + _ *userReconcileState, +) { + condition := &userv1.Condition{ + Type: userv1.Ready, + Status: v1.ConditionTrue, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: string(userv1.Ready), + Message: "User is available now", + } + defer r.saveCondition(user, condition) + + if !helper.IsConditionsTrue(user.Status.Conditions) { + condition.LastHeartbeatTime = metav1.Now() + condition.Status = v1.ConditionFalse + condition.Reason = "Not" + string(userv1.Ready) + condition.Message = "User is not available now" + user.Status.Phase = userv1.UserUnknown + } else { + user.Status.Phase = userv1.UserActive + } +} + +func (r *UserReconciler) shouldRotateKubeConfig(user *userv1.User) bool { + return userKubeConfigNeedsRotation(user) +} + +func userKubeConfigNeedsRotation(user *userv1.User) bool { + if user == nil { + return false + } + if user.Spec.KubeConfigRotateAt == nil { + return false + } + if user.Status.ObservedKubeConfigRotateAt == nil { + return true + } + return !user.Spec.KubeConfigRotateAt.Equal(user.Status.ObservedKubeConfigRotateAt) +} + +func kubeConfigSyncFailed(user *userv1.User) bool { + if user == nil { + return false + } + for _, condition := range user.Status.Conditions { + if condition.Type == kubeConfigReadyCondition { + return condition.Status == v1.ConditionFalse + } + } + return false +} + +func (r *UserReconciler) updateStatus( + ctx context.Context, + user *userv1.User, + originalStatus *userv1.UserStatus, +) error { + original := user.DeepCopy() + original.Status = *originalStatus.DeepCopy() + return r.Client.Status().Patch(ctx, user, client.MergeFrom(original)) +} + +func (r *UserReconciler) handleLicenseLimit( + ctx context.Context, + user *userv1.User, + originalStatus *userv1.UserStatus, +) (bool, error) { + if !r.isNewUser(user) { + user.Status.Conditions = helper.DeleteCondition( + user.Status.Conditions, + licenseLimitedCondition, + ) + return false, nil + } + + if r.userCounter == nil || !r.userCounter.Initialized() { + return false, errors.New("user count cache is not initialized") + } + userCount := r.userCounter.CountExcluding(user.Name) + if licensegate.AllowNewUser(userCount) { + user.Status.Conditions = helper.DeleteCondition( + user.Status.Conditions, + licenseLimitedCondition, + ) + return false, nil + } + limitCondition := &userv1.Condition{ + Type: licenseLimitedCondition, + Status: v1.ConditionFalse, + LastTransitionTime: metav1.Now(), + LastHeartbeatTime: metav1.Now(), + Reason: "LicenseLimitExceeded", + Message: licensegate.LimitMessage(), + } + user.Status.Phase = userv1.UserPending + user.Status.Conditions = helper.UpdateCondition(user.Status.Conditions, *limitCondition) + if err := r.updateStatus( + ctx, + user, + originalStatus, + ); err != nil { + return false, err + } + r.Recorder.Eventf( + user, + v1.EventTypeWarning, + "LicenseLimitExceeded", + "%s: %d", + licensegate.LimitMessage(), + licensegate.UserLimit(), + ) + return true, nil +} + +func (r *UserReconciler) isNewUser(user *userv1.User) bool { + if user == nil || user.Status.ObservedGeneration != 0 { + return false + } + for _, condition := range user.Status.Conditions { + if condition.Type != licenseLimitedCondition { + return false + } + } + return true +} + +func nextKubeConfigRequeueDuration( + user *userv1.User, + state *userReconcileState, +) time.Duration { + if state != nil && state.tokenExpirationDeadline != nil && + !state.tokenExpirationDeadline.IsZero() { + refreshDuration := time.Until(state.tokenExpirationDeadline.Time) * 8 / 10 + if refreshDuration <= 0 { + return time.Second + } + return refreshDuration + } + if user == nil || user.Status.KubeConfigRefreshAt == nil || + user.Status.KubeConfigRefreshAt.IsZero() { + return 0 + } + refreshDuration := time.Until(user.Status.KubeConfigRefreshAt.Time) + if refreshDuration <= 0 { + return time.Second + } + return refreshDuration +} + +func (r *UserReconciler) licenseToUserRequests( + ctx context.Context, + obj client.Object, +) []ctrl.Request { + userList := &userv1.UserList{} + if err := r.cache.List(ctx, userList); err != nil { + r.Logger.Error(err, "list users for license change failed") + return nil + } + requests := make([]ctrl.Request, 0, len(userList.Items)) + for i := range userList.Items { + requests = append( + requests, + ctrl.Request{NamespacedName: client.ObjectKeyFromObject(&userList.Items[i])}, + ) + } + return requests +} diff --git a/controllers/user/controllers/user_controller_reconcile_test.go b/controllers/user/controllers/user_controller_reconcile_test.go new file mode 100644 index 000000000000..40b48edbd4e8 --- /dev/null +++ b/controllers/user/controllers/user_controller_reconcile_test.go @@ -0,0 +1,699 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "testing" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + usercache "github.com/labring/sealos/controllers/user/controllers/cache" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func reconcileTestScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + return scheme +} + +func reconcileTestUser(name string) *userv1.User { + return &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + UID: types.UID("uid-" + name), + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "owner", + }, + }, + } +} + +func reconcileTestOwnerReference(user *userv1.User) metav1.OwnerReference { + controller := true + return metav1.OwnerReference{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", + Name: user.Name, + UID: user.UID, + Controller: &controller, + } +} + +func reconcileTestRole(user *userv1.User, roleType userv1.RoleType) *rbacv1.Role { + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(roleType), + Namespace: config.GetUsersNamespace(user.Name), + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner", + }, + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + Rules: config.GetUserRole(roleType), + } +} + +func TestRoleBindingMatchesUserUsesProjectedSpecHash(t *testing.T) { + user := reconcileTestUser("alice") + binding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: user.Name, + Namespace: config.GetUsersNamespace(user.Name), + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner", + }, + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + } + binding.Annotations[usercache.RoleBindingSpecHashAnnotation] = usercache.RoleBindingSpecHash( + rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + }, + config.GetUsersSubject(user.Name), + ) + if !roleBindingMatchesUser(binding, user) { + t.Fatal("projected role binding hash was not accepted") + } + binding.Annotations[usercache.RoleBindingSpecHashAnnotation] = "drifted" + if roleBindingMatchesUser(binding, user) { + t.Fatal("drifted projected role binding hash was accepted") + } +} + +func TestResourceSyncIfNeededRepairsMissingResources(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userName string + adminRole bool + objects func(*userv1.User) []client.Object + sync func(context.Context, *UserReconciler, *userv1.User, *userReconcileState) + verify func(*testing.T, client.Client, *userv1.User) + }{ + { + name: "namespace", + userName: "alice", + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncNamespaceIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + ns := &corev1.Namespace{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: config.GetUsersNamespace(user.Name)}, + ns, + ); err != nil { + t.Fatalf("get repaired namespace: %v", err) + } + if !namespaceMatchesUser(ns, user, false) { + t.Fatal("repaired namespace does not match user") + } + }, + }, + { + name: "service account", + userName: "alice", + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncServiceAccountIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + sa := &corev1.ServiceAccount{} + if err := cli.Get(context.Background(), client.ObjectKey{ + Name: user.Name, Namespace: config.GetUserSystemNamespace(), + }, sa); err != nil { + t.Fatalf("get repaired service account: %v", err) + } + if !metadataMatchesUserResource(sa, user) { + t.Fatal("repaired service account does not match user") + } + }, + }, + { + name: "missing developer role", + userName: "alice", + objects: func(user *userv1.User) []client.Object { + return []client.Object{ + reconcileTestRole(user, userv1.OwnerRoleType), + reconcileTestRole(user, userv1.ManagerRoleType), + } + }, + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncRolesIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + role := &rbacv1.Role{} + if err := cli.Get(context.Background(), client.ObjectKey{ + Name: string(userv1.DeveloperRoleType), + Namespace: config.GetUsersNamespace(user.Name), + }, role); err != nil { + t.Fatalf("get repaired developer role: %v", err) + } + if !roleMatchesUser( + context.Background(), + cli, + client.ObjectKeyFromObject(role), + userv1.DeveloperRoleType, + user, + ) { + t.Fatal("repaired developer role does not match user") + } + }, + }, + { + name: "drifted developer role", + userName: "alice", + objects: func(user *userv1.User) []client.Object { + role := reconcileTestRole(user, userv1.DeveloperRoleType) + role.Rules = []rbacv1.PolicyRule{{ + Resources: []string{"pods"}, + Verbs: []string{"get"}, + }} + return []client.Object{ + reconcileTestRole(user, userv1.OwnerRoleType), + reconcileTestRole(user, userv1.ManagerRoleType), + role, + } + }, + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncRolesIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + role := &rbacv1.Role{} + if err := cli.Get(context.Background(), client.ObjectKey{ + Name: string(userv1.DeveloperRoleType), + Namespace: config.GetUsersNamespace(user.Name), + }, role); err != nil { + t.Fatalf("get repaired developer role: %v", err) + } + if !roleMatchesUser( + context.Background(), + cli, + client.ObjectKeyFromObject(role), + userv1.DeveloperRoleType, + user, + ) { + t.Fatal("drifted developer role was not repaired") + } + }, + }, + { + name: "role binding", + userName: "alice", + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncRoleBindingIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + binding := &rbacv1.RoleBinding{} + if err := cli.Get(context.Background(), client.ObjectKey{ + Name: user.Name, Namespace: config.GetUsersNamespace(user.Name), + }, binding); err != nil { + t.Fatalf("get repaired role binding: %v", err) + } + if !roleBindingMatchesUser(binding, user) { + t.Fatal("repaired role binding does not match user") + } + }, + }, + { + name: "admin cluster role binding", + userName: adminUserName, + adminRole: true, + sync: func(ctx context.Context, r *UserReconciler, user *userv1.User, state *userReconcileState) { + r.syncClusterRoleBindingIfNeeded(ctx, user, state) + }, + verify: func(t *testing.T, cli client.Client, user *userv1.User) { + t.Helper() + binding := &rbacv1.ClusterRoleBinding{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: adminClusterRoleBindingName}, + binding, + ); err != nil { + t.Fatalf("get repaired admin cluster role binding: %v", err) + } + if !clusterRoleBindingMatchesUser(binding, user) { + t.Fatal("repaired admin cluster role binding does not match user") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + scheme := reconcileTestScheme(t) + user := reconcileTestUser(tt.userName) + var objects []client.Object + if tt.objects != nil { + objects = tt.objects(user) + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + r := &UserReconciler{ + Client: cli, + Scheme: scheme, + Recorder: record.NewFakeRecorder(32), + EnableAdminClusterAdmin: tt.adminRole, + cache: cli, + } + + tt.sync(context.Background(), r, user, &userReconcileState{}) + tt.verify(t, cli, user) + }) + } +} + +func benchmarkProcessedUser(name string, refreshAt time.Time) *userv1.User { + return &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + UID: types.UID("uid-" + name), + Generation: 1, + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "owner", + }, + }, + Spec: userv1.UserSpec{ + CSRExpirationSeconds: userv1.DefaultCSRExpirationSeconds, + }, + Status: userv1.UserStatus{ + Phase: userv1.UserActive, + ObservedGeneration: 1, + ObservedCSRExpirationSeconds: userv1.DefaultCSRExpirationSeconds, + KubeConfigRefreshAt: &metav1.Time{Time: refreshAt}, + ObservedKubeConfigSecretUID: "secret-" + name, + Conditions: []userv1.Condition{ + {Type: userv1.Initialized, Status: corev1.ConditionTrue}, + { + Type: namespaceSyncReadyCondition, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "sync namespace successfully", + }, + { + Type: serviceAccountReadyCondition, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "sync namespace sa successfully", + }, + { + Type: kubeConfigReadyCondition, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "sync kube config successfully", + }, + { + Type: roleSyncReadyCondition, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "sync namespace role successfully", + }, + { + Type: roleBindingReadyCondition, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "sync namespace role binding successfully", + }, + { + Type: userv1.Ready, + Status: corev1.ConditionTrue, + Reason: string(userv1.Ready), + Message: "User is available now", + }, + }, + }, + } +} + +// healthyStartupCache returns only the fields used by the fast-path checks, +// matching the projected objects held by the production cache. +type healthyStartupCache struct{} + +func (healthyStartupCache) Get( + _ context.Context, + key client.ObjectKey, + obj client.Object, + _ ...client.GetOption, +) error { + owner := "owner" + var userName string + switch typed := obj.(type) { + case *metav1.PartialObjectMetadata: + if typed.GroupVersionKind().Kind == "Secret" { + userName = strings.TrimPrefix(key.Name, "token-") + typed.Name = key.Name + typed.Namespace = key.Namespace + typed.UID = types.UID("secret-" + userName) + typed.Annotations = map[string]string{corev1.ServiceAccountNameKey: userName} + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + return nil + } + userName = config.GetUserNameByNamespace(key.Name) + typed.Name = key.Name + typed.Annotations = map[string]string{ + userv1.UserAnnotationCreatorKey: userName, + userv1.UserAnnotationOwnerKey: owner, + } + typed.Labels = config.SetPodSecurity(map[string]string{ + userv1.UserLabelOwnerKey: owner, + }) + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + return nil + case *corev1.ServiceAccount: + userName = key.Name + typed.Name = key.Name + typed.Namespace = key.Namespace + typed.Annotations = benchmarkAnnotations(userName, owner) + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + typed.Secrets = []corev1.ObjectReference{{Name: "token-" + userName}} + return nil + case *corev1.Secret: + userName = strings.TrimPrefix(key.Name, "token-") + typed.Name = key.Name + typed.Namespace = key.Namespace + typed.UID = types.UID("secret-" + userName) + typed.Annotations = map[string]string{corev1.ServiceAccountNameKey: userName} + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + return nil + case *rbacv1.Role: + userName = config.GetUserNameByNamespace(key.Namespace) + typed.Name = key.Name + typed.Namespace = key.Namespace + typed.Annotations = benchmarkAnnotations(userName, owner) + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + typed.Rules = config.GetUserRole(userv1.RoleType(key.Name)) + return nil + case *rbacv1.RoleBinding: + userName = config.GetUserNameByNamespace(key.Namespace) + typed.Name = key.Name + typed.Namespace = key.Namespace + typed.Annotations = benchmarkAnnotations(userName, owner) + typed.OwnerReferences = []metav1.OwnerReference{benchmarkOwnerReference(userName)} + typed.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + } + typed.Subjects = config.GetUsersSubject(userName) + return nil + default: + return fmt.Errorf("unexpected cache object type %T", obj) + } +} + +func (healthyStartupCache) List(context.Context, client.ObjectList, ...client.ListOption) error { + return errors.New("unexpected cache list call") +} + +func benchmarkOwnerReference(userName string) metav1.OwnerReference { + controller := true + return metav1.OwnerReference{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", + Name: userName, + UID: types.UID("uid-" + userName), + Controller: &controller, + } +} + +func benchmarkAnnotations(userName, owner string) map[string]string { + return map[string]string{ + userv1.UserAnnotationCreatorKey: userName, + userv1.UserAnnotationOwnerKey: owner, + } +} + +func TestReconcileSkipsHealthyCachedUserResources(t *testing.T) { + t.Parallel() + user := benchmarkProcessedUser("alice", time.Now().Add(time.Hour)) + originalStatus := user.Status.DeepCopy() + r := &UserReconciler{ + cache: healthyStartupCache{}, + minRequeueDuration: time.Minute, + } + + if _, err := r.reconcile(context.Background(), user); err != nil { + t.Fatalf("reconcile healthy cached user: %v", err) + } + if !reflect.DeepEqual(user.Status, *originalStatus) { + t.Fatalf( + "healthy cached user status changed: before=%#v after=%#v", + originalStatus, + user.Status, + ) + } +} + +func TestReconcileSkipsHealthyStatusWithoutLegacyConditions(t *testing.T) { + t.Parallel() + user := benchmarkProcessedUser("legacy-user", time.Now().Add(time.Hour)) + filteredConditions := make([]userv1.Condition, 0, len(user.Status.Conditions)-2) + for _, condition := range user.Status.Conditions { + if condition.Type != kubeConfigReadyCondition && condition.Type != roleSyncReadyCondition { + filteredConditions = append(filteredConditions, condition) + } + } + user.Status.Conditions = filteredConditions + originalStatus := user.Status.DeepCopy() + r := &UserReconciler{ + cache: healthyStartupCache{}, + minRequeueDuration: time.Minute, + } + if _, err := r.reconcile(context.Background(), user); err != nil { + t.Fatalf("reconcile healthy legacy user: %v", err) + } + if !reflect.DeepEqual(user.Status, *originalStatus) { + t.Fatalf( + "missing conditions were restored for healthy resources: before=%#v after=%#v", + originalStatus, + user.Status, + ) + } +} + +func TestKubeConfigSyncFailedConditionForcesRetry(t *testing.T) { + t.Parallel() + user := benchmarkProcessedUser("failed-kubeconfig", time.Now().Add(time.Hour)) + for i := range user.Status.Conditions { + if user.Status.Conditions[i].Type == kubeConfigReadyCondition { + user.Status.Conditions[i].Status = corev1.ConditionFalse + break + } + } + if !kubeConfigSyncFailed(user) { + t.Fatal("failed kubeconfig condition was not detected") + } + + for _, status := range []corev1.ConditionStatus{ + corev1.ConditionTrue, + corev1.ConditionUnknown, + } { + for i := range user.Status.Conditions { + if user.Status.Conditions[i].Type == kubeConfigReadyCondition { + user.Status.Conditions[i].Status = status + break + } + } + if kubeConfigSyncFailed(user) { + t.Fatalf("kubeconfig condition %q incorrectly forced a retry", status) + } + } +} + +func TestBoundTokenSecretMatchesDetectsIdentityDrift(t *testing.T) { + t.Parallel() + scheme := reconcileTestScheme(t) + user := reconcileTestUser("alice") + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: user.Name, + Namespace: config.GetUserSystemNamespace(), + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + Secrets: []corev1.ObjectReference{{Name: "token-alice"}}, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "token-alice", + Namespace: config.GetUserSystemNamespace(), + UID: types.UID("secret-a"), + Annotations: map[string]string{corev1.ServiceAccountNameKey: user.Name}, + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sa, secret).Build() + r := &UserReconciler{cache: cli} + state := &userReconcileState{serviceAccount: sa} + user.Status.ObservedKubeConfigSecretUID = string(secret.UID) + + if !r.boundTokenSecretMatches(context.Background(), user, state) { + t.Fatal("matching bound token secret was rejected") + } + + secret.UID = types.UID("secret-b") + if err := cli.Update(context.Background(), secret); err != nil { + t.Fatalf("update recreated secret: %v", err) + } + if r.boundTokenSecretMatches(context.Background(), user, state) { + t.Fatal("recreated bound token secret was accepted with stale UID") + } + + user.Status.ObservedKubeConfigSecretUID = "" + if !r.boundTokenSecretMatches(context.Background(), user, state) { + t.Fatal("legacy user with a valid bound token secret was rejected") + } + if user.Status.ObservedKubeConfigSecretUID != "" { + t.Fatalf( + "legacy user secret UID was unexpectedly backfilled: got %q", + user.Status.ObservedKubeConfigSecretUID, + ) + } + + secret.Annotations[corev1.ServiceAccountNameKey] = "other-user" + if err := cli.Update(context.Background(), secret); err != nil { + t.Fatalf("update secret owner annotation: %v", err) + } + if r.boundTokenSecretMatches(context.Background(), user, state) { + t.Fatal("secret bound to another service account was accepted") + } +} + +func TestKubeConfigSyncSkipsSecretUIDRotationForLegacyStatus(t *testing.T) { + t.Parallel() + scheme := reconcileTestScheme(t) + user := benchmarkProcessedUser("alice", time.Now().Add(time.Hour)) + user.Status.ObservedKubeConfigSecretUID = "" + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: user.Name, + Namespace: config.GetUserSystemNamespace(), + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + Secrets: []corev1.ObjectReference{{Name: "token-alice"}}, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "token-alice", + Namespace: config.GetUserSystemNamespace(), + UID: types.UID("recreated-secret"), + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: user.Name, + }, + OwnerReferences: []metav1.OwnerReference{reconcileTestOwnerReference(user)}, + }, + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sa, secret).Build() + r := &UserReconciler{cache: cli} + state := &userReconcileState{serviceAccount: sa} + + r.syncKubeConfigIfNeeded(context.Background(), user, state) + + if state.kubeConfigSyncAttempted { + t.Fatal("recreated Secret triggered kubeconfig rotation for legacy status") + } + if user.Status.ObservedKubeConfigSecretUID != "" { + t.Fatalf( + "legacy status UID was unexpectedly backfilled: %q", + user.Status.ObservedKubeConfigSecretUID, + ) + } +} + +func TestNewUserStatusNeedsInitialization(t *testing.T) { + t.Parallel() + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}} + if !(&UserReconciler{}).isNewUser(user) { + t.Fatal("empty user was not classified as new") + } + if !userStatusNeedsSync(user) { + t.Fatal("new user was classified as already synchronized") + } +} + +func BenchmarkStartupReconcileProcessedUsers100K(b *testing.B) { + const userCount = 100_000 + refreshAt := time.Now().Add(time.Hour) + users := make([]*userv1.User, userCount) + for i := range users { + users[i] = benchmarkProcessedUser(fmt.Sprintf("user-%d", i), refreshAt) + } + reconciler := &UserReconciler{cache: healthyStartupCache{}} + ctx := context.Background() + b.ReportAllocs() + b.ReportMetric(float64(userCount), "users/op") + b.ResetTimer() + for range b.N { + for _, user := range users { + if _, err := reconciler.reconcile(ctx, user); err != nil { + b.Fatal(err) + } + } + } +} + +func BenchmarkStartupNewUserAfterHistoricalQueue100K(b *testing.B) { + const historicalUserCount = 100_000 + refreshAt := time.Now().Add(time.Hour) + historicalUsers := make([]*userv1.User, historicalUserCount) + for i := range historicalUsers { + historicalUsers[i] = benchmarkProcessedUser(fmt.Sprintf("user-%d", i), refreshAt) + } + newUser := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}} + b.ReportAllocs() + b.ReportMetric(float64(historicalUserCount+1), "users/op") + b.ResetTimer() + for range b.N { + for _, user := range historicalUsers { + if userStatusNeedsSync(user) { + b.Fatalf("historical user %s was classified as needing initialization", user.Name) + } + } + if !userStatusNeedsSync(newUser) { + b.Fatalf("new user %s was classified as already processed", newUser.Name) + } + } +} diff --git a/controllers/user/controllers/user_count.go b/controllers/user/controllers/user_count.go new file mode 100644 index 000000000000..c59af10f1b31 --- /dev/null +++ b/controllers/user/controllers/user_count.go @@ -0,0 +1,97 @@ +// Copyright © 2026 sealos. +// +// 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 controllers + +import ( + "context" + "errors" + "fmt" + "net/http" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/pkg/usercount" + toolscache "k8s.io/client-go/tools/cache" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" +) + +type userCountRunnable struct { + cache ctrlcache.Cache +} + +func (r *userCountRunnable) Start(ctx context.Context) error { + <-ctx.Done() + return nil +} + +func (r *userCountRunnable) GetCache() ctrlcache.Cache { + return r.cache +} + +func (r *userCountRunnable) NeedLeaderElection() bool { + return false +} + +type userCountCache struct { + ctrlcache.Cache + counter *usercount.Counter + handlerRegistration toolscache.ResourceEventHandlerRegistration +} + +func (c *userCountCache) WaitForCacheSync(ctx context.Context) bool { + if !c.Cache.WaitForCacheSync(ctx) { + return false + } + if !toolscache.WaitForCacheSync(ctx.Done(), c.handlerRegistration.HasSynced) { + return false + } + c.counter.MarkInitialized() + return true +} + +func SetupUserCount(mgr ctrl.Manager) (*usercount.Counter, error) { + counter := usercount.NewCounter() + informer, err := mgr.GetCache().GetInformer(context.Background(), &userv1.User{}) + if err != nil { + return nil, fmt.Errorf("get user informer: %w", err) + } + registration, err := informer.AddEventHandler(toolscache.ResourceEventHandlerFuncs{ + AddFunc: counter.Add, + UpdateFunc: counter.Update, + DeleteFunc: counter.Delete, + }) + if err != nil { + return nil, fmt.Errorf("add user count event handler: %w", err) + } + + if err := mgr.Add(&userCountRunnable{ + cache: &userCountCache{ + Cache: mgr.GetCache(), + counter: counter, + handlerRegistration: registration, + }, + }); err != nil { + return nil, fmt.Errorf("add user count runnable: %w", err) + } + if err := mgr.AddReadyzCheck("user-count-cache", func(_ *http.Request) error { + if !counter.Initialized() { + return errors.New("user count cache is not initialized") + } + return nil + }); err != nil { + return nil, fmt.Errorf("add user count readiness check: %w", err) + } + return counter, nil +} diff --git a/controllers/user/controllers/user_count_test.go b/controllers/user/controllers/user_count_test.go new file mode 100644 index 000000000000..4715e2ca97da --- /dev/null +++ b/controllers/user/controllers/user_count_test.go @@ -0,0 +1,93 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "testing" + + "github.com/labring/sealos/controllers/user/pkg/usercount" + toolscache "k8s.io/client-go/tools/cache" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" +) + +type stubCache struct { + ctrlcache.Cache + synced bool +} + +func (c *stubCache) WaitForCacheSync(context.Context) bool { + return c.synced +} + +type stubHandlerRegistration struct { + synced bool +} + +func (r stubHandlerRegistration) HasSynced() bool { + return r.synced +} + +func TestUserCountCacheWaitForCacheSync(t *testing.T) { + tests := []struct { + name string + cacheSynced bool + handlerSynced bool + wantReady bool + }{ + { + name: "cache not synced", + cacheSynced: false, + handlerSynced: true, + }, + { + name: "handler not synced", + cacheSynced: true, + handlerSynced: false, + }, + { + name: "cache and handler synced", + cacheSynced: true, + handlerSynced: true, + wantReady: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + counter := usercount.NewCounter() + cache := &userCountCache{ + Cache: &stubCache{synced: tt.cacheSynced}, + counter: counter, + handlerRegistration: stubHandlerRegistration{synced: tt.handlerSynced}, + } + ctx := context.Background() + if !tt.handlerSynced { + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + cancel() + } + + if got := cache.WaitForCacheSync(ctx); got != tt.wantReady { + t.Fatalf("WaitForCacheSync() = %v, want %v", got, tt.wantReady) + } + if got := counter.Initialized(); got != tt.wantReady { + t.Fatalf("counter.Initialized() = %v, want %v", got, tt.wantReady) + } + }) + } +} + +var _ toolscache.ResourceEventHandlerRegistration = stubHandlerRegistration{} diff --git a/controllers/user/controllers/user_event_handler.go b/controllers/user/controllers/user_event_handler.go new file mode 100644 index 000000000000..9bd1fff7e3ba --- /dev/null +++ b/controllers/user/controllers/user_event_handler.go @@ -0,0 +1,111 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" +) + +// userEventHandler keeps incomplete Users ahead of the initial historical +// queue after a restart. Creation time is insufficient because a User may have +// been waiting for reconciliation before the previous controller terminated. +type userEventHandler struct{} + +var _ handler.EventHandler = userEventHandler{} + +func (userEventHandler) Create( + _ context.Context, + evt event.CreateEvent, + q workqueue.TypedRateLimitingInterface[ctrl.Request], +) { + if evt.Object == nil { + return + } + addUserRequest(q, evt.Object, userPriority(evt.Object)) +} + +func (userEventHandler) Update( + _ context.Context, + evt event.UpdateEvent, + q workqueue.TypedRateLimitingInterface[ctrl.Request], +) { + obj := evt.ObjectNew + if obj == nil { + obj = evt.ObjectOld + } + if obj == nil { + return + } + // Accepted User updates change generation or the owner annotation, so they + // represent live drift and must use the normal priority. + addUserRequest(q, obj, 0) +} + +func (userEventHandler) Delete( + _ context.Context, + evt event.DeleteEvent, + q workqueue.TypedRateLimitingInterface[ctrl.Request], +) { + if evt.Object == nil { + return + } + addUserRequest(q, evt.Object, 0) +} + +func (userEventHandler) Generic( + _ context.Context, + evt event.GenericEvent, + q workqueue.TypedRateLimitingInterface[ctrl.Request], +) { + if evt.Object == nil { + return + } + addUserRequest(q, evt.Object, 0) +} + +func addUserRequest( + q workqueue.TypedRateLimitingInterface[ctrl.Request], + obj client.Object, + priority int, +) { + request := ctrl.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }} + priorityQueue, ok := q.(priorityqueue.PriorityQueue[ctrl.Request]) + if !ok { + q.Add(request) + return + } + + priorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: priority}, request) +} + +func userPriority(obj client.Object) int { + user, ok := obj.(*userv1.User) + if ok && !userStatusNeedsSync(user) { + return handler.LowPriority + } + return 0 +} diff --git a/controllers/user/controllers/user_event_handler_test.go b/controllers/user/controllers/user_event_handler_test.go new file mode 100644 index 000000000000..8e09cdc02dac --- /dev/null +++ b/controllers/user/controllers/user_event_handler_test.go @@ -0,0 +1,229 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "testing" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/pkg/licensegate" + "github.com/labring/sealos/controllers/user/pkg/usercount" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestUserEventHandlerPrioritizesIncompleteUsers(t *testing.T) { + historical := metav1.NewTime(time.Now().Add(-time.Hour)) + newUser := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-user", + Generation: 1, + CreationTimestamp: historical, + }, + } + failedUser := benchmarkProcessedUser("failed-user", time.Now().Add(time.Hour)) + failedUser.CreationTimestamp = historical + for i := range failedUser.Status.Conditions { + if failedUser.Status.Conditions[i].Type == userv1.Ready { + failedUser.Status.Conditions[i].Status = corev1.ConditionFalse + } + } + healthyUser := benchmarkProcessedUser("healthy-user", time.Now().Add(time.Hour)) + healthyUser.CreationTimestamp = historical + + tests := []struct { + name string + user *userv1.User + priority int + }{ + {name: "new", user: newUser, priority: 0}, + {name: "failed", user: failedUser, priority: 0}, + {name: "healthy historical", user: healthyUser, priority: -100}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := priorityqueue.New[reconcile.Request]("user-event-test") + t.Cleanup(q.ShutDown) + userEventHandler{}.Create( + context.Background(), + event.CreateEvent{Object: tt.user}, + q, + ) + + request, priority, shutdown := q.GetWithPriority() + if shutdown { + t.Fatal("priority queue shut down before delivering User") + } + q.Done(request) + if request.Name != tt.user.Name { + t.Fatalf("request name = %q, want %q", request.Name, tt.user.Name) + } + if priority != tt.priority { + t.Fatalf("priority = %d, want %d", priority, tt.priority) + } + }) + } +} + +func TestUserEventHandlerUpdatePromotesQueuedUser(t *testing.T) { + t.Parallel() + + user := benchmarkProcessedUser("healthy-user", time.Now().Add(time.Hour)) + user.CreationTimestamp = metav1.NewTime(time.Now().Add(-time.Hour)) + updated := user.DeepCopy() + updated.Generation++ + updated.ResourceVersion = "2" + + q := priorityqueue.New[reconcile.Request]("user-event-promotion-test") + t.Cleanup(q.ShutDown) + handler := userEventHandler{} + handler.Create(context.Background(), event.CreateEvent{Object: user}, q) + handler.Update(context.Background(), event.UpdateEvent{ + ObjectOld: user, + ObjectNew: updated, + }, q) + + request, priority, shutdown := q.GetWithPriority() + if shutdown { + t.Fatal("priority queue shut down before delivering User") + } + q.Done(request) + if priority != 0 { + t.Fatalf("promoted priority = %d, want 0", priority) + } +} + +func TestAddUserRequestUsesCallerPriority(t *testing.T) { + t.Parallel() + + q := priorityqueue.New[reconcile.Request]("user-event-custom-priority-test") + t.Cleanup(q.ShutDown) + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "custom-priority"}} + + addUserRequest(q, user, 42) + request, priority, shutdown := q.GetWithPriority() + if shutdown { + t.Fatal("priority queue shut down before delivering User") + } + q.Done(request) + if request.Name != user.Name { + t.Fatalf("request name = %q, want %q", request.Name, user.Name) + } + if priority != 42 { + t.Fatalf("priority = %d, want caller priority 42", priority) + } +} + +func TestIgnorePreStartCreatePredicate(t *testing.T) { + t.Parallel() + + startedAt := time.Unix(1_000, 0) + predicate := ignorePreStartCreatePredicate{startedAt: startedAt} + tests := []struct { + name string + createdAt time.Time + want bool + }{ + {name: "before controller start", createdAt: startedAt.Add(-time.Nanosecond)}, + {name: "at controller start", createdAt: startedAt, want: true}, + {name: "after controller start", createdAt: startedAt.Add(time.Second), want: true}, + {name: "missing timestamp", want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + obj := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + CreationTimestamp: metav1.NewTime(tt.createdAt), + }} + if got := predicate.Create(event.CreateEvent{Object: obj}); got != tt.want { + t.Fatalf("Create() = %t, want %t", got, tt.want) + } + }) + } + if predicate.Create(event.CreateEvent{}) { + t.Fatal("nil Create object was accepted") + } +} + +func TestLicenseLimitedUserRemainsNewAcrossRestart(t *testing.T) { + scheme := reconcileTestScheme(t) + stored := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "new-user", Generation: 1}} + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&userv1.User{}). + WithObjects(stored). + Build() + + counter := usercount.NewCounter() + counter.Add(&userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "existing-user"}}) + counter.MarkInitialized() + licensegate.SetState(true, 1) + t.Cleanup(func() { + licensegate.SetState(false, licensegate.DefaultUserLimit) + }) + + r := &UserReconciler{ + Client: cli, + Recorder: record.NewFakeRecorder(2), + userCounter: counter, + } + user := &userv1.User{} + if err := cli.Get(context.Background(), client.ObjectKey{Name: stored.Name}, user); err != nil { + t.Fatalf("get new User: %v", err) + } + blocked, err := r.handleLicenseLimit( + context.Background(), + user, + user.Status.DeepCopy(), + ) + if err != nil { + t.Fatalf("apply license limit: %v", err) + } + if !blocked { + t.Fatal("license-limited User was not blocked") + } + + restartedUser := &userv1.User{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: stored.Name}, + restartedUser, + ); err != nil { + t.Fatalf("get persisted User status: %v", err) + } + if !r.isNewUser(restartedUser) { + t.Fatalf("license-limited User was classified as old: %#v", restartedUser.Status) + } + blocked, err = r.handleLicenseLimit( + context.Background(), + restartedUser, + restartedUser.Status.DeepCopy(), + ) + if err != nil { + t.Fatalf("retry license limit: %v", err) + } + if !blocked { + t.Fatal("license-limited User was allowed before capacity became available") + } +} diff --git a/controllers/user/controllers/user_expiration_controller.go b/controllers/user/controllers/user_expiration_controller.go new file mode 100644 index 000000000000..b2d0909a0d5d --- /dev/null +++ b/controllers/user/controllers/user_expiration_controller.go @@ -0,0 +1,103 @@ +/* +Copyright 2022 labring. + +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 controllers + +import ( + "context" + "errors" + + "github.com/go-logr/logr" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper/finalizer" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// UserExpirationReconciler reconciles a Secret object +type UserExpirationReconciler struct { + Logger logr.Logger + Recorder record.EventRecorder + config *rest.Config + *runtime.Scheme + client.Client + finalizer *finalizer.Finalizer +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Secret object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile +func (r *UserExpirationReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { + r.Logger.V(1).Info("start reconcile for users expiration") + user := &userv1.User{} + if err := r.Get(ctx, req.NamespacedName, user); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if ok, err := r.finalizer.RemoveFinalizer(ctx, user, finalizer.DefaultFunc); ok { + return ctrl.Result{}, err + } + + if ok, err := r.finalizer.AddFinalizer(ctx, user); ok { + if err != nil { + return ctrl.Result{}, err + } + return r.reconcile(ctx, user) + } + return ctrl.Result{}, errors.New("reconcile error from Finalizer") +} + +// SetupWithManager sets up the controller with the Manager. +func (r *UserExpirationReconciler) SetupWithManager(mgr ctrl.Manager) error { + const controllerName = "user_expiration_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + if r.Recorder == nil { + r.Recorder = mgr.GetEventRecorderFor(controllerName) + } + if r.finalizer == nil { + r.finalizer = finalizer.NewFinalizer(r.Client, "sealos.io/user.expiration.finalizers"). + WithReader(mgr.GetAPIReader()) + } + r.Scheme = mgr.GetScheme() + r.config = mgr.GetConfig() + r.Logger.V(1).Info("init reconcile controller user expiration") + return ctrl.NewControllerManagedBy(mgr). + For(&userv1.User{}). + Complete(r) +} + +func (r *UserExpirationReconciler) reconcile( + _ context.Context, + _ client.Object, +) (ctrl.Result, error) { + // TODO add Expiration logic + return ctrl.Result{}, nil +} diff --git a/controllers/user/controllers/user_requeue_test.go b/controllers/user/controllers/user_requeue_test.go new file mode 100644 index 000000000000..507d98e47ff1 --- /dev/null +++ b/controllers/user/controllers/user_requeue_test.go @@ -0,0 +1,52 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "testing" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNextKubeConfigRequeueDuration(t *testing.T) { + t.Parallel() + if got := nextKubeConfigRequeueDuration(nil, nil); got != 0 { + t.Fatalf("ordinary requeue duration = %v, want 0", got) + } + + refreshAt := metav1.NewTime(time.Now().Add(time.Hour)) + user := &userv1.User{Status: userv1.UserStatus{KubeConfigRefreshAt: &refreshAt}} + if got := nextKubeConfigRequeueDuration(user, nil); got < 59*time.Minute || got > time.Hour { + t.Fatalf("persisted refresh requeue duration = %v, want about 1h", got) + } + + expiresAt := metav1.NewTime(time.Now().Add(10 * time.Hour)) + state := &userReconcileState{tokenExpirationDeadline: &expiresAt} + if got := nextKubeConfigRequeueDuration( + nil, + state, + ); got < 7*time.Hour+59*time.Minute || + got > 8*time.Hour { + t.Fatalf("new token refresh requeue duration = %v, want about 8h", got) + } + + past := metav1.NewTime(time.Now().Add(-time.Minute)) + user.Status.KubeConfigRefreshAt = &past + if got := nextKubeConfigRequeueDuration(user, nil); got != time.Second { + t.Fatalf("overdue refresh requeue duration = %v, want 1s", got) + } +} diff --git a/controllers/user/controllers/user_status_test.go b/controllers/user/controllers/user_status_test.go new file mode 100644 index 000000000000..9d2ba2badbcf --- /dev/null +++ b/controllers/user/controllers/user_status_test.go @@ -0,0 +1,1082 @@ +// Copyright 2026 labring. +// +// 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 controllers + +import ( + "context" + "errors" + "testing" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers/helper" + "github.com/labring/sealos/controllers/user/controllers/helper/config" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +type namespaceCreateErrorClient struct { + client.Client +} + +func (c namespaceCreateErrorClient) Create( + ctx context.Context, + obj client.Object, + opts ...client.CreateOption, +) error { + if _, ok := obj.(*corev1.Namespace); ok { + return errors.New("namespace create failed") + } + return c.Client.Create(ctx, obj, opts...) +} + +type roleSyncErrorClient struct { + client.Client +} + +func (c roleSyncErrorClient) Create( + ctx context.Context, + obj client.Object, + opts ...client.CreateOption, +) error { + if _, ok := obj.(*rbacv1.Role); ok { + return errors.New("role create failed") + } + return c.Client.Create(ctx, obj, opts...) +} + +func (c roleSyncErrorClient) Update( + ctx context.Context, + obj client.Object, + opts ...client.UpdateOption, +) error { + if _, ok := obj.(*rbacv1.Role); ok { + return errors.New("role update failed") + } + return c.Client.Update(ctx, obj, opts...) +} + +type roleBindingSyncErrorClient struct { + client.Client +} + +func (c roleBindingSyncErrorClient) Create( + ctx context.Context, + obj client.Object, + opts ...client.CreateOption, +) error { + if _, ok := obj.(*rbacv1.RoleBinding); ok { + return errors.New("role binding create failed") + } + return c.Client.Create(ctx, obj, opts...) +} + +func TestUpdateStatusPreservesUncachedKubeConfig(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + stored := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "user-a"}, + Status: userv1.UserStatus{ + Phase: userv1.UserActive, + KubeConfig: "existing-kubeconfig", + }, + } + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&userv1.User{}). + WithObjects(stored). + Build() + + projected := &userv1.User{} + if err := cli.Get( + context.Background(), + client.ObjectKeyFromObject(stored), + projected, + ); err != nil { + t.Fatalf("get user: %v", err) + } + projected.Status.KubeConfig = "" + originalStatus := projected.Status.DeepCopy() + projected.Status.ObservedGeneration = 1 + + reconciler := &UserReconciler{Client: cli} + if err := reconciler.updateStatus(context.Background(), projected, originalStatus); err != nil { + t.Fatalf("patch status: %v", err) + } + + got := &userv1.User{} + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), got); err != nil { + t.Fatalf("get updated user: %v", err) + } + if got.Status.KubeConfig != stored.Status.KubeConfig { + t.Fatalf("kubeconfig = %q, want %q", got.Status.KubeConfig, stored.Status.KubeConfig) + } + if got.Status.ObservedGeneration != 1 { + t.Fatalf("observed generation = %d, want 1", got.Status.ObservedGeneration) + } +} + +func TestOwnerAnnotationChangedPredicate(t *testing.T) { + t.Parallel() + + oldUser := &userv1.User{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "owner-a", + userv1.UserAnnotationDisplayKey: "old-display", + }, + }} + newUser := oldUser.DeepCopy() + newUser.Annotations[userv1.UserAnnotationDisplayKey] = "new-display" + predicate := OwnerAnnotationChangedPredicate{} + if predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) { + t.Fatal("unrelated annotation change triggered reconciliation") + } + + newUser.Annotations[userv1.UserAnnotationOwnerKey] = "owner-b" + if !predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) { + t.Fatal("owner annotation change did not trigger reconciliation") + } +} + +func TestDeletionTimestampChangedPredicate(t *testing.T) { + t.Parallel() + + oldUser := &userv1.User{} + newUser := oldUser.DeepCopy() + predicate := DeletionTimestampChangedPredicate{} + if predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) { + t.Fatal("unchanged deletion timestamp triggered reconciliation") + } + + now := metav1.Now() + newUser.DeletionTimestamp = &now + if !predicate.Update(event.UpdateEvent{ObjectOld: oldUser, ObjectNew: newUser}) { + t.Fatal("deletion timestamp change did not trigger reconciliation") + } +} + +func TestNamespacePodSecurityPredicate(t *testing.T) { + t.Parallel() + + oldNamespace := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{ + Name: "ns-user-a", + Labels: map[string]string{ + config.PodSecurityLabelPrefix + "enforce": "baseline", + }, + }} + newNamespace := oldNamespace.DeepCopy() + delete(newNamespace.Labels, config.PodSecurityLabelPrefix+"enforce") + p := NamespacePodSecurityPredicate{} + if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("Pod Security label deletion did not trigger reconciliation") + } + + newNamespace = oldNamespace.DeepCopy() + newNamespace.Labels["unused.example/label"] = "changed" + if p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("unrelated label change triggered reconciliation") + } + + newNamespace = oldNamespace.DeepCopy() + newNamespace.Annotations = map[string]string{userv1.UserAnnotationOwnerKey: "owner-b"} + if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("owner annotation change did not trigger reconciliation") + } + + newNamespace = oldNamespace.DeepCopy() + newNamespace.Labels[userv1.UserLabelOwnerKey] = "owner-b" + if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("owner label change did not trigger reconciliation") + } + + newNamespace = oldNamespace.DeepCopy() + newNamespace.Annotations = map[string]string{userv1.UserAnnotationCreatorKey: "user-a"} + if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("creator annotation change did not trigger reconciliation") + } + + newNamespace = oldNamespace.DeepCopy() + newNamespace.OwnerReferences = []metav1.OwnerReference{{Name: "user-a"}} + if !p.Update(event.UpdateEvent{ObjectOld: oldNamespace, ObjectNew: newNamespace}) { + t.Fatal("owner reference change did not trigger reconciliation") + } + + if !p.Create(event.CreateEvent{Object: oldNamespace}) { + t.Fatal("ns-* namespace creation did not trigger reconciliation") + } + if p.Create(event.CreateEvent{ + Object: &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}, + }, + }) { + t.Fatal("unmanaged namespace creation triggered reconciliation") + } + if !p.Delete(event.DeleteEvent{Object: oldNamespace}) { + t.Fatal("managed namespace deletion did not trigger reconciliation") + } + if p.Delete(event.DeleteEvent{ + Object: &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}, + }, + }) { + t.Fatal("unmanaged namespace deletion triggered reconciliation") + } +} + +func TestKubeConfigSyncDueUsesPersistedRefreshAt(t *testing.T) { + t.Parallel() + r := &UserReconciler{} + future := metav1.NewTime(time.Now().Add(time.Hour)) + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}, Status: userv1.UserStatus{ + KubeConfigRefreshAt: &future, + }} + if r.kubeConfigSyncDue(user) { + t.Fatal("future persisted refresh time was considered due") + } + past := metav1.NewTime(time.Now().Add(-time.Second)) + user.Status.KubeConfigRefreshAt = &past + if !r.kubeConfigSyncDue(user) { + t.Fatal("past persisted refresh time was not considered due") + } + user.Status.KubeConfigRefreshAt = nil + if !r.kubeConfigSyncDue(user) { + t.Fatal("missing persisted refresh time was not considered due") + } +} + +func TestCSRExpirationStatusMatchingUsesMinimumForLowValues(t *testing.T) { + t.Parallel() + minimum := userv1.DefaultCSRExpirationSeconds + if !csrExpirationStatusMatches(7_200, 0) { + t.Fatal("low spec and empty observed expiration should use the minimum") + } + if !csrExpirationStatusMatches(7_200, minimum) { + t.Fatal("low spec and minimum observed expiration should match") + } + if csrExpirationStatusMatches(7_200, minimum+1) { + t.Fatal("low spec should not match a different above-minimum observed expiration") + } + if csrExpirationStatusMatches(minimum+1, 7_200) { + t.Fatal("spec above minimum should detect a different effective expiration") + } + if !csrExpirationStatusMatches(minimum+1, minimum+1) { + t.Fatal("equal above-minimum expirations should match") + } +} + +func TestUserStatusNeedsSyncIgnoresBelowMinimumExpirationDrift(t *testing.T) { + t.Parallel() + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "alice", Generation: 1}, + Spec: userv1.UserSpec{CSRExpirationSeconds: 7_200}, + Status: userv1.UserStatus{ + Phase: userv1.UserActive, + ObservedGeneration: 1, + ObservedCSRExpirationSeconds: 7_200, + Conditions: []userv1.Condition{ + {Type: userv1.Initialized, Status: corev1.ConditionTrue}, + {Type: userv1.Ready, Status: corev1.ConditionTrue}, + }, + }, + } + if userStatusNeedsSync(user) { + t.Fatal("below-minimum expiration drift triggered a status sync") + } + if user.Status.ObservedCSRExpirationSeconds != 7_200 { + t.Fatalf( + "observed expiration was normalized outside a kubeconfig refresh: %d", + user.Status.ObservedCSRExpirationSeconds, + ) + } +} + +func TestSetObservedCSRExpirationSecondsNormalizesOnlyDuringRefresh(t *testing.T) { + t.Parallel() + user := &userv1.User{ + Spec: userv1.UserSpec{CSRExpirationSeconds: 7_200}, + Status: userv1.UserStatus{ + ObservedCSRExpirationSeconds: 7_200, + }, + } + setObservedCSRExpirationSeconds(user) + if user.Status.ObservedCSRExpirationSeconds != userv1.DefaultCSRExpirationSeconds { + t.Fatalf( + "observed expiration = %d, want minimum %d", + user.Status.ObservedCSRExpirationSeconds, + userv1.DefaultCSRExpirationSeconds, + ) + } + + user.Spec.CSRExpirationSeconds = userv1.DefaultCSRExpirationSeconds + 1 + setObservedCSRExpirationSeconds(user) + want := userv1.DefaultCSRExpirationSeconds + 1 + if user.Status.ObservedCSRExpirationSeconds != want { + t.Fatalf( + "observed expiration = %d, want %d", + user.Status.ObservedCSRExpirationSeconds, + want, + ) + } +} + +func TestFailedKubeConfigSyncRetriesImmediately(t *testing.T) { + t.Parallel() + r := &UserReconciler{} + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}} + state := &userReconcileState{kubeConfigSyncAttempted: true} + state.recordSyncError(errors.New("token request failed")) + if err := r.finishKubeConfigSync(user, state); err == nil { + t.Fatal("failed kubeconfig sync did not return its error") + } + value, ok := r.nextKubeConfigSync.Load("alice") + deadline, deadlineOK := value.(time.Time) + if !ok || !deadlineOK || deadline.After(time.Now()) { + t.Fatalf("failed kubeconfig sync deadline = %v, want due now", value) + } +} + +func TestSuccessfulKubeConfigSyncUsesPersistedRefreshDeadline(t *testing.T) { + t.Parallel() + r := &UserReconciler{} + refreshAt := metav1.NewTime(time.Now().Add(30 * 24 * time.Hour)) + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "alice"}, + Status: userv1.UserStatus{ + KubeConfigRefreshAt: &refreshAt, + }, + } + state := &userReconcileState{ + kubeConfigSyncAttempted: true, + kubeConfigSynced: true, + } + + if err := r.finishKubeConfigSync(user, state); err != nil { + t.Fatalf("successful kubeconfig sync returned an error: %v", err) + } + value, ok := r.nextKubeConfigSync.Load(user.Name) + deadline, deadlineOK := value.(time.Time) + if !ok || !deadlineOK { + t.Fatalf("kubeconfig deadline = %v, want persisted refresh time", value) + } + if !deadline.Equal(refreshAt.Time) { + t.Fatalf("kubeconfig deadline = %s, want %s", deadline, refreshAt.Time) + } +} + +func TestKubeConfigSyncAddsFailureCondition(t *testing.T) { + t.Parallel() + refreshAt := metav1.NewTime(time.Now().Add(time.Hour)) + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: "alice"}} + user.Status.KubeConfig = "existing-kubeconfig" + user.Status.KubeConfigRefreshAt = &refreshAt + user.Status.ObservedCSRExpirationSeconds = userv1.DefaultCSRExpirationSeconds + r := &UserReconciler{Recorder: record.NewFakeRecorder(1)} + state := &userReconcileState{} + r.syncKubeConfig(context.Background(), user, state) + if state.syncError == nil { + t.Fatal("kubeconfig sync failure was not recorded") + } + condition := helper.GetCondition( + user.Status.Conditions, + &userv1.Condition{Type: kubeConfigReadyCondition}, + ) + if condition.Status != corev1.ConditionFalse { + t.Fatalf("kubeconfig condition status = %s, want False", condition.Status) + } + if condition.Reason != "SyncUserError" { + t.Fatalf("kubeconfig condition reason = %q, want SyncUserError", condition.Reason) + } + if user.Status.KubeConfig != "existing-kubeconfig" || user.Status.KubeConfigRefreshAt == nil || + !user.Status.KubeConfigRefreshAt.Equal(&refreshAt) || + user.Status.ObservedCSRExpirationSeconds != userv1.DefaultCSRExpirationSeconds { + t.Fatalf("kubeconfig failure cleared existing status: %#v", user.Status) + } +} + +func TestUserResourcesNeedSyncFromReader(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add rbac scheme: %v", err) + } + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + controller := true + user := &userv1.User{ObjectMeta: metav1.ObjectMeta{ + Name: "alice", + UID: "user-a", + Annotations: map[string]string{userv1.UserAnnotationOwnerKey: "owner-a"}, + }} + ownerRef := metav1.OwnerReference{ + APIVersion: userv1.GroupVersion.String(), + Kind: "User", + Name: user.Name, + UID: user.UID, + Controller: &controller, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.GetUsersNamespace(user.Name), + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner-a", + }, + Labels: config.SetPodSecurity( + map[string]string{userv1.UserLabelOwnerKey: "owner-a"}, + ), + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + } + sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + Name: user.Name, + Namespace: config.GetUserSystemNamespace(), + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner-a", + }, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, Secrets: []corev1.ObjectReference{{Name: "token-alice"}}} + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Name: "token-alice", Namespace: config.GetUserSystemNamespace(), + Annotations: map[string]string{corev1.ServiceAccountNameKey: user.Name}, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }} + objects := make([]client.Object, 0, 8) + objects = append(objects, user, ns, sa, secret) + for _, roleType := range []userv1.RoleType{userv1.OwnerRoleType, userv1.ManagerRoleType, userv1.DeveloperRoleType} { + objects = append(objects, &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{ + Name: string(roleType), + Namespace: ns.Name, + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner-a", + }, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, Rules: config.GetUserRole(roleType)}) + } + objects = append(objects, &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: user.Name, + Namespace: ns.Name, + Annotations: map[string]string{ + userv1.UserAnnotationCreatorKey: user.Name, + userv1.UserAnnotationOwnerKey: "owner-a", + }, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: string(userv1.OwnerRoleType), + }, + Subjects: config.GetUsersSubject(user.Name), + }) + reader := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + withFinalizer := user.DeepCopy() + withFinalizer.Finalizers = []string{userFinalizerName} + withFinalizer.Status = userv1.UserStatus{ + Phase: userv1.UserActive, + ObservedGeneration: withFinalizer.Generation, + Conditions: []userv1.Condition{ + {Type: userv1.Initialized, Status: corev1.ConditionTrue}, + {Type: userv1.Ready, Status: corev1.ConditionTrue}, + }, + } + if !controllerutil.ContainsFinalizer(withFinalizer, userFinalizerName) { + t.Fatal("test user finalizer was not configured") + } + namespace := &corev1.Namespace{} + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: ns.Name}, + namespace, + ); err != nil { + t.Fatalf("get namespace: %v", err) + } + partialNamespace := &metav1.PartialObjectMetadata{ObjectMeta: namespace.ObjectMeta} + if !namespaceMatchesUser(partialNamespace, user, false) { + t.Fatal("healthy namespace did not match user") + } + serviceAccount := &corev1.ServiceAccount{} + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: user.Name, Namespace: config.GetUserSystemNamespace()}, + serviceAccount, + ); err != nil { + t.Fatalf("get service account: %v", err) + } + if !metadataMatchesUserResource(serviceAccount, user) { + t.Fatal("healthy service account did not match user") + } + if !controlledByUser(secret, user) || + secret.Annotations[corev1.ServiceAccountNameKey] != user.Name { + t.Fatal("healthy bound secret did not match user") + } + for _, roleType := range []userv1.RoleType{userv1.OwnerRoleType, userv1.ManagerRoleType, userv1.DeveloperRoleType} { + if !roleMatchesUser( + context.Background(), + reader, + client.ObjectKey{Name: string(roleType), Namespace: ns.Name}, + roleType, + user, + ) { + t.Fatalf("healthy %s role did not match user", roleType) + } + } + roleBinding := &rbacv1.RoleBinding{} + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: user.Name, Namespace: ns.Name}, + roleBinding, + ); err != nil { + t.Fatalf("get role binding: %v", err) + } + if !roleBindingMatchesUser(roleBinding, user) { + t.Fatal("healthy role binding did not match user") + } + role := &rbacv1.Role{} + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: string(userv1.DeveloperRoleType), Namespace: ns.Name}, + role, + ); err != nil { + t.Fatalf("get developer role: %v", err) + } + role.Rules = []rbacv1.PolicyRule{ + {APIGroups: []string{"*"}, Resources: []string{"pods"}, Verbs: []string{"get"}}, + } + if err := reader.Update(context.Background(), role); err != nil { + t.Fatalf("update developer role: %v", err) + } + if roleMatchesUser( + context.Background(), + reader, + client.ObjectKey{Name: string(userv1.DeveloperRoleType), Namespace: ns.Name}, + userv1.DeveloperRoleType, + user, + ) { + t.Fatal("developer role rule drift was not detected") + } + role.Rules = config.GetUserRole(userv1.DeveloperRoleType) + if err := reader.Update(context.Background(), role); err != nil { + t.Fatalf("restore developer role: %v", err) + } + + storedNamespace := &corev1.Namespace{} + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: ns.Name}, + storedNamespace, + ); err != nil { + t.Fatalf("get namespace: %v", err) + } + storedNamespace.Annotations[userv1.UserAnnotationOwnerKey] = "owner-b" + if err := reader.Update(context.Background(), storedNamespace); err != nil { + t.Fatalf("update namespace: %v", err) + } + partialNamespace.Annotations = storedNamespace.Annotations + if namespaceMatchesUser(partialNamespace, user, false) { + t.Fatal("namespace metadata drift was not detected") + } + storedNamespace.Annotations[userv1.UserAnnotationOwnerKey] = "owner-a" + if err := reader.Update(context.Background(), storedNamespace); err != nil { + t.Fatalf("restore namespace: %v", err) + } + + if err := reader.Delete(context.Background(), secret); err != nil { + t.Fatalf("delete bound secret: %v", err) + } + if err := reader.Get( + context.Background(), + client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, + &corev1.Secret{}, + ); err == nil { + t.Fatal("bound secret drift was not applied") + } +} + +func TestRoleSyncFailureUpdatesRoleCondition(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "alice", UID: "user-a"}, + } + baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(user).Build() + r := &UserReconciler{ + Client: roleSyncErrorClient{Client: baseClient}, + Scheme: scheme, + Recorder: record.NewFakeRecorder(10), + } + state := &userReconcileState{} + r.syncRolesIfNeeded(context.Background(), user, state) + if state.syncError == nil { + t.Fatal("role sync failure was not recorded") + } + condition := helper.GetCondition( + user.Status.Conditions, + &userv1.Condition{Type: roleSyncReadyCondition}, + ) + if condition.Status != corev1.ConditionFalse { + t.Fatalf("role condition status = %s, want False", condition.Status) + } + if condition.Reason != "SyncUserError" { + t.Fatalf("role condition reason = %q, want SyncUserError", condition.Reason) + } +} + +func TestRoleBindingSyncFailureRecordsRetryableError(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + user := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{Name: "alice", UID: "user-a"}, + } + baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(user).Build() + r := &UserReconciler{ + Client: roleBindingSyncErrorClient{Client: baseClient}, + Scheme: scheme, + Recorder: record.NewFakeRecorder(10), + } + state := &userReconcileState{} + r.syncRoleBindingIfNeeded(context.Background(), user, state) + if state.syncError == nil { + t.Fatal("role binding sync failure was not recorded") + } + condition := helper.GetCondition( + user.Status.Conditions, + &userv1.Condition{Type: roleBindingReadyCondition}, + ) + if condition.Status != corev1.ConditionFalse { + t.Fatalf("role binding condition status = %s, want False", condition.Status) + } +} + +func TestNamespaceToUserRequests(t *testing.T) { + t.Parallel() + r := &UserReconciler{} + requests := r.namespaceToUserRequests(context.Background(), &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "ns-admin"}, + }) + if len(requests) != 1 || requests[0].Name != "admin" { + t.Fatalf("admin namespace request = %#v", requests) + } + if requests := r.namespaceToUserRequests(context.Background(), &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "kube-system"}, + }); requests != nil { + t.Fatalf("unmanaged namespace requests = %#v", requests) + } +} + +func TestReconcileStrictNamespacePodSecurity(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "ns-external", + Labels: map[string]string{"example.com/keep": "value"}, + }} + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build() + r := &UserReconciler{Client: cli, EnableStrictNamespacePodSecurity: true} + if _, err := r.Reconcile( + context.Background(), + ctrl.Request{NamespacedName: client.ObjectKey{Name: "external"}}, + ); err != nil { + t.Fatalf("reconcile orphan namespace: %v", err) + } + got := &corev1.Namespace{} + if err := cli.Get(context.Background(), client.ObjectKey{Name: ns.Name}, got); err != nil { + t.Fatalf("get orphan namespace: %v", err) + } + if got.Labels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" || + got.Labels["example.com/keep"] != "value" { + t.Fatalf("orphan namespace labels = %#v", got.Labels) + } +} + +func TestReconcileDisabledStrictNamespacePodSecurityPreservesOrphanLabels(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "ns-external", + Labels: map[string]string{config.PodSecurityLabelPrefix + "enforce": "privileged"}, + }} + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ns).Build() + r := &UserReconciler{Client: cli} + if _, err := r.Reconcile( + context.Background(), + ctrl.Request{NamespacedName: client.ObjectKey{Name: "external"}}, + ); err != nil { + t.Fatalf("reconcile orphan namespace with strict mode disabled: %v", err) + } + got := &corev1.Namespace{} + if err := cli.Get(context.Background(), client.ObjectKey{Name: ns.Name}, got); err != nil { + t.Fatalf("get orphan namespace: %v", err) + } + if got.Labels[config.PodSecurityLabelPrefix+"enforce"] != "privileged" { + t.Fatalf("orphan namespace labels changed: %#v", got.Labels) + } +} + +func TestAdminClusterRoleBindingPredicate(t *testing.T) { + t.Parallel() + p := AdminClusterRoleBindingPredicate{} + target := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName}, + } + other := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "other-binding"}} + if !p.Create(event.CreateEvent{Object: target}) || + !p.Update(event.UpdateEvent{ObjectNew: target}) || + !p.Delete(event.DeleteEvent{Object: target}) { + t.Fatal("admin cluster role binding events were not accepted") + } + if p.Create(event.CreateEvent{Object: other}) || + p.Update(event.UpdateEvent{ObjectNew: other}) || + p.Delete(event.DeleteEvent{Object: other}) { + t.Fatal("unrelated cluster role binding event was accepted") + } +} + +func TestDesiredNamespaceLabels(t *testing.T) { + t.Parallel() + labels := map[string]string{ + config.PodSecurityLabelPrefix + "enforce": "baseline", + "example.com/keep": "value", + } + adminLabels := desiredNamespaceLabels("ns-admin", labels, false) + if adminLabels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" { + t.Fatal( + "admin namespace did not receive Pod Security labels when admin privilege is disabled", + ) + } + privilegedLabels := desiredNamespaceLabels("ns-admin", adminLabels, true) + if _, ok := privilegedLabels[config.PodSecurityLabelPrefix+"enforce"]; ok { + t.Fatal("admin Pod Security labels were retained when admin privilege is enabled") + } + if privilegedLabels["example.com/keep"] != "value" { + t.Fatal("unrelated namespace label was removed") + } +} + +func TestAdminClusterRoleBindingCleanup(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + binding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName}, + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(binding).Build() + cleanup := &adminPrivilegeMigration{client: cli} + if err := cleanup.Start(context.Background()); err != nil { + t.Fatalf("cleanup binding: %v", err) + } + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: adminClusterRoleBindingName}, + &rbacv1.ClusterRoleBinding{}, + ); !apierrors.IsNotFound( + err, + ) { + t.Fatalf("binding still exists, get error = %v", err) + } +} + +func TestReconcileMissingAdminCleansDisabledClusterRoleBinding(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + enableAdmin bool + bindingExists bool + wantBinding bool + }{ + { + name: "disabled admin privilege removes binding", + bindingExists: true, + }, + { + name: "disabled admin privilege tolerates missing binding", + }, + { + name: "enabled admin privilege preserves binding", + enableAdmin: true, + bindingExists: true, + wantBinding: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + + var objects []client.Object + if tt.bindingExists { + objects = append(objects, &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName}, + }) + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + r := &UserReconciler{ + Client: cli, + EnableAdminClusterAdmin: tt.enableAdmin, + } + + if _, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: client.ObjectKey{Name: adminUserName}, + }); err != nil { + t.Fatalf("reconcile missing admin: %v", err) + } + + err := cli.Get( + context.Background(), + client.ObjectKey{Name: adminClusterRoleBindingName}, + &rbacv1.ClusterRoleBinding{}, + ) + if tt.wantBinding { + if err != nil { + t.Fatalf("get preserved binding: %v", err) + } + return + } + if !apierrors.IsNotFound(err) { + t.Fatalf("binding was not removed, get error = %v", err) + } + }) + } +} + +func TestAdminPrivilegeMigrationDisablesLegacyBinding(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}}, + &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: adminClusterRoleBindingName}, + }, + ). + Build() + r := &UserReconciler{Client: cli, Scheme: scheme} + migration := &adminPrivilegeMigration{ + client: cli, + reader: cli, + reconciler: r, + } + if err := migration.Start(context.Background()); err != nil { + t.Fatalf("disable admin privilege: %v", err) + } + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: adminClusterRoleBindingName}, + &rbacv1.ClusterRoleBinding{}, + ); !apierrors.IsNotFound( + err, + ) { + t.Fatalf("legacy binding still exists, get error = %v", err) + } + ns := &corev1.Namespace{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: config.GetUsersNamespace(adminUserName)}, + ns, + ); err != nil { + t.Fatalf("get admin namespace: %v", err) + } + if ns.Labels[config.PodSecurityLabelPrefix+"enforce"] != "baseline" { + t.Fatalf("admin namespace labels = %#v", ns.Labels) + } +} + +func TestAdminPrivilegeMigrationEnablesLegacyBinding(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}}, + ).Build() + r := &UserReconciler{Client: cli, Scheme: scheme, EnableAdminClusterAdmin: true} + migration := &adminPrivilegeMigration{ + client: cli, + reader: cli, + reconciler: r, + enableAdminClusterAdmin: true, + } + if err := migration.Start(context.Background()); err != nil { + t.Fatalf("enable admin privilege: %v", err) + } + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: adminClusterRoleBindingName}, + &rbacv1.ClusterRoleBinding{}, + ); err != nil { + t.Fatalf("get restored binding: %v", err) + } + ns := &corev1.Namespace{} + if err := cli.Get( + context.Background(), + client.ObjectKey{Name: config.GetUsersNamespace(adminUserName)}, + ns, + ); err != nil { + t.Fatalf("get admin namespace: %v", err) + } + if _, ok := ns.Labels[config.PodSecurityLabelPrefix+"enforce"]; ok { + t.Fatalf("admin namespace retained Pod Security labels: %#v", ns.Labels) + } +} + +func TestAdminPrivilegeMigrationPropagatesSyncError(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + t.Fatalf("add RBAC scheme: %v", err) + } + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + baseClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &userv1.User{ObjectMeta: metav1.ObjectMeta{Name: adminUserName}}, + ).Build() + failingClient := namespaceCreateErrorClient{Client: baseClient} + r := &UserReconciler{ + Client: failingClient, + Scheme: scheme, + Recorder: record.NewFakeRecorder(10), + } + migration := &adminPrivilegeMigration{ + client: failingClient, + reader: baseClient, + reconciler: r, + enableAdminClusterAdmin: true, + } + if err := migration.Start(context.Background()); err == nil { + t.Fatal("admin privilege migration reported success after namespace sync failed") + } +} + +func TestPatchUserOwnerPreservesUncachedAnnotations(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := userv1.AddToScheme(scheme); err != nil { + t.Fatalf("add user scheme: %v", err) + } + stored := &userv1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-a", + Annotations: map[string]string{ + userv1.UserAnnotationOwnerKey: "old-owner", + userv1.UserAnnotationDisplayKey: "display-name", + }, + }, + } + cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build() + + projected := &userv1.User{} + if err := cli.Get( + context.Background(), + client.ObjectKeyFromObject(stored), + projected, + ); err != nil { + t.Fatalf("get user: %v", err) + } + projected.Annotations = map[string]string{ + userv1.UserAnnotationOwnerKey: projected.Annotations[userv1.UserAnnotationOwnerKey], + } + + reconciler := &OperationReqReconciler{Client: cli} + if err := reconciler.patchUserOwner(context.Background(), projected, "new-owner"); err != nil { + t.Fatalf("patch user owner: %v", err) + } + + got := &userv1.User{} + if err := cli.Get(context.Background(), client.ObjectKeyFromObject(stored), got); err != nil { + t.Fatalf("get updated user: %v", err) + } + if got.Annotations[userv1.UserAnnotationOwnerKey] != "new-owner" { + t.Fatalf("owner = %q, want new-owner", got.Annotations[userv1.UserAnnotationOwnerKey]) + } + if got.Annotations[userv1.UserAnnotationDisplayKey] != "display-name" { + t.Fatalf( + "display annotation = %q, want display-name", + got.Annotations[userv1.UserAnnotationDisplayKey], + ) + } +} diff --git a/controllers/user/deploy/Kubefile b/controllers/user/deploy/Kubefile new file mode 100644 index 000000000000..e4e4c2d632f3 --- /dev/null +++ b/controllers/user/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY drop drop +COPY user-controller-entrypoint.sh user-controller-entrypoint.sh +CMD ["bash user-controller-entrypoint.sh"] diff --git a/controllers/user/deploy/README.md b/controllers/user/deploy/README.md new file mode 100644 index 000000000000..ad95de6c5a97 --- /dev/null +++ b/controllers/user/deploy/README.md @@ -0,0 +1,16 @@ +### How to build image + +```shell +sealos build -t docker.io/labring/sealos-user-controller:latest -f Kubefile . +``` + +### How to run + +```shell +# 可选:使用 HELM_OPTS 传递 Helm 参数,例如覆盖云 API Server 域名/端口。 +# adminClusterAdmin.enabled 默认关闭;只有兼容旧部署时才建议显式开启。 +# strictNamespacePodSecurity.enabled 默认开启,用于给所有 ns-* Namespace 添加 PSA labels。 +# export HELM_OPTS="--set cloudAPIServerDomain=my.domain --set cloudAPIServerPort=6443 --set kubeAPI.qps=50 --set kubeAPI.burst=100 --set adminClusterAdmin.enabled=true" + +sealos run docker.io/labring/sealos-user-controller:latest +``` diff --git a/controllers/user/deploy/charts/user-controller/.helmignore b/controllers/user/deploy/charts/user-controller/.helmignore new file mode 100644 index 000000000000..5206f4e91245 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/controllers/user/deploy/charts/user-controller/Chart.yaml b/controllers/user/deploy/charts/user-controller/Chart.yaml new file mode 100644 index 000000000000..d65e133a54bd --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: user +description: Helm chart for the sealos user controller +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml new file mode 100644 index 000000000000..c1ba1c5112e6 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_deleterequests.yaml @@ -0,0 +1,69 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: deleterequests.user.sealos.io +spec: + group: user.sealos.io + names: + kind: DeleteRequest + listKind: DeleteRequestList + plural: deleterequests + singular: deleterequest + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.user + name: User + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: DeleteRequest is the Schema for the deleterequests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DeleteRequestSpec defines the desired state of DeleteRequest + properties: + user: + type: string + type: object + status: + description: DeleteRequestStatus defines the observed state of DeleteRequest + properties: + phase: + enum: + - Pending + - Processing + - Completed + - Failed + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml new file mode 100644 index 000000000000..6b98fd95054a --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_operationrequests.yaml @@ -0,0 +1,95 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: operationrequests.user.sealos.io +spec: + group: user.sealos.io + names: + kind: Operationrequest + listKind: OperationrequestList + plural: operationrequests + singular: operationrequest + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.action + name: Action + type: string + - jsonPath: .spec.namespace + name: Namespace + type: string + - jsonPath: .spec.user + name: User + type: string + - jsonPath: .spec.role + name: Role + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: Operationrequest is the Schema for the operation requests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OperationrequestSpec defines the desired state of Operationrequest + properties: + action: + enum: + - Grant + - Update + - Deprive + type: string + namespace: + description: Namespace is the workspace that needs to be operated. + type: string + role: + enum: + - Owner + - Manager + - Developer + type: string + user: + type: string + type: object + status: + description: OperationrequestStatus defines the observed state of Operationrequest + properties: + phase: + default: Pending + description: Phase is the recently observed lifecycle phase of operationrequest. + enum: + - Pending + - Processing + - Completed + - Failed + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml new file mode 100644 index 000000000000..2913f4f3150d --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/crds/user.sealos.io_users.yaml @@ -0,0 +1,142 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: users.user.sealos.io +spec: + group: user.sealos.io + names: + kind: User + listKind: UserList + plural: users + singular: user + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.observedCSRExpirationSeconds + name: ExpirationSeconds + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: User is the Schema for the users API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: UserSpec defines the desired state of User + properties: + csrExpirationSeconds: + default: 1000000000 + description: |- + expirationSeconds is the requested duration of validity of the issued + kubeconfig credential. The issuer may issue a credential with a different + validity duration so a client must check the issued credential to determine + the actual duration. + + + The minimum effective value for expirationSeconds is 1_000_000_000 seconds. + format: int32 + type: integer + kubeConfigRotateAt: + description: kubeConfigRotateAt is a manual trigger for kubeconfig + rotation. When set/updated, controller will request a new token + and recreate kubeconfig. + format: date-time + type: string + type: object + status: + description: UserStatus defines the observed state of User + properties: + conditions: + description: Conditions contains the different condition statuses + for this user. + items: + properties: + lastHeartbeatTime: + description: LastHeartbeatTime is the last time this condition + was updated. + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time the condition + changed from one status to another. + format: date-time + type: string + message: + description: Message is a human-readable message indicating + details about the last status change. + type: string + reason: + description: Reason is a (brief) reason for the condition's + last status change. + type: string + status: + description: Status is the status of the condition. One of True, + False, Unknown. + type: string + type: + type: string + required: + - status + - type + type: object + type: array + kubeConfig: + type: string + observedCSRExpirationSeconds: + default: 1000000000 + format: int32 + type: integer + observedKubeConfigRotateAt: + description: ObservedKubeConfigRotateAt records the last processed + rotation request. + format: date-time + type: string + kubeConfigRefreshAt: + description: KubeConfigRefreshAt is the next time the controller + should refresh the kubeconfig token. + format: date-time + type: string + observedKubeConfigSecretUID: + description: ObservedKubeConfigSecretUID identifies the Secret bound + to the current kubeconfig token. + type: string + observedGeneration: + description: The generation observed by the user controller. + format: int64 + type: integer + phase: + default: Unknown + description: Phase is the recently observed lifecycle phase of user + type: string + required: + - kubeConfig + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl b/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl new file mode 100644 index 000000000000..d51131e97224 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "user.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "user.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "user.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "user.labels" -}} +helm.sh/chart: {{ include "user.chart" . }} +{{ include "user.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "user.selectorLabels" -}} +app.kubernetes.io/name: {{ include "user.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "user.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "user.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/controllers/user/deploy/charts/user-controller/templates/cert.yaml b/controllers/user/deploy/charts/user-controller/templates/cert.yaml new file mode 100644 index 000000000000..a8334bd3d42b --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/cert.yaml @@ -0,0 +1,53 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "user.fullname" . }}-selfsigned-issuer + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "user.fullname" . }}-serving-cert + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ include "user.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc + - {{ include "user.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ include "user.fullname" . }}-selfsigned-issuer + secretName: webhook-server-cert +--- +{{- if .Values.metrics.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + {{- include "user.labels" . | nindent 4 }} + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + {{- include "user.labels" . | nindent 4 }} + name: metrics-certs +spec: + privateKey: + rotationPolicy: Always + dnsNames: + - {{ include "user.fullname" . }}-metrics.{{.Release.Namespace}}.svc + - {{ include "user.fullname" . }}-metrics.{{.Release.Namespace}}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: {{.Values.metrics.secretName}} +{{- end }} \ No newline at end of file diff --git a/controllers/user/deploy/charts/user-controller/templates/deployment.yaml b/controllers/user/deploy/charts/user-controller/templates/deployment.yaml new file mode 100644 index 000000000000..2b48b9c12cdf --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/deployment.yaml @@ -0,0 +1,131 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "user.fullname" . }}-controller-manager + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + control-plane: controller-manager + {{- include "user.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "user.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: manager + command: + - /manager + args: + - --health-probe-bind-address=:8081 + - --leader-elect + - --kube-api-qps={{ .Values.kubeAPI.qps }} + - --kube-api-burst={{ .Values.kubeAPI.burst }} + - --enable-admin-cluster-admin={{ .Values.adminClusterAdmin.enabled }} + - --enable-strict-namespace-pod-security={{ .Values.strictNamespacePodSecurity.enabled }} + {{- if .Values.metrics.enabled }} + - --metrics-secure=true + - --metrics-bind-address=:8443 + {{- end }} + env: + - name: NAMESPACE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: SEALOS_CLOUD_APISERVER_HOST + value: "{{.Values.cloudAPIServerDomain}}" + - name: SEALOS_CLOUD_APISERVER_PORT + value: "{{.Values.cloudAPIServerPort}}" + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + image: "{{ .Values.image }}" + imagePullPolicy: IfNotPresent + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics-certs + mountPath: {{.Values.metrics.certPath}} + readOnly: true + {{- end }} + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + terminationGracePeriodSeconds: 10 + affinity: + {{- if .Values.affinity }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: control-plane + operator: In + values: + - controller-manager + topologyKey: kubernetes.io/hostname + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if .Values.metrics.enabled }} + - name: metrics-certs + secret: + secretName: {{.Values.metrics.secretName}} + optional: false + items: + - key: ca.crt + path: ca.crt + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/controllers/user/deploy/charts/user-controller/templates/rbac.yaml b/controllers/user/deploy/charts/user-controller/templates/rbac.yaml new file mode 100644 index 000000000000..8382490aacf0 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/rbac.yaml @@ -0,0 +1,82 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "user.fullname" . }}-leader-election-role + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "user.fullname" . }}-manager-role +rules: + - apiGroups: + - "*" + resources: + - "*" + verbs: + - "*" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "user.fullname" . }}-leader-election-rolebinding + namespace: {{ .Release.Namespace }} + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "user.fullname" . }}-leader-election-role +subjects: + - kind: ServiceAccount + name: {{ include "user.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "user.fullname" . }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "user.fullname" . }}-manager-role +subjects: + - kind: ServiceAccount + name: {{ include "user.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/user/deploy/charts/user-controller/templates/service.yaml b/controllers/user/deploy/charts/user-controller/templates/service.yaml new file mode 100644 index 000000000000..73986aaf9189 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/service.yaml @@ -0,0 +1,34 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "user.fullname" . }}-controller-manager-metrics-service + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +spec: + ports: + - name: https + port: {{ .Values.service.metrics.port }} + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager + {{- include "user.selectorLabels" . | nindent 4 }} +{{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "user.fullname" . }}-webhook-service + labels: + control-plane: controller-manager + {{- include "user.labels" . | nindent 4 }} +spec: + ports: + - port: {{ .Values.service.webhook.port }} + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager + {{- include "user.selectorLabels" . | nindent 4 }} diff --git a/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml b/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml new file mode 100644 index 000000000000..e66d5ac343b1 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "user.serviceAccountName" . }} + labels: + {{- include "user.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/controllers/user/deploy/charts/user-controller/templates/webhook.yaml b/controllers/user/deploy/charts/user-controller/templates/webhook.yaml new file mode 100644 index 000000000000..42515b21fa25 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/templates/webhook.yaml @@ -0,0 +1,96 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ include "user.fullname" . }}-mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "user.fullname" . }}-serving-cert +webhooks: + - name: moperationrequest.kb.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "user.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None + - name: muser.kb.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "user.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-user-sealos-io-v1-user + failurePolicy: Fail + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ include "user.fullname" . }}-validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "user.fullname" . }}-serving-cert +webhooks: + - name: voperationrequest.kb.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "user.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None + - name: vuser.kb.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "user.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-user-sealos-io-v1-user + timeoutSeconds: 30 + failurePolicy: Fail + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None diff --git a/controllers/user/deploy/charts/user-controller/user-controller-values.yaml b/controllers/user/deploy/charts/user-controller/user-controller-values.yaml new file mode 100644 index 000000000000..d1c20f818aa4 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/user-controller-values.yaml @@ -0,0 +1,25 @@ +# Custom values for user controller helm chart. +# This file contains user-customizable configurations. + +replicaCount: 1 + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + +kubeAPI: + qps: 50 + burst: 100 + +# Preserve the legacy cluster-admin binding for the admin user. +# When disabled, the admin namespace uses Pod Security Admission labels and +# any sealos-cloudadmin ClusterRoleBinding is removed. +adminClusterAdmin: + enabled: false + +strictNamespacePodSecurity: + enabled: true diff --git a/controllers/user/deploy/charts/user-controller/values.yaml b/controllers/user/deploy/charts/user-controller/values.yaml new file mode 100644 index 000000000000..f179518fc324 --- /dev/null +++ b/controllers/user/deploy/charts/user-controller/values.yaml @@ -0,0 +1,80 @@ +# Default values for user controller helm chart. + +image: ghcr.io/labring/sealos-user-controller:latest + +imagePullSecrets: [] +fullnameOverride: "user" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "user-controller-manager" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + +securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + +service: + metrics: + port: 8443 + webhook: + port: 443 + +livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + +readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + +kubeAPI: + # Kubernetes API client-side rate limit used by the controller manager. + qps: 50 + burst: 100 + +# Preserve the legacy cluster-admin binding for the admin user. +# When disabled, the admin namespace uses Pod Security Admission labels and +# any sealos-cloudadmin ClusterRoleBinding is removed. +adminClusterAdmin: + enabled: false + +# Apply Pod Security labels to all ns-* namespaces, including orphan namespaces. +# Disabling this leaves existing labels unchanged. +strictNamespacePodSecurity: + enabled: true + +# Cloud API server configuration +# cloudAPIServerDomain is auto-configured by entrypoint.sh from sealos-config ConfigMap +cloudAPIServerDomain: 127.0.0.1.nip.io +cloudAPIServerPort: "6443" + +metrics: + # Enable metrics endpoint + enabled: false + # Path to the metrics certificate + certPath: /tmp/k8s-metrics-server/metrics-certs + secretName: metrics-server-cert + +nodeSelector: {} + +tolerations: [] + +# If no affinity is provided a default podAntiAffinity is used to spread controllers. +affinity: {} diff --git a/controllers/user/deploy/drop/deploy.yaml b/controllers/user/deploy/drop/deploy.yaml new file mode 100644 index 000000000000..c26bf3f19993 --- /dev/null +++ b/controllers/user/deploy/drop/deploy.yaml @@ -0,0 +1,470 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: user-controller-manager + namespace: user-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: user-leader-election-role + namespace: user-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-manager-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - deleterequests/finalizers + verbs: + - update +- apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get + - patch + - update +- apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - user.sealos.io + resources: + - operationrequests/finalizers + verbs: + - update +- apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: user-leader-election-rolebinding + namespace: user-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: user-leader-election-role +subjects: +- kind: ServiceAccount + name: user-controller-manager + namespace: user-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: user-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: user-manager-role +subjects: +- kind: ServiceAccount + name: user-controller-manager + namespace: user-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: user-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: user-proxy-role +subjects: +- kind: ServiceAccount + name: user-controller-manager + namespace: user-system +--- +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: 785548a1.sealos.io + # leaderElectionReleaseOnCancel defines if the leader should step down volume + # 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 + +kind: ConfigMap +metadata: + name: user-manager-config + namespace: user-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: user-controller-manager-metrics-service + namespace: user-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + name: user-webhook-service + namespace: user-system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: user-controller-manager + namespace: user-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + - --kube-api-qps=5 + - --kube-api-burst=10 + command: + - /manager + env: + - name: NAMESPACE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: ghcr.io/labring/sealos-user-controller:latest + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 1000m + memory: 1024Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + securityContext: + runAsNonRoot: true + serviceAccountName: user-controller-manager + terminationGracePeriodSeconds: 10 + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "control-plane" + operator: In + values: + - controller-manager + topologyKey: "kubernetes.io/hostname" + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: user-serving-cert + namespace: user-system +spec: + dnsNames: + - user-webhook-service.user-system.svc + - user-webhook-service.user-system.svc.cluster.local + issuerRef: + kind: Issuer + name: user-selfsigned-issuer + secretName: webhook-server-cert +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: user-selfsigned-issuer + namespace: user-system +spec: + selfSigned: {} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: user-system/user-serving-cert + name: user-mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: user-webhook-service + namespace: user-system + path: /mutate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + name: moperationrequest.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: user-webhook-service + namespace: user-system + path: /mutate-user-sealos-io-v1-user + failurePolicy: Fail + name: muser.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: user-system/user-serving-cert + name: user-validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: user-webhook-service + namespace: user-system + path: /validate-user-sealos-io-v1-operationrequest + failurePolicy: Fail + name: voperationrequest.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - operationrequests + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: user-webhook-service + namespace: user-system + path: /validate-user-sealos-io-v1-user + timeoutSeconds: 30 + failurePolicy: Fail + name: vuser.kb.io + rules: + - apiGroups: + - user.sealos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - users + sideEffects: None diff --git a/controllers/user/deploy/drop/rbac.yaml b/controllers/user/deploy/drop/rbac.yaml new file mode 100644 index 000000000000..33012a2237ba --- /dev/null +++ b/controllers/user/deploy/drop/rbac.yaml @@ -0,0 +1,102 @@ +# Copyright © 2023 sealos. +# +# 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. + +# permissions for end users to edit users. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: user-editor-role +rules: + - apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - users/status + verbs: + - get +--- +# permissions for end users to edit operationrequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: operationrequest-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: operationrequest-editor-role +rules: + - apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get +--- +# permissions for end users to edit deleterequests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: deleterequest-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: user + app.kubernetes.io/part-of: user + app.kubernetes.io/managed-by: kustomize + name: deleterequest-editor-role +rules: + - apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get diff --git a/controllers/user/deploy/user-controller-entrypoint.sh b/controllers/user/deploy/user-controller-entrypoint.sh new file mode 100644 index 000000000000..dafc54de1dad --- /dev/null +++ b/controllers/user/deploy/user-controller-entrypoint.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -eo pipefail + +HELM_OPTS=${HELM_OPTS:-""} +HELM_OPTIONS=${HELM_OPTIONS:-""} +RELEASE_NAME=${RELEASE_NAME:-"user-controller"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"user-system"} +CHART_PATH=${CHART_PATH:-"./charts/user-controller"} +HELM_SET_ARGS=() + +get_cm_value() { + local namespace="$1" + local name="$2" + local key="$3" + kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true +} + +add_set_string() { + local key="$1" + local value="$2" + value=${value//\\/\\\\} + value=${value//,/\\,} + HELM_SET_ARGS+=(--set-string "${key}=${value}") +} + +# Clean up old resources +kubectl delete -f ./drop/ --ignore-not-found + +# Auto configure from sealos-config, then fall back to chart defaults. +DEFAULT_SEALOS_CLOUD_DOMAIN="127.0.0.1.nip.io" +DEFAULT_SEALOS_CLOUD_APISERVER_PORT="6443" + +SEALOS_CLOUD_DOMAIN=${SEALOS_CLOUD_DOMAIN:-"${cloudDomain:-$(get_cm_value sealos-system sealos-config cloudDomain)}"} +SEALOS_CLOUD_DOMAIN=${SEALOS_CLOUD_DOMAIN:-"${DEFAULT_SEALOS_CLOUD_DOMAIN}"} +SEALOS_CLOUD_APISERVER_PORT=${SEALOS_CLOUD_APISERVER_PORT:-"${apiserverPort:-$(get_cm_value sealos-system sealos-config apiserverPort)}"} +SEALOS_CLOUD_APISERVER_PORT=${SEALOS_CLOUD_APISERVER_PORT:-"${DEFAULT_SEALOS_CLOUD_APISERVER_PORT}"} + +add_set_string cloudAPIServerDomain "${SEALOS_CLOUD_DOMAIN}" +add_set_string cloudAPIServerPort "${SEALOS_CLOUD_APISERVER_PORT}" + +# Prepare values files +SERVICE_NAME="user-controller" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/${SERVICE_NAME}-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +# Deploy Helm chart +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + "${HELM_SET_ARGS[@]}" \ + ${HELM_OPTIONS} \ + ${HELM_OPTS} + +# Apply CRDs +helm show crds ./charts/${SERVICE_NAME} | kubectl apply -f - --server-side --force-conflicts diff --git a/controllers/user/go.mod b/controllers/user/go.mod new file mode 100644 index 000000000000..057d316db6bb --- /dev/null +++ b/controllers/user/go.mod @@ -0,0 +1,72 @@ +module github.com/labring/sealos/controllers/user + +go 1.24.0 + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/go-logr/logr v1.4.2 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.36.1 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/time v0.7.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.32.1 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 + sigs.k8s.io/controller-runtime v0.20.4 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // 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/nxadm/tail v1.4.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/controllers/user/go.sum b/controllers/user/go.sum new file mode 100644 index 000000000000..993353c578c1 --- /dev/null +++ b/controllers/user/go.sum @@ -0,0 +1,239 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/controllers/user/hack/boilerplate.go.txt b/controllers/user/hack/boilerplate.go.txt new file mode 100644 index 000000000000..fb1b985cb87d --- /dev/null +++ b/controllers/user/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2022 labring. + +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. +*/ \ No newline at end of file diff --git a/controllers/user/main.go b/controllers/user/main.go new file mode 100644 index 000000000000..78f9da188299 --- /dev/null +++ b/controllers/user/main.go @@ -0,0 +1,332 @@ +/* +Copyright 2022 labring. + +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" + "time" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + userv1 "github.com/labring/sealos/controllers/user/api/v1" + "github.com/labring/sealos/controllers/user/controllers" + usercache "github.com/labring/sealos/controllers/user/controllers/cache" + ratelimiter "github.com/labring/sealos/controllers/user/controllers/helper/ratelimiter" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // 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/client-go/rest" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + //+kubebuilder:scaffold:imports + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(licensev1.AddToScheme(scheme)) + utilruntime.Must(userv1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var ( + metricsAddr string + pprofBindAddress string + enableLeaderElection bool + gracefulShutdownTimeout time.Duration + probeAddr string + rateLimiterOptions ratelimiter.RateLimiterOptions + syncPeriod time.Duration + minRequeueDuration time.Duration + maxRequeueDuration time.Duration + operationReqExpirationTime time.Duration + restartPredicateDuration time.Duration + operationReqRetentionTime time.Duration + kubeAPIQPS float64 + kubeAPIBurst int + secureMetrics bool + enableHTTP2 bool + enableAdminClusterAdmin bool + enableStrictNamespacePSA bool + tlsOpts []func(*tls.Config) + ) + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + flag.StringVar( + &pprofBindAddress, + "pprof-bind-address", + "", + "The address the pprof endpoint binds to. Empty disables pprof.", + ) + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.DurationVar( + &gracefulShutdownTimeout, + "graceful-shutdown-timeout", + 8*time.Second, + "Maximum time to stop manager runnables before releasing leadership.", + ) + flag.DurationVar( + &syncPeriod, + "sync-period", + 0, + "Informer synthetic resync period. Zero disables periodic resync.", + ) + flag.DurationVar( + &minRequeueDuration, + "min-requeue-duration", + time.Hour*24, + "Retry interval for a User blocked by the license limit.", + ) + flag.DurationVar( + &maxRequeueDuration, + "max-requeue-duration", + time.Hour*24*2, + "Deprecated: retained for compatibility and ignored.", + ) + flag.DurationVar( + &operationReqExpirationTime, + "operation-req-expiration-time", + time.Minute*3, + "Sets the expiration time duration for an operation request. By default, the duration is set to 3 minutes.", + ) + flag.DurationVar( + &operationReqRetentionTime, + "operation-req-retention-time", + time.Minute*3, + "Sets the retention time duration for an operation request. By default, the duration is set to 3 minutes.", + ) + flag.DurationVar( + &restartPredicateDuration, + "restart-predicate-time", + time.Hour*2, + "Deprecated: retained for compatibility and ignored. User startup reconciliation processes all cached Users.", + ) + flag.Float64Var( + &kubeAPIQPS, + "kube-api-qps", + float64(rest.DefaultQPS), + "Sets the Kubernetes API client QPS used by the manager and reconciler clients.", + ) + flag.IntVar( + &kubeAPIBurst, + "kube-api-burst", + rest.DefaultBurst, + "Sets the Kubernetes API client burst used by the manager and reconciler clients.", + ) + 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( + &enableAdminClusterAdmin, + "enable-admin-cluster-admin", + false, + "Preserve the legacy cluster-admin binding and privileged admin namespace labels.", + ) + flag.BoolVar( + &enableStrictNamespacePSA, + "enable-strict-namespace-pod-security", + true, + "Apply Pod Security labels to all ns-* namespaces, including namespaces without a Sealos User.", + ) + rateLimiterOptions.BindFlags(flag.CommandLine) + opts := zap.Options{ + Development: true, + } + 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) + } + // 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. + } + + cfg := ctrl.GetConfigOrDie() + cfg.QPS = float32(kubeAPIQPS) + cfg.Burst = kubeAPIBurst + setupLog.Info("configured Kubernetes API client rate limit", "qps", cfg.QPS, "burst", cfg.Burst) + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + PprofBindAddress: pprofBindAddress, + Cache: usercache.Options(&syncPeriod), + Client: client.Options{Cache: &client.CacheOptions{ + DisableFor: usercache.UncachedObjects(), + }}, + // WebhookServer: webhook.NewServer(webhook.Options{ + // Port: 9443, + // }), + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "785548a1.sealos.io", + LeaderElectionReleaseOnCancel: true, + GracefulShutdownTimeout: &gracefulShutdownTimeout, + Controller: config.Controller{ + UsePriorityQueue: ptr.To(true), + }, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + if err := controllers.SetupLicenseGate(mgr); err != nil { + setupLog.Error(err, "unable to set up license gate") + os.Exit(1) + } + userCounter, err := controllers.SetupUserCount(mgr) + if err != nil { + setupLog.Error(err, "unable to set up user count cache") + os.Exit(1) + } + + if err = (&controllers.UserReconciler{ + EnableAdminClusterAdmin: enableAdminClusterAdmin, + EnableStrictNamespacePodSecurity: enableStrictNamespacePSA, + }).SetupWithManager( + mgr, + rateLimiterOptions, + minRequeueDuration, + maxRequeueDuration, + restartPredicateDuration, + userCounter, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "User") + os.Exit(1) + } + + if os.Getenv("DISABLE_WEBHOOKS") == "true" { + setupLog.Info("disable all webhooks") + } else { + if err = (&userv1.User{}).SetupWebhookWithManager(mgr, userCounter); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "User") + os.Exit(1) + } + } + + if err = (&controllers.OperationReqReconciler{}).SetupWithManager( + mgr, + rateLimiterOptions, + operationReqExpirationTime, + operationReqRetentionTime, + ); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Operationrequest") + os.Exit(1) + } + if os.Getenv("DISABLE_WEBHOOKS") == "true" { + setupLog.Info("disable all webhooks") + } else { + if err = (&userv1.Operationrequest{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Operationrequest") + os.Exit(1) + } + } + if err = (&controllers.DeleteRequestReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DeleteRequest") + os.Exit(1) + } + // if err = (&controllers.AdaptRoleBindingReconciler{ + // Client: mgr.GetClient(), + // Scheme: mgr.GetScheme(), + // }).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "AdaptRoleBinding") + // 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, "failed to running manager") + os.Exit(1) + } +} diff --git a/controllers/user/pkg/licensegate/gate.go b/controllers/user/pkg/licensegate/gate.go new file mode 100644 index 000000000000..d7a9c1b35ae1 --- /dev/null +++ b/controllers/user/pkg/licensegate/gate.go @@ -0,0 +1,122 @@ +// Copyright © 2026 sealos. +// +// 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 licensegate + +import ( + "context" + "sync/atomic" + "time" + + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + licensepkg "github.com/labring/sealos/controllers/pkg/license" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const DefaultUserLimit = 1 + +var ( + activeFlag atomic.Uint32 + initializedFlag atomic.Uint32 + userLimit atomic.Int64 +) + +func Initialized() bool { + return initializedFlag.Load() == 1 +} + +func HasActiveLicense() bool { + return activeFlag.Load() == 1 +} + +func AllowNewUser(currentCount int) bool { + if !Initialized() { + return false + } + limit := UserLimit() + if limit < 0 { + return true + } + return currentCount < limit +} + +func UserLimit() int { + return int(userLimit.Load()) +} + +func LimitMessage() string { + if HasActiveLicense() { + return "license active: user limit reached" + } + return "license inactive: user limit reached" +} + +func SetActive(active bool) { + if active { + activeFlag.Store(1) + } else { + activeFlag.Store(0) + } + initializedFlag.Store(1) +} + +func SetUserLimit(limit int) { + userLimit.Store(int64(limit)) + initializedFlag.Store(1) +} + +func SetState(active bool, limit int) { + SetActive(active) + SetUserLimit(limit) +} + +func Refresh(ctx context.Context, reader client.Reader) error { + licenseList := &licensev1.LicenseList{} + if err := reader.List(ctx, licenseList); err != nil { + return err + } + var selected *licensev1.License + for i := range licenseList.Items { + license := &licenseList.Items[i] + if license.Status.Phase != licensev1.LicenseStatusPhaseActive { + continue + } + if selected == nil || licenseTimestamp(license).After(licenseTimestamp(selected)) { + selected = license + } + } + if selected == nil { + SetState(false, DefaultUserLimit) + return nil + } + claims, err := licensepkg.GetClaimsFromLicense(selected) + if err != nil { + SetState(false, DefaultUserLimit) + return err + } + clusterData := &licensepkg.ClusterClaimData{} + if err := claims.Data.SwitchToClusterData(clusterData); err != nil { + SetState(false, DefaultUserLimit) + return err + } + SetState(true, clusterData.UserCount) + return nil +} + +func licenseTimestamp(license *licensev1.License) time.Time { + if !license.Status.ActivationTime.IsZero() { + return license.Status.ActivationTime.Time + } + return license.CreationTimestamp.Time +} diff --git a/controllers/user/pkg/licensegate/gate_test.go b/controllers/user/pkg/licensegate/gate_test.go new file mode 100644 index 000000000000..cc7f0e3df393 --- /dev/null +++ b/controllers/user/pkg/licensegate/gate_test.go @@ -0,0 +1,146 @@ +// Copyright © 2026 sealos. +// +// 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 licensegate + +import ( + "context" + "encoding/base64" + "os" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + licensev1 "github.com/labring/sealos/controllers/license/api/v1" + licensepkg "github.com/labring/sealos/controllers/pkg/license" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func newTestLicense( + t *testing.T, + userCount int, + phase licensev1.LicenseStatusPhase, + activationTime time.Time, +) *licensev1.License { + t.Helper() + if os.Getenv("LICENSE_KEY") == "" { + t.Skip("requires LICENSE_KEY to sign test licenses") + } + decodeKey, err := base64.StdEncoding.DecodeString(licensepkg.GetEncryptionKey()) + if err != nil { + t.Fatalf("decode encryption key failed: %v", err) + } + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodeKey) + if err != nil { + t.Fatalf("parse private key failed: %v", err) + } + claims := &licensepkg.Claims{ + Type: licensev1.ClusterLicenseType, + Data: licensepkg.ClaimData{ + "userCount": userCount, + }, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + signed, err := token.SignedString(privateKey) + if err != nil { + t.Fatalf("sign token failed: %v", err) + } + return &licensev1.License{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-license", + }, + Spec: licensev1.LicenseSpec{ + Token: signed, + Type: claims.Type, + }, + Status: licensev1.LicenseStatus{ + Phase: phase, + ActivationTime: metav1.NewTime(activationTime), + }, + } +} + +func TestRefreshActiveLicense(t *testing.T) { + scheme := runtime.NewScheme() + if err := licensev1.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme failed: %v", err) + } + activeLicense := newTestLicense(t, 10, licensev1.LicenseStatusPhaseActive, time.Now()) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(activeLicense).Build() + if err := Refresh(context.Background(), client); err != nil { + t.Fatalf("refresh failed: %v", err) + } + if !HasActiveLicense() { + t.Fatalf("expected active license") + } + if UserLimit() != 10 { + t.Fatalf("expected user limit 10, got %d", UserLimit()) + } +} + +func TestRefreshNoActiveLicense(t *testing.T) { + scheme := runtime.NewScheme() + if err := licensev1.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme failed: %v", err) + } + inactiveLicense := newTestLicense(t, 10, licensev1.LicenseStatusPhaseFailed, time.Now()) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(inactiveLicense).Build() + if err := Refresh(context.Background(), client); err != nil { + t.Fatalf("refresh failed: %v", err) + } + if HasActiveLicense() { + t.Fatalf("expected inactive license") + } + if UserLimit() != DefaultUserLimit { + t.Fatalf("expected default user limit %d, got %d", DefaultUserLimit, UserLimit()) + } +} + +func TestAllowNewUserUnlimited(t *testing.T) { + scheme := runtime.NewScheme() + if err := licensev1.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme failed: %v", err) + } + activeLicense := newTestLicense(t, -1, licensev1.LicenseStatusPhaseActive, time.Now()) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(activeLicense).Build() + if err := Refresh(context.Background(), client); err != nil { + t.Fatalf("refresh failed: %v", err) + } + if !AllowNewUser(1000) { + t.Fatalf("expected unlimited user allow") + } +} + +func TestRefreshUsesLatestActiveLicense(t *testing.T) { + scheme := runtime.NewScheme() + if err := licensev1.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme failed: %v", err) + } + older := newTestLicense(t, 5, licensev1.LicenseStatusPhaseActive, time.Now().Add(-time.Hour)) + newer := newTestLicense(t, 20, licensev1.LicenseStatusPhaseActive, time.Now()) + older.Name = "older-license" + newer.Name = "newer-license" + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(older, newer).Build() + if err := Refresh(context.Background(), client); err != nil { + t.Fatalf("refresh failed: %v", err) + } + if UserLimit() != 20 { + t.Fatalf("expected user limit 20, got %d", UserLimit()) + } +} diff --git a/controllers/user/pkg/usercount/count.go b/controllers/user/pkg/usercount/count.go new file mode 100644 index 000000000000..e69131949656 --- /dev/null +++ b/controllers/user/pkg/usercount/count.go @@ -0,0 +1,160 @@ +// Copyright © 2026 sealos. +// +// 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 usercount + +import ( + "sync" + "sync/atomic" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + toolscache "k8s.io/client-go/tools/cache" +) + +// Counter tracks users that are present and not being deleted. It stores only +// object names so quota checks do not retain or copy User status fields. +type Counter struct { + mu sync.RWMutex + users map[string]struct{} + count atomic.Int64 + initialized atomic.Bool +} + +func NewCounter() *Counter { + return &Counter{users: make(map[string]struct{})} +} + +func (c *Counter) Initialized() bool { + return c != nil && c.initialized.Load() +} + +func (c *Counter) Count() int { + if c == nil { + return 0 + } + return int(c.count.Load()) +} + +func (c *Counter) CountExcluding(name string) int { + if c == nil { + return 0 + } + + c.mu.RLock() + defer c.mu.RUnlock() + count := int(c.count.Load()) + if _, ok := c.users[name]; ok { + return count - 1 + } + return count +} + +// MarkInitialized marks the counter ready after the informer's initial events +// have been delivered to the counter's event handler. +func (c *Counter) MarkInitialized() { + if c == nil { + return + } + c.initialized.Store(true) +} + +func (c *Counter) Add(obj any) { + metadata, ok := objectMetadata(obj) + if !ok { + return + } + c.set(metadata.GetName(), isQuotaUser(metadata)) +} + +func (c *Counter) Update(oldObj, newObj any) { + oldMetadata, oldOK := objectMetadata(oldObj) + newMetadata, newOK := objectMetadata(newObj) + if oldOK && newOK && oldMetadata.GetName() != newMetadata.GetName() { + c.set(oldMetadata.GetName(), false) + } + if !newOK { + return + } + c.set(newMetadata.GetName(), isQuotaUser(newMetadata)) +} + +func (c *Counter) Delete(obj any) { + metadata, ok := objectMetadata(obj) + if !ok { + return + } + c.set(metadata.GetName(), false) +} + +func (c *Counter) set(name string, present bool) { + if c == nil || name == "" { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + if c.users == nil { + c.users = make(map[string]struct{}) + } + if present { + if _, ok := c.users[name]; ok { + return + } + c.users[name] = struct{}{} + c.count.Add(1) + return + } + if _, ok := c.users[name]; !ok { + return + } + delete(c.users, name) + c.count.Add(-1) +} + +func isQuotaUser(obj metav1.Object) bool { + return obj.GetName() != "" && + (obj.GetDeletionTimestamp() == nil || obj.GetDeletionTimestamp().IsZero()) +} + +func objectMetadata(obj any) (metav1.Object, bool) { + switch tombstone := obj.(type) { + case toolscache.DeletedFinalStateUnknown: + obj = tombstone.Obj + case *toolscache.DeletedFinalStateUnknown: + obj = tombstone.Obj + } + metadata, err := meta.Accessor(obj) + return metadata, err == nil && metadata != nil +} + +// The following process-local functions are retained for the license +// controller, which refreshes its own user count independently. +var ( + processUserCount atomic.Int64 + processUserCountInitialized atomic.Uint32 +) + +func Initialized() bool { + return processUserCountInitialized.Load() == 1 +} + +func Get() int { + return int(processUserCount.Load()) +} + +func Set(count int) { + processUserCount.Store(int64(count)) + processUserCountInitialized.Store(1) +} diff --git a/controllers/user/pkg/usercount/count_test.go b/controllers/user/pkg/usercount/count_test.go new file mode 100644 index 000000000000..f30be3d39337 --- /dev/null +++ b/controllers/user/pkg/usercount/count_test.go @@ -0,0 +1,75 @@ +// Copyright © 2026 sealos. +// +// 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 usercount + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + toolscache "k8s.io/client-go/tools/cache" +) + +func TestCounterTracksInformerEvents(t *testing.T) { + counter := NewCounter() + deletionTimestamp := metav1.Now() + active := &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "active-user"}} + deleting := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "active-user", + DeletionTimestamp: &deletionTimestamp, + }, + } + + counter.Add(active) + counter.MarkInitialized() + if !counter.Initialized() || counter.Count() != 1 { + t.Fatalf( + "initialized counter = (%t, %d), want (true, 1)", + counter.Initialized(), + counter.Count(), + ) + } + if got := counter.CountExcluding("active-user"); got != 0 { + t.Fatalf("CountExcluding() = %d, want 0", got) + } + + counter.Update(active, deleting) + if got := counter.Count(); got != 0 { + t.Fatalf("count after deletion update = %d, want 0", got) + } + + counter.Add(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}}) + if got := counter.Count(); got != 1 { + t.Fatalf("count after add = %d, want 1", got) + } + counter.Delete(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "new-user"}}) + if got := counter.Count(); got != 0 { + t.Fatalf("count after delete = %d, want 0", got) + } +} + +func TestCounterIgnoresDeletedFinalStateUnknown(t *testing.T) { + counter := NewCounter() + counter.Add(&metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "user"}}) + counter.MarkInitialized() + + counter.Delete(toolscache.DeletedFinalStateUnknown{ + Key: "user", + Obj: &metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{Name: "user"}}, + }) + if got := counter.Count(); got != 0 { + t.Fatalf("count after tombstone delete = %d, want 0", got) + } +} diff --git a/controllers/user/user.env b/controllers/user/user.env new file mode 100644 index 000000000000..a4f5f190c97d --- /dev/null +++ b/controllers/user/user.env @@ -0,0 +1,2 @@ +KUBECONFIG=kubeconfig.test +DISABLE_WEBHOOKS=true diff --git a/docs/4.0/README.md b/docs/4.0/README.md deleted file mode 100644 index cc872d5f5192..000000000000 --- a/docs/4.0/README.md +++ /dev/null @@ -1,157 +0,0 @@ - - trackgit-views - - -![](https://socialify.git.ci/fanux/sealos/image?description=1&font=Source%20Code%20Pro&forks=1&language=1&pattern=Charlie%20Brown&stargazers=1&theme=Light) - -
-

- 让云原生简单普及! -

-

- - [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/labring/sealos) - [![Build Status](https://github.com/labring/sealos/actions/workflows/release.yml/badge.svg)](https://github.com/labring/sealos/actions) - [![Website](https://img.shields.io/website?url=https%3A%2F%2Fpostwoman.io&logo=Postwoman)](https://sealyun.com) - [![Go Report Card](https://goreportcard.com/badge/github.com/labring/sealos)](https://goreportcard.com/report/github.com/labring/sealos) - [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-blueviolet?logo=Telegram)](https://t.me/gsealyun) - -

-
- ---- - -**文档: _[官方文档](https://www.sealyun.com), [English Docs](/README_en.md), [博客](https://fuckcloudnative.io)_** - -**加入组织: 钉钉群(35371178), [Telegram](https://t.me/gsealyun), QQ群(98488045),作者微信:fangnux** - -## sealos 是什么 - -**sealos 是以kubernetes为内核的云操作系统发行版** - -早期单机操作系统也是分层架构,后来才演变成 linux windows这种内核架构,云操作系统从容器诞生之日起分层架构被击穿,未来也会朝着高内聚的"云内核"架构迁移 - -![](https://user-images.githubusercontent.com/8912557/162092037-82b1fc5b-cf55-4224-8266-c1c6a989a602.png) - -* 从现在开始,把你数据中心所有机器想象成一台"抽象"的超级计算机,sealos就是用来管理这台超级计算机的操作系统,kubernetes就是这个操作系统的内核! -* 云计算从此刻起再无IaaS PaaS SaaS之分,只有云操作系统驱动(CSI CNI CRI实现) 云操作系统内核(kubernetes) 和 分布式应用组成 - -> 核心能力 - -* 集群镜像 - 实现整个集群的Build Ship Run,把docker的理念衍生到集群纬度,实现任意分布式软件的自由定义一键运行 -* hub.sealos - 集群镜像仓库,这里你可以找到绝大多数已经制作好的分布式应用如kubernetes基础集群镜像,pgsql高可用集群镜像,minio高可用集群镜像等 -* desktop.sealos - 云操作系统的桌面,并非传统意义上的云桌面,它长得像macOS但是管理的集群和分布式应用 -* 分布式应用矩阵 - 也就是各种你需要用的存储/网络/高可用数据库/消息队列/监控等,所有这些只需要点下鼠标或者sealos run即可获得 - -## sealos 愿景 - -* 让所有企业使用基于kubernetes的云操作系统像使用macOS一样简单 -* 让任何人都可以用一条命令或动动鼠标即可构建复杂的云服务 -* 让所有企业只需要雇佣一个实习生即可维护整个云 -* 让所有企业能拥有更开放的AWS, 公有云与私有云可以有完全一致性的体验 -* 让任何分布式软件都可以一键在系统中运行并实现自运维 - -## sealos 能干啥 - -* 对集群生命周期进行管理,一键安装高可用kubernetes集群,增删节点清理集群自恢复等 -* 通过sealos hub 下载和使用完全兼容OCI标准的各种分布式软件如openebs,minio,ingress,pgsql,mysql,redis等 -* 通过sealos desktop 像使用macOS一样管理整个集群,以及管理系统上跑的分布式应用 -* sealos 可以管理kubernetes但并不是一个kubernetes管理器, 而是一个抽象的云操作系统。要管理kubernetes下载一个对应管理应用即可。 -* sealos 可以安装kubernetes但是并不是一个安装工具,安装只是sealos 的一个boot的最基本的能力。 - -## sealos 适合谁用 - -* 小白用户 - 针对小白连kubernetes单词也不会拼写的人也可以通过命令或者图形界面完全无障碍使用sealos,获取一些需要的软件,如点击一下即可获得一个高可用数据库。 -* SaaS应用开发者 - 你可能需要的也是一个数据库,一个高可用消息队列或者一个开发环境,一条命令即可让你获得所需要的服务。你也不想关心底层如何实现,你只要结果。 -* 集群维护者 - 你可能很懂kubernetes,sealos市场里提供各种管理应用如lens官方dashboard等等,针对极客还提供webterminal, 各种云原生生态软件监控系统也是信手拈来。 -* 云操作系统开发者 - 你可能很擅长operator开发,那么恭喜你可以编写sealos的应用,并提交到sealos hub上供其他所有用户使用你的产品。 -* 私有云交付人员 - sealos集群镜像机制可以保证在离线环境中的高度一致性,是私有云交付之王,也能极好的封装SaaS应用,实现一键交付到客户环境中。 -* 企业用户 - 你可以直接使用sealos公有云服务,也可以分钟级在自己的机房中运行出一个一模一样的私有云为整个企业提供服务, 还可以把sealos运行到各大公有云IaaS上,再也不用受厂商绑定之苦,想切就切。 - -## sealos 为什么不一样 - -> kubernetes是手段不是目的 - -对于大众用户来说kubernetes并不重要,重要的是kubernetes上面跑了什么东西,这些东西才是用户最终关心的,中间过程并不关心。 -当然熟悉kubernetes的极客不用担心,你同样会有非常好的使用体验。 - -> 化整为零,不同的应用,不同的形态 - -sealos 最简单的版本几乎不包含任何东西,除了最最基本的集群镜像的能力,其它能力几乎都是通过应用云扩展的,最基础的sealos除了一个很"裸"的kuberentes不包含其他任何东西。 -这使得sealos可以很简单,也可以很强大,可以个人使用,也可以服务公有云这种庞大的多租户应用场景,可以在一台机器上玩,也可以在数千台服务器上大规模运行。 - -> 包容性 - -意味着不管你是什么样的喜好都可以在sealos上得到完美使用体验,比如以CI/CD场景为例,有些用户喜欢drone 有些喜欢argo,这两类用户只需要自己安装不同应用即可,sealos不会深度集成某一款CI/CD工具 -用户自由的安装卸载。 - -sealos不会追求系统上分布式应用风格的统一,就像macOS上的office软件和Email软件不可能一致一样,这样的好处是给不同的分布式软件最大发挥空间,不然sealos会花非常大的代价让他们统一,一旦某个技术 -被淘汰意味着极大的替换成本。 - -sealos也不会追求各种软件账户信息的统一,因为不同的分布式软件有不同账户管理方式,这些管理方式对其应用自身的适配性是最强的。 - -> 不同的用户不同的使用方式 - -和macOS很类似,普通大众用户用图形界面,开发者终端敲敲命令,系统应用开发者调用system API -sealos的大众用户用GUI或者简单的一键使用,云原生从业者kubectl 各种dashboard 所有apiserver, 开发者直接基于kubernetes开发operator - -> 简单不失强大 - -sealos提供的是最基础的系统框架,其强大的能力都由上层应用提供,sealos的职责是管理好这些应用,所以系统复杂度不会随着功能的增加而变复杂。 - -## sealos 实践案例 - -Boss:我司需要紧跟云原生浪潮,需要构建一个基于kubernetes云平台,要有存储,要有paas,要有ci/cd,还要有云开发,还要有数据库,还要有。。。xx总监你来评估一下成本 - -CTO(掰掰手指头): kubernetes 3人,存储专家 1人,开发1人,paas 3人, ci/cd 3人, 云开发5人, 运维3人。。。 老板我仅需要15人 给我半年就做出来! - -众人议论纷纷,此时角落传来一个声音: - -小张(默默举手): 我了解一个开源软件,貌似一键就可以搞定,小张共享了一下屏幕,默默敲下: - -```shell script -sealos run labring/kubernetes:v1.24.0 labring/openebs:v1.9.0 labring/mysql:v8.0 labring/minio:v4.4.16 labring/ingress:v4.1.0 labring/laf:v0.8.0 - -m 192.168.0.2 -n 192.168.0.3 -p 123456 -``` - -会没结束,任务完成。。。 - -后面的故事大家都知道了,除了小张 CTO往下都被裁了。。。 - -## 快速开始 - -> 安装一个高可用的kubernetes集群,并用calico作为网络插件 - -这里的 `kubernetes:v1.24.0` 和 `calico:v3.22.1` 就是存在registry里的集群镜像,完全兼容OCI标准, 当然聪明同学立马想到是不是可以用flannel,答案是当然! - -```shell script -# 下载并安装sealos, sealos是个golang的二进制工具,直接下载拷贝到bin目录即可, release页面也可下载 -wget -c https://sealyun-home.oss-cn-beijing.aliyuncs.com/sealos-4.0/latest/sealos-amd64 -O sealos && \ - chmod +x sealos && mv sealos /usr/bin -# 创建一个集群 -sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] -``` - -> 构建一个自定义集群镜像 - -[构建一个ingress集群镜像](https://github.com/labring/sealos/blob/main/docs/4.0/build-example-ingress-helm.md) - -> 存储/消息/数据库 等 - -接下来请不要震惊: - -```shell script -sealos run labring/helm:v3.8.2 # 安装helm -sealos run labring/openebs:v1.9.0 # 安装openebs -sealos run labring/minio-operator:v4.4.16 labring/ingress-nginx:4.1.0 \ - labring/mysql-operator:8.0.23-14.1 labring/redis-operator:3.1.4 # 喜欢的话可以把它们写一起 -``` - -然后你就啥都有了 - -## 其它链接 - -* [sealos 3.0 老版本](https://github.com/labring/sealos/tree/release-v3.3.9#readme) 老版本用户访问这里,4.0全面掉打老版本,请尽快升级。 -* [buildah](https://github.com/containers/buildah) 本着不重复造轮子,sealos 4.0中使用了大量buildah的构建能力,使集群镜像完全兼容容器镜像和docker registry。 diff --git a/docs/4.0/build-example-ingress-helm.md b/docs/4.0/build-example-ingress-helm.md deleted file mode 100644 index 731add255347..000000000000 --- a/docs/4.0/build-example-ingress-helm.md +++ /dev/null @@ -1,51 +0,0 @@ -# Example for build a CloudImage from helm - -This is an example for build nginx-ingress CloudImage using helm. - -## Download helm chart - -```shell script -mkdir ingress-nginx && cd ingress-nginx -helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx -helm pull ingress-nginx/ingress-nginx -``` - -Then you will got the chart: -```shell script -root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# ls -ingress-nginx-4.1.0.tgz -``` - -## Add image list - -sealos will download images in image list, and cache it into registry dir. -The dir must be `images/shim/[your image list filename]` - -```shell script -root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# cat images/shim/nginxImages -k8s.gcr.io/ingress-nginx/controller:v1.2.0 -k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1 -``` - - -## Add a Dockerfile - -```shell script -root@iZj6ceuntkc5q5p95bbqb8Z:~/nginx-ingress# cat Dockerfile -FROM scratch -COPY . . -CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"] -``` - -## Build it - -```shell script -sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 . -``` - -Then push it into registry - -```shell script -sealos login docker.io -sealos push docker.io/fanux/ingress-nginx:v1.2.0 -``` diff --git a/docs/4.0/build.md b/docs/4.0/build.md deleted file mode 100644 index e42255e3d85e..000000000000 --- a/docs/4.0/build.md +++ /dev/null @@ -1,122 +0,0 @@ -## build calico image in offline module - -``` -. -├── Kubefile -├── cni -│   ├── custom-resources.yaml -│   └── tigera-operator.yaml -├── images -│   └── shim -│   └── CalicoImageList -└── registry - └── docker - └── registry -``` - -run one image build func - -```dockerfile -FROM labring/kubernetes:v1.24.0-amd64 -COPY cni ./cni -COPY images ./images -COPY registry ./registry -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] -``` - -run multi image build func - -```dockerfile -FROM scratch -COPY cni ./cni -COPY images ./images -COPY registry ./registry -COPY manifests ./manifests -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] -``` - - -1. the `CalicoImageList` is offline image list file. -2. the cni dir is kubectl apply config files -3. the registry is the registry data dir -4. exec `buildah build -t kubernetes-calico:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image -5. manifests dis parse yaml images to offline image list - -## build calico image in online module - -``` -. -├── Kubefile -├── cni -│   ├── custom-resources.yaml -│   └── tigera-operator.yaml - -``` - -run one image build func - -```dockerfile -FROM labring/kubernetes:v1.24.0-amd64 -COPY cni ./cni -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] -``` - -run multi image build func - -```dockerfile -FROM scratch -COPY cni ./cni -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] -``` - -1. the cni dir is kubectl apply config files -2. exec `buildah build -t kubernetes-calico:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image - - -## build openebs image in online module - -``` -. -├── Kubefile -└── manifests - └── openebs-operator.yaml - -``` - -run one image build func - -```dockerfile -FROM labring/oci-kubernetes-calico:1.24.0-amd64 -COPY manifests ./manifests -CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml","kubectl apply -f manifests/openebs-operator.yaml"] -``` - -run multi image build func - -```dockerfile -FROM scratch -COPY manifests ./manifests -CMD ["kubectl apply -f manifests/openebs-operator.yaml"] -``` - - -1. the cni dir is kubectl apply config files -2. exec `buildah build -t labring/kubernetes-calico-openebs:1.24.0-amd64 --arch amd64 --os linux -f Kubefile .` build the oci image - -tips: you need add calico cmd to openebs cmd layer,because the dockerfile override to old layer in run one image func - - -## build multi-architecture image - -```shell -buildah build -t $prefix/oci-kubernetes:$version-amd64 --arch amd64 --os linux -f Kubefile . -buildah build -t $prefix/oci-kubernetes:$version-arm64 --arch arm64 --os linux -f Kubefile . - -buildah login --username $username --password $password $domain -buildah push $prefix/oci-kubernetes:$version-amd64 -buildah push $prefix/oci-kubernetes:$version-arm64 -buildah manifest create $prefix/oci-kubernetes:$version -buildah manifest add $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version-amd64 -buildah manifest add $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version-arm64 -buildah manifest push --all $prefix/oci-kubernetes:$version docker://$prefix/oci-kubernetes:$version -``` diff --git a/docs/4.0/config.md b/docs/4.0/config.md deleted file mode 100644 index 1c27438d1ba5..000000000000 --- a/docs/4.0/config.md +++ /dev/null @@ -1,35 +0,0 @@ -### config - -Clusterfile - -```yaml -... ---- -apiVersion: apps.sealos.io/v1beta1 -kind: Config -metadata: - name: calico -spec: - strategy: merge - path: manifests/calico.yaml - data: | - apiVersion: operator.tigera.io/v1 - kind: Installation - metadata: - name: default - spec: - # Configures Calico networking. - calicoNetwork: - # Note: The ipPools section cannot be modified post-install. - ipPools: - - blockSize: 26 - cidr: 100.63.0.0/10 - encapsulation: IPIP - natOutgoing: Enabled - nodeSelector: all() - nodeAddressAutodetectionV4: - # Interface enables IP auto-detection based on interfaces that match the given regex. - interface: "eth.*|en.*" - - -``` diff --git a/docs/4.0/design/registry-image.md b/docs/4.0/design/registry-image.md deleted file mode 100644 index 4936f92cc9a4..000000000000 --- a/docs/4.0/design/registry-image.md +++ /dev/null @@ -1,37 +0,0 @@ -# 镜像打包与分发设计 - -## 背景 - -当前镜像管理方式有几个问题: - -1. registry模块与filesystem耦合,应该单独拆出一个管理模块 -2. docker镜像放在集群镜像里面,会导致集群镜像大小非常大,而且推送到仓库中实际上是有很多多余文件,浪费空间 -3. 分发的时候通过scp分发非常低效,也会存在部分多余分发的情况 -4. 无法充分利用nydus的特性对集群镜像分发,需要作一些转换,集成nydus方式也不够干净 -5. 在线和离线两种场景都把所有东西拉下来,在可以联网的服务上没有这个必要 -6. 多架构分发需要自己去判断目标机器系统架构 - -## 方案设计 - -### 构建过程 - -1. Build的时候对集群镜像里面的Docker镜像不作任何处理,以保障集群镜像"很小" -2. CloudImage完全兼容OCI - -### Save过程 - -1. 把之前在Build过程中缓存容器镜像的动作后置到Save命令中,此时去解析manifests目录,chart目录以及imageList,然后把容器镜像单独放到 registry目录. -2. 把集群镜像也保存到registry目录中. -3. Save的产物就是registry目录和一些配置信息的打包. - -### Run 过程 - -1. registry module根据registry的配置拉起registry. -2. 根据CloudImage Name在所有节点拉取集群镜像 -3. 启动k8s和guest -4. kubelet自动拉起其它镜像 -5. 可以利用运行时自身能力去判断多架构并拉取对应架构的镜像 - -### 加速方案 - -在此方案基础上就可以直接制作containerd+nydus的runtime镜像,直接使用nydus的能力,同时提升集群镜像与容器镜像的分发速度 \ No newline at end of file diff --git a/docs/4.0/design/rootfs.md b/docs/4.0/design/rootfs.md deleted file mode 100644 index 0120fb414f96..000000000000 --- a/docs/4.0/design/rootfs.md +++ /dev/null @@ -1,98 +0,0 @@ -### rootfs - -``` -. -├── Kubefile -├── README.md -├── bin -│   ├── conntrack -│   ├── kubeadm -│   ├── kubectl -│   └── kubelet -├── cri -│   ├── cri-containerd-linux.tar.gz -│   ├── image-cri-shim -│   ├── lib64 -│   │   ├── libseccomp.so.2 -│   │   └── libseccomp.so.2.3.1 -│   └── nerdctl -├── etc -│   ├── 10-kubeadm.conf -│   ├── config.toml -│   ├── containerd.service -│   ├── crictl.yaml -│   ├── hosts.toml -│   ├── image-cri-shim.service -│   ├── image-cri-shim.yaml.tmpl -│   ├── kubelet.service -│   ├── registry.yml.tmpl -│   └── registry_config.yml -├── images -│   ├── registry.tar -│   └── shim -│   └── DefaultImageList -├── opt -│   └── sealctl -├── registry -│   └── docker -│   └── registry -├── scripts -│   ├── auth.sh -│   ├── check.sh -│   ├── clean-containerd.sh -│   ├── clean-kube.sh -│   ├── clean-registry.sh -│   ├── clean-shim.sh -│   ├── clean.sh -│   ├── common.sh -│   ├── init-containerd.sh -│   ├── init-kube.sh -│   ├── init-registry.sh -│   ├── init-shim.sh -│   ├── init.sh -│   ├── kubelet-post-stop.sh -│   └── kubelet-pre-start.sh -└── statics - └── audit-policy.yml -``` - -```dockerfile -FROM scratch -MAINTAINER sealyun -LABEL init="init.sh \$criData \$registryDomain \$registryPort \$registryUsername \$registryPassword" -LABEL version="v1.24.0" -LABEL image="" -LABEL clean="clean.sh \$criData" -LABEL check="check.sh" -LABEL init-registry="init-registry.sh \$registryPort \$registryData \$registryConfig" -LABEL clean-registry="clean-registry.sh \$registryData \$registryConfig" -LABEL auth="auth.sh" -ENV criData=/var/lib/containerd -ENV registryData=/var/lib/registry -ENV registryConfig=/etc/registry -ENV registryDomain=sealos.hub -ENV registryPort=5000 -ENV registryUsername=admin -ENV registryPassword=passw0rd -COPY .. . -``` - - -image-cri-shim can replace image to registry addr - -- offline - if offline module,you need add imageList file in dir `images/shim` - example ImageList file is : - ` - ghcr.io/sealyun/lvscare:v1.1.3-beta.2 - k8s.gcr.io/kube-apiserver:v1.24.0 - k8s.gcr.io/kube-controller-manager:v1.24.0 - k8s.gcr.io/kube-scheduler:v1.24.0 - k8s.gcr.io/kube-proxy:v1.24.0 - k8s.gcr.io/pause:3.5 - k8s.gcr.io/etcd:3.5.0-0 - k8s.gcr.io/coredns/coredns:v1.8.4 - ` - shim found image in imageList ,the `k8s.gcr.io/kube-apiserver:v1.24.0` replace to `sealos.hub:5000/kube-apiserver:v1.24.0` -- online (default module) - diff --git a/docs/4.0/images.md b/docs/4.0/images.md deleted file mode 100644 index 75b0a15ce75c..000000000000 --- a/docs/4.0/images.md +++ /dev/null @@ -1,11 +0,0 @@ -## All image list - -- labring/kubernetes:v1.24.0 -- labring/calico:v3.22.1 -- labring/helm:v3.8.2 -- labring/openebs:v1.9.0 -- labring/minio-operator:v4.4.16 -- labring/ingress-nginx:4.1.0 -- labring/mysql-operator:8.0.23-14.1 -- labring/redis-operator:3.1.4 -- labring/coredns:v0.0.1 diff --git a/docs/4.0/install-sealos.md b/docs/4.0/install-sealos.md deleted file mode 100644 index 59c0e302a964..000000000000 --- a/docs/4.0/install-sealos.md +++ /dev/null @@ -1,23 +0,0 @@ -### install buildah - -#### amd64 - -```shell -wget https://sealyun-home.oss-accelerate.aliyuncs.com/images/buildah.linux.amd64 --no-check-certificate -O buildah -chmod a+x buildah && mv buildah /usr/bin -``` - -#### arm64 - -```shell -wget https://sealyun-home.oss-accelerate.aliyuncs.com/images/buildah.linux.arm64 --no-check-certificate -O buildah -chmod a+x buildah && mv buildah /usr/bin -``` - - -### install sealos - -```shell -wget -c https://sealyun-home.oss-cn-beijing.aliyuncs.com/sealos-4.0/latest/sealos-amd64 -O sealos && \ -chmod +x sealos && mv sealos /usr/bin -``` diff --git a/docs/4.0/reset.md b/docs/4.0/reset.md deleted file mode 100644 index 53c47c9420e8..000000000000 --- a/docs/4.0/reset.md +++ /dev/null @@ -1,5 +0,0 @@ -### reset cluster - -```shell -sealos reset -``` diff --git a/docs/4.0/run.md b/docs/4.0/run.md deleted file mode 100644 index d9b5133f593c..000000000000 --- a/docs/4.0/run.md +++ /dev/null @@ -1,38 +0,0 @@ -### run calico use one image - -```shell -sealos run labring/oci-kubernetes-calico:1.24.0-amd64 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 -``` - -### run calico use multi image - -```shell -sealos run labring/kubernetes:v1.24.0 \ - labring/calico:v3.22.1 \ ---masters 192.168.64.2,192.168.64.22,192.168.64.20 \ ---nodes 192.168.64.21,192.168.64.19 -``` - - - -### run openebs use one image - -```shell -sealos run labring/oci-kubernetes-calico-openebs:1.24.0-amd64 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 -``` - - -### run openebs use multi image - -```shell -sealos run labring/kubernetes:v1.24.0 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ - --nodes 192.168.64.21,192.168.64.19 - -sealos run labring/calico:v3.22.1 -sealos run labring/openebs:3.1.0 -``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000000..1239323d0495 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# Documentation Migration + +The documentation for Sealos has been moved to [https://github.com/labring/sealos.io/](https://github.com/labring/sealos.io/). + +Please refer to the new location for the latest guides and references. + +--- + +**Notice:** +Documentation for previous versions (`3.0`, `4.0`, and `5.0`) has been archived and is no longer actively maintained in this repository. \ No newline at end of file diff --git a/docs/api/sealos.md b/docs/api/sealos.md deleted file mode 100644 index ba2d0bb4de83..000000000000 --- a/docs/api/sealos.md +++ /dev/null @@ -1,39 +0,0 @@ -## sealos - -simplest way install kubernetes tools. - -### Options - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger - -h, --help help for sealos -``` - -### SEE ALSO - -* [sealos add](sealos_add.md) - add some node -* [sealos apply](sealos_apply.md) - apply a kubernetes cluster -* [sealos build](sealos_build.md) - build an cloud image from a Kubefile -* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell -* [sealos create](sealos_create.md) - Create a cluster without running the CMD -* [sealos delete](sealos_delete.md) - delete some node -* [sealos docs](sealos_docs.md) - generate API reference -* [sealos exec](sealos_exec.md) - exec a shell command or script on all node. -* [sealos gen](sealos_gen.md) - Generate a Clusterfile -* [sealos images](sealos_images.md) - list cloud image -* [sealos load](sealos_load.md) - load cloud image -* [sealos login](sealos_login.md) - login image repository -* [sealos logout](sealos_logout.md) - logout image repository -* [sealos prune](sealos_prune.md) - prune image -* [sealos pull](sealos_pull.md) - pull cloud image -* [sealos push](sealos_push.md) - push cloud image -* [sealos reset](sealos_reset.md) - Simplest way to reset your cluster -* [sealos rmi](sealos_rmi.md) - Remove one or more cloud images -* [sealos run](sealos_run.md) - Simplest way to run your kubernetes HA cluster -* [sealos save](sealos_save.md) - save cloud image to a tar file -* [sealos scp](sealos_scp.md) - copy local file to remote on all node. -* [sealos tag](sealos_tag.md) - tag a image as a new one -* [sealos version](sealos_version.md) - version - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_add.md b/docs/api/sealos_add.md deleted file mode 100644 index 0078bfc81f37..000000000000 --- a/docs/api/sealos_add.md +++ /dev/null @@ -1,42 +0,0 @@ -## sealos add - -add some node - -``` -sealos add [flags] -``` - -### Examples - -``` - -add to nodes : - sealos add --nodes x.x.x.x - -add to default cluster: - sealos add --masters x.x.x.x --nodes x.x.x.x - sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y - -``` - -### Options - -``` - -c, --cluster string delete a kubernetes cluster with cluster name (default "default") - -h, --help help for add - -m, --masters string reduce Count or IPList to masters - -n, --nodes string reduce Count or IPList to nodes -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_apply.md b/docs/api/sealos_apply.md deleted file mode 100644 index a026288b79a4..000000000000 --- a/docs/api/sealos_apply.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos apply - -apply a kubernetes cluster - -``` -sealos apply [flags] -``` - -### Examples - -``` -sealos apply -f Clusterfile -``` - -### Options - -``` - -f, --Clusterfile string apply a kubernetes cluster (default "Clusterfile") - -h, --help help for apply -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_build.md b/docs/api/sealos_build.md deleted file mode 100644 index 16e8d3864aa0..000000000000 --- a/docs/api/sealos_build.md +++ /dev/null @@ -1,37 +0,0 @@ -## sealos build - -build an cloud image from a Kubefile - -``` -sealos build [flags] PATH -``` - -### Examples - -``` -sealos build -t labring/kubernetes:v1.24.0 . -``` - -### Options - -``` - --basic-auth pull image auth policy,default is token auth - -h, --help help for build - -f, --kubefile string kubefile filepath (default "Kubefile") - -m, --max-pull-procs int maximum number of goroutines for pulling (default 5) - --platform string set the OS/ARCH/VARIANT of the image to the provided value instead of the current operating system and architecture of the host (for example linux/arm) (default "linux/arm64") - -t, --tag string tagged name to apply to the built image -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_completion.md b/docs/api/sealos_completion.md deleted file mode 100644 index 0171f17fb476..000000000000 --- a/docs/api/sealos_completion.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos completion - -Generate the autocompletion script for the specified shell - -### Synopsis - -Generate the autocompletion script for sealos for the specified shell. -See each sub-command's help for details on how to use the generated script. - - -### Options - -``` - -h, --help help for completion -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. -* [sealos completion bash](sealos_completion_bash.md) - Generate the autocompletion script for bash -* [sealos completion fish](sealos_completion_fish.md) - Generate the autocompletion script for fish -* [sealos completion powershell](sealos_completion_powershell.md) - Generate the autocompletion script for powershell -* [sealos completion zsh](sealos_completion_zsh.md) - Generate the autocompletion script for zsh - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_completion_bash.md b/docs/api/sealos_completion_bash.md deleted file mode 100644 index 18382ceafede..000000000000 --- a/docs/api/sealos_completion_bash.md +++ /dev/null @@ -1,51 +0,0 @@ -## sealos completion bash - -Generate the autocompletion script for bash - -### Synopsis - -Generate the autocompletion script for the bash shell. - -This script depends on the 'bash-completion' package. -If it is not installed already, you can install it via your OS's package manager. - -To load completions in your current shell session: - - source <(sealos completion bash) - -To load completions for every new session, execute once: - -#### Linux: - - sealos completion bash > /etc/bash_completion.d/sealos - -#### macOS: - - sealos completion bash > /usr/local/etc/bash_completion.d/sealos - -You will need to start a new shell for this setup to take effect. - - -``` -sealos completion bash -``` - -### Options - -``` - -h, --help help for bash - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_completion_fish.md b/docs/api/sealos_completion_fish.md deleted file mode 100644 index f6596d086919..000000000000 --- a/docs/api/sealos_completion_fish.md +++ /dev/null @@ -1,42 +0,0 @@ -## sealos completion fish - -Generate the autocompletion script for fish - -### Synopsis - -Generate the autocompletion script for the fish shell. - -To load completions in your current shell session: - - sealos completion fish | source - -To load completions for every new session, execute once: - - sealos completion fish > ~/.config/fish/completions/sealos.fish - -You will need to start a new shell for this setup to take effect. - - -``` -sealos completion fish [flags] -``` - -### Options - -``` - -h, --help help for fish - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_completion_powershell.md b/docs/api/sealos_completion_powershell.md deleted file mode 100644 index b7a554636583..000000000000 --- a/docs/api/sealos_completion_powershell.md +++ /dev/null @@ -1,39 +0,0 @@ -## sealos completion powershell - -Generate the autocompletion script for powershell - -### Synopsis - -Generate the autocompletion script for powershell. - -To load completions in your current shell session: - - sealos completion powershell | Out-String | Invoke-Expression - -To load completions for every new session, add the output of the above command -to your powershell profile. - - -``` -sealos completion powershell [flags] -``` - -### Options - -``` - -h, --help help for powershell - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_completion_zsh.md b/docs/api/sealos_completion_zsh.md deleted file mode 100644 index 4df9ded0517b..000000000000 --- a/docs/api/sealos_completion_zsh.md +++ /dev/null @@ -1,49 +0,0 @@ -## sealos completion zsh - -Generate the autocompletion script for zsh - -### Synopsis - -Generate the autocompletion script for the zsh shell. - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - - echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions for every new session, execute once: - -#### Linux: - - sealos completion zsh > "${fpath[1]}/_sealos" - -#### macOS: - - sealos completion zsh > /usr/local/share/zsh/site-functions/_sealos - -You will need to start a new shell for this setup to take effect. - - -``` -sealos completion zsh [flags] -``` - -### Options - -``` - -h, --help help for zsh - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos completion](sealos_completion.md) - Generate the autocompletion script for the specified shell - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_create.md b/docs/api/sealos_create.md deleted file mode 100644 index 574394dc6d50..000000000000 --- a/docs/api/sealos_create.md +++ /dev/null @@ -1,42 +0,0 @@ -## sealos create - -Create a cluster without running the CMD - -### Synopsis - -sealos create [image name] [args] - -``` -sealos create [flags] -``` - -### Examples - -``` - -create a mysql cluster: - sealos create mysql:8.0 -with custom cluster name: - sealos create mysql:8.0 -c mysql - -``` - -### Options - -``` - -c, --cluster-name string set custom cluster name (default "default") - -h, --help help for create -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_delete.md b/docs/api/sealos_delete.md deleted file mode 100644 index 783f4b0f29e9..000000000000 --- a/docs/api/sealos_delete.md +++ /dev/null @@ -1,51 +0,0 @@ -## sealos delete - -delete some node - -``` -sealos delete [flags] -``` - -### Examples - -``` - -delete nodes: - sealos delete --nodes x.x.x.x - if accidentally deleted; - Use 'sealos add' to recover: - sealos add --nodes x.x.x.x - -delete masters: - sealos delete --masters x.x.x.x - -delete masters and nodes: - sealos delete --masters x.x.x.x --nodes x.x.x.x - sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y - -Please note that sealos will delete your master if the --masters parameter is specified. - -``` - -### Options - -``` - -c, --cluster string delete a kubernetes cluster with cluster name (default "default") - --force We also can input an --force flag to delete cluster by force - -h, --help help for delete - -m, --masters string reduce Count or IPList to masters - -n, --nodes string reduce Count or IPList to nodes -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_docs.md b/docs/api/sealos_docs.md deleted file mode 100644 index 7ad0488c08dd..000000000000 --- a/docs/api/sealos_docs.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos docs - -generate API reference - -``` -sealos docs [flags] -``` - -### Examples - -``` -sealos docs -``` - -### Options - -``` - -h, --help help for docs - -p, --path string path to output docs (default "./docs/api") -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_exec.md b/docs/api/sealos_exec.md deleted file mode 100644 index ba2068765958..000000000000 --- a/docs/api/sealos_exec.md +++ /dev/null @@ -1,44 +0,0 @@ -## sealos exec - -exec a shell command or script on all node. - -``` -sealos exec [flags] -``` - -### Examples - -``` - -exec to default cluster: default - sealos exec "cat /etc/hosts" -specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ): - sealos exec -c my-cluster "cat /etc/hosts" -set role label to exec cmd: - sealos exec -c my-cluster -r master,slave,node1 "cat /etc/hosts" -set ips to exec cmd: - sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" - -``` - -### Options - -``` - -c, --cluster-name string submit one cluster name (default "default") - -h, --help help for exec - --ips strings ssh ips list on node - -r, --roles string set role label to roles -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_gen.md b/docs/api/sealos_gen.md deleted file mode 100644 index bdc6a0dbfbee..000000000000 --- a/docs/api/sealos_gen.md +++ /dev/null @@ -1,59 +0,0 @@ -## sealos gen - -Generate a Clusterfile - -### Synopsis - -Generate a Clusterfile of the kubernetes cluster, which can be applied by 'sealos apply' command - -``` -sealos gen [flags] -``` - -### Examples - -``` - -generate a cluster with multi images, specify masters and nodes: - sealos gen labring/kubernetes:v1.24.0 labring/calico:v3.22.1 \ - --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx - -specify server InfraSSH port: - all servers use the same InfraSSH port: - sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx - different InfraSSH port numbers: - sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ - --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx - -``` - -### Options - -``` - --cmd strings set cmd for image cmd instruction - -e, --env strings set custom environment variables - -h, --help help for gen - -m, --masters string set Count or IPList to masters - --name string set cluster name variables (default "default") - -n, --nodes string set Count or IPList to nodes - -p, --passwd string set cloud provider or baremetal server password - --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa") - --pk-passwd string set baremetal server private key password - --port uint16 set the sshd service port number for the server (default 22) - -u, --user string set baremetal server username (default "root") -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_images.md b/docs/api/sealos_images.md deleted file mode 100644 index 5270311b41b5..000000000000 --- a/docs/api/sealos_images.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos images - -list cloud image - -``` -sealos images [flags] -``` - -### Examples - -``` -sealos images -``` - -### Options - -``` - -h, --help help for images -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_load.md b/docs/api/sealos_load.md deleted file mode 100644 index 0e1c40daa07d..000000000000 --- a/docs/api/sealos_load.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos load - -load cloud image - -``` -sealos load [flags] -``` - -### Examples - -``` -sealos load -i kubernetes.tar -``` - -### Options - -``` - -h, --help help for load - -i, --input string read image from tar archive file -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_login.md b/docs/api/sealos_login.md deleted file mode 100644 index 865a63f0289d..000000000000 --- a/docs/api/sealos_login.md +++ /dev/null @@ -1,34 +0,0 @@ -## sealos login - -login image repository - -``` -sealos login [flags] -``` - -### Examples - -``` -sealos login registry.cn-qingdao.aliyuncs.com -u [username] -p [password] -``` - -### Options - -``` - -h, --help help for login - -p, --passwd string password for login registry - -u, --username string user name for login registry -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_logout.md b/docs/api/sealos_logout.md deleted file mode 100644 index a0b34aa35766..000000000000 --- a/docs/api/sealos_logout.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos logout - -logout image repository - -``` -sealos logout [flags] -``` - -### Examples - -``` -sealos logout registry.cn-qingdao.aliyuncs.com -``` - -### Options - -``` - -h, --help help for logout -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_prune.md b/docs/api/sealos_prune.md deleted file mode 100644 index 4b4f91794557..000000000000 --- a/docs/api/sealos_prune.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos prune - -prune image - -``` -sealos prune [flags] -``` - -### Examples - -``` -sealos prune -``` - -### Options - -``` - -h, --help help for prune -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_pull.md b/docs/api/sealos_pull.md deleted file mode 100644 index 6e675f5ccab9..000000000000 --- a/docs/api/sealos_pull.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos pull - -pull cloud image - -``` -sealos pull [flags] -``` - -### Examples - -``` -sealos pull labring/kubernetes:v1.24.0 -``` - -### Options - -``` - -h, --help help for pull -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_push.md b/docs/api/sealos_push.md deleted file mode 100644 index fc14f179ed1e..000000000000 --- a/docs/api/sealos_push.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos push - -push cloud image - -``` -sealos push [flags] -``` - -### Examples - -``` -sealos push labring/kubernetes:v1.24.0 -``` - -### Options - -``` - -h, --help help for push -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_reset.md b/docs/api/sealos_reset.md deleted file mode 100644 index 2ed9e2c937c8..000000000000 --- a/docs/api/sealos_reset.md +++ /dev/null @@ -1,48 +0,0 @@ -## sealos reset - -Simplest way to reset your cluster - -### Synopsis - -sealos reset --name [arg] - -``` -sealos reset [flags] -``` - -### Examples - -``` - -reset you current cluster: - sealos reset --name xxx [--force] - -``` - -### Options - -``` - --force we also can input an --force flag to reset cluster by force - -h, --help help for reset - -m, --masters string set Count or IPList to masters - --name string set cluster name variables (default "default") - -n, --nodes string set Count or IPList to nodes - -p, --passwd string set cloud provider or baremetal server password - --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa") - --pk-passwd string set baremetal server private key password - --port uint16 set the sshd service port number for the server (default 22) - -u, --user string set baremetal server username (default "root") -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_rmi.md b/docs/api/sealos_rmi.md deleted file mode 100644 index c56520712817..000000000000 --- a/docs/api/sealos_rmi.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos rmi - -Remove one or more cloud images - -``` -sealos rmi [flags] -``` - -### Examples - -``` -sealos rmi [-f] labring/kubernetes:v1.24.0 -``` - -### Options - -``` - -f, --force force removal all of the image - -h, --help help for rmi -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_run.md b/docs/api/sealos_run.md deleted file mode 100644 index 2d7b4cb7e8e6..000000000000 --- a/docs/api/sealos_run.md +++ /dev/null @@ -1,65 +0,0 @@ -## sealos run - -Simplest way to run your kubernetes HA cluster - -### Synopsis - -sealos run labring/kubernetes:v1.24.0 --masters [arg] --nodes [arg] - -``` -sealos run [flags] -``` - -### Examples - -``` - -create cluster to your baremetal server, appoint the iplist: - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx - multi image: - sealos run labring/kubernetes:v1.24.0 calico:v3.22.1 \ - --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 - Specify server InfraSSH port : - All servers use the same InfraSSH port (default port: 22): - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd xxx - Different InfraSSH port numbers exist: - sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ - --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd xxx - -create a cluster with custom environment variables: - sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ - --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd xxx - -``` - -### Options - -``` - --cmd strings set cmd for image cmd instruction - -e, --env strings set custom environment variables - -f, --force we also can input an --force flag to run app in this cluster by force - -h, --help help for run - -m, --masters string set Count or IPList to masters - --name string set cluster name variables (default "default") - -n, --nodes string set Count or IPList to nodes - -p, --passwd string set cloud provider or baremetal server password - --pk string set baremetal server private key (default "/home/ubuntu/.ssh/id_rsa") - --pk-passwd string set baremetal server private key password - --port uint16 set the sshd service port number for the server (default 22) - -u, --user string set baremetal server username (default "root") -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_save.md b/docs/api/sealos_save.md deleted file mode 100644 index 0ef24c475216..000000000000 --- a/docs/api/sealos_save.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos save - -save cloud image to a tar file - -``` -sealos save [flags] -``` - -### Examples - -``` -sealos save -o kubernetes.tar labring/kubernetes:v1.24.0 -``` - -### Options - -``` - -h, --help help for save - -o, --output string read image from tar archive file -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_scp.md b/docs/api/sealos_scp.md deleted file mode 100644 index 17d6f27fd285..000000000000 --- a/docs/api/sealos_scp.md +++ /dev/null @@ -1,44 +0,0 @@ -## sealos scp - -copy local file to remote on all node. - -``` -sealos scp [flags] -``` - -### Examples - -``` - -copy file to default cluster: default - sealos scp "/root/aa.txt" "/root/dd.txt" -specify the cluster name(If there is only one cluster in the $HOME/.sealos directory, it should be applied. ): - sealos scp -c my-cluster "/root/aa.txt" "/root/dd.txt" -set role label to copy file: - sealos scp -c my-cluster -r master,slave,node1 "cat /etc/hosts" -set ips to copy file: - sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" - -``` - -### Options - -``` - -c, --cluster-name string submit one cluster name (default "default") - -h, --help help for scp - --ips strings ssh ips list on node - -r, --roles string set role label to roles -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_tag.md b/docs/api/sealos_tag.md deleted file mode 100644 index 038e50617c76..000000000000 --- a/docs/api/sealos_tag.md +++ /dev/null @@ -1,32 +0,0 @@ -## sealos tag - -tag a image as a new one - -``` -sealos tag [flags] -``` - -### Examples - -``` -sealos tag labring/kubernetes:v1.24.0 oci-kubernetes:v1.24.0 -``` - -### Options - -``` - -h, --help help for tag -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/api/sealos_version.md b/docs/api/sealos_version.md deleted file mode 100644 index 829896a689ba..000000000000 --- a/docs/api/sealos_version.md +++ /dev/null @@ -1,33 +0,0 @@ -## sealos version - -version - -``` -sealos version [flags] -``` - -### Examples - -``` -sealos version -``` - -### Options - -``` - -h, --help help for version - --short if true, print just the version number. -``` - -### Options inherited from parent commands - -``` - --cluster-root string cluster root directory (default "/var/lib/sealos") - --debug enable debug logger -``` - -### SEE ALSO - -* [sealos](sealos.md) - simplest way install kubernetes tools. - -###### Auto generated by spf13/cobra on 1-Jul-2022 diff --git a/docs/3.0/README_en.md b/docs/archived/3.0/README_en.md similarity index 100% rename from docs/3.0/README_en.md rename to docs/archived/3.0/README_en.md diff --git a/docs/3.0/README_zh.md b/docs/archived/3.0/README_zh.md similarity index 100% rename from docs/3.0/README_zh.md rename to docs/archived/3.0/README_zh.md diff --git a/docs/3.0/command/cloud.md b/docs/archived/3.0/command/cloud.md similarity index 100% rename from docs/3.0/command/cloud.md rename to docs/archived/3.0/command/cloud.md diff --git a/docs/3.0/command/etcd.md b/docs/archived/3.0/command/etcd.md similarity index 100% rename from docs/3.0/command/etcd.md rename to docs/archived/3.0/command/etcd.md diff --git a/docs/3.0/command/exec.md b/docs/archived/3.0/command/exec.md similarity index 100% rename from docs/3.0/command/exec.md rename to docs/archived/3.0/command/exec.md diff --git a/docs/3.0/command/route.md b/docs/archived/3.0/command/route.md similarity index 100% rename from docs/3.0/command/route.md rename to docs/archived/3.0/command/route.md diff --git a/docs/3.0/command/upgrade.md b/docs/archived/3.0/command/upgrade.md similarity index 100% rename from docs/3.0/command/upgrade.md rename to docs/archived/3.0/command/upgrade.md diff --git a/docs/3.0/design/design.md b/docs/archived/3.0/design/design.md similarity index 100% rename from docs/3.0/design/design.md rename to docs/archived/3.0/design/design.md diff --git a/docs/3.0/history/v2.0.md b/docs/archived/3.0/history/v2.0.md similarity index 100% rename from docs/3.0/history/v2.0.md rename to docs/archived/3.0/history/v2.0.md diff --git a/docs/3.0/history/v3.0.md b/docs/archived/3.0/history/v3.0.md similarity index 100% rename from docs/3.0/history/v3.0.md rename to docs/archived/3.0/history/v3.0.md diff --git a/docs/3.0/plan/join_master_failed.md b/docs/archived/3.0/plan/join_master_failed.md similarity index 100% rename from docs/3.0/plan/join_master_failed.md rename to docs/archived/3.0/plan/join_master_failed.md diff --git a/docs/3.0/plan/multi_network_install.md b/docs/archived/3.0/plan/multi_network_install.md similarity index 100% rename from docs/3.0/plan/multi_network_install.md rename to docs/archived/3.0/plan/multi_network_install.md diff --git a/docs/3.0/plan/recovery_sealos.md b/docs/archived/3.0/plan/recovery_sealos.md similarity index 100% rename from docs/3.0/plan/recovery_sealos.md rename to docs/archived/3.0/plan/recovery_sealos.md diff --git a/docs/3.0/quick_start.md b/docs/archived/3.0/quick_start.md similarity index 100% rename from docs/3.0/quick_start.md rename to docs/archived/3.0/quick_start.md diff --git a/docs/3.0/user_guide.md b/docs/archived/3.0/user_guide.md similarity index 100% rename from docs/3.0/user_guide.md rename to docs/archived/3.0/user_guide.md diff --git a/docs/archived/4.0/code.json b/docs/archived/4.0/code.json new file mode 100644 index 000000000000..7f236043c689 --- /dev/null +++ b/docs/archived/4.0/code.json @@ -0,0 +1,646 @@ +{ + "theme.ErrorPageContent.title": { + "message": "页面已崩溃。", + "description": "The title of the fallback page when the page crashed" + }, + "theme.ErrorPageContent.tryAgain": { + "message": "重试", + "description": "The label of the button to try again rendering when the React error boundary captures an error" + }, + "theme.NotFound.title": { + "message": "找不到页面", + "description": "The title of the 404 page" + }, + "theme.NotFound.p1": { + "message": "我们找不到您要找的页面。", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.BackToTopButton.buttonAriaLabel": { + "message": "回到顶部", + "description": "The ARIA label for the back to top button" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "博文列表分页导航", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "较新的博文", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "较旧的博文", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.archive.title": { + "message": "历史博文", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "历史博文", + "description": "The page & hero description of the blog archive page" + }, + "theme.blog.post.readingTime.plurals": { + "message": "{readingTime} 分钟阅读", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.post.readMoreLabel": { + "message": "阅读 {title} 的全文", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, + "theme.blog.post.readMore": { + "message": "阅读更多", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "博文分页导航", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "较新一篇", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "较旧一篇", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.blog.post.plurals": { + "message": "{count} 篇博文", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.tagTitle": { + "message": "{nPosts} 含有标签「{tagName}」", + "description": "The title of the page for a blog tag" + }, + "theme.tags.tagsPageLink": { + "message": "查看所有标签", + "description": "The label of the link targeting the tag list page" + }, + "theme.colorToggle.ariaLabel": { + "message": "切换浅色/暗黑模式(当前为{mode})", + "description": "The ARIA label for the navbar color mode toggle" + }, + "theme.colorToggle.ariaLabel.mode.dark": { + "message": "暗黑模式", + "description": "The name for the dark color mode" + }, + "theme.colorToggle.ariaLabel.mode.light": { + "message": "浅色模式", + "description": "The name for the light color mode" + }, + "theme.docs.breadcrumbs.home": { + "message": "主页面", + "description": "The ARIA label for the home page in the breadcrumbs" + }, + "theme.docs.breadcrumbs.navAriaLabel": { + "message": "页面路径", + "description": "The ARIA label for the breadcrumbs" + }, + "theme.docs.DocCard.categoryDescription": { + "message": "{count} 个项目", + "description": "The default description for a category card in the generated index about how many items this category includes" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "文档分页导航", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "上一页", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "下一页", + "description": "The label used to navigate to the next doc" + }, + "theme.docs.tagDocListPageTitle.nDocsTagged": { + "message": "{count} 篇文档带有标签", + "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.tagDocListPageTitle": { + "message": "{nDocsTagged}「{tagName}」", + "description": "The title of the page for a docs tag" + }, + "theme.docs.versionBadge.label": { + "message": "版本:{versionLabel}" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。", + "description": "The label used to tell the user to check the latest version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "最新版本", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.editThisPage": { + "message": "编辑此页", + "description": "The link label to edit the current page" + }, + "theme.common.headingLinkTitle": { + "message": "标题的直接链接", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": "于 {date} ", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": "由 {user} ", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "最后{byUser}{atDate}更新", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.navbar.mobileVersionsDropdown.label": { + "message": "选择版本", + "description": "The label for the navbar versions dropdown on mobile view" + }, + "theme.common.skipToMainContent": { + "message": "跳到主要内容", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + }, + "theme.tags.tagsListLabel": { + "message": "标签:", + "description": "The label alongside a tag list" + }, + "theme.blog.sidebar.navAriaLabel": { + "message": "最近博文导航", + "description": "The ARIA label for recent posts in the blog sidebar" + }, + "theme.CodeBlock.copied": { + "message": "复制成功", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "复制代码到剪贴板", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copy": { + "message": "复制", + "description": "The copy button label on code blocks" + }, + "theme.CodeBlock.wordWrapToggle": { + "message": "切换自动换行", + "description": "The title attribute for toggle word wrapping button of code block lines" + }, + "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { + "message": "打开/收起侧边栏菜单「{label}」", + "description": "The ARIA label to toggle the collapsible sidebar category" + }, + "theme.navbar.mobileLanguageDropdown.label": { + "message": "选择语言", + "description": "The label for the mobile language switcher dropdown" + }, + "theme.TOCCollapsible.toggleButtonLabel": { + "message": "本页总览", + "description": "The label used by the button on the collapsible TOC component" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { + "message": "← 回到主菜单", + "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "Simple": { + "message": "简单", + "description": "homepage simple" + }, + "Any highly available distributed application on Kubernetes can be easily installed with just one click.": { + "message": "像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用程序,几乎不需要任何专业的交付和运维成本", + "description": "homepage simple" + }, + "Flexible": { + "message": "灵活", + "description": "homepage flexible" + }, + "Easily customize the cloud you need by freely combining various distributed applications.": { + "message": "利用独特的集群镜像能力将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用程序,轻松订制所需的云", + "description": "homepage flexible intro" + }, + "Powerful": { + "message": "强大", + "description": "homepage powerful" + }, + "The cloud services can be easily found and acquired in the application marketplace, offering simplicity and power.": { + "message": "通过应用商店来灵活满足各类用户的需求,形成强大的应用生态", + "description": "homepage flexible intro" + }, + "Kernel Arch": { + "message": "以 kubernetes 为云内核架构" + }, + "Cloud Driver": { + "message": "云驱动" + }, + "Cloud Kernel": { + "message": "云内核" + }, + "Distributed Applications": { + "message": "分布式应用程序" + }, + "Used By": { + "message": "客户列表" + }, + "theme.SearchBar.seeAll": { + "message": "查看全部 {count} 个结果" + }, + "theme.SearchBar.label": { + "message": "搜索", + "description": "The ARIA label and placeholder for search button" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "找到 {count} 份文件", + "description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "「{query}」的搜索结果", + "description": "The search page title for non-empty query" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "在文档中搜索", + "description": "The search page title for empty query" + }, + "theme.SearchPage.inputPlaceholder": { + "message": "在此输入搜索字词", + "description": "The placeholder for search page input" + }, + "theme.SearchPage.inputLabel": { + "message": "搜索", + "description": "The ARIA label for search page input" + }, + "theme.SearchPage.algoliaLabel": { + "message": "通过 Algolia 搜索", + "description": "The ARIA label for Algolia mention" + }, + "theme.SearchPage.noResultsText": { + "message": "未找到任何结果", + "description": "The paragraph for empty search result" + }, + "theme.SearchPage.fetchingNewResults": { + "message": "正在获取新的搜索结果...", + "description": "The paragraph for fetching new search results" + }, + "Contact": { + "message": "联系我们" + }, + "Contact US": { + "message": "联系我们" + }, + "theme.admonition.note": { + "message": "备注", + "description": "The default label used for the Note admonition (:::note)" + }, + "theme.admonition.tip": { + "message": "提示", + "description": "The default label used for the Tip admonition (:::tip)" + }, + "theme.admonition.danger": { + "message": "危险", + "description": "The default label used for the Danger admonition (:::danger)" + }, + "theme.admonition.info": { + "message": "信息", + "description": "The default label used for the Info admonition (:::info)" + }, + "theme.admonition.caution": { + "message": "警告", + "description": "The default label used for the Caution admonition (:::caution)" + }, + "theme.docs.sidebar.closeSidebarButtonAriaLabel": { + "message": "Close navigation bar", + "description": "The ARIA label for close button of mobile sidebar" + }, + "theme.docs.sidebar.toggleSidebarButtonAriaLabel": { + "message": "Toggle navigation bar", + "description": "The ARIA label for hamburger menu button of mobile navigation" + }, + "theme.SearchModal.searchBox.resetButtonTitle": { + "message": "清除查询", + "description": "The label and ARIA label for search box reset button" + }, + "theme.SearchModal.searchBox.cancelButtonText": { + "message": "取消", + "description": "The label and ARIA label for search box cancel button" + }, + "theme.SearchModal.startScreen.recentSearchesTitle": { + "message": "最近搜索", + "description": "The title for recent searches" + }, + "theme.SearchModal.startScreen.noRecentSearchesText": { + "message": "没有最近搜索", + "description": "The text when no recent searches" + }, + "theme.SearchModal.startScreen.saveRecentSearchButtonTitle": { + "message": "保存这个搜索", + "description": "The label for save recent search button" + }, + "theme.SearchModal.startScreen.removeRecentSearchButtonTitle": { + "message": "从历史记录中删除这个搜索", + "description": "The label for remove recent search button" + }, + "theme.SearchModal.startScreen.favoriteSearchesTitle": { + "message": "收藏", + "description": "The title for favorite searches" + }, + "theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": { + "message": "从收藏列表中删除这个搜索", + "description": "The label for remove favorite search button" + }, + "theme.SearchModal.errorScreen.titleText": { + "message": "无法获取结果", + "description": "The title for error screen of search modal" + }, + "theme.SearchModal.errorScreen.helpText": { + "message": "你可能需要检查网络连接。", + "description": "The help text for error screen of search modal" + }, + "theme.SearchModal.footer.selectText": { + "message": "选中", + "description": "The explanatory text of the action for the enter key" + }, + "theme.SearchModal.footer.selectKeyAriaLabel": { + "message": "Enter 键", + "description": "The ARIA label for the Enter key button that makes the selection" + }, + "theme.SearchModal.footer.navigateText": { + "message": "导航", + "description": "The explanatory text of the action for the Arrow up and Arrow down key" + }, + "theme.SearchModal.footer.navigateUpKeyAriaLabel": { + "message": "向上键", + "description": "The ARIA label for the Arrow up key button that makes the navigation" + }, + "theme.SearchModal.footer.navigateDownKeyAriaLabel": { + "message": "向下键", + "description": "The ARIA label for the Arrow down key button that makes the navigation" + }, + "theme.SearchModal.footer.closeText": { + "message": "关闭", + "description": "The explanatory text of the action for Escape key" + }, + "theme.SearchModal.footer.closeKeyAriaLabel": { + "message": "Esc 键", + "description": "The ARIA label for the Escape key button that close the modal" + }, + "theme.SearchModal.footer.searchByText": { + "message": "搜索提供", + "description": "The text explain that the search is making by Algolia" + }, + "theme.SearchModal.noResultsScreen.noResultsText": { + "message": "没有结果:", + "description": "The text explains that there are no results for the following search" + }, + "theme.SearchModal.noResultsScreen.suggestedQueryText": { + "message": "试试搜索", + "description": "The text for the suggested query when no results are found for the following search" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsText": { + "message": "认为这个查询应该有结果?", + "description": "The text for the question where the user thinks there are missing results" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": { + "message": "请告知我们。", + "description": "The text for the link to report missing results" + }, + "theme.SearchModal.placeholder": { + "message": "搜索文档", + "description": "The placeholder of the input of the DocSearch pop-up modal" + }, + "theme.tags.tagsPageTitle": { + "message": "标签", + "description": "The title of the tag list page" + }, + "Run Your Business on Sealos Cloud": { + "message": "Sealos - 以 kubernetes 为内核的云操作系统发行版" + }, + "Features": { + "message": "特性" + }, + "Sealos is a Kubernetes distribution, a general-purpose cloud operating system for managing cloud-native applications.": { + "message": "Sealos 以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,实现一个开源的、可随处运行的 AWS 替代方案" + }, + "Copy": { + "message": "复制" + }, + "START NOW": { + "message": "在线使用" + }, + "CONTACT US NOW": { + "message": "联系我们" + }, + "Sealos cloud-native App Store": { + "message": "Sealos 应用商店" + }, + "Fully compatible with Docker registry, enjoy seamless one-click installation of various cloud-native distributed applications": { + "message": "Sealos 应用商店中的每一个应用都是一个集群镜像,集群镜像中包含了分布式应用的所有依赖(例如 Helm Chart、容器镜像和二进制文件),可一键安装。" + }, + "Sealos PostgreSQL Database": { + "message": "Sealos PostgreSQL 数据库" + }, + "One-click creation of a highly available PostgreSQL database, with multi-database cluster management and backup and recovery capabilities": { + "message": "Sealos 注重为数据库用户提供沉浸式的使用体验,可一键创建高可用 PostgreSQL 数据库,并提供多数据库集群的管理、备份和恢复功能。" + }, + "Sealos Cloud Provider": { + "message": "Sealos Cloud Provider" + }, + "Effortlessly establish an independent Kubernetes cluster within minutes on AWS or other public clouds, and effortlessly manage multiple clusters": { + "message": "可在 AWS 等公有云上数分钟内创建一个独立的 Kubernetes 集群并管理多个集群,启动性能高出绝大多数公有云的三倍以上,而价格仅为 AWS 的 80%。" + }, + "Examples": { + "message": "使用样例" + }, + "Anything else as can be freely combined as Cloud OS Applications, allowing for unlimited combinations to fulfill a wide range of requirements and scenarios.": { + "message": "任意分布式应用都可以作为云操作系统的应用进行自由组合,以满足各种需求和场景。" + }, + "By utilizing Kubernetes as the core of the Cloud OS, we can abstract underlying resources, define resource interfaces, and standardize application management.": { + "message": "使用 Kubernetes 作为云操作系统内核,抽象底层资源 / 定义资源接口 / 标准化应用程序管理。" + }, + "The Cloud OS leverages CRI, CNI, and CSI as drivers and utilizes technologies such as Containerd, Calico, and OpenEBS to pool resources and ensure seamless access across the compute, storage, and network domains.": { + "message": "使用 CRI、CNI、CSI 作为云操作系统驱动程序,并利用 Containerd、Calico、OpenEBS 等插件将所有的计算、存储和网络等资源池化。" + }, + "Comprehensive Kubernetes Cluster management throughout its entire lifecycle, including Installation, Scaling, Backup, Recovery, and Upgrades": { + "message": "管理 Kubernetes 集群的整个生命周期,包括集群的包括安装、扩容、备份、恢复和升级。" + }, + "Storage": { + "message": "存储" + }, + "Supports block storage, object storage, and file storage with one-click operation": { + "message": "一键安装各种分布式存储。" + }, + "Network": { + "message": "网络" + }, + "Supports network plugins such as Calico, Flannel, and Cilium with the freedom of customization and choice": { + "message": "支持 Calico、Flannel 和 Cilium 等网络插件,可自由选择和定制。" + }, + "Supports both relational and non-relational databases with high availability, automatic backups, and multi-database instance management capabilities": { + "message": "支持关系型数据库和非关系型数据库,并提供高可用、自动备份和多数据库实例管理等功能。" + }, + "Monitoring": { + "message": "可观测性" + }, + "Easily deploy a monitoring and alarm system with just one click, without the need for additional configuration, and enjoy a simple and intuitive visual experience": { + "message": "一键轻松部署监控和告警系统,无需额外配置,享受简单直观的可视化体验。" + }, + "One-click construction of GPU drivers, device discovery, GPU resource monitoring, and the easy creation of a deep learning platform with AI capabilities": { + "message": "一键安装 GPU 驱动,监控 GPU 资源,轻松打造具备 AI 能力的深度学习平台。" + }, + "MQ": { + "message": "消息队列" + }, + "Supports various mainstream message queues with high availability and automatic monitoring": { + "message": "支持各种主流消息队列,并提供高可用和自动监控功能。" + }, + "Dashboard": { + "message": "可视化" + }, + "Supports various dashboards, enabling users to manage Kubernetes clusters with ease": { + "message": "一键安装各种 Kubernetes 可视化管理工具,轻松管理 Kubernetes 集群。" + }, + "Platform": { + "message": "容器管理平台" + }, + "Support various Container Platforms, enabling users to manage Kubernetes clusters with ease": { + "message": "一键安装各种 企业级容器管理平台,轻松管理 Kubernetes 集群。" + }, + "Support one-click operation for mainstream CI/CD systems such as Argo CD and Drone": { + "message": "一键安装 Argo CD 和 Drone 等主流 DevOps 系统。" + }, + "More than 4,000 Companies and 100,000+ Individuals!": { + "message": "已被 4000 多家公司和 10w+ 个人用户采用!" + }, + "Start": { + "message": "Home" + }, + "GitOps": { + "message": "GitOps" + }, + "Kubernetes": { + "message": "Kubernetes" + }, + "GPU": { + "message": "GPU" + }, + "theme.NavBar.navAriaLabel": { + "message": "主导航", + "description": "The ARIA label for the main navigation" + }, + "theme.docs.sidebar.navAriaLabel": { + "message": "文档侧边栏", + "description": "The ARIA label for the sidebar navigation" + }, + "Docs": { + "message": "文档", + "description": "The Header Documentation" + }, + "Community": { + "message": "社区", + "description": "The Header Community" + }, + "Join Us": { + "message": "加入我们" + }, + "The Capabilities of Sealos": { + "message": "Sealos 的能力" + }, + "Application Management": { + "message": "应用管理" + }, + "Easy management and quick release of publicly accessible distributed applications in the app store.": { + "message": "在应用商店中轻松管理并快速发布可公网访问的分布式应用。" + }, + "Database": { + "message": "数据库管理" + }, + "Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, MongoDB, and Redis.": { + "message": "秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。" + }, + "Cloud Universality": { + "message": "公私一致" + }, + "Equally effective in both public and private cloud, enabling a seamless transition of traditional applications to the cloud.": { + "message": "即是公有云也是私有云,支持传统应用无缝迁移到云环境。" + }, + "Why Choose Sealos?": { + "message": "为什么选择 Sealos" + }, + "Efficient & Cost-Effective": { + "message": "高效 & 经济" + }, + "Universal & User-Friendly": { + "message": "通用性强,无心智负担" + }, + "Flexible & Secure": { + "message": "灵活 & 安全" + }, + "Pay only for the containers you use. Auto-scaling prevents wasted resources, saving you a lot of money.": { + "message": "仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。" + }, + "Focus on your business without worrying about complexity. There is almost no learning curve.": { + "message": "专注于业务本身,无需担心复杂性,几乎没有学习成本。" + }, + "The multi-tenant sharing mechanism ensures security while providing resource isolation and efficient collaboration.": { + "message": "多租户共享机制在确保安全的同时,实现资源隔离与高效协作。" + }, + "Get early access to the latest Sealos versions and stay connected with developers and users on Discord.": { + "message": "加入 Sealos 社区,在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通" + }, + "Who are Using Sealos": { + "message": "谁在使用 Sealos" + }, + "Join Sealos Community": { + "message": "加入 Sealos 社区" + }, + "Experience the latest version of Sealos for the first time and communicate with developers and users in Discord!": { + "message": "在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通" + }, + "Start Now": { + "message": "立即使用" + }, + "for all applications": { + "message": "托管所有分布式应用" + }, + "Hosting": { + "message": "私有云" + }, + "A cloud operating system based on the Kubernetes kernel": { + "message": "以 Kubernetes 为内核的云操作系统" + }, + "Explore": { + "message": "了解更多" + }, + "Blog": { + "message": "博客" + }, + "App Store": { + "message": "应用商店" + } +} \ No newline at end of file diff --git a/docs/archived/4.0/docs/Community.md b/docs/archived/4.0/docs/Community.md new file mode 100644 index 000000000000..2f1b01d67106 --- /dev/null +++ b/docs/archived/4.0/docs/Community.md @@ -0,0 +1,7 @@ +# Community + +Sealos is an open source project that is driven by the participation of users and contributors. If you have questions or suggestions about using the product while reading this documentation, please try the following ways to seek support. Our team and community will do their best to help you. + ++ 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. ++ 🐦 Tweet at @Sailos_io on [Twitter](https://twitter.com/Sailos_io) and follow us. ++ 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. \ No newline at end of file diff --git a/docs/archived/4.0/docs/Intro.md b/docs/archived/4.0/docs/Intro.md new file mode 100644 index 000000000000..3278cb7a7108 --- /dev/null +++ b/docs/archived/4.0/docs/Intro.md @@ -0,0 +1,53 @@ +import Highlight from '@site/src/components/Highlight' + +# What is Sealos? + + + +The vastness and complexity of the cloud-native ecosystem undoubtedly leaves many businesses in disarray, yearning for an efficient, ready-to-use distribution, which this ecosystem sorely lacks. The cloud-native market is in dire need of a cloud operating system to further reduce barriers and costs. + +The advent of Sealos opens a new window for enterprises. It provides a novel choice for businesses and developers alike, necessitating only the installation of a cloud operating system, regardless of public or private cloud use. It allows various applications to operate stably and securely, tackling all sorts of dependency issues they might require. + +Sealos adheres to the philosophy that **cloud usage can be as simple as Linux**. You can commence usage right after installation, as you would with Linux, without entanglement in excessive complexities. The sole difference is that while Linux is installed on a single server, Sealos is installed across an entire data center. + +In Sealos's architecture, **there's no essential difference between public and private clouds**. They should be separate instances of the same codebase, the only distinction lying in their configuration and installed applications. Installing it on an intranet renders it a private cloud, while offering services on the public internet converts it into a public cloud. + +We firmly believe that with the continuous development and advancement of Sealos, the future of cloud computing will be more open, more straightforward. The benefits of cloud computing should belong to all providers of computational power, and the value of the cloud should be shared by all participants of cloud computing. **They will be able to enjoy the various conveniences of cloud computing in a more economical fashion in a diversified market environment.** + +## Overview + +Sealos is a **cloud operating system distribution** based on the Kubernetes kernel. It adopts a cloud-native approach, discarding traditional cloud computing architecture, and shifting towards a new architecture with Kubernetes as the cloud kernel. This allows enterprises to use the cloud **as effortlessly as they would use a personal computer**. + +Users will be able to install any high-availability distributed application on Kubernetes with the click of a button, similar to using a personal computer. It practically requires no professional delivery or operational costs. Simultaneously, utilizing unique cluster imaging capabilities, users can package any distributed application into an OCI image, freely combine a variety of distributed applications, and easily customize their desired cloud. The powerful and flexible app store function can meet the diverse needs of various users. + +![](./images/sealos-desktop.webp) + +## Scenarios & Advantages + +Sealos is a powerful business operating platform, providing flawless support for various applications such as Java, Go, Python, PHP, and more, unrestricted by the programming language. The platform offers a stable operating environment for applications and resolves backend dependency issues like databases, object storage, and messaging queues. Further, it can flexibly handle application configuration management, service discovery, public network exposure, and automatic scaling issues. + +### Public Cloud + +If your business requires operation within a public cloud environment, you can directly use the [public cloud services provided by Sealos](https://cloud.sealos.io). + +#### Capabilities of Sealos + +- 🚀 **Application Management**: Easy management and quick release of publicly accessible distributed applications in the app store. +- 🗄️ **Database Management**: Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, MongoDB, and Redis. +- 🌥️ **Cloud Universality**: Equally effective in both public and private cloud, enabling a seamless transition of traditional applications to the cloud. + +#### Advantages of Sealos + +- 💰 **Efficient & Economical**: Pay solely for the containers you utilize; automatic scaling prevents resource squandering and substantially reduces costs. +- 🌐 **High Universality & Ease of Use**: Concentrate on your core business activities without worrying about system complexities; negligible learning costs involved. +- 🛡️ **Agility & Security**: The distinctive multi-tenancy sharing model ensures both effective resource segmentation and collaboration, all under a secure framework. + +### Private Cloud + +The Sealos cloud operating system is **100% open source**. You can find all source code on [GitHub](https://github.com/labring/sealos), which means all features of the **Sealos Public Cloud** can also be deployed in a private cloud environment to meet more diversified business needs. + +### Strong Application Delivery Capability + +- Sealos has excellent Kubernetes life cycle management capabilities, and you can freely customize the Kubernetes environment. +- Sealos can package an entire cluster and deliver it to the customer environment with a single click. All services and businesses can be delivered as a whole. +- Unlike Docker, which mainly focuses on single-machine imaging, Sealos goes further, packaging the **entire cluster** or a distributed application. \ No newline at end of file diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md b/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md new file mode 100644 index 000000000000..afd8f35841bf --- /dev/null +++ b/docs/archived/4.0/docs/advanced-guide/Architecture/Architecture.md @@ -0,0 +1,23 @@ +# Architecture Description + +Sealos is built using the approach of application separation and application interconnection, where different applications can be deployed independently or combined with other applications to provide services. + +All services are authenticated using `kubeconfig` as the application identity, ensuring a consistent user experience whether in a browser, using the `sealos` command line locally, or even with a third-party client. + +## Overall Architecture + +![Architecture](./images/architecture_light.png#gh-light-mode-only)![Architecture](./images/architecture_dark.png#gh-dark-mode-only) + +## Application Architecture + +Applications on Sealos adopt a front-end and back-end separation architecture, and the front-end can provide `SSR` capabilities, allowing applications to provide services independently to the outside world without being bound to the Sealos monolith. + +Using the following diagram as an example: + +**App Launchpad** is a application deployment tool provided by Sealos, and **Terminal** is a terminal application for the Sealos system, providing command-line services consistent like a single-machine operating system terminal. +For various applications on Sealos, they can interact directly with `Kubernetes` services, or interact with various CRD controllers such as Terminal Controller and Ingress-Nginx Controller, etc. +Applications can also call each other, for example, a database created by a user in the **Database** application can directly invoke the **Terminal** application and automatically connect to the database for debugging. + +For more information, please refer to the [link](../../guides) documentation. + +![Application](./images/application_light.png#gh-light-mode-only)![Application](./images/application_dark.png#gh-dark-mode-only) \ No newline at end of file diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png new file mode 100644 index 000000000000..755a293e3468 Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_dark.png differ diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png new file mode 100644 index 000000000000..b735ab6cd70f Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/application_light.png differ diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png new file mode 100644 index 000000000000..8a007f9727ca Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_dark.png differ diff --git a/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png new file mode 100644 index 000000000000..1607eabd53b6 Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/Architecture/images/architecture_light.png differ diff --git a/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md b/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md new file mode 100644 index 000000000000..e4934c8f05fb --- /dev/null +++ b/docs/archived/4.0/docs/advanced-guide/how-to-deploy-the-application-to-desktop.md @@ -0,0 +1,39 @@ +# How to deploy the application to desktop + +## Use Terminal to write app.yaml + +### Key Information + +- type: iframe, identified as a web application +- spec name is the name displayed on the desktop +- url application address +- icon is the icon displayed on the desktop + +```yaml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: app-sample +spec: + name: Image Hub Demo + icon: + type: iframe + data: + url: https://hub.sealos.io/ + desc: + icon: https://cloud.sealos.io/logo.svg + menuData: + displayType: normal +``` + +![appyaml.png](./images/app-yaml.png) + +### apply yaml + +``` +kubectl apply -f app.yaml +``` + +![apply](./images/app-apply-command.png) + +### Refresh the browser, you can see the app on the desktop diff --git a/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png b/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png new file mode 100644 index 000000000000..f8a5fe3e636f Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/images/app-apply-command.png differ diff --git a/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png b/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png new file mode 100644 index 000000000000..54ac66a7bfe1 Binary files /dev/null and b/docs/archived/4.0/docs/advanced-guide/images/app-yaml.png differ diff --git a/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png new file mode 100644 index 000000000000..7313d36f5487 Binary files /dev/null and b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos1.png differ diff --git a/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png new file mode 100644 index 000000000000..32f0fb67f319 Binary files /dev/null and b/docs/archived/4.0/docs/examples/ai-applications/imgs/sealos2.png differ diff --git a/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md new file mode 100644 index 000000000000..c9557b00a946 --- /dev/null +++ b/docs/archived/4.0/docs/examples/ai-applications/install-fastgpt-on-desktop.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +--- + +# Quick Installation of FastGpt AI Knowledge Base + +No server, no magic, no domain name, click to deploy 👇 + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt) + +The database needs to be deployed. After the deployment, you need to wait 2 to 4 minutes to access the database. The minimum configuration is used by default, and the first access is a little slow. + +![](./imgs/sealos1.png) + +## Run + +Click on the [external address] provided by sealos to use it. The login user name is root, and the password is the newly set environment variable (1234 in the figure above) + +![](./imgs/sealos2.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/baas/install-fireboom.md b/docs/archived/4.0/docs/examples/baas/install-fireboom.md new file mode 100644 index 000000000000..4b9708a73a73 --- /dev/null +++ b/docs/archived/4.0/docs/examples/baas/install-fireboom.md @@ -0,0 +1,103 @@ +# Quick Installation of Fireboom + +[Fireboom](https://fireboom.io) is a visual API development platform designed for developers, providing a declarative development framework, a visual editing suite, and a hook mechanism. + + +Official website: https://fireboom.io + +**Features** + +- Minute-level delivery: Reduce the interface development time from 2 days to 2 minutes in traditional mode. + +- Front-end to full-stack: Visual development significantly reduces the threshold for interface development, allowing front-end developers to easily become full-stack developers. + +- Back-end without bricklaying: Declarative programming avoids copy and paste, reduces the probability of bugs, and allows back-end developers to focus on business development. + +## Step 1: Create a New Application + +- Open the "Application Management" application in the [Sealos](https://cloud.sealos.io) desktop environment. + +![](../images/fireboom/1-1.png) + +- In "Application Management", click "Create New Application" to create a new application. + + +## Step 2: Application Deployment + +- Basic Configuration: + + - Application Name (custom): fireboom + + - Image Name (default latest version): fireboomapi/fireboom_server:latest + + - CPU (recommended): 2 Core (minimum 1 Core) + + - Memory (recommended): 4 G (minimum 512 M) + +- Deployment Mode: + + - Number of Instances (custom): 1 + +![](../images/fireboom/2-1.png) + +- Network Configuration: + + - Control Panel: 9123 + - API Endpoint: 9991, enable external access + +![](../images/fireboom/2-2.png) + +- Advanced Configuration: + + - Command Settings: + - Parameter Settings: dev (dev for development mode, start for production mode) + + - Persistent Storage: + + - Metadata: /fbserver/store + - Uploaded Files: /fbserver/upload + + +![](../images/fireboom/2-3.png) + +## Step 3: Deploy the Application + +1. Click "Deploy Application" to start deploying the application. + +![](../images/fireboom/3-1.png) + +2. Click "Details" to check. When the STATUS of the application changes from Pending to Running, it means that the application has been successfully launched. + +![](../images/fireboom/3-2.png) + +## Step 4: Access the Application + +- Access the Control Panel: Domain corresponding to 9123 + +![](../images/fireboom/4-1.png) + +- Access the API Endpoint: Domain corresponding to 9991 + +``` +status ok +``` + +## Step 5: Configure Fireboom (optional) + +After setting, you can easily use the Swagger document~ + +1. View the public address of the API: Settings -> System + +![](../images/fireboom/5-1.png) + +2. Modify it to the public domain of the API endpoint, corresponding to 9991 + + - Static Value: Select static value and set it to the public domain of the **API Endpoint** + - Environment Variable: Go to Environment Variables, find FB_API_PUBLIC_URL and set it to the public domain of the **API Endpoint** + +![](../images/fireboom/5-2.png) + + +3. Use the Swagger document to test the API + +![](../images/fireboom/5-3.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/blog-platform/install-halo.md b/docs/archived/4.0/docs/examples/blog-platform/install-halo.md new file mode 100644 index 000000000000..1f81e39bc787 --- /dev/null +++ b/docs/archived/4.0/docs/examples/blog-platform/install-halo.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 2 +--- + +# Quick Installation of Halo Blogging Platform + +Deploy Halo Blogging Platform swiftly on Sealos + +![](../images/halo_img-1.png) + +[Halo](https://github.com/halo-dev/halo) is a powerful and easy-to-use open-source website building tool. +This article will introduce how to deploy Halo on Sealos, while deploying PostgreSQL on Sealos can help you easily manage and maintain the database to meet different business needs. + +## Step 1: Deploy PostgreSQL on Sealos + +### First, open [Sealos](https://cloud.sealos.io) and click on "More Apps" to enter the PostgreSQL deployment interface: + +![](../images/halo_img-2.png) + +### Fill in the configuration + +In the PostgreSQL deployment interface, click on "Create Cluster" and fill in the relevant configuration information as prompted. + +![](../images/halo_img-3.png) + +After entering the corresponding configuration, click on "Create Cluster": + +![](../images/halo_img-4.png) + +### Check the database configuration: + +![](../images/halo_img-5.png) + +Save the generated configuration containing the database username, password, and connection method for use in the following Halo configuration: + +![](../images/halo_img-6.png) + +> The PostgreSQL configuration can be seen in the image as follows: +> +> Username: root +> +> Password: jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3 +> +> pg DNS name: acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432 + +## Step 2: Deploy Halo on Sealos + +### Open App Launchpad + +![](../images/halo_img-7.png) + +![](../images/halo_img-8.png) + +### Fill in the configuration + +- Customize the application name + +- Set the image name to `halohub/halo:2.5` + +- CPU and storage should be configured according to actual conditions, memory requires 500Mi to start Halo, you can refer to the official [prepare](https://docs.halo.run/getting-started/prepare) + +- Expose port should be 8090. At the same time, setting it to external network access will automatically configure an exit domain for the application for external network access, and you also need to configure this address in Halo + +- Environment variables include database configuration and other related configurations. It is important to note that you need to configure the database connection information based on the username, password, and DNS you set in Step 1. Additionally, you need to configure the username and password for the super administrator of Halo. + +Configure Halo environment variables as follows: + +```Bash +spring.sql.init.platform=postgresql +spring.r2dbc.url=r2dbc:pool:postgresql://acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432/halo +spring.r2dbc.username=root +spring.r2dbc.password=jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3 +halo.external-url=tmtdvfjiyyfh.cloud.sealos.io +halo.security.initializer.superadminusername=root +halo.security.initializer.superadminpassword=sealos +``` + +Detailed explanation of environment variable configuration: + +| Parameter Name | Description | +|------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| spring.r2dbc.url | Database connection address, see Database Link Format below for details | +| spring.r2dbc.username | Database username | +| spring.r2dbc.password | Database password | +| spring.sql.init.platform | Database platform name, supports postgresql, mysql, h2 | +| halo.external-url | External access link, if you need public network access, you need to configure the actual access address | +| halo.security.initializer.superadminusername | Initial super administrator username | +| halo.security.initializer.superadminpassword | Initial super administrator password | + +Database link format (here we use the postgresql format): + +| Connection Method | Connection Address Format | spring.sql.init.platform | +|-------------------|------------------------------------------------------------------------------------|--------------------------| +| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql | +| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 | + +### Network configuration + +![](../images/halo_img-9.png) + +### Environment variable configuration + +![](../images/halo_img-10.png) + +### Configure Persistent Storage Volume + +Mount `/root/.halo2` directory to persist Halo data: + +![](../images/halo_img-11.png) + +## Step 3: Access Halo via the Public Network + +After successfully launching the application, you can access Halo through the public network address for configuration: + +![](../images/halo_img-12.png) + +![](../images/halo_img-13.png) + +### **Initialize Halo** + +![](../images/halo_img-14.png) + +![](../images/halo_img-15.png) + +### **First Halo:** + +![](../images/halo_img-16.png) + +![](../images/halo_img-17.png) + +![](../images/halo_img-18.png) + +## FAQ + +### Forgot Password + +#### Enter the App Launchpad and click on "Logs": + +![](../images/halo_img-19.png) + +![](../images/halo_img-20.png) + +You can see the following information in the logs: Username: `admin` Password: `QTu2J2xgUQ3ngqUo` + +#### Alternatively, you can view the logs by executing the following command in the terminal: + +![](../images/halo_img-21.png) + +``` +root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1 +2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin === +``` + +![](../images/halo_img-22.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md b/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md new file mode 100644 index 000000000000..13fbdcd12120 --- /dev/null +++ b/docs/archived/4.0/docs/examples/blog-platform/install-wordpress.md @@ -0,0 +1,103 @@ +--- +sidebar_position: 1 +--- + +# Quick Installation of WordPress + +[WordPress](https://github.com/WordPress/WordPress) is a powerful, user-friendly and open source content management system (CMS) that can be used to publish websites, news, blogs and other content online. It provides convenient methods for content editing, publishing and updating, and can also be highly customized through themes and plugins. WordPress has become a very successful and popular CMS system, with over 30 million websites built on WordPress worldwide. + +> By deploying **WordPress** with **Sealos**, you can greatly reduce deployment time and start users' content publishing journey at lightning speed. + +WordPress requires a MySQL database to be installed and used together. As a best practice for using Sealos, it is recommended to deploy the MySQL database independently using a database application, and quickly install WordPress based on the independent database. + + +## Create MySQL instance + +Open the **Database** application in the [Sealos](https://cloud.sealos.io) desktop environment, quickly set up the required database instance and wait for the database deployment to be completed. + +![](../images/wordpress_15.png) + +![](../images/wordpress_16.png) + +![](../images/wordpress_17.png) + +## Create a New Database + +When installing a MySQL instance through the **Database** application, user-defined databases are not automatically created. When connecting an application to the MySQL database, you need to manually create the corresponding database in the MySQL instance. + +You can access the database terminal command line in the following ways: + +![](../images/wordpress_12.png) +![](../images/wordpress_13.png) + +Alternatively, you can access the database terminal command line using the following methods: + +`mysql -u username -p -h yourhost` + +![](../images/wordpress_14.png) + +After successfully connecting to the database, you can create a new database using the following command: + +`CREATE DATABASE yourdb;` + +At this point, you have manually created a user-defined database in the MySQL instance. + +## App Launchpad + +Click on App Launchpad to enter the installation interface. + +![](../images/wordpress_1.png) + +## Basic Configuration + +Begin by setting a custom application name and selecting the image to install. Sealos supports both public and private image sources. In this example, the official image source is used, and if no image version is specified, the latest version will be pulled by default. + +Next, configure CPU and memory resources for WordPress. WordPress is very lightweight, and a small amount of resources is sufficient to ensure the normal operation of the program. + +![](../images/wordpress_8.png) + +## NetWork Configuration + +For network configuration, enable external access, and Sealos will automatically assign an exit domain name for users to access from the internet. If the user has already registered their custom domain, they can create a CNAME record pointing to the assigned exit domain name, allowing the custom domain to be used to access the installed application. + +Note: In subsequent demonstrations, a randomly generated exit domain name by Sealos will be used. + +![](../images/wordpress_9.png) + + +## Advanced Configuration + +In the advanced configuration, you need to configure the environment variables for WordPress based on the actual database situation. + +In the advanced configuration, you need to set environment variables and local storage for WordPress. + +Common WordPress environment variables are shown below: + +```Plain +WORDPRESS_DB_HOST IP address of the database instance +WORDPRESS_DB_USER Database user for connection +WORDPRESS_DB_PASSWORD User password required for connecting to the database +WORDPRESS_DB_NAME Name of the connected database +``` + +Based on the previously installed MySQL example, you can configure the environment variables as follows: + +```Plain +WORDPRESS_DB_HOST Database IP Address +WORDPRESS_DB_USER Database username +WORDPRESS_DB_PASSWORD Database password +``` + +The storage volume mount point for the official WordPress image is: /var/www/html. Users can allocate storage space for the application according to their needs. + +With this, WordPress is also successfully installed. Click on the details, and you can successfully access it through the external network address. + +![](../images/wordpress_10.png) + +Click on the link, and the following page shows that the installation was successful: + +![](../images/wordpress_11.png) + +**Note: If the deployment fails, first check whether the user-defined database that WordPress is trying to access exists in the MySQL instance.** + + diff --git a/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md new file mode 100644 index 000000000000..2fbc0e449e8b --- /dev/null +++ b/docs/archived/4.0/docs/examples/dial-testing-system/install-uptime-kuma.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 1 +--- + +# Quick Installation of Uptime Kuma + + + +![](../images/uptimekuma_img-10.png) + +[Uptime Kuma](https://github.com/louislam/uptime-kuma) is an open-source and easy-to-use server monitoring tool. It helps you monitor the real-time status, response time, and other key metrics of your server to ensure that it always remains in optimal condition. If you want to quickly install Uptime Kuma, follow these steps: + +### Step 1: First, enter Sealos and open the App Launchpad + +![](../images/uptimekuma_img-1.png) + +### Step 2: Create a New Application + +Open the App Launchpad in the [Sealos](https://cloud.sealos.io) desktop environment, click "New Application" to create a new application. + +![](../images/uptimekuma_img-2.png) + +### Step 3: Set Startup Parameters + +Configure the startup parameters as follows: + +- Set the container exposed port to 3001 +- Enable public network access to access the application via the provided domain + +![](../images/uptimekuma_img-3.png) + +![](../images/uptimekuma_img-4.png) + +### Step 4: Set up Storage Volumes + +In the advanced settings, add a storage volume mount to persist the Uptime Kuma data directory `/app/data`. This ensures that the application data is not lost when the container restarts. + +![](../images/uptimekuma_img-5.png) + +### Step 5: Deploy the Application + +Click 「Deploy Application」 to start the application: + +![](../images/uptimekuma_img-6.png) + +### Step 6: Access the Application + +Once the application is successfully launched, you can access it using its public network address. Enter the application domain in your browser to access the main interface. + +![](../images/uptimekuma_img-7.png) + +### Step 7: Create an Administrator Account + +To ensure the security of Uptime Kuma, you will need to create an administrator account when you first access the application. Fill in the basic information for the administrator account, including the username and password. After completing the form, click the "Create" button to register the account. + +![](../images/uptimekuma_img-8.png) + +### Step 8: Add Custom Monitoring Items + +![](../images/uptimekuma_img-9.png) + +![](../images/uptimekuma_img-0.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/gitea/install-gitea.md b/docs/archived/4.0/docs/examples/gitea/install-gitea.md new file mode 100644 index 000000000000..beae8c0a2ee1 --- /dev/null +++ b/docs/archived/4.0/docs/examples/gitea/install-gitea.md @@ -0,0 +1,112 @@ +# Quick Installation of Gitea + +Git with a cup of tea! Painless self-hosted all-in-one software development service, includes Git hosting, code review, team collaboration, package registry and CI/CD. + +## Step 1: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](../images/gitea-0.png) + +## Step 2: Open the App Launchpad + +![](../images/gitea-1.png) + +## Step 3: Create a new application + +- In App Launchpad, click "Create New Application" to create a new application. + +![](../images/gitea-2.png) + +## Step 4: Application deployment + +- Basic configuration: + + - Application name (custom): gitea + - Image name (default latest version): gitea/gitea:latest-rootless + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + +- Deployment mode: + + - Number of instances (custom): 1 + +![](../images/gitea-3.png) + +- Network configuration: + + - Container port: 3000 + - Accessible to the Public: enabled + +![](../images/gitea-4.png) + +- Advanced configuration: + + - Custom local storage, persist Gitea data (recommended 1 G). + + +![](../images/gitea-5.png) + +## Step 5: Deploy the application + +- Click「Deploy Application」 to start deploying the application. + +![](../images/gitea-6.png) + +## Step 6: Configure the database + +- Configure MySQL database for Gitea via Sealos +- Click Database + +![](../images/gitea-9.png) + +- Create a new database + +![](../images/gitea-10.png) + +- Basic configuration: + + - Cluster type: mysql + - Database version: ac-mysql-8.0.30 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + + +![](../images/gitea-11.png) + +- After deployment is successful, enter the details page to view the MySQL connection information + +![](../images/gitea-12.png) + +- Click "One-click Connection" to enter the MySQL terminal connection + +![](../images/gitea-13.png) + +- Execute + +```sql +CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +``` + +Create the database needed for Gitea + +![](../images/gitea-14.png) + +## Step 7: Access the application + +- Click 「App Launchpad」 to view.When the application's STATUS changes from Pending to Running, it means the application has started successfully. + +- When the STATUS is Running, you can directly access the external address. + +![](../images/gitea-7.png) + +- After accessing the external address, enter the configuration page and configure according to the details of the MySQL just created + +![](../images/gitea-15.png) + +- After configuring, click Install, wait for a while and you can enter the login page, deployment successful! + +![](../images/gitea-16.png) + +![](../images/gitea-17.png) + diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png new file mode 100644 index 000000000000..025a0c4e45a9 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-10-52-H2ZWUZ.png differ diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png new file mode 100644 index 000000000000..1ca1cd0ed53a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-31-vrNHJU.png differ diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png new file mode 100644 index 000000000000..5ba9b3ad8784 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-1iudM0.png differ diff --git a/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png new file mode 100644 index 000000000000..ac8de099a1ae Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2022-04-10-14-32-ADfk8T.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg new file mode 100644 index 000000000000..1b1fe554ea55 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-54-EIVahX.jpg differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg new file mode 100644 index 000000000000..a773666c4ff2 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-55-NDkuEg.jpg differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png new file mode 100644 index 000000000000..fdc343a80edc Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-11-59-FxJE12.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png new file mode 100644 index 000000000000..b4d2db4792a7 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-01-DKect7.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png new file mode 100644 index 000000000000..d512ad14195a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-05-CWczxm.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png new file mode 100644 index 000000000000..28a241ade20c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-06-lvv6ms.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png new file mode 100644 index 000000000000..aa8e415fb5b7 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-07-s8W7iu.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png new file mode 100644 index 000000000000..0935a645d999 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-09-RslDGj.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png new file mode 100644 index 000000000000..a446015a5c59 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-24-QHYKZt.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png new file mode 100644 index 000000000000..6996e718eb53 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-26-HYOaBJ.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png new file mode 100644 index 000000000000..c76f2e0535ae Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-28-ccnUOj.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png new file mode 100644 index 000000000000..ba4d166331c6 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-29-z5E9gi.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg new file mode 100644 index 000000000000..7ca839b52b2f Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png new file mode 100644 index 000000000000..ac7134504f86 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-FwsbfW.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png new file mode 100644 index 000000000000..59460d087f22 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-Vs9ccy.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png new file mode 100644 index 000000000000..0fc1e2d152b9 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-YFHPYc.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png new file mode 100644 index 000000000000..00e7aa10391a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-hdHfxP.png differ diff --git a/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png new file mode 100644 index 000000000000..08ff96c0c126 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/2023-06-26-13-09-nwrxrv.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-1.png b/docs/archived/4.0/docs/examples/images/appsmith-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-2.png b/docs/archived/4.0/docs/examples/images/appsmith-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-3.png b/docs/archived/4.0/docs/examples/images/appsmith-3.png new file mode 100644 index 000000000000..01876eeff7a0 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-4.png b/docs/archived/4.0/docs/examples/images/appsmith-4.png new file mode 100644 index 000000000000..fad0d90a71df Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-4.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-5.png b/docs/archived/4.0/docs/examples/images/appsmith-5.png new file mode 100644 index 000000000000..d6567da245ae Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-5.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-6.png b/docs/archived/4.0/docs/examples/images/appsmith-6.png new file mode 100644 index 000000000000..78b505e75298 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-6.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-7.png b/docs/archived/4.0/docs/examples/images/appsmith-7.png new file mode 100644 index 000000000000..9771645376a2 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-7.png differ diff --git a/docs/archived/4.0/docs/examples/images/appsmith-8.png b/docs/archived/4.0/docs/examples/images/appsmith-8.png new file mode 100644 index 000000000000..ca7f7e62e60c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/appsmith-8.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/1-1.png b/docs/archived/4.0/docs/examples/images/fireboom/1-1.png new file mode 100644 index 000000000000..2e8a8d45325b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/1-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-1.png b/docs/archived/4.0/docs/examples/images/fireboom/2-1.png new file mode 100644 index 000000000000..5fb4c0ec51f5 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-2.png b/docs/archived/4.0/docs/examples/images/fireboom/2-2.png new file mode 100644 index 000000000000..714ad3891845 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/2-3.png b/docs/archived/4.0/docs/examples/images/fireboom/2-3.png new file mode 100644 index 000000000000..e0d5a1a605d4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/2-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/3-1.png b/docs/archived/4.0/docs/examples/images/fireboom/3-1.png new file mode 100644 index 000000000000..10d9ef1b793a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/3-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/3-2.png b/docs/archived/4.0/docs/examples/images/fireboom/3-2.png new file mode 100644 index 000000000000..eef62cdfc5f9 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/3-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/4-1.png b/docs/archived/4.0/docs/examples/images/fireboom/4-1.png new file mode 100644 index 000000000000..f717abd1abf4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/4-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-1.png b/docs/archived/4.0/docs/examples/images/fireboom/5-1.png new file mode 100644 index 000000000000..477f9ebd26ef Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-2.png b/docs/archived/4.0/docs/examples/images/fireboom/5-2.png new file mode 100644 index 000000000000..e353d414d31b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/5-3.png b/docs/archived/4.0/docs/examples/images/fireboom/5-3.png new file mode 100644 index 000000000000..d1bddbf12c65 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/5-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg new file mode 100644 index 000000000000..7f9660c847fa Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-architecture.jpeg differ diff --git a/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg new file mode 100644 index 000000000000..848bd2b5d97d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/fireboom/fireboom-intro.jpeg differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-0.png b/docs/archived/4.0/docs/examples/images/gitea-0.png new file mode 100644 index 000000000000..946d921d4ce3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-0.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-1.png b/docs/archived/4.0/docs/examples/images/gitea-1.png new file mode 100644 index 000000000000..cae3923a16fb Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-10.png b/docs/archived/4.0/docs/examples/images/gitea-10.png new file mode 100644 index 000000000000..0087724d0b14 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-10.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-11.png b/docs/archived/4.0/docs/examples/images/gitea-11.png new file mode 100644 index 000000000000..b820dc0b7817 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-11.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-12.png b/docs/archived/4.0/docs/examples/images/gitea-12.png new file mode 100644 index 000000000000..31c2451f5e33 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-12.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-13.png b/docs/archived/4.0/docs/examples/images/gitea-13.png new file mode 100644 index 000000000000..5557c68da9c4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-13.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-14.png b/docs/archived/4.0/docs/examples/images/gitea-14.png new file mode 100644 index 000000000000..566c44f970bd Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-14.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-15.png b/docs/archived/4.0/docs/examples/images/gitea-15.png new file mode 100644 index 000000000000..90e280b8053a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-15.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-16.png b/docs/archived/4.0/docs/examples/images/gitea-16.png new file mode 100644 index 000000000000..901bcb8ac4ae Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-16.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-17.png b/docs/archived/4.0/docs/examples/images/gitea-17.png new file mode 100644 index 000000000000..d0f6300f2494 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-17.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-2.png b/docs/archived/4.0/docs/examples/images/gitea-2.png new file mode 100644 index 000000000000..295b148d1798 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-3.png b/docs/archived/4.0/docs/examples/images/gitea-3.png new file mode 100644 index 000000000000..2d9a9a6554b5 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-4.png b/docs/archived/4.0/docs/examples/images/gitea-4.png new file mode 100644 index 000000000000..9a7d30cba2e6 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-4.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-5.png b/docs/archived/4.0/docs/examples/images/gitea-5.png new file mode 100644 index 000000000000..9fe9a5f7ac9d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-5.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-6.png b/docs/archived/4.0/docs/examples/images/gitea-6.png new file mode 100644 index 000000000000..e7db5c844514 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-6.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-7.png b/docs/archived/4.0/docs/examples/images/gitea-7.png new file mode 100644 index 000000000000..6ced9251709d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-7.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-8.png b/docs/archived/4.0/docs/examples/images/gitea-8.png new file mode 100644 index 000000000000..9395b5fdd4de Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-8.png differ diff --git a/docs/archived/4.0/docs/examples/images/gitea-9.png b/docs/archived/4.0/docs/examples/images/gitea-9.png new file mode 100644 index 000000000000..9da2f0456b52 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/gitea-9.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-1.png b/docs/archived/4.0/docs/examples/images/halo_img-1.png new file mode 100644 index 000000000000..ccc68e469f5e Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-10.png b/docs/archived/4.0/docs/examples/images/halo_img-10.png new file mode 100644 index 000000000000..e1137ca41541 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-10.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-11.png b/docs/archived/4.0/docs/examples/images/halo_img-11.png new file mode 100644 index 000000000000..707d5e9c313f Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-11.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-12.png b/docs/archived/4.0/docs/examples/images/halo_img-12.png new file mode 100644 index 000000000000..a62bad32ef89 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-12.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-13.png b/docs/archived/4.0/docs/examples/images/halo_img-13.png new file mode 100644 index 000000000000..d270bab0a421 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-13.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-14.png b/docs/archived/4.0/docs/examples/images/halo_img-14.png new file mode 100644 index 000000000000..2865276b96df Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-14.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-15.png b/docs/archived/4.0/docs/examples/images/halo_img-15.png new file mode 100644 index 000000000000..c9afff34a93e Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-15.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-16.png b/docs/archived/4.0/docs/examples/images/halo_img-16.png new file mode 100644 index 000000000000..8d843a1f0e7f Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-16.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-17.png b/docs/archived/4.0/docs/examples/images/halo_img-17.png new file mode 100644 index 000000000000..6e7d3f8e8585 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-17.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-18.png b/docs/archived/4.0/docs/examples/images/halo_img-18.png new file mode 100644 index 000000000000..a3d0fcb8dd93 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-18.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-19.png b/docs/archived/4.0/docs/examples/images/halo_img-19.png new file mode 100644 index 000000000000..5254c15d9678 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-19.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-2.png b/docs/archived/4.0/docs/examples/images/halo_img-2.png new file mode 100644 index 000000000000..5a5cfc3f2684 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-20.png b/docs/archived/4.0/docs/examples/images/halo_img-20.png new file mode 100644 index 000000000000..d20e5101e4b4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-20.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-21.png b/docs/archived/4.0/docs/examples/images/halo_img-21.png new file mode 100644 index 000000000000..42dcb2303a18 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-21.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-22.png b/docs/archived/4.0/docs/examples/images/halo_img-22.png new file mode 100644 index 000000000000..e560a5aec5b7 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-22.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-3.png b/docs/archived/4.0/docs/examples/images/halo_img-3.png new file mode 100644 index 000000000000..3a23c04cef78 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-4.png b/docs/archived/4.0/docs/examples/images/halo_img-4.png new file mode 100644 index 000000000000..13665f4535d1 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-4.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-5.png b/docs/archived/4.0/docs/examples/images/halo_img-5.png new file mode 100644 index 000000000000..200db6367fde Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-5.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-6.png b/docs/archived/4.0/docs/examples/images/halo_img-6.png new file mode 100644 index 000000000000..19ce13c9b30b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-6.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-7.png b/docs/archived/4.0/docs/examples/images/halo_img-7.png new file mode 100644 index 000000000000..d450ab20f873 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-7.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-8.png b/docs/archived/4.0/docs/examples/images/halo_img-8.png new file mode 100644 index 000000000000..5d857a1b40e7 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-8.png differ diff --git a/docs/archived/4.0/docs/examples/images/halo_img-9.png b/docs/archived/4.0/docs/examples/images/halo_img-9.png new file mode 100644 index 000000000000..4e32d5475cf3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/halo_img-9.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-1.png b/docs/archived/4.0/docs/examples/images/pageplug-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-10.png b/docs/archived/4.0/docs/examples/images/pageplug-10.png new file mode 100644 index 000000000000..0aa0f2546ce3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-10.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-11.png b/docs/archived/4.0/docs/examples/images/pageplug-11.png new file mode 100644 index 000000000000..ef702aadb7e3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-11.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-2.png b/docs/archived/4.0/docs/examples/images/pageplug-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-3.png b/docs/archived/4.0/docs/examples/images/pageplug-3.png new file mode 100644 index 000000000000..00edfb9382b2 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-4.png b/docs/archived/4.0/docs/examples/images/pageplug-4.png new file mode 100644 index 000000000000..c947ad852ca0 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-4.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-5.png b/docs/archived/4.0/docs/examples/images/pageplug-5.png new file mode 100644 index 000000000000..289789fd639f Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-5.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-6.png b/docs/archived/4.0/docs/examples/images/pageplug-6.png new file mode 100644 index 000000000000..a58fbc16537b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-6.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-7.png b/docs/archived/4.0/docs/examples/images/pageplug-7.png new file mode 100644 index 000000000000..414ff44e067b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-7.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-8.png b/docs/archived/4.0/docs/examples/images/pageplug-8.png new file mode 100644 index 000000000000..dc7bb21bd4eb Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-8.png differ diff --git a/docs/archived/4.0/docs/examples/images/pageplug-9.png b/docs/archived/4.0/docs/examples/images/pageplug-9.png new file mode 100644 index 000000000000..0011841a7d14 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/pageplug-9.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/0.png b/docs/archived/4.0/docs/examples/images/tailchat/0.png new file mode 100644 index 000000000000..197223de4a8c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/0.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/1.png b/docs/archived/4.0/docs/examples/images/tailchat/1.png new file mode 100644 index 000000000000..3eced36954fd Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/1.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/10.png b/docs/archived/4.0/docs/examples/images/tailchat/10.png new file mode 100644 index 000000000000..599ead17397d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/10.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/11.png b/docs/archived/4.0/docs/examples/images/tailchat/11.png new file mode 100644 index 000000000000..2841c01c2057 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/11.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/12.png b/docs/archived/4.0/docs/examples/images/tailchat/12.png new file mode 100644 index 000000000000..43310c129d89 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/12.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/13.png b/docs/archived/4.0/docs/examples/images/tailchat/13.png new file mode 100644 index 000000000000..c89cc68375b3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/13.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/14.png b/docs/archived/4.0/docs/examples/images/tailchat/14.png new file mode 100644 index 000000000000..3c637c422d93 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/14.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/15.png b/docs/archived/4.0/docs/examples/images/tailchat/15.png new file mode 100644 index 000000000000..3203e025d9fb Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/15.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/16.png b/docs/archived/4.0/docs/examples/images/tailchat/16.png new file mode 100644 index 000000000000..5f9b13ef61eb Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/16.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/17.png b/docs/archived/4.0/docs/examples/images/tailchat/17.png new file mode 100644 index 000000000000..21e6091eb4bd Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/17.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/2.png b/docs/archived/4.0/docs/examples/images/tailchat/2.png new file mode 100644 index 000000000000..234358eae33c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/2.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/3.png b/docs/archived/4.0/docs/examples/images/tailchat/3.png new file mode 100644 index 000000000000..e8341c9130cb Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/3.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/4.png b/docs/archived/4.0/docs/examples/images/tailchat/4.png new file mode 100644 index 000000000000..8d8db3ca8bb8 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/4.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/5.png b/docs/archived/4.0/docs/examples/images/tailchat/5.png new file mode 100644 index 000000000000..4efdc860247d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/5.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/6.png b/docs/archived/4.0/docs/examples/images/tailchat/6.png new file mode 100644 index 000000000000..f0dc20aee71a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/6.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/7.png b/docs/archived/4.0/docs/examples/images/tailchat/7.png new file mode 100644 index 000000000000..563c28b48c19 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/7.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/8.png b/docs/archived/4.0/docs/examples/images/tailchat/8.png new file mode 100644 index 000000000000..602a961b7035 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/8.png differ diff --git a/docs/archived/4.0/docs/examples/images/tailchat/9.png b/docs/archived/4.0/docs/examples/images/tailchat/9.png new file mode 100644 index 000000000000..06ad0d6ea766 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/tailchat/9.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png new file mode 100644 index 000000000000..e2940389dae3 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-0.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png new file mode 100644 index 000000000000..b250072db39a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-1.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png new file mode 100644 index 000000000000..6411f1d99ccf Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-10.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png new file mode 100644 index 000000000000..083044758d59 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-2.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png new file mode 100644 index 000000000000..566495613ee1 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-3.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png new file mode 100644 index 000000000000..ec61518674cf Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-4.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png new file mode 100644 index 000000000000..4e5a2e13b63a Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-5.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png new file mode 100644 index 000000000000..cdeb8f3db368 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-6.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png new file mode 100644 index 000000000000..a33d5580c9dd Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-7.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png new file mode 100644 index 000000000000..850d72fbb991 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-8.png differ diff --git a/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png b/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png new file mode 100644 index 000000000000..2003e96b9002 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/uptimekuma_img-9.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_1.png b/docs/archived/4.0/docs/examples/images/wordpress_1.png new file mode 100644 index 000000000000..bbfd6001594b Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_1.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_10.png b/docs/archived/4.0/docs/examples/images/wordpress_10.png new file mode 100644 index 000000000000..d1cd7993028d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_10.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_11.png b/docs/archived/4.0/docs/examples/images/wordpress_11.png new file mode 100644 index 000000000000..103bf474654c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_11.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_12.png b/docs/archived/4.0/docs/examples/images/wordpress_12.png new file mode 100644 index 000000000000..80c69d185806 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_12.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_13.png b/docs/archived/4.0/docs/examples/images/wordpress_13.png new file mode 100644 index 000000000000..250114b79ef9 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_13.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_14.png b/docs/archived/4.0/docs/examples/images/wordpress_14.png new file mode 100644 index 000000000000..d180432b02c4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_14.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_15.png b/docs/archived/4.0/docs/examples/images/wordpress_15.png new file mode 100644 index 000000000000..877112038094 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_15.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_16.png b/docs/archived/4.0/docs/examples/images/wordpress_16.png new file mode 100644 index 000000000000..b490fade878c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_16.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_17.png b/docs/archived/4.0/docs/examples/images/wordpress_17.png new file mode 100644 index 000000000000..9d0bc0391604 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_17.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_2.png b/docs/archived/4.0/docs/examples/images/wordpress_2.png new file mode 100644 index 000000000000..83254bdb6cd1 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_2.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_3.png b/docs/archived/4.0/docs/examples/images/wordpress_3.png new file mode 100644 index 000000000000..1b0a1f926bb8 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_3.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_4.png b/docs/archived/4.0/docs/examples/images/wordpress_4.png new file mode 100644 index 000000000000..86ad0a605c7d Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_4.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_5.png b/docs/archived/4.0/docs/examples/images/wordpress_5.png new file mode 100644 index 000000000000..1a9a1566ae1c Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_5.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_6.png b/docs/archived/4.0/docs/examples/images/wordpress_6.png new file mode 100644 index 000000000000..52b0dc908d42 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_6.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_7.png b/docs/archived/4.0/docs/examples/images/wordpress_7.png new file mode 100644 index 000000000000..a18bfa6d1703 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_7.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_8.png b/docs/archived/4.0/docs/examples/images/wordpress_8.png new file mode 100644 index 000000000000..484684f79ae4 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_8.png differ diff --git a/docs/archived/4.0/docs/examples/images/wordpress_9.png b/docs/archived/4.0/docs/examples/images/wordpress_9.png new file mode 100644 index 000000000000..24db5cadb900 Binary files /dev/null and b/docs/archived/4.0/docs/examples/images/wordpress_9.png differ diff --git a/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md b/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md new file mode 100644 index 000000000000..93333797c2af --- /dev/null +++ b/docs/archived/4.0/docs/examples/low-code-platform/install-appsmith.md @@ -0,0 +1,65 @@ +# Quick installation of Appsmith + +[Appsmith](https://github.com/appsmithorg/appsmith) is an open-source platform to build, deploy, and maintain internal apps. You can build anything from simple CRUD apps, admin panels, dashboards to custom business apps and complicated multi-step workflows. + +## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment + +![](../images/appsmith-1.png) + +## Step 2: create a new application + +- In App Launchpad, click "New Application" to create a new application. + +![](../images/appsmith-2.png) + +## Step 3: application deployment + +- Basic configuration: + + - Application name (Custom): appsmith + + - Image name (default latest version): appsmith/appsmith-ce + + - CPU (recommended): 2 Core + + - Memory (recommended): 4 GB + +- deployment mode: + + - number of instances (custom): 1 + +![](../images/appsmith-3.png) + +- Network configuration: + + - Container exposure port: 80 + + - Public network access: enabled + +![](../images/appsmith-4.png) + +- Advanced configuration: + + - Customize local storage and persist Appsmith data (15 GB is recommended). + +![](../images/appsmith-5.png) + +## Step 4: deploy the application + +- Click "deploy Application" to start deploying the application. + +![](../images/appsmith-6.png) + +## Step 5: access the application + +- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the application has been launched successfully. + +- When STATUS is Running, you can directly access the public network address. + +![](../images/appsmith-7.png) + +- The visit was successful! + +![](../images/appsmith-8.png) + + diff --git a/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md b/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md new file mode 100644 index 000000000000..833ae0ea299b --- /dev/null +++ b/docs/archived/4.0/docs/examples/low-code-platform/install-pageplug.md @@ -0,0 +1,72 @@ +# Quick installation of PagePlug + +[PagePlug](https://github.com/cloudtogo/pageplug) is a Chinese project of [Appsmith](https://github.com/appsmithorg/appsmith), which optimizes the overall performance and Sinicizes based on Appsmith, and also integrates the characteristic form solution Formily component, chart solution Echarts component, low code Mini Program development, etc. +Is an open source, declarative, visual, intuitive front-end low-code framework for research and development. + +## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment + +![](../images/pageplug-1.png) + +## Step 2: create a new application + +- In App Launchpad, click "New Application" to create a new application. + +![](../images/pageplug-2.png) + +## Step 3: application deployment + +- Basic configuration: + + - Application name (Custom): pageplug + + - Image name (default latest version): cloudtogouser/pageplug-ce + + - CPU (recommended): 2 Core + + - Memory (recommended): 4 GB + +- Deployment model: + + - Number of instances (custom): 1 + +![](../images/pageplug-3.png) + +- Network configuration: + + - Container exposure port: 80 + + - Public network access: enabled + +![](../images/pageplug-4.png) + +- Advanced configuration: + + - Customize local storage and persist PagePlug data. + +![](../images/pageplug-5.png) + +## Step 4: deploy the application + +- Click "deploy Application" to start deploying the application. + +![](../images/pageplug-6.png) + +## Step 5: access the application + +- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the application has been launched successfully. + +![](../images/pageplug-7.png) + +![](../images/pageplug-8.png) + +- When STATUS is Running, you can directly access the public network address. + +![](../images/pageplug-9.png) + +- If a 503 exception occurs in the access, wait for a while and try again. + +![](../images/pageplug-10.png) + +- The visit was successful! + +![](../images/pageplug-11.png) diff --git a/docs/archived/4.0/docs/examples/others/install-anki.md b/docs/archived/4.0/docs/examples/others/install-anki.md new file mode 100644 index 000000000000..a368f5c0bfd7 --- /dev/null +++ b/docs/archived/4.0/docs/examples/others/install-anki.md @@ -0,0 +1,144 @@ +# Quick Installation of a Custom Anki Sync Server + +Anki is a spaced repetition flashcard program. Essentially, it is a card sorting tool that **actively tests users on custom card content, allows users to self-grade their responses, and then uses an algorithm to reschedule cards based on the judgments to optimize long-term retention**. + +The so-called "cards" are technically called flash cards. They are small cards with a question or prompt on one side and the answer on the reverse side. You first read the question/prompt, try to recall the answer mentally, then flip the card to verify against the answer provided. + +The core principle of flashcard creation is:**one knowledge point per card**. As such, Anki is well-suited for learning languages, memorizing historical dates, formulas, etc. + +Anki's official sync server is hosted overseas and is a personal project with limited bandwidth. Syncing is very slow. To sync learning progress and new cards across multiple clients would be extremely painful. + +To address this, we need to deploy a custom sync server and have the clients connect to it instead. + +## Deploying an Anki Sync Server + +Since the February 2023 release of Anki 2.1.57 for desktop, Anki on desktop, Android, and iOS now support custom sync servers without needing to install plugins. Anki users no longer need to worry about sync issues. The longstanding sync problem has finally been completely solved. + +Currently, the only project that supports the latest Anki versions is [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs). Other sync server projects are now mostly obsolete. This Rust project tracks progress on Anki's official sync server and also uses sqlite as the backend data store. + +Below we will deploy anki-sync-server-rs on Sealos and configure it. + +First, enter the URL https://cloud.sealos.io/ in your browser to access the Sealos desktop. Then open "App Launchpad": + +![](../images/2023-06-26-11-54-EIVahX.jpg) + +Click "Create Application": + +![](../images/2023-06-26-11-55-NDkuEg.jpg) + +Enter the application name and image name. The exposed container port is `27701`. Enable external network access: + +![](../images/2023-06-26-11-59-FxJE12.png) + +Scroll down and expand "Advanced Configuration". Click "Edit Environment Variables": + +![](../images/2023-06-26-12-01-DKect7.png) + +Paste the following into the environment variable input box: + +``` +bash +Copy code + +ANKISYNCD_USERNAME= +ANKISYNCD_PASSWORD= +``` + +Replace `` with your username and `` with your password. + +![](../images/2023-06-26-12-05-CWczxm.png) + +Click "Add Storage Volume": + +![](../images/2023-06-26-12-06-lvv6ms.png) + +Set the mount path to `/app` and confirm: + +![](../images/2023-06-26-12-07-s8W7iu.png) + +Finally, click "Deploy Application" in the top right corner. + +After deployment, click "Details" to enter the application details screen. + +![](../images/2023-06-26-12-09-RslDGj.png) + +Here you can see the instance status. Wait until the status shows as "running" before proceeding. If it stays pending for a while, click "Details" to check the failure reason: + +![](../images/2023-06-26-13-09-Vs9ccy.png) + +Once deployed successfully, you can monitor metrics like CPU and memory usage. Click the external URL to directly access the sync server's web interface. + +![](../images/2023-06-26-13-09-YFHPYc.png) + +If you see the following screen, the deployment succeeded: + +![](../images/2023-06-26-13-09-FwsbfW.png) + +Viewing logs is also straightforward - click the "three dots" on the instance panel and select "Logs": + +![](../images/2023-06-26-13-09-hdHfxP.png) + +![](../images/2023-06-26-13-09-nwrxrv.png) + +## Client Configuration + +### Desktop + +To configure the desktop client (macOS/Windows/Linux): + +1. Open "Preferences" + + ![](../images/2023-06-26-12-24-QHYKZt.png) + +2. Click "Network" and look for the "self-hosted sync server" section. Enter your server's address: + + ![](../images/2023-06-26-12-26-HYOaBJ.png) + +3. Restart Anki and click "Sync": + + ![](../images/2023-06-26-12-28-ccnUOj.png) + +4. A prompt will appear asking for username and password. Enter what you configured earlier: + + ![](../images/2023-06-26-12-29-z5E9gi.png) + +5. Click OK and syncing will begin. + +### Android + +On Android, directly configure via "Settings -> Advanced -> Custom sync server": + +![](../images/2022-04-10-14-31-vrNHJU.png) + +Also enter username and password: + +> Settings -> Basic -> AnkiWeb Account + +This completes the configuration. All card decks should sync over: + +![](../images/2022-04-10-14-32-ADfk8T.png) +![](../images/2022-04-10-14-32-1iudM0.png) + +The official app version is quite old. For the latest community version, download the Beta here: + +- https://github.com/ankidroid/Anki-Android/releases + +The **arm64-v8a** version is recommended. + +After installing, the sync server can be configured under "Settings -> Sync -> Custom sync server": + +![](../images/2023-06-26-12-39-1jsF0t.jpeg) + +Also enter the username and password: + +> Settings -> Sync -> AnkiWeb Account + +### iOS + +AnkiMobile also supports syncing with a custom server. At least version Ankimobile 2.0.90(20090.2) seems to work based on [reports in the Anki forums](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862). + +If you encounter sync issues after configuring, try toggling "Allow Anki to access local network" off and on in iOS settings per Anki's docs: + +> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and on again. + +The tip above is excerpted from the [Anki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup). \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md new file mode 100644 index 000000000000..619b7db8abc8 --- /dev/null +++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Go Apps.md @@ -0,0 +1,143 @@ +# Quick installation of Go Apps + +First, make sure you have installed the following tools: + +- Docker + +## Step 1: Write Your Go Program + +```go +package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") + }) + + http.ListenAndServe(":8080", nil) +} +``` + +## Step 2: Create Docker Image + +- In the same directory as `main.go`, create a file named `Dockerfile` with the following content: + +``` +FROM golang:1.17 as builder + +WORKDIR /app +COPY . . + +RUN go mod init myapp +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /app/main /app/ + +EXPOSE 8080 +ENTRYPOINT ["/app/main"] +``` + +This `Dockerfile` defines a multi-stage build process. In the first stage, we use the official `golang` image as the base image and then compile the Go application. In the second stage, we use the lightweight `alpine` image, copy the compiled binary file to the `/app` directory, and expose port 8080. + +- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Go application: + +``` +docker build -t your_image_name . +``` + +Replace `your_image_name` with your image name and tag. + +## Step 3: Push Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., latest). + + For example: + + ``` + docker tag demo damager6666/demo:latest + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/demo:latest + ``` + +## Step 4: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 5: Open the "App Launchpad" App + +![](images/java-example-3.png) + +## Step 6: Create a New Application + +- In "App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 7: Application Deployment + +- Basic Configuration: + - Application Name (custom): go-demo + - Image Name: damager6666/demo:latest + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment Mode: + - Number of Instances (custom): 1 + +![](images/java-example-5.png) + +- Network Configuration: + - Container Exposed Port: 8080 + - Internet Access: Enable + +![](images/java-example-6.png) + +## Step 8: Deploy Application + +- Click "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 9: Access Application + +- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the application has started successfully. +- When STATUS is Running, you can directly access the external network address. + +![](images/java-example-8.png) + +- In the browser, enter + +``` +https://tmgkflgdlstl.cloud.sealos.io/hello +``` + +![](images/java-example-9.png) + +- The page displays "Hello, World!", indicating that your Go application is running on Sealos. \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md new file mode 100644 index 000000000000..fbec744248df --- /dev/null +++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Java Apps.md @@ -0,0 +1,259 @@ +# Quick installation of Java Apps + +First, make sure you have installed the following tools: + +- Docker +- Maven + +## Step 1: Write Your Java Program + +- Here, directly generate a basic Spring Boot project through [Spring Initializr](https://start.spring.io/). + +![](images/java-example-1.png) + +- Open the project's `pom.xml` file and add the following content to the `` section to include the MySQL JDBC driver: + + ``` + + mysql + mysql-connector-java + + ``` + +- Next, add the following content to the `application.properties` file in the `src/main/resources` directory to configure the database connection: + + ``` + spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 + spring.datasource.username=root + spring.datasource.password=your_password + spring.jpa.hibernate.ddl-auto=update + ``` + +## Step 2: Create Entity and Repository Classes + +Create a new Java class in the project, such as `Person.java`, to represent the entity in the database. Add the following content: + +```java +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + // Getters and setters + // ... +} +``` + +- Then create a repository interface, such as `PersonRepository.java`: + + ```java + import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.stereotype.Repository; + + @Repository + public interface PersonRepository extends JpaRepository { + } + ``` + +## Step 3: Create a Web Controller + +- In the `src/main/java` directory of the project, create a simple web controller. For example, create a file named `PersonController.java` and add the following content: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class PersonController { + @Autowired + private PersonRepository personRepository; + + @GetMapping("/getPersons") + public String getAllPersons() { + List persons = personRepository.findAll(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < persons.size(); i++) { + sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n"); + } + System.out.println(sb.toString()); + return sb.toString(); + } + + @PostMapping(value = "/addPerson") + public Person addPerson(@RequestBody Person person) { + return personRepository.save(person); + } + + +} +``` + +## Step 4: Build the Application + +- Build the project using Maven by running the following command: + + ``` + mvn clean install + ``` + +![](images/java-example-2.png) + +## Step 5: Create a Docker Image + +- In the root directory of the Java project, create a file named `Dockerfile`. This file will contain the instructions required to build the Docker image. Here's a simple example Dockerfile: + + ```` + # Use the official OpenJDK image as the base image + FROM openjdk:8-jre-slim + + # Set the working directory + WORKDIR /app + + # Copy the built JAR file into the image + COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar + + # Expose the application's port + EXPOSE 8080 + + # Set the start command + CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"] + ``` + ```` + +- In the project root directory, run the following command to build the Docker image: + +``` +docker build -t java-demo . +``` + +## Step 6: Push the Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., `latest`). + + - Then, push the tagged image to the Docker repository: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + +## Step 7: Log in to Sealos + +- Visit the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 8: Open the "Database" application + +![](images/java-example-10.png) + +## Step 9: Create a database + +- Click on "Create New Database" + +![](images/java-example-11.png) + +- Basic configuration: + - Cluster type: mysql + - Cluster name: demo-db + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + - Number of instances: 1 + - Storage capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 10: Configure the database + +![](images/java-example-12.png) + +- Click on "One-click Connect to Database" and execute the following statements: + + - Create the `test_db` database + + ```sql + create database test_db; + ``` + + - Create the `test` table + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 11: Open the "App Launchpad" application + +![](images/java-example-3.png) + +## Step 12: Create a new application + +- In "App Launchpad", click on "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 13: Deploy the application + +- Basic configuration: + - Application name (custom): java-demo + - Image name: damager6666/java-demo:v2 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment mode: + - Number of instances (custom): 1 + +![](images/java-example-5.png) + +- Network configuration: + - Exposed container port: 8080 + - Public network access: enabled + +![](images/java-example-6.png) + +## Step 14: Deploy the application + +- Click on "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 15: Access the application + +- Click on "Application Management" to view the application. When the application's STATUS changes from Pending to Running, it indicates that the application has started successfully. +- When the STATUS is Running, you can directly access the public network address. + +![](images/java-example-8.png) + +- Enter the following in your browser: + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/java-example-13.png) + +- The page displays the data inserted into the database earlier, indicating that your Java application is now running on Sealos. + diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md new file mode 100644 index 000000000000..c8ec7fd3323e --- /dev/null +++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Node.js Apps.md @@ -0,0 +1,218 @@ +# Quick installation of Node.js Apps + +First, make sure you have installed the following tools: + +- Docker + +## Step 1: Write your Node.js program + +- Create a file named `app.js` in your directory and add the following code: + + ```js + const express = require('express'); + const mysql = require('mysql'); + const app = express(); + + const db = mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + + db.connect((err) => { + if (err) throw err; + console.log('Connected to the database.'); + }); + + app.get('/', (req, res) => { + db.query('SELECT id, name FROM users', (err, results) => { + if (err) throw err; + res.send(JSON.stringify(results)); + }); + }); + + app.listen(8080, () => { + console.log('Server is running on port 8080'); + }); + ``` + +- Create a file named `package.json` in the root directory of your project, and include the following content: + + ~~~json + { + "name": "nodejs-web-app-mysql", + "version": "1.0.0", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "^4.17.1", + "mysql": "^2.18.1" + } + } + ``` + ~~~ + +## Step 2: Create a Docker image + +- In the root directory of your project, create a file named `Dockerfile`. This file will contain the instructions needed to build the Docker image. Here is a simple example Dockerfile: + +```dockerfile +FROM node:16 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "start"] +``` + +## Step 3: Build the Docker image + +- Run the following command in the directory containing the `Dockerfile` to build the Docker image: + + ``` + docker build -t nodejs-demo . + ``` + +## Step 4: Push the Docker image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you have logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with the name of your local image, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g. latest). + + For example: + + ``` + docker tag nodejs-demo damager6666/nodejs-demo:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/nodejs-demo:v1 + ``` + +## Step 5: Login to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) website. + +![](images/java-example-0.png) + +## Step 6: Open the "Database" application + +![](images/java-example-10.png) + +## Step 7: Create a database + +- Click on "Create a database" + +![](images/java-example-11.png) + +- Basic configuration: + - Cluster type: mysql + - Cluster name: demo-db + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + - Number of instances: 1 + - Storage capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 8: Configure the database + +![](images/java-example-12.png) + +- Click "Connect to the database" to execute the following statements: + + - Create a database named test_db + + ```sql + create database test_db; + ``` + + - Create a table named test + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert some data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 9: Open the "App Launchpad" application + +![](images/java-example-3.png) + +## Step 10: Create a new application + +- In App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 11: Deploy the application + +- Basic configuration: + - Application name (customizable): nodejs-demo + - Image name : damager6666/nodejs-demo:v1 + - CPU (recommended): 1 core + - Memory (recommended): 1 G +- Deployment mode: + - Number of instances (customizable): 1 + +![](images/nodejs-example-0.png) + +- Network configuration: + - Exposed container port: 8080 + - External network access: enabled + +![](images/java-example-6.png) + +## Step 12: Deploy the application + +- Click "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 13: Access the application + +- Click "App Launchpad" to view the application status. When the STATUS changes from Pending to Running, it means that the application has been successfully launched. +- When the STATUS is Running, you can directly access the external network address. + +![](images/java-example-8.png) + +- Enter the following URL in your browser: + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/nodejs-example-1.png) + +- The page will display the data inserted into the database, indicating that your Node.js application is running on Sealos. \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md new file mode 100644 index 000000000000..7e4885dfc28f --- /dev/null +++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Python Apps.md @@ -0,0 +1,237 @@ +# Quick installation of Python Apps + +First, make sure you have the following tools installed: + +- Docker + +## Step 1: Write your Python program + +- Create a file named `app.py` in your project directory and add the following code: + + ```python + from flask import Flask, render_template + import mysql.connector + + app = Flask(__name__) + + def get_data_from_database(): + cnx = mysql.connector.connect( + host="java-demo-db-mysql.ns-7otl3mb2.svc", + user="root", + password="l9h8f24b", + database="test_db" + ) + + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + data = cursor.fetchall() + cursor.close() + cnx.close() + + return data + + @app.route('/') + def index(): + data = get_data_from_database() + return render_template('index.html', data=data) + + if __name__ == '__main__': + app.run(host='0.0.0.0', debug=True) + + ``` + +- Create a file named `requirements.txt` in the directory to list the dependencies required by the application. + +``` +Flask +mysql-connector-python +``` + +- Create a folder named `templates` in your project directory and create a file named `index.html` inside it. Add the following content to `index.html`: + + ```html + + + + + Data from MySQL + + +

Data from MySQL

+ + + + + + + {% for row in data %} + + + + + + {% endfor %} +
Column1Column2Column3
{{ row[0] }}{{ row[1] }}{{ row[2] }}
+ + + + Replace `Column1`, `Column2`, and `Column3` with the actual column names you want to display. + ``` + +## Step 2: Create a Docker image + +- Create a file named `Dockerfile` in the root directory of the project. This file will contain the instructions needed to build the Docker image. Here is a simple example Dockerfile: + + ```dockerfile + FROM python:3.8 + + WORKDIR /app + + COPY requirements.txt . + + RUN pip install --no-cache-dir -r requirements.txt + + COPY . . + + EXPOSE 5000 + + CMD ["python", "app.py"] + ``` + +## Step 3: Build the Docker image + +- Run the following command in the directory containing the `Dockerfile` to build the Docker image: + + ``` + docker build -t python-demo . + ``` + +## Step 4: Push the Docker image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming that you have logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g. latest). + + For example: + + ``` + docker tag python-demo damager6666/python-demo:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/python-demo:v1 + ``` + +## Step 5: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) website. + +![](images/java-example-0.png) + +## Step 6: Open the "Database" application + +![](images/java-example-10.png) + +## Step 7: Create a database + +- Click "Create Database" + +![](images/java-example-11.png) + +- Basic Configuration: + - Cluster Type: mysql + - Cluster Name: demo-db + - CPU (Recommended): 1 Core + - Memory (Recommended): 1 G + - Number of Instances: 1 + - Storage Capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 8: Configure the database + +![](images/java-example-12.png) + +- Click "Connect to the Database" and execute the following SQL statements: + + - Create the test_db database + + ```sql + create database test_db; + ``` + + - Create the test table + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 9: Open the "App Management" application + +![](images/java-example-3.png) + +## Step 10: Create a new app + +- In "App Management", click "New App" to create a new app. + +![](images/java-example-4.png) + +## Step 11: App deployment + +- Basic Configuration: + - Application Name (Customized): python-demo + - Image Name: damager6666/python-demo:v1 + - CPU (Recommended): 1 Core + - Memory (Recommended): 1 G +- Deployment Mode: + - Number of Instances (Customized): 1 + +![](images/python-example-0.png) + +- Network Configuration: + - Container Exposed Port: 5000 + - External Access: Enabled + + + +## Step 12: Deploy the app + +![](images/python-example-2.png) + +## Step 13: Access the app + +- Click "App Management" to check when the STATUS of the app changes from "Pending" to "Running", indicating that the app has been successfully launched. +- When the STATUS is Running, you can directly access the external network address. + +![](images/python-example-3.png) + +- Enter the external network address in the browser to access the data that was just stored in MySQL, indicating that the program has been successfully deployed on Sealos. + + ```http + https://mkqreaqfgwac.cloud.sealos.io/ + ``` + +![](images/python-example-4.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md new file mode 100644 index 000000000000..83b0260994c9 --- /dev/null +++ b/docs/archived/4.0/docs/examples/programming-languages/Quick installation of Vue Apps.md @@ -0,0 +1,137 @@ +# Quick installation of Vue Apps + +First, make sure you have installed the following tools: + +- Docker +- Node.js +- npm + +## Step 1: Install Vue CLI + +```bash +npm install -g @vue/cli +``` + +## Step 2: Create a new project with Vue CLI + +```bash +vue create my-vue-app +``` + +## Step 3: Build the production version of the project with Vue CLI + +```bash +cd my-vue-app +npm run build +``` + +## Step 4: Create a Docker image + +- In the `my-vue-app` directory, create a file named `Dockerfile` with the following content: + +```dockerfile +# 使用 nginx 镜像作为基础镜像 +FROM nginx:1.21-alpine + +# 复制构建的前端文件到 nginx 容器中 +COPY dist/ /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] +``` + +- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Vue application: + +```bash +docker build -t my-vue-app . +``` + +## Step 5: Push Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., latest). + + For example: + + ``` + docker tag my-vue-app damager6666/my-vue-app:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/my-vue-app:v1 + ``` + +## Step 6: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 7: Open the "App Launchpad" App + +![](images/java-example-3.png) + +## Step 8: Create a New Application + +- In "App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 9: Application Deployment + +- Basic Configuration: + - Application Name (custom): my-vue-app + - Image Name: damager6666/my-vue-app:v1 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment Mode: + - Number of Instances (custom): 1 + +![](images/vue-example-0.png) + +- Network Configuration: + - Container Exposed Port: 80 + - Internet Access: Enable + +![](images/vue-example-1.png) + +## Step 10: Deploy Application + +- Click "Deploy Application" to start deploying the application. + +![](images/vue-example-3.png) + +## Step 11: Access Application + +- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the application has started successfully. +- When STATUS is Running, you can directly access the external network address. + +![](images/vue-example-2.png) + +- In the browser, enter + +``` +https://mvpztqzczudy.cloud.sealos.io +``` + +![](images/vue-example-4.png)) + +- The page displays "Hello, Vue.js!", indicating that your Vue application is running on Sealos. \ No newline at end of file diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png new file mode 100644 index 000000000000..6ef487195a8c Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-0.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png new file mode 100644 index 000000000000..b149426e0e64 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-1.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png new file mode 100644 index 000000000000..3b10465747db Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-10.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png new file mode 100644 index 000000000000..b1cf562ca8c0 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-11.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png new file mode 100644 index 000000000000..6eedee90f87c Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-12.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png new file mode 100644 index 000000000000..3548b505e15f Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-13.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png new file mode 100644 index 000000000000..179f4b85a5fb Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-2.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png new file mode 100644 index 000000000000..3aa6b5e891aa Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-3.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png new file mode 100644 index 000000000000..bcb67e312203 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-4.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png new file mode 100644 index 000000000000..21df65190ff2 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-5.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png new file mode 100644 index 000000000000..ae30ba940afc Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-6.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png new file mode 100644 index 000000000000..f77e89f54c54 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-7.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png new file mode 100644 index 000000000000..19a4f0f3276e Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-8.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png new file mode 100644 index 000000000000..b5582f255cd6 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/java-example-9.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png new file mode 100644 index 000000000000..6e95ed74b246 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-0.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png new file mode 100644 index 000000000000..ad9450487f21 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/nodejs-example-1.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png new file mode 100644 index 000000000000..86c16c2d71df Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-0.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png new file mode 100644 index 000000000000..b783a48a786c Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-1.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png new file mode 100644 index 000000000000..86144296b4aa Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-2.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png new file mode 100644 index 000000000000..54ceb5547f34 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-3.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png new file mode 100644 index 000000000000..3ffbe07560ba Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/python-example-4.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png new file mode 100644 index 000000000000..648276d14120 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-0.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png new file mode 100644 index 000000000000..e47a1b27115d Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-1.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png new file mode 100644 index 000000000000..dab8d56621b8 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-2.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png new file mode 100644 index 000000000000..9ac654f24398 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-3.png differ diff --git a/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png new file mode 100644 index 000000000000..b00209521090 Binary files /dev/null and b/docs/archived/4.0/docs/examples/programming-languages/images/vue-example-4.png differ diff --git a/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md b/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md new file mode 100644 index 000000000000..d81e411431ad --- /dev/null +++ b/docs/archived/4.0/docs/examples/social-communication/install-tailchat.md @@ -0,0 +1,147 @@ +# Quick installation of Tailchat + +[Tailchat](https://tailchat.msgbyte.com/) is an open source IM application that is pluggable and easy to expand. Plugin architecture gives Tailchat unlimited possibilities. + +Front-end micro-kernel architecture + back-end micro-service architecture makes Tailchat to control any customized/privatized scenarios + +Created for enterprises and private domain users, highly free group management and customized panel display allow private domain owners to better display their works, manage users, and build their own brand and circle. + +![](../images/tailchat/0.png) + +## First, enter Sealos and open "Application Management" + +![](../images/tailchat/1.png) + +## Create a new application + +![](../images/tailchat/2.png) + +### Create dependencies + +As an enterprise-level application, `tailchat` has the minimum dependencies of `mongodb`, `redis`, and `minio`. Let's create them one by one. + +#### MongoDB + +For convenience, we will fix one instance and bind it to local storage. The image used is `mongo:4`. Note that because we did not set a password for the database, do not provide network services to the public network. The container exposes port 27017, which is the default database service port. The content is as follows: + +![](../images/tailchat/3.png) + +Click "Deploy Application" to submit the deployment. Wait patiently for a while, and you can see that the application has started up. + +![](../images/tailchat/4.png) + +> Note: that the initial allocation of 64m is too small for MongoDB, so I changed it to 128m by modifying the application. Resource allocation can be changed at any time, which is also a convenient feature of Sealos/Kubernetes. + +#### Minio + +Next, we will create Minio, an open-source object storage service. We can also quickly create it through Sealos's UI. The image used is `minio/minio`. Note that we need to make some adjustments: + +- Expose port: 9000 +- Change the run command to: `minio` +- Change the command parameters to: `server /data` +- Set environment variables: + - MINIO_ROOT_USER: tailchat + - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat +- Local storage: `/data` + +The final result is as follows: + +![](../images/tailchat/5.png) + +Click the "Deploy" button and you can see that the service has started up normally. + +#### Redis + +Finally, we need to deploy Redis as a content cache and message forwarding. The image used is `redis:alpine`, and the exposed port is `6379`. The final result is as follows: + +![](../images/tailchat/6.png) + +### Create Tailchat itself + +At this point, all the dependencies required by Tailchat have been deployed, as shown below: + +![](../images/tailchat/7.png) + +Now we can deploy the Tailchat itself. The Tailchat itself will be relatively complex, but because Sealos is purely UI-based, it will not be too complicated. + +- Use image: `moonrailgun/tailchat` +- Expose port: `11000` (remember to open external access) +- Configure environment variables as follows: + ``` + SERVICEDIR=services,plugins + TRANSPORTER=redis://redis:6379 + REDIS_URL=redis://redis:6379 + MONGO_URL=mongodb://mongo/tailchat + MINIO_URL=minio:9000 + MINIO_USER=tailchat + MINIO_PASS=com.msgbyte.tailchat + ``` + +The final effect is as follows: + +![](../images/tailchat/8.png) + +After waiting patiently for a while, you can see that the Tailchat service has started up. + +![](../images/tailchat/9.png) + +## Preview service + +First, we can check the availability of the Tailchat service by adding `/health` to the external address provided by the service, such as `https://.cloud.sealos.io/health`. When it starts up, the Tailchat service will return content like this: + +![](../images/tailchat/10.png) + +This JSON string contains the image version used, node name, system usage, and microservice loading status. Here we can see that my common services, such as `user`/`chat.message`, and some services with plugin prefixes such as `plugin.registry`, have all started up normally, indicating that our server is running normally. Now we can directly access our external address and see that after a short loading time, the page opens normally and automatically jumps to the login page. + +![](../images/tailchat/11.png) + +Register an account casually, and you can see that we can enter the main interface of Tailchat normally, as shown in the following figure: + +![](../images/tailchat/12.png) + +At this point, our service has successfully landed in Sealos. + +## Scaling service + +Of course, as a distributed architecture system, Tailchat naturally supports horizontal scaling. In Sealos, scaling is also very simple. Just modify the number of instances through the change operation: + +![](../images/tailchat/13.png) + +![](../images/tailchat/14.png) + +![](../images/tailchat/15.png) + +At this point, when we access `https://.cloud.sealos.io/health`, we can see that we can access different nodes. + +![](../images/tailchat/16.png) + +## Add Tailchat entry to desktop + +Open Terminal, enter `vim app.yml` to create and edit a configuration file + +Enter the following content, note that the url should be replaced with the url deployed by yourself + +```yml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: tailchat-app-entry +spec: + name: Tailchat + icon: + type: iframe + data: + url: + desc: + icon: https://tailchat.msgbyte.com/img/logo.svg + menuData: + displayType: normal +``` + +Press `esc` to exit edit mode, press `:wq` to save and exit vim + +Type `kubectl apply -f app.yml` to start the configuration. + +After refreshing the page, we can see that our entry appears on the desktop of `sealos` + +![](../images/tailchat/17.png) diff --git a/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md b/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md new file mode 100644 index 000000000000..dbdbb52ee6f7 --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/add-domain.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 2 +--- + +# Add a domain + +Assigning a custom domain to your project guarantees that visitors to your application will have a tailored experience that aligns with your brand. + +## When Deploying + +Just enable "Public Access" when you're deploying, and [Sealos](https://cloud.sealos.io) will sorts you out with a domain. + +![](./images/app-launchpad-domain.png) + +Now, on your domain provider's end, link the 'CNAME' to the one Sealos provided. + +Once it's active, jump back to Sealos, click on "Custom Domain" to the side: + +![](./images/app-launchpad-domain2.png) + +Enter your custom domain in the pop-up box and click confirm. + +![](./images/app-launchpad-domain3.png) + +To wrap up, click the "Deploy" button. Once your app's live, click on the external address to access the app via the custom domain. + +## Post Deployment + +For the apps you've deployed, just click "Update" top-right on the app details page. Then, follow the earlier steps to integrate your custom domain. \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md b/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md new file mode 100644 index 000000000000..4f12047ba05b --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/applaunchpad.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 1 +--- + +# App Launchpad + +**App Launchpad** is a feature within Sealos that serves as a single-image deployment tool. Its main goal is to streamline and expedite the process of deploying applications, allowing you to launch your application in as little as 5 minutes. + +The tool currently boasts a range of functionalities: + +- Capability to deploy applications using private images. +- Flexibility to tailor CPU and memory resources according to the specific needs of the application. +- Support for deploying multiple replicas. +- Horizontal Pod Autoscaling (HPA) for dynamic scaling. +- Provision of external URLs for easy access from the public network. +- Option to assign custom domain to applications, enhancing both brand visibility and the user experience. +- Utilization of ConfigMap for configuration file management. +- Persistent storage solutions for application data, ensuring both its security and continuity. +- Real-time monitoring features for applications and Pods to facilitate prompt issue detection and resolution. +- Comprehensive logging of application activities, aiding in troubleshooting and performance optimization. +- Analysis of system events (Events) to extract critical insights for enhancing application performance. +- A convenient one-click feature to access the container terminal, simplifying management and debugging tasks. +- Ability to expose several ports of an application to the external network. + +## [Quick Start](/quick-start/use-app-launchpad.md) + +For quick and easy installation of commonly utilized applications. + +## [Update Application](/guides/applaunchpad/update-app.md) + +Guidance on modifying application configurations after initial deployment. + +## [Add a domain](/guides/applaunchpad/add-domain.md) + +Instructions for integrating a custom domain with your application. + +## [Exposing Multiple Ports](/guides/applaunchpad/expose-multi-ports.md) + +Details on how to make multiple ports of an application accessible externally. + +## [Environment](/guides/applaunchpad/environment.md) + +Directions for configuring applications through the use of environment variables. + +## [ConfigMap](/guides/applaunchpad/configmap.md) + +Guidelines for setting up application configurations via configuration files. + +## [Autoscaling](/guides/applaunchpad/autoscale.md) + +Strategy for autoscaling the number of application instances in response to varying workloads. + +## [Persistent Volume](/guides/applaunchpad/persistent-volume.md) + +Utilizing persistent storage for the long-term preservation of data. diff --git a/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md b/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md new file mode 100644 index 000000000000..48a5e5ebfa4f --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/autoscale.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 6 +--- + +# Autoscaling + +In [Sealos](https://cloud.sealos.io), the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" feature enables the automatic adjustment of application instance numbers to effectively respond to varying load conditions. This functionality is known as "**Autoscaling**," or more technically, the Horizontal Pod Autoscaler (HPA). + +Autoscaling operates by dynamically altering the count of application instances based on specific metrics like CPU and memory usage. This ensures that the applications run efficiently and resources are optimally utilized. + +> Key Point: In Autoscaling, "usage" typically refers to the average use across all instances of an application. For example, if an app runs on two instances, its average CPU usage is calculated as the mean of the usage of these two instances. + +The workings of Autoscaling are as follows: + +1. **Monitoring**: It continuously monitors crucial performance indicators like CPU and memory usage. +2. **Decision Making**: Based on predefined thresholds (e.g., maintaining CPU usage below 50%), it calculates the required adjustments in the instance count. +3. **Adjustment**: Following this, Autoscaling automatically instructs the Sealos controller to modify the number of instances, ensuring the usage stays within the desired range. + +For instance, if we set up an application with specific Autoscaling rules such as a maximum CPU usage of 50% and the ability for instance numbers to vary between 1 and 5, Autoscaling will: + +- Increase the number of instances when the average CPU usage exceeds 50%, up to a maximum of 5. +- Decrease the number of instances when the average CPU usage drops below 50%, but always maintain at least one instance in operation. + +![](./images/autoscale.png) diff --git a/docs/archived/4.0/docs/guides/applaunchpad/configmap.md b/docs/archived/4.0/docs/guides/applaunchpad/configmap.md new file mode 100644 index 000000000000..64f1f6488c18 --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/configmap.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 5 +--- + +# ConfgMap + +In [Sealos](https://cloud.sealos.io), the implementation of configuration files plays a vital role, particularly when the application deals with numerous or complex configurations. Differing from environment variables, configuration files are a more versatile and dependable means for managing settings. Environment variables are more apt for simple, small-scale configuration tasks. + +The primary strength of configuration files is their capability to hold and control elaborate configuration data, including aspects like configuration files, command-line arguments, and environment variables. These pieces of data can be incorporated into the container upon the launch of the application container, facilitating adjustments to the application's functionalities without the necessity of recompiling the image. + +Take, for example, the Nginx container. The utilization of configuration files in this context can be described as follows: + +- **Filename**: This pertains to a file within the Nginx container, for which references can be drawn from the instructions provided by the image supplier. +- **File Value**: This is the content corresponding to the file. In cases where the content is elaborate, it's recommended to complete editing it offline and then paste it into the specified location. +- **Key Points**: The approach involves mounting an individual file, not an entire directory. It is imperative to precisely identify the file to be mounted, rather than just a directory path. + +![](./images/applaunchpad13.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/applaunchpad/environment.md b/docs/archived/4.0/docs/guides/applaunchpad/environment.md new file mode 100644 index 000000000000..72f493ecf436 --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/environment.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 4 +--- + +# Environment + +In [Sealos](https://cloud.sealos.io), environment variables are pivotal in managing the configuration data for container applications. These variables enable the provision of essential configuration information to applications without necessitating changes to the application's code or image, thus bolstering their maintainability and scalability. + +The process of defining environment variables in the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" interface is streamlined through a bulk input method. Users can define multiple variables by entering them line by line. Each variable comprises a key and a value, separated by either an equal sign (=) or a colon (:). The interface is designed to automatically eliminate any invalid characters from the key, ensuring the accuracy and validity of the environment variables. + +![](./images/applaunchpad12.png) + +**Environment Variable Formats That Are Correctly Interpreted:** + +```shell +host=127.0.0.1 +port:3000 +name: sealos +- username=123 +- password:123 +# Comments like this line are ignored, as they don't include an equal sign (=) or a colon (:), which are the key markers. +``` + +**Environment Variable Formats That Cannot Be Interpreted:** + +```shell +host=127.0.0.1 # This line is interpreted because it contains an equal sign (=) or a colon (:). The comment here is also considered part of the variable due to the preceding equal sign (=). +``` \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md b/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md new file mode 100644 index 000000000000..ae41b094222b --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/expose-multi-ports.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 3 +--- + +# Exposing Multiple Ports + +In complex application environments, it's common for services to expose multiple ports simultaneously to cater to diverse needs. These requirements can emerge from various scenarios: + ++ **Multi-protocol support**: For example, an application might support both HTTP and HTTPS, necessitating the exposure of both ports 80 and 443. ++ **Multi-functional application**: An application might have a web service and an admin service, both of which listen on different ports. ++ **Compatibility considerations**: To remain compatible with older versions or other services, you might need to expose ports for both the new and old interfaces. ++ **Combined Database + App**: For instance, if you have an application and a database within the same Pod, you might need to expose ports for both the application and the database. ++ **Prometheus Monitoring and App Service**: If your application has a business port and another port for Prometheus monitoring via `/metrics`, you might need to expose both. ++ **Coexistence of GRPC and RESTful services**: If your application offers both GRPC and RESTful services, you might need to expose separate ports for each type of service. + +When deploying applications using "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" on [Sealos](https://cloud.sealos.io), you can easily choose to expose multiple ports. During the deployment process, users simply click on the "Network" option and then select "Add Port" to configure multiple ports. + +![](./images/multi-ports1.png) + +Furthermore, the Sealos platform offers external access to these ports. Once exposed to the public network, each port will be assigned a unique sub-domain, facilitating easier remote access and management. \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png new file mode 100644 index 000000000000..013dfd23003e Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..840e42c1e952 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain2.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..7ff6bfd47d46 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad-domain3.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg new file mode 100644 index 000000000000..1a64b79192e3 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad.jpg differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png new file mode 100644 index 000000000000..2a88dbdda281 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad10.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png new file mode 100644 index 000000000000..acc586c337db Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad11.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png new file mode 100644 index 000000000000..d8490adeccc8 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad12.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png new file mode 100644 index 000000000000..e64eadecb89a Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad3.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png new file mode 100644 index 000000000000..01c66a4b184d Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad4.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png new file mode 100644 index 000000000000..6019b61ca10f Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad6.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png new file mode 100644 index 000000000000..57114556318f Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad7.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png new file mode 100644 index 000000000000..b72c79adb57d Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad8.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png new file mode 100644 index 000000000000..8a111d8424a8 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-launchpad9.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png b/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png new file mode 100644 index 000000000000..a9461aae6b7e Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/app-list.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png new file mode 100644 index 000000000000..b92744e027a7 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad12.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png new file mode 100644 index 000000000000..5e0307ba334a Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/applaunchpad13.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png b/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png new file mode 100644 index 000000000000..0c59a5f3bfb0 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/autoscale.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png b/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png new file mode 100644 index 000000000000..74e5bd569bf1 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/multi-ports1.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png b/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png new file mode 100644 index 000000000000..af83504c71ca Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/persistent-volume.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png new file mode 100644 index 000000000000..a4ba01524001 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert1.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png new file mode 100644 index 000000000000..b7ea59eed18a Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert2.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png new file mode 100644 index 000000000000..0fddf4574890 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert3.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png new file mode 100644 index 000000000000..aa4fc627bc11 Binary files /dev/null and b/docs/archived/4.0/docs/guides/applaunchpad/images/set-cert4.png differ diff --git a/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md b/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md new file mode 100644 index 000000000000..6b0b80f0b764 --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/persistent-volume.md @@ -0,0 +1,13 @@ +--- +sidebar_position: 7 +--- + +# Persistent Volume + +[Sealos](https://cloud.sealos.io) offers a flexible environment where containers can be effortlessly created and destroyed. This flexibility is advantageous for application deployment and management, but it also raises the issue of maintaining data persistence. In scenarios where data is stored within an application container, its destruction leads to the loss of all stored data. + +To counter this problem, the use of persistent storage is essential. Persistent storage ensures that data is stored externally, thereby preserving it even through container restarts or redeployments. This is particularly vital for applications requiring data retention, like databases, file storage systems, or any services involving user data. + +For instance, in deploying Nextcloud, all data associated with its container is located in the `/var/www/html` directory. To maintain data continuity, it's necessary to use external storage solutions for persisting data in this directory. + +![](./images/persistent-volume.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md b/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md new file mode 100644 index 000000000000..8875d6483766 --- /dev/null +++ b/docs/archived/4.0/docs/guides/applaunchpad/set-cert.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 8 +--- + +# Set a Custom Domain Certificate + +Successfully set up a custom domain in "App Launchpad" but cannot access the domain, and it shows the certificate is not +secure. This is because cert-manager did not successfully issue the certificate. To resolve this issue, we can manually +set up the certificate. + +First, make sure you have successfully set up CNAME in your cloud provider and have downloaded the certificate +corresponding to your domain. + +Open "App Launchpad", set a custom domain. + +![](./images/set-cert1.png) + +Open the "Terminal", and execute the following commands in sequence. + +```bash +# Create tls.crt using the certificate file information (replace xxxx with the actual certificate file information). +cat > tls.crt < tls.key <established with the database simultaneously. | +| max_wal_size | Sets the maximum size of WAL (Write-Ahead Logging) files. | +| min_wal_size | Sets the minimum size of WAL files. | +| max_worker_processes | Sets the maximum number of background processes that
PostgreSQL can start. | +| shared_buffers | The size of memory used for data caching. | + +**Common MySQL Parameters**: + +| Parameter Name | Description | +|-------------------------|-----------------------------------------------------------| +| innodb_buffer_pool_size | Sets the size of the InnoDB buffer pool. | +| max_connections | The maximum number of concurrent connections allowed. | +| query_cache_size | The size of the query cache. | +| thread_cache_size | The size of the thread cache. | +| max_allowed_packet | The maximum packet size. | +| innodb_log_file_size | The size of the InnoDB log file. | + +**Common MongoDB Parameters**: + +| Parameter Name | Description | +|-------------------------|-----------------------------------------------------------| +| storage.dbPath | The path for storing data files. | +| storage.journal.enabled | Sets the maximum size of WAL (Write-Ahead Logging) files. | +| min_wal_size | Enables logging. | +| net.port | The server port. | +| net.bindIp | The bound IP address. | + +**Common Redis Parameters**: + +| Parameter Name | Description | +|-------------------------|-----------------------------------------------------------| +| maxclients | The maximum number of client connections. | +| maxmemory | The maximum amount of memory usage. | +| maxmemory-policy | The memory eviction policy. | +| appendonly | Switch for AOF persistence. | +| appendfsync | The frequency of AOF file flushing. | + +4. **Apply pg-config.yaml**: + +```bash +$ kubectl apply -f pg-config.yaml +``` +![config_4](./imgs/config_4.png) + +5. **Check whether pg-config.yaml is successfully applied**: +```bash +# If the status of OpsRequest is Succeed and the status of pod is Running, the application is successfully configured +$ kubectl get OpsRequest +$ kubectl get pod +``` +![config_5](./imgs/config_5.png) + +6. **Access the database to check whether the configuration takes effect**: +```bash +$ show max_connections; +``` +![config_6](./imgs/config_6.png) +![config_7](./imgs/config_7.png) + diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png new file mode 100644 index 000000000000..d8c8c62be3a6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_1.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png new file mode 100644 index 000000000000..4bd234485bf3 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_2.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png new file mode 100644 index 000000000000..1f6d2ff3383b Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_3.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png new file mode 100644 index 000000000000..02f9a61edddf Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_4.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png new file mode 100644 index 000000000000..412e0bbe7fc7 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_5.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png new file mode 100644 index 000000000000..5f49dd84f12e Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_6.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png new file mode 100644 index 000000000000..88f930a876f0 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/config-docs/imgs/config_7.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md b/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md new file mode 100644 index 000000000000..8092c4359491 --- /dev/null +++ b/docs/archived/4.0/docs/guides/dbprovider/dbprovider.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 2 +--- + +# Database + +** Database ** is the core component of an application, Sealos provides a database cluster deployment management tool, can help you quickly deploy database clusters. It supports MySQL, PostgreSQL, MongoDB, Redis, Kafka, and more. Currently the ** database ** supports the following features. + ++ Elastic expansion ++ Support multi-version and multi-type databases ++ Database connection: one-click connection, external network access ++ Database connection: manual backup, automatic backup ++ Database monitoring: resource, status, and performance monitoring ++ Database migration: public network migration and file migration ++ Database high availability: Supports multiple database instances ++ Visual database management: Create, delete, update, pause, restart the database + + +## Quick start + +Take deploying a PostgreSQL database as an example to experience the convenience of a database. + +Deployment and access in just 6 steps: + +1. Enter the Database application from the [Sealos](https://cloud.sealos.io) desktop: + +![start_1](./imgs/start_1.png) + +2. Click on the creation of a new cluster.: + +![start_2](./imgs/start_2.png) + +3. Select the corresponding database and parameters: + +![start_3](./imgs/start_3.png) + +4. Click on deployment: + +![start_4](./imgs/start_4.png) + +5. Enter DB to view details: + +![start_5](./imgs/start_5.png) + +6. Database details: + +![start_6](./imgs/start_6.png) + +![start_7](./imgs/start_7.png) + +![start_8](./imgs/start_8.png) + +![start_9](./imgs/start_9.png) + +![start_10](./imgs/start_10.png) + +![start_11](./imgs/start_11.png) + +![start_12](./imgs/start_12.png) + + +## Database connection + +### One-click connection + +1. Enter ** database **, click Connect: + +![connect_1](./imgs/connect_1.png) + +2. Manipulate the database at the terminal: + +![connect_2](./imgs/connect_2.png) + + +### Extranet access + +1. Enter ** database **, click Connect: + +![connect_3](./imgs/connect_3.png) + +2. Confirm open ** database **: + +![connect_4](./imgs/connect_4.png) + +3. Copy the database connection information: + +![connect_5](./imgs/connect_5.png) + +4. Connect to the database in the Database Connection tool: + +![connect_6](./imgs/connect_6.png) + +![connect_7](./imgs/connect_7.png) + + +## Database backup + +### Manual backup +1. Enter the database backup page, click Backup: + +![backup_1](./imgs/backup_1.png) + +2. Enter the backup information to start the backup: + +![backup_2](./imgs/backup_2.png) + +3. Check Backup status: + +![backup_3](./imgs/backup_3.png) + +![backup_4](./imgs/backup_4.png) + + +### Automatic backup +1. Enter the database backup page, click Backup: + +![backup_5](./imgs/backup_5.png) + +2. Enable automatic backup and enter backup information: + +![backup_6](./imgs/backup_6.png) + + +## Database migration + +This section uses the MySQL database as an example to describe how to migrate a database. + +### Public network migration + +Public network migration involves two databases: the source database and the target database. The source database is the data source for migration, and the target database is the data destination for migration. The following introduction demonstrates the local database as the source database. + +1. Enter the target database and connect to the target database: + +![migration_1](./imgs/migration_1.png) + +2. Create the corresponding database in the terminal interface (skip this step if the corresponding database already exists): + +![migration_2](./imgs/migration_2.png) +```bash +# Example of creating a database sql statement: +$ create database testmysql; +``` + +3. Enter the public network migration page of the target database, click the Migrate button: + +![migration_3](./imgs/migration_3.png) + +4. View migration configuration information: + +![migration_4](./imgs/migration_4.png) + +Copy the migration configuration information as follows: +```bash +# Example of an sql statement for setting configuration information: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` + +5. Set the configuration information in the source database (MySQL and Postgres require manual configuration information, Mongo does not require configuration): +```bash +# xample of an sql statement for setting configuration information: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` +![migration_5](./imgs/migration_5.png) + +6. Enter the target database, fill in the migration information of the source database, which tables to migrate, and fill in which database in the target database to migrate: + +![migration_6](./imgs/migration_6.png) + +If you need to continuously migrate data from the source database, enable continuous migration in the advanced configuration as follows: + +![migration_7](./imgs/migration_7.png) + +7. View migration task information: + +![migration_8](./imgs/migration_8.png) + +8. Enter the target database, connect to the target database, and check whether the migration data is complete: + +![migration_9](./imgs/migration_9.png) + +### File migration + +1. Enter the target database and connect to the target database: + +![migration_10](./imgs/migration_10.png) + +2. Create the corresponding database in the terminal interface (skip this step if the corresponding database already exists): +```bash +# Example of creating a database sql statement: +$ create database testmysql; +``` +![migration_11](./imgs/migration_11.png) + +3. Upload the migration file, enter the database name, and start the migration: + +![migration_12](./imgs/migration_12.png) + +4. File migrating, Wait for the migration result: + +![migration_13](./imgs/migration_13.png) + +![migration_14](./imgs/migration_14.png) + +5. Connect to the database to check whether the migrated data is complete: + +![migration_15](./imgs/migration_15.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png new file mode 100644 index 000000000000..1755f5622f18 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_1.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png new file mode 100644 index 000000000000..44917f8d0a63 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_2.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png new file mode 100644 index 000000000000..0ddf8b9aa425 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_3.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png new file mode 100644 index 000000000000..ef9cbf5e9829 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_4.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png new file mode 100644 index 000000000000..297b3965f89e Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_5.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png new file mode 100644 index 000000000000..aff13dcc9b0d Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/backup_6.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png new file mode 100644 index 000000000000..7d7152c94a1d Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_1.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png new file mode 100644 index 000000000000..46f35589f728 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_2.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png new file mode 100644 index 000000000000..3b1c76fe6024 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_3.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png new file mode 100644 index 000000000000..eb861ab4b7f9 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_4.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png new file mode 100644 index 000000000000..408532d632ba Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_5.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png new file mode 100644 index 000000000000..92f075af1fb0 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_6.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png new file mode 100644 index 000000000000..c45553504155 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/connect_7.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png new file mode 100644 index 000000000000..fc27c6626da6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_1.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png new file mode 100644 index 000000000000..fc27c6626da6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_10.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png new file mode 100644 index 000000000000..9ddbee463462 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_11.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_12.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png new file mode 100644 index 000000000000..137e4ea3d8d5 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_13.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png new file mode 100644 index 000000000000..b7a2267dd5eb Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_14.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_15.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png new file mode 100644 index 000000000000..9b2b39d5df8b Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_2.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png new file mode 100644 index 000000000000..c7f7411bd67f Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_3.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png new file mode 100644 index 000000000000..214e9f5477c3 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_4.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png new file mode 100644 index 000000000000..f86eeb99def4 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_5.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png new file mode 100644 index 000000000000..8c0949d5da92 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_6.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png new file mode 100644 index 000000000000..0f2814956c68 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_7.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png new file mode 100644 index 000000000000..d15a9d1ba453 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_8.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/migration_9.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png new file mode 100644 index 000000000000..379a09ef5664 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_1.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png new file mode 100644 index 000000000000..544b065a2be5 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_10.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png new file mode 100644 index 000000000000..2ef27b4ecb4f Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_11.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png new file mode 100644 index 000000000000..c1519b2d62fb Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_12.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png new file mode 100644 index 000000000000..f83990d4edb8 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_2.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png new file mode 100644 index 000000000000..971e368049b5 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_3.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png new file mode 100644 index 000000000000..058b39327035 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_4.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png new file mode 100644 index 000000000000..333ffbed2bb5 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_5.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png new file mode 100644 index 000000000000..42acc0ad91f7 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_6.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png new file mode 100644 index 000000000000..63f79d1ac789 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_7.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png new file mode 100644 index 000000000000..b3ab09bf8c2e Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_8.png differ diff --git a/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png new file mode 100644 index 000000000000..40b2a7308f15 Binary files /dev/null and b/docs/archived/4.0/docs/guides/dbprovider/imgs/start_9.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/1.png b/docs/archived/4.0/docs/guides/objectstorage/images/1.png new file mode 100644 index 000000000000..703fe96200f9 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/1.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/10.png b/docs/archived/4.0/docs/guides/objectstorage/images/10.png new file mode 100644 index 000000000000..e636144a8eeb Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/10.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/11.png b/docs/archived/4.0/docs/guides/objectstorage/images/11.png new file mode 100644 index 000000000000..771f415a78f2 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/11.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/12.png b/docs/archived/4.0/docs/guides/objectstorage/images/12.png new file mode 100644 index 000000000000..76c1487cf46e Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/12.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/13.png b/docs/archived/4.0/docs/guides/objectstorage/images/13.png new file mode 100644 index 000000000000..5abeca82fcee Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/13.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/14.png b/docs/archived/4.0/docs/guides/objectstorage/images/14.png new file mode 100644 index 000000000000..0c4f82ee2374 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/14.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/15.png b/docs/archived/4.0/docs/guides/objectstorage/images/15.png new file mode 100644 index 000000000000..7884c768e0d3 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/15.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/16.png b/docs/archived/4.0/docs/guides/objectstorage/images/16.png new file mode 100644 index 000000000000..3f76223396a7 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/16.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/17.png b/docs/archived/4.0/docs/guides/objectstorage/images/17.png new file mode 100644 index 000000000000..21c2d258ae63 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/17.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/2.png b/docs/archived/4.0/docs/guides/objectstorage/images/2.png new file mode 100644 index 000000000000..25d649f88c7c Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/2.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/3.png b/docs/archived/4.0/docs/guides/objectstorage/images/3.png new file mode 100644 index 000000000000..176138f21cc5 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/3.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/4.png b/docs/archived/4.0/docs/guides/objectstorage/images/4.png new file mode 100644 index 000000000000..acbf8f0358b1 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/4.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/5.png b/docs/archived/4.0/docs/guides/objectstorage/images/5.png new file mode 100644 index 000000000000..52bb97ac5538 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/5.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/6.png b/docs/archived/4.0/docs/guides/objectstorage/images/6.png new file mode 100644 index 000000000000..d9f63a24dd9a Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/6.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/7.png b/docs/archived/4.0/docs/guides/objectstorage/images/7.png new file mode 100644 index 000000000000..b5a7d5ea362d Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/7.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/8.png b/docs/archived/4.0/docs/guides/objectstorage/images/8.png new file mode 100644 index 000000000000..e0bb62a7b74b Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/8.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/images/9.png b/docs/archived/4.0/docs/guides/objectstorage/images/9.png new file mode 100644 index 000000000000..f2284d0f79b9 Binary files /dev/null and b/docs/archived/4.0/docs/guides/objectstorage/images/9.png differ diff --git a/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md b/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md new file mode 100644 index 000000000000..e6e0e365d43d --- /dev/null +++ b/docs/archived/4.0/docs/guides/objectstorage/objectstorage.md @@ -0,0 +1,189 @@ +--- +sidebar_position: 0 +--- + +# Object Storage + +**Object Storage** is Sealos' built-in object storage service, which is primarily used to store and manage unstructured +data. + +Currently, **Object Storage** has the following features: + +- Upload files to bucket +- Download files from bucket +- Expose the access permission of the bucket +- Use SDK to access bucket +- Monitors bucket resource metrics +- Static host + +## Quick start + +### Upload files to bucket + +Go to Object Storage +![](./images/1.png) + +Create a bucket +![](./images/2.png) + +Set bucket name to test and permission to private +![](./images/3.png) + +Bucket is created successfully +![](./images/4.png) + +Upload file +![](./images/5.png) + +File uploaded successfully +![](./images/6.png) + +### Expose the access permission of the bucket + +Click the Edit button +![](./images/7.png) + +Set Bucket Permission to publicRead and click the Application button +![](./images/8.png) + +Copy file link +![](./images/9.png) + +Paste to browser address bar to access files +![](./images/10.png) + +### View the access key configuration + +An Object Storage user consists of a unique access key (username) and corresponding secret key (password). Internal is +the internal access address of Object Storage, and External is the external access address of Object Storage. +![](./images/11.png) + +### Use SDK to access bucket + +The SDK requires three parameters to access bucket: AccessKey, SecretKey, and Endpoint (Internal or External). If the +Region parameter is required, us-east-1 is used by default. + +#### Go Client SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/go/API.html + +Example: Use the Go Client SDK to upload the style.css file to the sv3dd7u4-test bucket, and set the endpoint to the +external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address. + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" +) +import "github.com/minio/minio-go/v7" +import "github.com/minio/minio-go/v7/pkg/credentials" + +func main() { + endpoint := "objectstorageapi.xxx.xxx.xxx" + accessKey := "xxxxxxxx" + secretKey := "xxxxxxxxxxxxxxxx" + // init minio client + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + }) + if err != nil { + log.Fatalln(err) + } + // get local file + file, err := os.Open("./style.css") + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + fmt.Println(err) + return + } + // put object + uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"}) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Successfully uploaded bytes: ", uploadInfo) +} +``` + +File uploaded successfully +![](./images/12.png) + +#### Java Client SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/java/API.html + +Example: Use the Java Client SDK to upload the style1.css file to the sv3dd7u4-test bucket, and set the endpoint to the +external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address. + +```xml + + + io.minio + minio + 8.5.9 + +``` + +```javascript +package org.example; + +import io.minio.MinioClient; +import io.minio.UploadObjectArgs; + +public class FileUploader { + public static void main(String[] args) throws Exception { + + MinioClient minioClient = + MinioClient.builder() + .endpoint("https://objectstorageapi.xxx.xxx.xxx") + .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx") + .build(); + + + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket("sv3dd7u4-test") + .object("style1.css") + .filename("src/main/java/org/example/style1.css") + .build()); + + System.out.println("Successfully uploaded bytes."); + } +} +``` + +File uploaded successfully +![](./images/13.png) + +#### Omit other language SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/minio-drivers.html + +### Static host + +Create a permission for publicRead/publicReadwrite bucket, click Enable Hosting. +![](./images/14.png) + +Click on the address to jump to access. +![](./images/15.png) + +Click Custom Domain. +![](./images/16.png) + +In the Update area of App Launchpad, you can customize the domain. +![](./images/17.png) + + + + diff --git a/docs/archived/4.0/docs/guides/templates/images/templates.png b/docs/archived/4.0/docs/guides/templates/images/templates.png new file mode 100644 index 000000000000..ccfc2aace37c Binary files /dev/null and b/docs/archived/4.0/docs/guides/templates/images/templates.png differ diff --git a/docs/archived/4.0/docs/guides/templates/submit-template.md b/docs/archived/4.0/docs/guides/templates/submit-template.md new file mode 100644 index 000000000000..5fd6ebcd1d70 --- /dev/null +++ b/docs/archived/4.0/docs/guides/templates/submit-template.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 2 +--- + +# Submit template + +Every template in the Sealos template marketplace is directly and continuously updated from the [Sealos Template Repository](https://github.com/labring-actions/templates). For those interested in contributing new templates, the process involves submitting a Pull Request (PR) to this repository. + +To craft a new template, reference is available in the form of the [template.yaml](https://github.com/labring-actions/templates/blob/main/template.yaml) file. The system is equipped with a variety of common environment variables and functions that are accessible during the template development process. These integrated features enable the use of syntax akin to `GitHub Actions`. For instance, environment variables such as `${{ SEALOS_NAMESPACE }}` can be utilized to configure specific parameters in the template. Comprehensive details about these built-in environment variables are available in the [Template Guidelines](https://github.com/labring-actions/templates/blob/main/example.md). \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/templates/templates.md b/docs/archived/4.0/docs/guides/templates/templates.md new file mode 100644 index 000000000000..ae2fec02015d --- /dev/null +++ b/docs/archived/4.0/docs/guides/templates/templates.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 0 +--- + +# Templates + +import Highlight from '@site/src/components/Highlight' + + + +Sealos's [templates](https://template.cloud.sealos.io/) offers an array of pre-designed templates, ideal for quickly setting up and launching websites and a range of applications. This marketplace hosts a variety of template types, including blogs, AI applications, low-code solutions, cloud storage, IM applications, and middleware. These templates aim to make the development process more efficient, allowing developers to rapidly initiate and deploy projects without the need to build a website from scratch or concern themselves with the intricacies of application dependencies. + +Contrasting with Vercel's template marketplace, Sealos provides a different range of applications and functionalities. Vercel primarily targets front-end project deployment, offering templates that enhance website interfaces and user interactions. However, Vercel does not offer persistent data storage, resulting in data being temporary and lost upon application restart. + +In contrast, Sealos's application templates encompass both front-end and back-end projects, along with a variety of other applications. Significantly, Sealos includes support for persistent storage, an essential aspect for applications that require extensive data storage or need to maintain data continuity. This is particularly critical for applications like e-commerce websites, large-scale social media platforms, and enterprise-level applications, where data persistence is a key requirement. + +![](./images/templates.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png b/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png new file mode 100644 index 000000000000..f1fcc577c7f2 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/database-terminal-1.png differ diff --git a/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png b/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png new file mode 100644 index 000000000000..01dba426f6b0 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/database-terminal.png differ diff --git a/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png new file mode 100644 index 000000000000..ecb9443c9621 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal-1.png differ diff --git a/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png new file mode 100644 index 000000000000..c8fa6b4ae9f9 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/nginx-terminal.png differ diff --git a/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png b/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png new file mode 100644 index 000000000000..d9aada4d4da3 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/terminal-pod.png differ diff --git a/docs/archived/4.0/docs/guides/terminal/images/terminal.webp b/docs/archived/4.0/docs/guides/terminal/images/terminal.webp new file mode 100644 index 000000000000..d1daf72539d7 Binary files /dev/null and b/docs/archived/4.0/docs/guides/terminal/images/terminal.webp differ diff --git a/docs/archived/4.0/docs/guides/terminal/terminal.md b/docs/archived/4.0/docs/guides/terminal/terminal.md new file mode 100644 index 000000000000..3962b3c16bf6 --- /dev/null +++ b/docs/archived/4.0/docs/guides/terminal/terminal.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 4 +--- + +# Terminal + +Integrated within the [Sealos](https://cloud.sealos.io) Cloud Operating System is the Terminal, an application of profound potency. Mirroring the terminal in standalone operating systems, it facilitates users to interact directly with the operating system, executing various operations via command entry. + +![](./images/terminal.webp) + +The value of the Terminal rests in its efficiency, flexibility, and powerful capabilities. For a plethora of tasks, employing the Terminal proves swifter and more concise than utilizing a Graphical User Interface (GUI). It is an indispensable instrument for developers, permitting users to gain a profound understanding and control of the Sealos Cloud Operating System. + +**Every user's Terminal is in fact operating within a Pod of Kubernetes**, which can be observed in the Terminal using the following command: + +![](./images/terminal-pod.png) + +## Accessing the Terminal of an Application Container + +One may directly access the Terminal of any application container via the Terminal App. Suppose you have deployed an application such as Nginx via application management, one can navigate directly to the Nginx application's detail page, click on the three dots on the right side of the details, and then click on 'Terminal' to access the Terminal of the Nginx application. + +![](./images/nginx-terminal.png) + +![](./images/nginx-terminal-1.png) + +## 终端一键直连数据库 + +It is also possible to establish a direct connection to the database created in the[Database App](../dbprovider/dbprovider.md) with a single click via the Terminal. + +Navigate to the database details page and click on '一键连接' on the left: + +![](./images/database-terminal.png) + +This will take you directly to the Terminal App and connect to the database: + +![](./images/database-terminal-1.png) + +## Direct Manipulation of Kubernetes Resources via Terminal + +:::danger + +The following operations necessitate foundational knowledge of Kubernetes and container-related concepts, avoid casual execution without this understanding. + +::: + +Veteran Kubernetes users can utilize the Terminal to manipulate Kubernetes resources directly, affording many advantages for such seasoned users: + ++ Enables batch operations and automation ++ Offers high flexibility ++ Allows in-depth troubleshooting + +Below are examples of common operations with Kubernetes resources within the Terminal: + ++ View the list of user's Pods + +```bash +kubectl get pods +``` + ++ Create and manage resource + +```bash +# Create resources using YAML or JSON files +$ kubectl create -f + +# Create or update resources using YAML or JSON files +$ kubectl apply -f + +# Delete specified resource type and name +$ kubectl delete + +# View the status of a specified Deployment's rolling update +$ kubectl rollout status deployment/ +``` + ++ Troubleshooting + +```bash +# Obtain detailed information about a specific resource, including events and status +$ kubectl describe + +# Obtain logs of a specific container in a specific Pod +$ kubectl logs -c + +# Open a terminal in a specific Pod for debugging purposes +$ kubectl exec -it +``` \ No newline at end of file diff --git a/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif b/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif new file mode 100644 index 000000000000..404c7bde776a Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/accept-invite.gif differ diff --git a/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif b/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif new file mode 100644 index 000000000000..8be2cb426d82 Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/create-workspace.gif differ diff --git a/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif b/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif new file mode 100644 index 000000000000..0de5fbabc405 Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/invite-user.gif differ diff --git a/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif b/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif new file mode 100644 index 000000000000..1091cfc7ad9c Binary files /dev/null and b/docs/archived/4.0/docs/guides/workspace/images/switch-workspace.gif differ diff --git a/docs/archived/4.0/docs/guides/workspace/worksapce.md b/docs/archived/4.0/docs/guides/workspace/worksapce.md new file mode 100644 index 000000000000..68b46db27da9 --- /dev/null +++ b/docs/archived/4.0/docs/guides/workspace/worksapce.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 5 +--- + +# Team Collaboration and Workspace + +## Term Definitions + +### Workspace + +Workspace is a core feature of the Sealos Cloud Operating System, implemented based on Kubernetes namespaces. It serves +as a multi-tenant resource isolation mechanism, allowing the partitioning of a Kubernetes cluster into multiple +workspaces. Each workspace has its own resource quotas and permissions, enabling the allocation of different users to +distinct workspaces for resource isolation and permission control. + +Personal workspace is a special form of workspace. Each user has a personal workspace with a name identical to their +username. The resource quotas and permissions of personal workspaces are the same as regular workspaces. However, +personal workspaces cannot be deleted, and other users cannot be added to them. + +### Roles and Permissions + +Users within a workspace can have different roles, each with its own set of permissions. Currently, Sealos Cloud +Operating System includes the following roles: Owner, Manager, and Developer. + ++ Owner: Possesses all permissions within the workspace, including deleting the workspace, viewing/creating/modifying + all resources within the workspace, and inviting users to join the workspace as administrators/developers. ++ Manager: Possesses managerial permissions within the workspace, such as viewing/creating/modifying all resources + within the workspace and inviting users to join as developers. ++ Developer: Possesses development permissions within the workspace, including viewing the status of resources within + the workspace. + +## Quick Start + +### Create Workspace + +![create-workspace.gif](images%2Fcreate-workspace.gif) + +### Switch Workspace + +![switch-workspace.gif](images%2Fswitch-workspace.gif) + +### Invite Users to Join Workspace + +Invite users to join the workspace using their user ID. The invited users can choose to accept or decline the +invitation. If accepted, the invited users become members of the workspace with resource permissions. + +![invite-user.gif](images%2Finvite-user.gif) + +### Accept Invitation + +When a user is invited to join a workspace, they can view the invitation message in the management panel. By clicking on +the invitation message, they can see the details and accept the invitation. After acceptance, the user becomes a member +of the workspace with resource permissions. + +![accept-invite.gif](images%2Faccept-invite.gif) \ No newline at end of file diff --git a/docs/archived/4.0/docs/images/sealos-desktop.webp b/docs/archived/4.0/docs/images/sealos-desktop.webp new file mode 100644 index 000000000000..8a1e98ad2d87 Binary files /dev/null and b/docs/archived/4.0/docs/images/sealos-desktop.webp differ diff --git a/docs/archived/4.0/docs/msa/privacy-policy.md b/docs/archived/4.0/docs/msa/privacy-policy.md new file mode 100644 index 000000000000..fdf4525f71cf --- /dev/null +++ b/docs/archived/4.0/docs/msa/privacy-policy.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 2 +--- + +# Sealos Cloud Privacy Policy + +We prioritize your privacy immensely. This policy details the collection, use, disclosure, and protection of your personal information when you utilize our services. Please thoroughly understand its contents. + +**Information Collection** + +1. We may gather your personal details like name, phone number, email, and address during registration or service usage. +2. We also collect data produced during service use, such as operational logs, IP addresses, and device types. +3. For enhanced user experience, technologies like cookies may be employed to store information related to your service interaction. + +**Information Usage** + +1. The handling of your personal data adheres to legal regulations and user agreements. +2. We utilize the information for improving service quality, innovating new products, and other similar purposes. +3. Collected data might be used for delivering service-related notifications and advertisements. + +**Disclosure of Information** + +1. Your personal data will not be shared with third parties, except under these circumstances: + + 1. With your prior agreement; + 2. When legally or regulatorily required; + 3. To uphold our or other users' legal rights. + +2. We may share your information with affiliated entities or partners, ensuring data security through strict confidentiality. + +**Protection of Information** + +1. We apply various security measures, like encryption and access control, to prevent unauthorized data access, use, or disclosure. +2. Regular safety assessments are conducted on stored personal data to guarantee its security. +3. Should a data leak or similar security incident occur, we'll immediately implement emergency measures and inform you as required by law. + +**User Rights** + +1. You have complete authority to access, correct, or delete your personal data at any time. +2. Opting out of data collection can limit your access to certain service features. +3. You may request a halt in processing your personal data, which might affect service continuation. + +**Privacy Policy Updates** + +1. We may periodically update this policy, and any changes will be published on our service page. Continuing service use implies acceptance of any updated terms. +2. Regularly reviewing this policy is encouraged to stay informed about your data protection. + +**Minor Protection** + +Minors' data protection is crucial to us. If you're a minor, please engage with our services under guardian supervision, ensuring proper data management. + +**Cross-border Data Transfer** + +Considering our servers' diverse locations, your data may be transferred and processed internationally. We assure that cross-border data remains securely protected. + +**Contact Us** + +1. For any questions, suggestions, or complaints about this policy, please reach us via email fanux@sealos.io +2. We are committed to quickly resolving any issues you raise. \ No newline at end of file diff --git a/docs/archived/4.0/docs/msa/terms-of-service.md b/docs/archived/4.0/docs/msa/terms-of-service.md new file mode 100644 index 000000000000..718b60d41c64 --- /dev/null +++ b/docs/archived/4.0/docs/msa/terms-of-service.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 1 +--- + +# Sealos Cloud Terms of Service + +These Sealos Cloud Terms of Service (this “Agreement”) are entered into by and between you and Zhuhai Huanjie Cloud Computing Co., Ltd. (referred to as “we/us” or "the Company") regarding the use of our cloud services (hereafter referred to as "the Service"). We urge you to read and understand every clause, particularly those concerning limitations of our liability, restrictions on your rights, and the terms about dispute resolution and legal jurisdiction. If any aspect of this agreement is not acceptable to you, refrain from registering for or using the Service. + +**Article 1: Scope of Services** + +1. We offer a range of internet-based IT services, including but not limited to storage, computation, and network transmission. +2. Technical support and customer assistance will be provided to enhance your experience with the Service. +3. We reserve the right to modify, suspend, or discontinue any or all aspects of the Service without any obligation to you. + +**Article 2: Account Registration and Management** + +1. Prior to using the Service, registering an account is mandatory. You must ensure that the information provided at registration is accurate and updated regularly. +2. The security of your account credentials is your responsibility. Any misuse of your account should be reported to us immediately. +3. We hold the right to inspect your account and, if irregularities are detected, may halt or terminate the services provided to you. + +**Article 3: Usage Guidelines** + +1. The Service must not be used for illegal activities or in ways that infringe upon the rights of others, including intellectual property rights and confidentiality breaches. +2. The Service must not be used for malicious registration of accounts, whether for profit, speculation, or other purposes. +3. Dissemination of illegal, harmful, or malevolent content via the Service is strictly prohibited. +4. You are liable for adhering to all relevant laws and the terms of this agreement, including the responsibility for the content shared and outcomes arising from your use of the Service. + +**Article 4: Fees and Payment** + +1. You agree to the fee structure associated with the Service, as outlined in our published rates. +2. We may revise the fee structure based on market conditions, notifying you in advance. Continued use of the Service implies agreement to the revised fees. +3. Non-payment of fees may lead to the suspension or termination of the Service, and we retain the right to seek compensation. + +**Article 5: Disclaimer and Limitation of Liability** + +1. The Service is provided based on existing technology and conditions, and we do not guarantee uninterrupted, completely secure, or error-free service. +2. We are not liable for Service disruptions or unavailability due to force majeure. +3. We bear no responsibility for loss or damage of data caused by your actions. + +**Article 6: Privacy Policy** + +Your agreement to and compliance with our privacy policy, an integral part of this agreement, is required. Please refer to the detailed "Privacy Policy" section below. + +**Article 7: Intellectual Property Rights** + +1. The intellectual property of the Service and all related materials, including software and documentation, is owned by us. Unauthorized reproduction, distribution, or reverse engineering is prohibited. +2. While you retain intellectual property rights over your data and content generated within the Service, you grant us permission for necessary operations such as storage and backup to facilitate the Service. + +**Article 8: Agreement Termination** + +1. Violation of any terms of this agreement permits us to cease providing the Service immediately and to pursue legal remedies. +2. You can terminate this agreement by requesting the cancellation of your account. + +**Article 9: Dispute Resolution and Governing Law** + +1. This agreement is governed by the laws of the People's Republic of China. Disputes arising from this agreement should be resolved through negotiation; failing that, legal action may be pursued in a competent court. +2. The agreement includes provisions for resolving disputes and the applicable legal framework. + +**Article 10: Network Security** + +1. You bear the consequences for any services provided through the platform that violate national laws. +2. You are responsible for overseeing the services you offer and bear consequences for any illegal activities conducted by your users. +3. Should we discover your involvement in illegal activities, legal action will be taken and evidence provided to the authorities. + +**Article 11: Additional Provisions** + +1. Should parts of this agreement become invalid due to legal conflicts, it does not affect the validity of the remaining terms. +2. The Company retains the ultimate interpretive authority over this agreement and the privacy policy. For inquiries, please contact us. \ No newline at end of file diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png new file mode 100644 index 000000000000..834cb08f40d3 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-sokx.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png new file mode 100644 index 000000000000..b904addd338a Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tlpi.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg new file mode 100644 index 000000000000..8e477e550a4d Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-tpsc.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg new file mode 100644 index 000000000000..49ab8b95ae32 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ualx.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg new file mode 100644 index 000000000000..5f1da8a47b4b Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/SCR-20230529-ubqj.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png b/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png new file mode 100644 index 000000000000..7154f775b5d2 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment-2.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png b/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png new file mode 100644 index 000000000000..2c57fd60b2e2 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment-3.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-deployment.png b/docs/archived/4.0/docs/quick-start/images/app-deployment.png new file mode 100644 index 000000000000..3d3819f431d0 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-deployment.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg b/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg new file mode 100644 index 000000000000..1b6652464f9a Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-1.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png new file mode 100644 index 000000000000..013dfd23003e Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..840e42c1e952 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain2.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..7ff6bfd47d46 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad-domain3.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg b/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg new file mode 100644 index 000000000000..1a64b79192e3 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-launchpad.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-list-2.png b/docs/archived/4.0/docs/quick-start/images/app-list-2.png new file mode 100644 index 000000000000..355cd126eec3 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-list-2.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-list.png b/docs/archived/4.0/docs/quick-start/images/app-list.png new file mode 100644 index 000000000000..a9461aae6b7e Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-list.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-nginx.png b/docs/archived/4.0/docs/quick-start/images/app-nginx.png new file mode 100644 index 000000000000..79a1815c53e4 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-nginx.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/app-url.png b/docs/archived/4.0/docs/quick-start/images/app-url.png new file mode 100644 index 000000000000..39af8e02421d Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/app-url.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-configure.png b/docs/archived/4.0/docs/quick-start/images/database-configure.png new file mode 100644 index 000000000000..df6a0d91db4d Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-configure.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-connect.png b/docs/archived/4.0/docs/quick-start/images/database-connect.png new file mode 100644 index 000000000000..5155d12912b3 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-connect.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-creating.png b/docs/archived/4.0/docs/quick-start/images/database-creating.png new file mode 100644 index 000000000000..6589431fcd38 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-creating.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-launch.jpg b/docs/archived/4.0/docs/quick-start/images/database-launch.jpg new file mode 100644 index 000000000000..83800e085969 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-launch.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-more.png b/docs/archived/4.0/docs/quick-start/images/database-more.png new file mode 100644 index 000000000000..206a8a9b453f Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-more.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/database-select.png b/docs/archived/4.0/docs/quick-start/images/database-select.png new file mode 100644 index 000000000000..fe8ba2991ff1 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/database-select.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/deploy-template.png b/docs/archived/4.0/docs/quick-start/images/deploy-template.png new file mode 100644 index 000000000000..4c3aa7f6f9fd Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/deploy-template.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/multi-ports1.png b/docs/archived/4.0/docs/quick-start/images/multi-ports1.png new file mode 100644 index 000000000000..74e5bd569bf1 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/multi-ports1.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/nginx.png b/docs/archived/4.0/docs/quick-start/images/nginx.png new file mode 100644 index 000000000000..14adf1fcc771 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/nginx.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/template-detail.png b/docs/archived/4.0/docs/quick-start/images/template-detail.png new file mode 100644 index 000000000000..ddd16e9fc873 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/template-detail.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/templates-2.png b/docs/archived/4.0/docs/quick-start/images/templates-2.png new file mode 100644 index 000000000000..94c601d43930 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/templates-2.png differ diff --git a/docs/archived/4.0/docs/quick-start/images/templates.jpg b/docs/archived/4.0/docs/quick-start/images/templates.jpg new file mode 100644 index 000000000000..f52b412f94e5 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/templates.jpg differ diff --git a/docs/archived/4.0/docs/quick-start/images/terminal.png b/docs/archived/4.0/docs/quick-start/images/terminal.png new file mode 100644 index 000000000000..34dc2ed40a28 Binary files /dev/null and b/docs/archived/4.0/docs/quick-start/images/terminal.png differ diff --git a/docs/archived/4.0/docs/quick-start/next-steps.md b/docs/archived/4.0/docs/quick-start/next-steps.md new file mode 100644 index 000000000000..13028e9351cd --- /dev/null +++ b/docs/archived/4.0/docs/quick-start/next-steps.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 5 +--- + +# Next Steps + +Congratulations on getting started with Sealos! Now, you can continue learning more about Sealos's many features: \ No newline at end of file diff --git a/docs/archived/4.0/docs/quick-start/use-app-launchpad.md b/docs/archived/4.0/docs/quick-start/use-app-launchpad.md new file mode 100644 index 000000000000..e5d19ee02ad2 --- /dev/null +++ b/docs/archived/4.0/docs/quick-start/use-app-launchpad.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +--- + +# Use App Launchpad + +Sealos desktop is similar to that of PC operating systems, such as macOS or Windows. Each icon on the desktop represents an app, similar to the shortcuts in Windows. Like PC operating systems, Sealos also comes with built-in apps. To quickly install applications on this cloud operating system, you need to use the system's built-in **App Launchpad**. + +> For more information about application management, please refer to [Introduction to App Launchpad](/guides/applaunchpad/applaunchpad.md) + +This guide will show you how to quickly install some common applications on the Sealos cloud operating system. + +## Deploy nginx + +First, open App Launchpad in the [Sealos](https://cloud.sealos.io) desktop. + +![](./images/app-launchpad.jpg) + +Click on "New Application". + +Enter the application name and image name in sequence, enable external network access, and then click "Deploy Application" to proceed. + +![](./images/app-deployment.png) + +Once the deployment is finished, click "Details" to enter the application's detailed view. + +you can view the operational metrics of the Nginx application, including CPU and memory usage. Click on the external link to open the Nginx interface directly via the external domain name. + +![](./images/app-nginx.png) + +![](./images/SCR-20230529-sokx.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/quick-start/use-database.md b/docs/archived/4.0/docs/quick-start/use-database.md new file mode 100644 index 000000000000..f7b5bbec80bd --- /dev/null +++ b/docs/archived/4.0/docs/quick-start/use-database.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 4 +--- + +# Use Database + +Databases are fundamental tools for data management, designed for efficient storage and access. Sealos offers a user-friendly database interface, eliminating the complexities of command-line operations. This assists in managing an array of databases, such as relational databases, NoSQL, vector databases, and streaming databases. With just a few clicks in the "[Database](/guides/dbprovider/dbprovider.md)" app, you can seamlessly create a variety of databases, including MySQL, PostgreSQL, MongoDB, Redis, and many more. + +This guide will show you how to install MySQL use Databse app. + +## Install MySQL + +1. Open Database app in the [Sealos](https://cloud.sealos.io) desktop. + +![](./images/database-launch.jpg) + +2. Click on 'Create Database' and finalize the basic configuration. + +![](./images/database-configure.png) + +3. Click on 'Deploy' then click 'Confirm' to wait for the successful creation of the MySQL. + +![](./images/database-more.png) + +4. Click on 'connect', which will directly launch the "Terminal" application, leading you to the database command line. + +![](./images/database-connect.png) + +## Install PostgreSQL and MongoDB + +The installation process for PostgreSQL and MongoDB mirrors that of MySQL. Simply choose the respective database type on the basic configuration page. + +![](./images/database-select.png) diff --git a/docs/archived/4.0/docs/quick-start/use-template.md b/docs/archived/4.0/docs/quick-start/use-template.md new file mode 100644 index 000000000000..a86926811112 --- /dev/null +++ b/docs/archived/4.0/docs/quick-start/use-template.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 1 +--- + +# Use Template + +This guide will show you how to use templates to fast-track applications deployment. and maximize Sealos's features. + +## 1. Find a template + +Start by accessing the "**Templates**" in the Sealos desktop. + +![](./images/templates.jpg) + +select the template you’d like to deploy + +![Viewing the templates marketplace](./images/templates-2.png) + +Not sure which one to use? How about [exploring FastGPT](/examples/ai-applications/install-fastgpt-on-desktop.md) + +## 2. Deploy the template to Sealos + +Once you've selected a template, configure the necessary parameters, then click **Deploy Application** on the template page to start deployment. + +![Deploying your chosen template](./images/deploy-template.png) + +Once deployment concludes, click "Confirm" to navigate to the application's details. + +![](./images/template-detail.png) + +Wait for the application's status to switch to running. Subsequently, click on the external link to launch the application's Web interface directly through the external domain name. + +![](./images/app-url.png) + diff --git a/docs/archived/4.0/docs/quick-start/use-terminal.md b/docs/archived/4.0/docs/quick-start/use-terminal.md new file mode 100644 index 000000000000..3596109beb6d --- /dev/null +++ b/docs/archived/4.0/docs/quick-start/use-terminal.md @@ -0,0 +1,161 @@ +--- +sidebar_position: 3 +--- + +# Use Terminal + +Users familiar with Kubernetes and container fundamentals may opt to deploy applications using the **[Terminal](/guides/terminal/terminal.md)** app. This guide will show you how to use the terminal for deployment, using Nginx as an example. + +Start by launching the Terminal app within the [Sealos](https://cloud.sealos.io) desktop. + +![](./images/app-launchpad-1.jpg) + +Upon opening the Terminal app, a terminal window is automatically allocated, enabling you to execute various command-line operations. + +![](./images/terminal.png) + +① Begin by creating a deployment manifest for Nginx Deployment: + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + resources: + requests: + cpu: 0.2 + memory: 32Mi + limits: + cpu: 0.2 + memory: 32Mi +``` + +② Subsequently, create a deployment manifest for Nginx Service: + +```yaml +# service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + + selector: + app: nginx +``` + +③ Deploy both the Deployment and Service by executing the following: + +```bash +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml +``` + +Verify Nginx's accessibility: + +```bash +$ kubectl get pod -l app=nginx +NAME READY STATUS RESTARTS AGE +nginx-7546c75fb4-572x9 1/1 Running 0 5m37s + +$ kubectl get svc -l app=nginx +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +nginx ClusterIP 10.64.204.173 80/TCP 5m36s + +$ curl 10.64.204.173 + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +④ To expose Nginx to the internet and access it via a public domain, create an Ingress resource. Start by creating a deployment manifest: + +```yaml +# ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/backend-protocol: HTTP + nginx.ingress.kubernetes.io/client-body-buffer-size: 64k + nginx.ingress.kubernetes.io/proxy-body-size: 32m + nginx.ingress.kubernetes.io/proxy-buffer-size: 64k + nginx.ingress.kubernetes.io/server-snippet: | + client_header_buffer_size 64k; + large_client_header_buffers 4 128k; + nginx.ingress.kubernetes.io/ssl-redirect: "false" + name: nginx +spec: + rules: + - host: nginx.cloud.sealos.io + http: + paths: + - backend: + service: + name: nginx + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - nginx.cloud.sealos.io + secretName: wildcard-cloud-sealos-io-cert +``` + +⑤ Deploy the Ingress resource: + +```bash +$ kubectl apply -f ingress.yaml +``` + +⑥ Confirm the Nginx's accessibility through the public domain: + +![](./images/nginx.png) \ No newline at end of file diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md new file mode 100644 index 000000000000..7e213a92ead8 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/QA.md @@ -0,0 +1,185 @@ +--- +sidebar_position: 1 +--- + +# Frequently Asked Questions + +When using Sealos, you may encounter some common questions and issues. Here are answers and solutions to some of the common problems. + +## Image Building Issues + +### Q1: How to set up a proxy service during the build phase? + +During the execution of the build command, you can configure a proxy service by setting the HTTP_PROXY environment variable. + +```shell +HTTP_PROXY=socket5://127.0.0.1:7890 sealos build xxxxx +``` + +### Q2: How to enable debug logs for buildah? + +To view debug logs for buildah, you can set the `BUILDAH_LOG_LEVEL` environment variable. + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +### Q3: How to execute Sealos build within a Pod? + +If you want to execute Sealos build within a Pod, follow these steps: + +1. Build the image within the Pod. You can create a Deployment with the following YAML configuration: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sealoscli + name: sealoscli +spec: + replicas: 1 + selector: + matchLabels: + app: sealoscli + strategy: {} + template: + metadata: + labels: + app: sealoscli + spec: + containers: + - image: # Replace with your sealos image + name: sealoscli + stdin: true + stdinOnce: true + securityContext: + privileged: true +``` + +2. Create a Dockerfile. Here's an example that you can modify as per your needs: + +```dockerfile +FROM bitnami/minideb:buster + +ARG TARGETOS +ARG TARGETARCH + +LABEL from=bitnami/minideb:buster platform=rootcloud team=oam tag=buster name=base + +RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && sed -i "s@http://security.debian.org@http://mirrors.aliyun.com/debian-security@g" /etc/apt/sources.list +RUN install_packages curl iputils-ping net-tools telnet procps vim wget jq + +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV TZ=Asia/Shanghai +``` + +3. Execute the build command within the Pod. + +```shell +sealos build --arch arm64 --build-arg TARGETOS=linux --build-arg TARGETARCH=arm64 -t test -f Dockerfile . +``` + +### Q4: How to Build Cluster Images Using Other Build Tools? + +If you want to use other container tools like Docker or Podman to build cluster images, you can utilize [sreg](https://github.com/labring/sreg) to cache the images. + +Follow these steps: + +1. Install sreg: + ```shell + wget https://github.com/labring/sreg/releases/download/v0.1.1/sreg_0.1.1_linux_amd64.tar.gz + tar -xzf sreg_0.1.1_linux_amd64.tar.gz sreg + mv sreg /usr/bin/ + ``` +2. Cache the images: + ```shell + sreg save --registry-dir=registry . + ``` +3. Build the cluster image: + ```shell + docker build -t xxxx -f Sealfile . + ``` + +### Q5: Encounter the error "lgetxattr /var/lib/containers/storage/overlay/0c2afe770ec7870ad4639f18a1b50b3a84718f95c8907f3d54e14dbf0a01d50d/merged/dev/ptmx: no such device" during Sealos build. How to fix it? + +This issue might be related to the version of `fuse-overlayfs`. We recommend downloading the latest version from [here](https://github.com/containers/fuse-overlayfs/releases) and replacing `/bin/fuse-overlayfs`. + +## Runtime Selection Issues + +### Q1: How to select the Kubernetes runtime? + +Sealos determines the runtime based on the image you choose. If you select the `kubernetes-docker` image, Sealos will use Docker as the runtime. If you choose the `kubernetes-crio` image, Sealos will use CRI-O as the runtime. + +## Version Compatibility Issues + +### Q1: Error "Applied to cluster error: failed to + +init exec auth.sh failed exit status 127"? + +This error is often caused by a mismatch between the version of Sealos and the version of the image being used. Make sure that the image version and the Sealos version are compatible. For example, if you are using a Kubernetes version like `v1.xx.x`, you may need to upgrade Sealos, especially if you are using an older version of Sealos while the Sealos cluster image is using the latest version. Another solution is to choose the corresponding version of the Sealos image. For example, if your Sealos version is 4.1.3, then the cluster image should be something like `kubernetes:v1.24.0-4.1.3`. Ensuring that the image version and Sealos version are compatible can help avoid such issues. + +### Q2: Error when adding additional domains or modifying the service CIDR in the cluster during the addition of a master node + +To address this issue, the Sealos team made the necessary fixes in version 4.2.0. You can refer to the specific fix and discussion in this pull request: [https://github.com/labring/sealos/pull/2943](https://github.com/labring/sealos/pull/2943). + +Therefore, if you encounter this problem, we recommend upgrading to Sealos version 4.2.0. The updated version should handle these changes correctly and not produce errors when adding a master node. + +## File and Directory Location Issues + +### Q1: How to modify the default storage location for `/root/.sealos`? + +If you need to change the default storage location, you can set the `SEALOS_RUNTIME_ROOT` environment variable and then run the Sealos command. It is recommended to set this environment variable globally so that it can be conveniently used in other commands or scenarios. + +```shell +export SEALOS_RUNTIME_ROOT=/data/.sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q2: How to modify the default storage location for `/var/lib/sealos`? + +If you need to change the default storage location, you can set the `SEALOS_DATA_ROOT` environment variable and then run the Sealos command. Similarly, it is recommended to set this environment variable globally. + +```shell +export SEALOS_DATA_ROOT=/data/sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q3: How to modify the storage paths for Sealos image data and status? + +> When using the Sealos cluster, you may need to change the default storage paths for image data and status data. By default, these data are stored at the locations defined in the `/etc/containers/storage.conf` file. + +1. **View the current storage configuration** + First, you can use the following command to view the current image storage configuration: + ``` + sealos images --debug + ``` + This command will print the file that contains the current storage configuration, for example: + ``` + 2023-06-07T16:27:02 debug using file /etc/containers/storage.conf as container storage config + REPOSITORY TAG IMAGE ID CREATED SIZE + ``` +2. **Modify the storage path for image data** + If you want to change the storage path for image data, you can edit the `/etc/containers/storage.conf` file. In this file, find and modify the `graphroot` field to set it to the new path. For example: + ``` + vim /etc/containers/storage.conf + ``` + In the editor, modify the value of the `graphroot` field to the desired new path. +3. **Modify the storage + +path for status data** +Similar to the design of Buildah, Sealos also provides the ability to set the storage path for status data. In the same configuration file `/etc/containers/storage.conf`, find and modify the `runroot` field to the new path. + +By following these steps, you can save the image data and status data of the Sealos cluster to the new paths you set. Each time you run a Sealos command, it will use the new paths you set in `graphroot` and `runroot` to store the image data and status data, respectively. + +### Q4: How to disable file md5 check during SSH file transfer? + +When the network environment is good, disabling the md5 check can greatly improve transfer speed. If you don't want to check the md5 of files during SSH file transfer, you can add the `-o "HashKnownHosts no"` option to the SSH command. + +```shell +scp -o "HashKnownHosts no" local_file remote_user@remote_ip:/path/to/destination +``` + +This option tells SSH not to hash the hostnames in the known_hosts file, which avoids the md5 check during file transfer. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md new file mode 100644 index 000000000000..6a65a58e9d05 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md @@ -0,0 +1,87 @@ +--- +sidebar_position: 2 +--- + +# Guide to Image Building Improvement + +## Deep Understanding of Sealos Image Building + +To understand the work behind Sealos image building, we will first reveal what operations it actually performs at the underlying level. Here is an intuitive architectural diagram: + +![](images/build.png) + +Sealos covers the following core steps in the image building process: + +- **Cache images**: Parse the working directory during the build execution (here we call it the "context" directory), save the cache image to the registry structure, and store it in the ./registry directory. +- **Build images**: Build images in the context directory and generate new images. (Please note, you need to copy the ./registry directory when building images.) + +## Enhancing Image Building Efficiency + +In the current project, we have borrowed the source code of `github.com/distribution/distribution`. During the process of caching images, we directly call the registry's sdk and start the registry-proxy function. With the caching ability of the image repository, we cache the image and store it in the context/registry directory. + +The key to this process is to call the method of the distribution repository to save the image: + +- Start the registry-proxy function. +- Save image digest and related index data (by calling the saveManifestAndGetDigest method). +- Save image file data (by calling the saveBlobs method). + +This method does have some significant advantages: + +- Lightweight: Images can be saved without relying on other components. +- Free control: You can freely control the save logic without relying on third-party components. + +However, we have also noticed some potential problems: + +- For beginners, the code is difficult to understand and the logic here is not easy to grasp. +- Unable to cache using the token authentication method. +- Need to rely on some temporary storage space, which requires space. + +Considering these issues, we decided to try a new mode: start a lightweight registry locally and use the sdk of `skopeo copy` for code reuse. This change directly solves all the previous problems. + +![](images/registry-build.png) + +**Therefore, the new construction method ✨Image Repository Sync✨ gracefully debuts 🎉🎉** + +The [#3154](https://github.com/labring/sealos/pull/3154) PR in the official repository has completed the implementation of this feature. Currently, Sealos supports these two ways of image construction. Next, I will introduce how to start the new feature (if the new feature performs stably, we may abandon the old construction method). + +## How to Start the New Feature + +> Sealos v4.3.0 and later versions support this function by default. + +Starting the new feature is very simple, just add an environment variable before you build the image. This feature supports both build and merge commands. + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +``` + +Here is the expected output after executing the above command: + +```tex +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +Getting image source signatures +Copying blob fee740108510 done +Copying config f92f3ea6e4 done +Writing manifest to image destination +Storing signatures +Getting image source signatures +Copying blob 08409d417260 done +Copying config 44dd6f2230 done +Writing manifest to image destination +Storing signatures +2023-06-01T13:16:07 info saving images busybox, alpine +STEP 1/2: FROM scratch +STEP 2/2: COPY registry ./registry +COMMIT test +Getting image source signatures +Copying blob 13ab73c881c8 done +Copying config 4e22d16b36 done +Writing manifest to image destination +Storing signatures +--> 4e22d16 + +b366 +Successfully tagged localhost/test:latest +4e22d16b366e9fec25641522a74cbd73a7db67dc0516b8f8e00200c4d0551592 +``` + +I hope the above content can help you better understand and use Sealos's new image building method. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md new file mode 100644 index 000000000000..7cbfd71f7665 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md @@ -0,0 +1,789 @@ +--- +sidebar_position: 5 +--- + +# install Dual-stack cluster with Calico + +1. Prerequisites: + - Sealos version >=4.3.0 + - The hosts can communicate using both IPv6 and IPv4 addresses. + - Calico adopts the VXLAN mode,the kernel version must be >= 3.12 。 refer to [official docs](https://github.com/cyclinder/kubespray/blob/042c960c6617f8a360a8281464ff63f99ee2471c/docs/calico.md) +2. run`sealos gen` to generate a Clusterfile, for example: + +```shell +$ sealos gen labring/kubernetes:v1.26.1 labring/helm:v3.10.3 labring/calico:v3.25.0 --masters 192.168.0.10 --nodes 192.168.0.11 --passwd "xxx" >Clusterfile +``` + +Notice: labring/helm should be set before labring/calico. + +The generated Clusterfile is as follows: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + ExtraArgs: + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10 + ServiceSubnet: 10.96.0.0/22 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +``` + +
+ +3. After generating the Clusterfile, modify the cluster configuration.to add IPv6 pod and svc CIDR 。Here's use the fd85:ee78:d8a6:8607::1:0000/112、fd85:ee78:d8a6:8607::1000/116 as a example。The main modifications are as follows: + +
+Clusterfile + +```yaml +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a #The IPv6 address of the control node,If you need to access the APIserver using this IP,add it. + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 #Default to 64 + node-cidr-mask-size-ipv4: 24 #Default to 24 +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" #add pod IPv6 subnet +--- +# add configure dual stack for calico +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +The final Clusterfile would look like this: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 + node-cidr-mask-size-ipv4: 24 + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +4. run`sealos apply -f Clusterfile` to install the cluster。 + +5. More refer to [Calico official docs](https://docs.tigera.io/calico/latest/networking/ipam/ipv6) and [k8s offical docs](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/dual-stack-support/) + diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md new file mode 100644 index 000000000000..a844bc9ee068 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md @@ -0,0 +1,104 @@ +--- +sidebar_position: 1 +--- + +# Image Building and Standard Directory Configuration + +Before embarking on Sealos image building tasks, we suggest first constructing a directory structure that conforms to standards. This makes the building process more standardized, easier to manage, and reduces the likelihood of errors. This article will guide you in detail on how to create such a directory structure and explain the purpose of each directory. + +## Directory Structure Example + +A complete, standardized directory structure example is as follows: + +```shell +. +├── charts +│   └── nginx +│   ├── Chart.lock +│   ├── charts +│   ├── Chart.yaml +│   ├── README.md +│   ├── templates +│   ├── values.schema.json +│   └── values.yaml +├── images +│   └── shim +│   └── nginxImages +├── init.sh +├── Kubefile +├── manifests +│   └── nginx +│   ├── deployment.yaml +│   ├── ingress.yaml +│   └── service.yaml +├── opt +│   └── helm +└── registry +``` + +## Directory Descriptions + +Each directory plays a specific role during the build process, and their detailed descriptions are as follows: + +- `Kubefile` (required): This file is similar to Dockerfile and is the core file for image building. It defines various steps in the build process, such as the selection of the base image, setting of environment variables, file copying, etc. +- `manifests`: This directory is used to store Kubernetes yaml files, which describe the configuration information of your applications, such as Pod, Service, Deployment configurations. +- `charts`: This directory is used to store Helm chart files. Helm chart is a package management tool for Kubernetes that simplifies the deployment and management of Kubernetes applications. +- `images/shim`: This directory is used to store images that cannot be automatically extracted from yaml files or Helm charts. During the build process, sealos will automatically pull these images. +- `opt`: Binary files are stored here. +- `registry`: This directory is used to store images pulled locally during the build process. During the build process, this directory will be automatically generated, and there is no need to manually create it. +- `init.sh`: This script is automatically run by GitHub Action during the build process. You can write some automated tasks in this script, such as initializing the environment, preprocessing data, etc. (Following the rules of [cluster-image](https://github.com/labring-actions/cluster-image)) + +## Kubefile Parameters + +The `Kubefile` file is at the core of image building and supports various parameters. Below is a detailed analysis of these parameters: + +```shell +FROM labring/kubernetes:v1.24.0 +ENV version v1.1.0 +COPY manifests ./manifests +COPY registry ./registry +ENTRYPOINT ["kubectl apply -f manifests/tigera-operator.yaml"] +CMD ["kubectl apply -f manifests/custom-resources.yaml"] +``` + +Descriptions of each parameter: + +- `FROM`: This directive is used to set the base image for building. All build steps are based on this image. +- `LABEL`: `LABEL` defines some internal configurations of the sealos cluster image. + - `check`: Some check scripts operation before the cluster image runs. + - `clean`: Cleanup scripts for cluster reset or node deletion. + - `clean-registry`: The script to clean the image repository when the cluster is reset. + - `image`: The lvscare image address of the cluster (Sealos's IPVS image). + - `init`: Cluster initialization script. + + +- `init-registry`: The script to start the container image repository when initializing the cluster. + - `sealos.io.type`: Cluster image type, currently mainly rootfs, application, and patch. + - Rootfs is the basic image for running the cluster, such as Kubernetes, Kubernetes-docker, which includes images, binaries, etc. required by the cluster (**required for each node**). + - Application is the application image, such as calico, helm, istio, etc. application service images. (**only stored on the master0 node**) + - Patch is needed to adjust after the rootfs image. It is another way to modify the rootfs image (**another method is the Config method**), it will overwrite the first image of the default cluster running. + - `sealos.io.version`: The version number of the image, currently the opened version is v1beta1. + - `version`: The version number of the cluster, currently it's the version number of Kubernetes. + - `vip`: It's the VIP address for modifying the IPVS virtual IP. +- `ENV`: The `ENV` directive sets the environment variable `` to the value ``. (There will be some default environment variables in rootfs, which can modify some default parameters in rootfs, such as the username and password of the image repository, the storage directory of docker, containerd, etc.) + + For specific cluster images, you need to inspect it specifically, check the corresponding environment variables with `sealos inspect` image, different versions of the image have slight differences. + - SEALOS_SYS_CRI_ENDPOINT: The criSocket of the current cluster image (different types of cluster images may be different). + - criData: Data directory of cri. + - defaultVIP: Default VIP address. + - disableApparmor: Whether to disable apparmor (containerd has this issue). + - registryConfig: Configuration directory of the container image registry. + - registryData: Data directory of the container image registry (The configuration itself has no practical meaning. because the directory is mounted and it's actually stored in `/var/lib/sealos`). + - registryDomain: Default domain of the container image registry. + - registryPassword: Default Password of the container image registry. + - registryPort: Default port number of the container image registry. + - registryUsername: Default username of the container image registry. + - sandboxImage: Default sandbox_image for cri to start. (No need to write repo, just need to write image name, eg: pasue:3.7). +- `COPY`: The `COPY` directive copies new files or directories from `` and adds them to the file system path `` on the container. (**Note that the registry directory needs to be copied, otherwise the cluster has no container images**) +- `ENTRYPOINT`: This directive is used to set the startup command for the image. When the image starts, this command will be executed. +- `CMD`: This directive is also used to set the startup command for the image. However, the difference between it and the ENTRYPOINT directive is that if users provide a startup command when running the image (`sealos run --cmd`), the command in the CMD directive will be overridden. + +During the build process, Sealos will also automatically set some built-in environment variables, including (environment variables with the prefix 'SEALOS_SYS' cannot be modified): + +- SEALOS_SYS_KUBE_VERSION: The version number of Kubernetes, for example v1.26.0 +- SEALOS_SYS_SEALOS_VERSION: The version number of Sealos, for example 4.1.3. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png new file mode 100644 index 000000000000..d1ae7d6aeadd Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/build.png differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png new file mode 100644 index 000000000000..485b531d28f8 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png new file mode 100644 index 000000000000..70ef65ffa13f Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md new file mode 100644 index 000000000000..362863abc4b9 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/sealos-run.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 2 +--- + +# How Sealos Run Works + +Below is a detailed explanation of how the `sealos run` command works. To aid understanding, we break it down into nine main steps. + +## 1. Execution of Command + +Firstly, users launch Sealos by executing the following command: + +```bash +sealos run kubernetes:v1.23.8 +``` + +## 2. Execution of Pipeline + +After the command is executed, Sealos starts to execute a series of related operational steps, which we call the "Pipeline". This process includes the following substeps: + +### 2.1 Node Check + +Sealos will check all nodes to ensure they are available. For example, checking for duplicate hostnames, and whether the time of all nodes has been synchronized. + +### 2.2 Pulling Images + +Next, Sealos pulls the Kubernetes images from the remote repository and loads them into local storage. + +### 2.3 Configuration Rendering + +Then, Sealos renders the configuration of the pulled image's rootfs (file system) to prepare for the subsequent node distribution. + +### 2.4 Distribution of Image Files + +Once the configuration rendering is complete, Sealos distributes the rendered rootfs files to each node. + +### 2.5 Distribution of Registry Directory + +In addition, Sealos also distributes the registry directory in the image to the corresponding registry role nodes. Sealos supports two modes: + +- **scp mode**: Directly copy the directory to each node via scp, which consumes a lot of network bandwidth. +- **Image synchronization mode**: Using the skopeo sdk's image synchronization mechanism, incremental image synchronization can be achieved to save network bandwidth. This feature can be enabled with the environment variable `SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true`. + +### 2.6 Execution of Bootstrap + +Bootstrap is a crucial step, including the following operations: + +#### 2.6.1 Addition of Host Resolution + +Sealos adds host resolution for the registry on each node. + +#### 2.6.2 Execution of Registry-Init Script + +After adding host resolution, Sealos executes the registry-init script. + +#### 2.6.3 Execution of Init Script + +Finally, Sealos executes the init script to start the Kubernetes service. + +## 3. Execution of InitMaster0 + +After the Pipeline execution, Sealos enters the InitMaster0 phase. In this phase, Sealos performs the following operations: + +### 3.1 Generation of Kubeadm Configuration + +Firstly, Sealos generates the initialization configuration file for Kubeadm. + +### 3.2 Generation and Distribution of Certificate Files + +Then, Sealos generates all certificate files for the cluster and distributes them to each node. + +### 3.3 Initialization of Master0 + +Finally, Sealos executes `kubeadm init` to initialize the Master0 node. + +## 4. Execution of JoinMaster + +After the initialization of the Master0 node, Sealos enters the JoinMaster phase. In this phase, Sealos performs the following operations: + +### 4.1 Synchronization of Kubeconfig + +Firstly, Sealos synchronizes the Kubeconfig file to each node. + +### 4.2 Synchronization of Certificate Files + +Next, Sealos synchronizes certificate files to each node. + +### 4.3 Generation of Join Configuration + +Then, Sealos generates the JoinMaster configuration file. + +### 4.4 Generation of Join Token + +Next, Sealos generates the Join token. + +### 4.5 Execution of Join Master Process + +Finally, Sealos executes the `kubeadm join` process to add other Master nodes to the cluster. + +## 5. Execution of JoinNode + +After all Master nodes have joined the cluster, Sealos enters + +the JoinNode phase and performs the following operations: + +### 5.1 Generation of Join Token + +Firstly, Sealos generates the Join token again. + +### 5.2 Generation of IPVS Rules + +Then, Sealos generates IPVS rules. + +### 5.3 Generation of Join Configuration + +Next, Sealos generates the JoinNode configuration again. + +### 5.4 Execution of Join Node Process + +Finally, Sealos executes the Join Node process to add Worker nodes to the cluster. + +## 6. Synchronization of IPVS + +After all nodes have joined the cluster, Sealos synchronizes the IPVS rules of the Master to all nodes. This is mainly done through the lvscare static pod, which synchronizes the static pod configuration based on the number of Master nodes. + +## 7. Execution of Guest Phase + +After the IPVS rules are synchronized, Sealos executes the Guest phase. This is the stage to execute the cluster image command. + +## 8. Writing to the Clusterfile + +Finally, Sealos writes the final execution result to the Clusterfile. + +This is how the `sealos run` command works. Through this article, we hope that you have a deeper understanding of the `sealos run` command. Below is the panorama we mentioned to help you better understand the entire process. + +![](images/sealos-run.png) diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md new file mode 100644 index 000000000000..f99e389def22 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/advanced-guide/template-function.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 9 +--- + +# Template Inclusion and Function Enhancement + +In the image building process of Sealos, files with the "tmpl" extension under the "etc", "scripts", and "manifests" directories are specially treated. + +1. "etc" directory: This directory is usually used to store configuration files. During the build process, Sealos will render the files with the "tmpl" extension in this directory. The files, once rendered, are copied to the corresponding directory of the cluster image. + +2. "scripts" directory: This directory is usually used to store execution scripts. Sealos will render the files with the "tmpl" extension in this directory. The generated scripts will be executed during the cluster image build process. + +3. "manifests" directory: This directory is usually used to store Kubernetes resource manifest files. Sealos will render the files with the "tmpl" extension in this directory. The generated manifest files will be applied to the Kubernetes cluster during the cluster image build process. + +In summary, files with the "tmpl" extension in these three directories are treated as template files during the image build process of Sealos. This approach provides more flexibility, allowing us to dynamically generate configurations, scripts, or Kubernetes resource manifests during the build process. + +When building an image, we support `template` to allow maintainers to fully control the generated configuration files (module rendering). For example: + +```yaml +service: + name: {{ .serviceName }} + port: + {{- if typeIs "string" .servicePort }} + name: {{ .servicePort }} + {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} + number: {{ .servicePort | int }} + {{- end }} +``` + +Please refer to Golang's [text/template](https://pkg.go.dev/text/template) for a basic introduction and more details. + +## Template Function Enhancement + +In addition, we support `templateFunc` to enhance template functions. For example: + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://{{ .registryDomain }}:{{ .registryPort }} +force: true +debug: false +image: /var/lib/image-cri-shim +{{ if and (ne .SEALOS_SYS_KUBE_VERSION "") (semverCompare "^1.26.0" .SEALOS_SYS_KUBE_VERSION) }}version: v1{{ else }}version: v1alpha2{{ end }} +timeout: 15m +auth: {{ .registryUsername }}:{{ .registryPassword }} +``` + +Here we use `semverCompare` to check whether the user is running on k8s version v1.26.0 or above. If so, generate `version: v1`, otherwise generate `version: v1alpha2`. With this support, we can easily manage multiple versions of Kubernetes support with a single cluster image file. + +### Some Most Commonly Used Template Functions + +* [semverCompare](http://masterminds.github.io/sprig/semver.html) compares semantic versions, not string comparisons. +* [default](http://masterminds.github.io/sprig/defaults.html) default can provide a default value when a value is empty or undefined. +* [toYaml](https://github.com/labring/sealos/blob/main/lifecycle/pkg/template/funcmap.go#L66) displays the current value (object, map, array) as a yaml formatted string. + +For a complete list of supported functions, [click here](http://masterminds.github.io/sprig/). diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp b/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/images/sealos.webp differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md new file mode 100644 index 000000000000..b62233403420 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/lifecycle-management.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 0 +--- + +# Kubernetes Lifecycle Management + +Sealos provides a powerful set of tools that allow users to easily manage the entire lifecycle of a cluster. + +## Features + +With Sealos, you can install a bare Kubernetes cluster without any components. Additionally, Sealos can assemble various upper-layer distributed applications on top of Kubernetes using cluster image capabilities, such as databases, message queues, and more. + +Sealos not only allows you to install a single-node Kubernetes development environment but also enables you to build production-grade highly available clusters with thousands of nodes. + +Sealos offers features like cluster scaling, backup and recovery, and cluster release. It provides an excellent Kubernetes runtime experience even in offline environments. + +## Key Features + +- ARM support. Offline packages v1.20 and above support integration with both containerd and Docker. +- Provides 99-year certificates and supports cluster backup and upgrade. +- Does not rely on Ansible, HAProxy, or Keepalived. It is a standalone binary tool with zero dependencies. +- Provides offline installation. Different versions of Kubernetes only require different cluster images. +- High availability is achieved through localLB based on IPVS, which consumes fewer resources and provides stability and reliability, similar to kube-proxy implementation. +- Automatically recognizes image names using image-cri-shim, making offline delivery more convenient. +- Almost compatible with all x86_64 architectures that support systemd. +- Easy addition/deletion of cluster nodes. +- Trusted by tens of thousands of users in production environments, stable and reliable. +- Supports cluster images, allowing you to customize and combine the cluster components you need, such as OpenEBS storage + database + MinIO object storage. +- Uses the SDK of Buildah to standardize the image format, fully compatible with OCI standards. + +## Running a Kubernetes Cluster with Sealos + +Running a Kubernetes cluster with Sealos is straightforward. Just follow these steps: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.3.0/scripts/install.sh \ + | sh -s v4.3.0 labring/sealos +# Create a cluster +$ sealos run labring/kubernetes:v1.25.0-4.2.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +[![asciicast](https://asciinema.org/a/519263.svg)](https://asciinema.org/a/519263?speed=3) + +## Running Distributed Applications on the Cluster + +With the `sealos run` command, you can run various distributed applications on the cluster, such as databases, message queues, AI capabilities, and even enterprise-level SaaS software. For example: + +```shell +# MySQL cluster +$ sealos run labring/mysql-operator:8.0.23-14.1 + +# Clickhouse cluster +$ sealos run labring/clickhouse:0.18.4 + +# Redis cluster +$ sealos run labring/redis-operator:3.1.4 +``` + +## Customizing the Cluster + +For cluster images not available in the Sealos ecosystem, users can easily build and customize their own cluster images. For example: + +[Building an Ingress Cluster Image](/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md) + +You can also customize your own Kubernetes cluster: + +Sealfile: + +```shell +FROM kubernetes:v1.25.0 +COPY flannel-chart . +COPY mysql-chart . +CMD ["helm install flannel flannel-chart", "helm install mysql mysql-chart"] +``` + +```shell +sealos build -t my-kubernetes:v1.25.0 . +sealos run my-kubernetes:v1.25.0 ... +``` + +## Frequently Asked Questions + +**Is Sealos a Kubernetes installation tool?** + +Installation and deployment are basic functions of Sealos, similar to the boot module in a single-node operating system. Sealos' boot module effectively manages the lifecycle of Kubernetes in any scenario. + +**What are the differences between Sealos, Rancher, and KubeSphere?** + +Sealos is designed with the philosophy of "simplifying complexity, freely assembling, and simplicity as the ultimate goal." Sealos leverages the capabilities of Kubernetes to provide users with exactly what they need in a simple way. Users may not necessarily need Kubernetes; what they need is specific functionality. + +Sealos is highly flexible and does not impose additional burdens on users. Its form depends on user requirements and the applications being installed. The core of Sealos is distributed applications, and all applications are treated equally. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md new file mode 100644 index 000000000000..ef9ba1db8c37 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 4 +--- + +# Building Cluster Images Based on Binary Files + +This document primarily details how to use the `sealos` tool to package a single binary file (like `helm` or `kustomize`) into a cluster image and install them by deploying the cluster image on the master node. Using `helm` as an example, we will thoroughly discuss how to package a binary file into a cluster image. + +## Create a Build Workspace + +Firstly, create a base directory to serve as a build workspace: + +```shell +$ mkdir ~/cluster-images +``` + +In the workspace, create an `opt` directory for storing the binary files: + +```shell +$ cd cluster-images +$ mkdir opt/ +``` + +## Prepare the Binary File + +Next, we prepare the `helm` binary file. Here, we download from [github release](https://github.com/helm/helm/releases): + +```shell +wget https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz +tar -zxvf helm-v3.10.1-linux-amd64.tar.gz +chmod a+x linux-amd64/helm +mv linux-amd64/helm opt/ +``` + +## Create the `Sealfile` Required for Building the Image + +Create a file named `Sealfile`, with the following content: + +```shell +FROM scratch +COPY opt ./opt +CMD ["cp opt/helm /usr/bin/"] +``` + +The current directory structure is as follows: + +``` +. +├── Sealfile +└── opt + └── helm +``` + +## Build the Cluster Image + +Now, everything is ready, and you can begin building the cluster image: + +```shell +sealos build -t labring/helm:v3.10.1 . +``` + +**Note:** Firstly, you need to install the `sealos` command on the local host. + +You can view the build log to understand the building process. + +```shell +root@ubuntu:~/cluster-images# sealos build -t labring/helm:v3.10.1 . +... +``` + +View the built image, and now all the dependent binary files have been built into the cluster image: + +```shell +root@ubuntu:~/cluster-images# sealos images +labring/helm v3.10.1 19ed4a24f0fe 3 minutes ago 45.1 MB +``` + +## Push the Image + +You can push the image to any Docker image repository, the following command pushes the image to DockerHub: + +```shell +sealos push labring/helm:v3.10.1 +``` + +**Note:** Please use the `sealos` command to operate the cluster image; Docker commands are not supported. + +If you are using a private image repository, you can use the `sealos login` command to log into your image repository, then push or pull the image. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md new file mode 100644 index 000000000000..379ae451afaf --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 5 +--- + +# Building Cluster Images Based on go-template + +During the process of building cluster images, we can use the `--env` option to pass some variables through the sealos command line. These environment variables can be used by the `CMD` command of the Kubefile or the yaml file template. + +## Using Environment Variables in Kubefile + +This example defines a `SERVICE_TYPE` variable that allows the user to customize the service exposure type when installing the application and pass parameters to the helm command in CMD. + +Kubefile example: + +```shell +FROM scratch +ENV SERVICE_TYPE "NodePort" +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=$(SERVICE_TYPE)"] +``` + +Run the cluster application and set a custom `SERVICE_TYPE=LoadBalancer`, if not set, it will default to NodePort. + +```shell +sealos run labring/nginx:v1.23.1 --env SERVICE_TYPE=LoadBalancer +``` + +## Using Environment Variables in Yaml Files + +Prepare a simple nginx service yaml file, this file must be a `*.tmpl` extension to be rendered when running `sealos run --env` command. + +```shell +$ cat manifests/service.yaml.tmpl +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: {{ .serviceType }} + ports: + - port: 80 + nodePort: {{ .http_NodePort }} + name: http + - port: 443 + nodePort: {{ .https_NodePort }} + name: https + selector: + name: nginx +``` + +Here is a Kubefile example where you can set the default environment variables. + +```shell +FROM scratch +ENV serviceType NodePort +ENV http_NodePort 30080 +ENV https_NodePort 30443 + +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/service.yaml"] +``` + +When you build the image, nothing will happen, it only renders when running the application. If `--env` is not set, it will use the default ENV in Kubefile. + +```shell +sealos run labring/nginx:1.23.1 --env serviceType=LoadBalancer --env http_NodePort=30080 --env https_NodePort=30443 +``` + +You will find that sealos renders a new yaml file `service.yaml` based on `service.yaml.tmpl` on the local path of the master node. + +**Note** The new version of the application's rootfs is placed in the `/var/lib/sealos/data/default/applications` directory, each application has its independent directory. + +```shell +root@node1:~# ls /var/lib/sealos/data/default/rootfs/manifests |grep service +service.yaml +service.yaml.tmpl +``` + +Check the yaml content: + +```shell +root@node1:~# cat /var/lib/sealos/data/default/rootfs/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30080 + name: http + - port: 443 + nodePort: 30443 + name: https + selector: + name: nginx +``` + +**Note:** All types of files support this feature (the file name suffix is .tmpl and the build directory is in etc, scripts, and manifests), you can try it yourself. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md new file mode 100644 index 000000000000..fe440580aa7f --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md @@ -0,0 +1,199 @@ +--- +sidebar_position: 3 +--- + +# Building Cluster Images Based on Helm Charts + +Let's use the simplest nginx application as an example to introduce how to build a cluster image based on nginx using Helm Charts. + +## 1. Preparation + +Create a base directory for the build work. + +```shell +$ mkdir ~/cloud-images +``` + +Create a `charts` directory to store the Kubernetes nginx Helm Charts files. + +```shell +$ cd cloud-images +$ mkdir charts +``` + +## 2. Prepare Helm Charts + +Prepare the nginx Helm Charts. Here we use [the official nginx Helm Charts by bitnami](https://bitnami.com/stack/nginx). Let's pull the Helm Chart files locally and unzip them to the `charts` directory. + +```shell +helm repo add bitnami https://charts.bitnami.com/bitnami +helm search repo bitnami/nginx +helm pull bitnami/nginx --version=13.2.13 -d charts/ --untar +``` + +**Note:** First, you should install the Helm command tool to your local host. + +Now, the structure of the charts directory is as follows: + +``` +charts/ +└── nginx + ├── Chart.lock + ├── charts + ├── Chart.yaml + ├── README.md + ├── templates + ├── values.schema.json + └── values.yaml +``` + +## 3. Create Kubefile + +Create a file named `Kubefile` for image construction: + +```shell +$ cat Kubefile +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm install nginx charts/nginx --namespace=nginx --create-namespace"] +``` + +It is recommended to use `helm upgrade --install` instead of `helm install` so that you can rerun the same command when updating the application in the future. + +You can add other options as needed, such as exposing the service through NodePort. + +```shell +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +``` + +## 4. Build the Cluster Image + +Now everything is ready, and you can start building the cluster image. + +```shell +sealos build -t labring/nginx:v1.23.2 . +``` + +**Note:** You should first install the sealos command to your local host. + +You can view the build log. + +```shell +root@ubuntu:~/cloud-images# sealos build -t labring/nginx:v1.23.2 . +... +``` + +sealos will automatically extract the images from the charts directory, pull them locally, and store them in the registry directory. + +The current directory structure is as follows: + +```shell +. +├── charts +│ └── nginx +│ ├── Chart.lock +│ ├── charts +│ ├── Chart.yaml +│ ├── README.md +│ ├── templates +│ ├── values.schema.json +│ └── values.yaml +├── Kubefile +└── registry + └── docker + └── registry +``` + +Check the built image locally. Now all dependent deployment manifests and image caches are built into the cluster image. + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.2 521c85942ee4 4 minutes ago 56.8 MB +``` + +You can push the image to any Docker image repository. The following command pushes it to Docker Hub. + +```shell +sealos push labring/nginx:v1.23.2 +``` + +**Note:** Please use the sealos command to operate the cluster + +image, Docker commands are not supported. + +If you use a private image repository, just use the `sealos login` command to log in to the registry before pulling or pushing the image. + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +## 5. Install the Cluster Image + +Then, you can run the cluster image in your cluster. + +```shell +sealos run labring/nginx:v1.23.2 +``` + +The helm binary command will be installed on the master node of your Kubernetes cluster. + +```shell +root@ubuntu:~# helm -n nginx ls +``` + +## 6. Explanation + +By default, when building images, sealos only parses the default values.yml file. However, you can also provide a custom values.yaml file for sealos. + +**The custom values file must be placed in the same directory as your Chart, and must be named in the form of `.values.yaml`, for example `loki-stack.values.yaml`.** + +```shell +. +├── charts +│ ├── loki-stack +│ │ ├── charts +│ │ ├── Chart.yaml +│ │ ├── README.md +│ │ ├── requirements.lock +│ │ ├── requirements.yaml +│ │ ├── templates +│ │ └── values.yaml +│ └── loki-stack.values.yaml +├── init.sh +├── Kubefile +``` + +The content of `loki-stack.values.yaml` file is as follows: + +```shell +$ cat charts/loki-stack.values.yaml +promtail: + enabled: false +fluent-bit: + enabled: true +grafana: + enabled: true +``` + +Different values files may output different image lists, allowing sealos to automatically parse the images during the `sealos build` process. + +```shell +$ helm template charts/loki-stack/ -f charts/loki-stack/values.yaml|grep image: + image: "grafana/promtail:2.0.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + +$ helm template charts/loki-stack/ -f charts/loki-stack.values.yaml|grep image: + image: "grafana/fluent-bit-plugin-loki:1.6.0-amd64" + image: "kiwigrid/k8s-sidecar:0.1.209" + image: "grafana/grafana:6.7.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + image: bats/bats:v1.1.0 +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md new file mode 100644 index 000000000000..fa99e7aa89b7 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 1 +--- + +# Building Cluster Images Based on Image Manifests + +This guide will walk you through the process of building cluster images using image manifests or using existing tarballs stored in Docker. + +## Building from Image Manifests + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── shim +│   └── CalicoImageList +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM labring/kubernetes:v1.24.0 +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +Explanation: + +The images listed in CalicoImageList will be pulled locally and then applied to the cluster using the `kubectl apply -f` command. + +The image manifest currently supports the following: +- Remote images like docker.io/calico/cni:v3.20.0 +- Local OCI container images like containers-storage:docker.io/labring/coredns:v0.0.1 +- Local Docker container images like docker-daemon:docker.io/library/nginx:latest + +## Building from Image Tarballs + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── skopeo +│   ├── calico.tar +│   └── tar.txt +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM scratch +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +Explanation: + +The configurations in tar.txt will be pulled locally and redirected to the image list. Then, they will be applied to the cluster using the `kubectl apply -f` command. The configuration file format is as follows: + +``` +docker-archive:calico.tar@calico/cni:v3.20.0 +``` + +The image manifest currently supports the following: +- Docker archive images, supporting a single image, like docker-archive +- OCI archive images, supporting a single image, like oci-archive diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md new file mode 100644 index 000000000000..561e92e377b9 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 2 +--- + +# Building Cluster Images Based on Deployment Manifest + +This document will detail how to build cluster images based on Deployment Manifests. We will use a simple nginx application as an example. + +## I. Preparations + +1. First, create a base directory as the build workspace. + +```shell +$ mkdir ~/cloud-images +``` + +2. Create a directory named `manifests` to store the kubernetes nginx deployment yaml file. + +```shell +$ cd cloud-images +$ mkdir manifests +``` + +## II. Prepare the Manifest File + +At this stage, we will prepare a simple nginx kubernetes yaml file. + +```shell +$ cat manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.23.1 + ports: + - containerPort: 80 +``` + +## III. Create Kubefile + +At this stage, we need to create a Kubefile that will be used to build the image. + +```shell +FROM scratch +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/deployment.yaml"] +``` + +## IV. Build the Cluster Image + +After preparing all the necessary files and directories, we can start building the cluster image. + +```shell +sealos build -t labring/nginx:v1.23.1 . +``` + +**Note:** Before starting the build, you need to install the sealos command on your local host. + +During the build, you can view the build log. + +## V. Verify the Image + +After the build is complete, you can view the built image with the following command: + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.1 521c85942ee4 4 minutes ago 56.8 MB +``` + +## VI. Push the Image + +Finally, we can push the built image to any Docker image repository. The following command pushes it to DockerHub. + +```shell +sealos push labring/nginx:v1.23.1 +``` + +**Note:** Please use the sealos command to operate the cluster image, the Docker command is not supported. + +If you are using a private image repository, just use `sealos login` to log into the repository before pulling or pushing the image. + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +At this point, the cluster image based on the deployment manifest is successfully built. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md new file mode 100644 index 000000000000..c3337169e262 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md @@ -0,0 +1,129 @@ +--- +sidebar_position: 6 +--- + +# Building Cluster Images Using exec and scp Commands + +By default, `sealos run xx` only runs the command and copies the file on the first master node. When you want to run the command or copy files on specific nodes or all nodes, you can use the `sealos exec` or `sealos scp` commands when building the cluster image. + +- sealos exec: Connects to one or more nodes and runs any shell command; +- sealos scp: Connects to one or more nodes and copies local files to remote nodes. + +Although you can directly use these commands on the host, this article mainly describes how to use these two commands when building the cluster image using sealos build. + +## sealos exec Example + +Below is an example of building an openebs cluster image. Before installing openebs maystor, some initialization operations need to be performed on the node, you can use sealos exec to achieve this. + +First, create a base directory for building. + +```shell +$ mkdir ~/cloud-images +``` + +Create a `charts` directory to store the kubernetes nginx helm charts file. + +```shell +$ cd cloud-images +``` + +Create a file named `Kubefile` for image building: + +```shell +$ cat Kubefile +FROM scratch +COPY manifests manifests +COPY registry registry +COPY opt opt +COPY mayastor.sh mayastor.sh +CMD ["bash mayastor.sh"] +``` + +Create a script file named `mayastor.sh`, the shell command after sealos exec will be executed on all nodes (create hugepage, load kernel modules on all nodes), but other commands will only run on the master node. + +```shell +$ cat mayastor.sh +#!/usr/bin/env bash +set -e + +sealos exec " +echo vm.nr_hugepages = 1024 | sudo tee -a /etc/sysctl.d/mayastor.conf +sysctl -p +sudo modprobe -- nbd +sudo modprobe -- nvmet +sudo modprobe -- nvmet_rdma +sudo modprobe -- nvme_fabrics +sudo modprobe -- nvme_tcp +sudo modprobe -- nvme_rdma +sudo modprobe -- nvme_loop +cat < /tmp/buildimage/kubeadm.yml < /tmp/buildimage/Kubefile < Clusterfile +``` + +Notice: labring/helm should be set before labring/calico. + +The generated Clusterfile is as follows: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.24.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +``` + +
+ +2. After generating the Clusterfile, update the cluster configuration. For example, to modify the CIDR range of pods, you can modify the `networking.podSubnet` and `spec.data.spec.calicoNetwork.ipPools.cidr` fields. The final Clusterfile would look like this: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +--- +apiVersion: kubeadm.k8s.io/v1beta2 +kind: ClusterConfiguration +networking: + podSubnet: 10.160.0.0/12 +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + ipPools: + - blockSize: 26 + cidr: 10.160.0.0/12 + encapsulation: IPIP + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*" +``` + +
+ +3. Run `sealos apply -f Clusterfile` to start the cluster. After the cluster is successfully running, the Clusterfile will be saved in the `.sealos/default/Clusterfile` file. You can modify the fields in it to reapply changes to the cluster. + +**Note:** + +- You can refer to the [official documentation](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/) or run the `kubeadm config print init-defaults` command to print the kubeadm configuration. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md new file mode 100644 index 000000000000..80a6adf33d5e --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 0 +--- + +# Running a Sealos Cluster with Docker Images + +In this directory, we provide a series of detailed tutorials and resources to help you effectively run and manage a Sealos cluster. Here is a brief overview of each file or subdirectory: + +1. **Cluster Initialization**: This section provides all the necessary steps to start and initialize a new Sealos cluster. It includes information on how to configure Sealos and how to run initialization commands. +2. **Node Management**: This section covers how to add and remove cluster nodes, as well as how to maintain and manage detailed information about the cluster nodes. +3. **Service Management**: This section includes guides on how to deploy and manage various services on the Sealos cluster. It covers how to use Sealos' command-line tools to manage services and how to configure services to meet your specific requirements. +4. **Practical Examples**: This section contains various practical examples, including how to run and manage various applications and how to handle specific operational issues. + +We hope this directory provides comprehensive assistance for running and managing a Sealos cluster. If you have any questions or suggestions, please feel free to contact us at any time. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md new file mode 100644 index 000000000000..ee1ec670ae3f --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 2 +--- + +# Template Configuration Installation + +We can also use Go template syntax to write the Clusterfile (similar to [Helm](https://helm.sh/)), but partial template functions such as `include`/`tpl`/`require`/`lookup` are not supported yet. For example, create a `Clusterfile.yaml` as follows: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: {{ .Values.clusterName }} +spec: + hosts: + - ips: {{ .Values.masters | toYaml | nindent 8 }} + roles: ["master", "amd64"] + {{- with .Values.nodes }} + - ips: {{ . | toYaml | nindent 8 }} + roles: ["node", "amd64"] + {{- end }} + image: {{ .Values.images | toYaml | nindent 4 }} + ssh: + passwd: {{ env "SSH_PASSWORD" .Values.ssh.passwd }} + pk: {{ default "~/.ssh/id_rsa" .Values.ssh.pk }} + port: {{ default 22 .Values.ssh.port | int }} + user: {{ default "root" .Values.ssh.user }} +--- +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +networking: + dnsDomain: {{ default "cluster.local" .Values.networking.dnsDomain }} + serviceSubnet: {{ default "10.96.0.0/18" .Values.networking.serviceSubnet }} + podSubnet: {{ default "100.64.0.0/17" .Values.networking.podSubnet }} +``` + +Then, create a custom values file `example.values.yaml`: + +```yaml +clusterName: default +images: + - dockerhub.tencentcloudcr.com/labring/kubernetes:v1.23.8 + - dockerhub.tencentcloudcr.com/labring/calico:v3.24.1 +masters: + - 10.74.16.27:22 + - 10.74.16.140:22 + - 10.74.16.101:22 +nodes: [] +ssh: + # passwd: notSetYet + pk: /path/to/private/key/file + port: 22 + user: root +networking: + dnsDomain: cluster.local + serviceSubnet: 10.96.0.0/18 + podSubnet: 100.64.0.0/17 +``` + +You can then deploy the cluster like this: + +```shell +$ sealos apply -f Clusterfile.yaml --values example.values.yaml --set clusterName=testlocal --env SSH_PASSWORD=s3cret +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md new file mode 100644 index 000000000000..d95b23e7c118 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 3 +--- + +# How to Upgrade the Cluster + +If you want to upgrade your Kubernetes cluster, you just need to run the following command: + +```sh +sealos run labring/kubernetes: +``` + +Make sure you have already set up the cluster. + +## Example Scenario + +1. Let's say you have previously run the following command: + +```sh +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.8 --nodes 192.168.64.7 +``` + +2. Now, if you want to upgrade the cluster to v1.25.0, you can do the following: + +```sh +sealos run labring/kubernetes:v1.25.0 +``` + +During the execution of 'kubeadm upgrade v1.25.0', you will see: + +```txt +[upgrade/version] You have chosen to change the cluster version to "v1.25.0" +[upgrade/versions] Cluster version: v1.24.0 +[upgrade/versions] kubeadm version: v1.25.0 +[upgrade] Are you sure you want to proceed? [y/N]: +``` + +Type 'y' to proceed with the upgrade. + +If there are any **errors**, you can rerun the command 'sealos run labring/kubernetes:v1.25.0'. Even if it fails, it will ensure the same result. + +## Important Notes + +1. **Upgrades cannot skip minor version numbers**. For example, upgrading from 'v1.23.0' to 'v1.25.0' is not allowed. If you do need to upgrade from 'v1.23.0' to 'v1.25.0', you can do it in two steps, such as upgrading from 'v1.23.0' to 'v1.24.0' first, and then from 'v1.24.0' to 'v1.25.0'. + +2. Once the upgrade is successful, the old version images mounted by the cluster will be replaced. Adding master or worker nodes will apply the new version. + +This is the entire process of upgrading a Kubernetes cluster. If you encounter any issues during the upgrade process, don't hesitate to refer to relevant documentation or seek assistance. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md new file mode 100644 index 000000000000..a9cab905b7f0 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 3 +--- + +# Building an Ingress Cluster Image + +Here we demonstrate how to build an nginx-ingress cluster image using Helm. + +## Download the Helm Chart + +```shell +$ mkdir ingress-nginx && cd ingress-nginx +$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +$ helm pull ingress-nginx/ingress-nginx +``` + +You will find the downloaded chart: + +```shell +$ ls +ingress-nginx-4.1.0.tgz +``` + +## Add Image List + +Sealos will download the images in the image list and cache them in the registry directory. + +The directory must be in the format `images/shim/[your image list filename]`: + +```shell +$ cat images/shim/nginxImages +k8s.gcr.io/ingress-nginx/controller:v1.2.0 +k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1 +``` + +## Write the Dockerfile + +```Dockerfile +FROM scratch +COPY ../examples . +CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"] +``` + +## Build the Cluster Image + +```shell +$ sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 . +``` + +Sealos will automatically add the image dependencies from the image list to the cluster image, magically saving the Docker images it depends on inside. When running in another environment, it will magically check if the Docker images exist in the cluster. If they do, it will automatically download them; otherwise, it will download them from k8s.gcr.io. Users do not need to modify the Docker image addresses in the Helm chart. This utilizes the black technology of image caching proxy. + +## Push to the Image Registry + +```shell +$ sealos login docker.io +$ sealos push docker.io/fanux/ingress-nginx:v1.2.0 +``` + +## Run the Cluster Image + +```shell +$ sealos run docker.io/fanux/ingress-nginx:v1.2.0 +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md new file mode 100644 index 000000000000..0313c08fa442 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 2 +keywords: [K8s, K8s install, K8s deploy, Kubernetes cluster setup, multi-node Kubernetes, offline Kubernetes installation] +--- + +# Install Kubernetes + +Sealos supports installing Kubernetes clusters on `amd64` and `arm64` architecture machines. + +## Prerequisites + +You'll first need to [download the Sealos CLI tool](/self-hosting/lifecycle-management/quick-start/install-cli.md). Sealos is a simple Golang binary that can be installed on most Linux operating systems. + +Here are some basic installation requirements: + +- Each cluster node should have a unique hostname without underscores. +- System times must be synchronized across all nodes. +- The `sealos run` command must be run on the **first master node** of the Kubernetes cluster. **Installation on nodes outside the cluster is not currently supported**. +- It is recommended to use a clean OS to create the cluster. **Do not install Docker manually!** +- Supports most Linux distributions like Ubuntu, CentOS, Rocky Linux. +- Compatible with all Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags). +- Supports Containerd as the container runtime. +- When installing on public clouds, use **private IP** addresses. + +## View Available Cluster Images + +All Sealos cluster images are hosted in the [cluster-image-docs](https://github.com/labring-actions/cluster-image-docs) repository. + +You can browse all Sealos images on Docker Hub here: [https://hub.docker.com/u/labring](https://hub.docker.com/u/labring). + +Use [Registry Explorer](https://explore.ggcr.dev/) to view all versions of the Kubernetes cluster images by entering `labring/kubernetes` and clicking "Submit Query": + +![](images/registry-explorer.png) + +This will display all available tags for that image. + +:::info Note + +Kubernetes cluster stability improves with higher minor version numbers. For example in v1.28.x, x is the minor version. It is recommended to use versions with relatively high minor numbers. Currently, the highest v1.27 version is v1.27.7, while the highest v1.28 version is v1.28.3, so **v1.27.7 is recommended**. Choose the optimal Kubernetes version based on your needs. + +::: + +## Install Single-Node Kubernetes + +```shell +# sealos version must >= v4.1.0 +$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 --single +``` + +## Install Kubernetes Cluster + +```shell +$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 \ +--masters 192.168.64.2,192.168.64.22,192.168.64.20 \ +--nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +Note: labring/helm must come before labring/cilium. + +Parameter descriptions: + +| Parameter | Example | Description | +| --- | --- | --- | +| --masters | 192.168.0.2 | List of master node IP addresses | +| --nodes | 192.168.0.3 | List of worker node IP addresses | +| --ssh-passwd | [your-ssh-passwd] | SSH password | +| kubernetes | labring/kubernetes:v1.25.0 | Kubernetes cluster image | + +Running the above commands directly on clean servers will install a highly available Kubernetes cluster without any extra steps. + +## Install Various Distributed Apps + +```shell +sealos run labring/helm:v3.9.4 # Install Helm + +sealos run labring/openebs:v3.9.0 # Install OpenEBS + +sealos run labring/minio-operator:v4.5.5 labring/ingress-nginx:4.1.0 +``` + +This provides highly available apps like Minio without worrying about dependencies. + +## Add Kubernetes Nodes + +Add worker nodes: + +```shell +$ sealos add --nodes 192.168.64.21,192.168.64.19 +``` + +Add master nodes: + +```shell +$ sealos add --masters 192.168.64.21,192.168.64.19 +``` + +## Delete Kubernetes Nodes + +Delete worker nodes: + +```shell +$ sealos delete --nodes 192.168.64.21,192.168.64.19 +``` + +Delete master nodes: + +```shell +$ sealos delete --masters 192.168.64.21,192.168.64.19 +``` + +## Clean Up Kubernetes Cluster + +```shell +$ sealos reset +``` + +## Offline Installation + +For offline environments, first import the images - subsequent steps are identical to online installation. + +First, export images from an online environment: + +```shell +$ sealos pull registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 + +$ sealos save -o kubernetes.tar registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +``` + +### Import and Install + +Copy kubernetes.tar to the offline environment and import it with: + +```shell +$ sealos load -i kubernetes.tar +``` + +The rest of the installation process is identical to online installation: + +```shell +$ sealos images # Verify import succeeded + +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 # Single node +``` + +### Quick Cluster Install + +You can also install Kubernetes by directly running: + +```shell +$ sealos run kubernetes.tar +``` + +## Cluster Image Version Compatibility + +### Kubernetes Compatibility with Containerd + +It's advised to use Containerd as the container runtime interface (CRI) in Kubernetes clusters. Containerd stands out for its lightweight structure and high efficiency, and it's fully compatible with Docker. Kubernetes images that utilize Containerd benefit from enhanced performance and optimized resource usage. Here are the specific supported versions: + +| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version | +| ------------------ | ----------------------- | ----------- |----------------------------| +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes:v1.28.0 | + +The choice of Sealos and CRI versions is dependent on the Kubernetes version in question. For instance, Kubernetes v1.26.0 would require Sealos v4.1.4-rc3 or newer, along with the v1 CRI version. + +### Kubernetes Compatibility with Docker + +Alternatively, Docker can also be used as the container runtime. The following table provides a breakdown of compatible Kubernetes versions with their corresponding Sealos and CRI versions for Docker-based setups: + +| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version | +|--------------------|-------------------------| ----------- |-----------------------------------| +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes-docker:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes-docker:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes-docker:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes-docker:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes-docker:v1.28.0 | + +As with the Containerd setup, the appropriate Sealos and CRI versions must be matched with the specific version of Kubernetes being used. For a Kubernetes v1.26.0 setup, this means selecting Sealos v4.1.4-rc3 or later, and a v1 CRI version. + +### k3s Compatibility with Containerd + +| k3s Version | Required Sealos Version | Cluster Image Version | +|-------------|-------------------------|-----------------------| +| `>=1.24` | `>=v5.0.0` | labring/k3s:v1.24.0 | + +## Summary + +You can choose from a variety of image types and versions to suit your individual needs and preferences. Don't forget to consult the [CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) to stay informed about the latest updates and bug fixes for each version. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png new file mode 100644 index 000000000000..a9e28004026c Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md new file mode 100644 index 000000000000..855f1e2dcad6 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/quick-start/install-cli.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 1 +keywords: [sealos, sealos cli] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Download Sealos CLI + +You can get the list of versions by running: + +```bash +curl --silent "https://api.github.com/repos/labring/sealos/releases" | jq -r '.[].tag_name' +``` + +Note: While choosing the version, it's recommended to use a stable version. The versions like `v4.3.0-rcx`, `v4.3.0-alpha1` are pre-releases, use them with caution. + +Set the 'VERSION' environment variable to the latest VERSION number, or replace 'version' with the Sealos version you want to install: + +```shell +VERSION=`curl -s https://api.github.com/repos/labring/sealos/releases/latest | grep -oE '"tag_name": "[^"]+"' | head -n1 | cut -d'"' -f4` +``` + +## Binary Auto Download + +```bash +curl -sfL https://raw.githubusercontent.com/labring/sealos/${VERSION}/scripts/install.sh | + sh -s ${VERSION} labring/sealos + +``` + +## Binary Manual Download + + + + +```bash +$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_amd64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +```bash +$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_arm64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +## Package Management Tool Installation + +### DEB Repository + +```bash +echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | sudo tee /etc/apt/sources.list.d/labring.list +sudo apt update +sudo apt install sealos +``` + +### RPM Repository + +```bash +sudo cat > /etc/yum.repos.d/labring.repo << EOF +[fury] +name=labring Yum Repo +baseurl=https://yum.fury.io/labring/ +enabled=1 +gpgcheck=0 +EOF +sudo yum clean all +sudo yum install sealos +``` + +## Source Code Installation + +### Prerequisites +1. `linux` +2. `git` +3. `golang` 1.20+ +4. `libgpgme-dev libbtrfs-dev libdevmapper-dev` + +If you are in an `arm64` environment, add the `:arm64` suffix. + +### Build + +```bash +# git clone the repo +git clone https://github.com/labring/sealos.git +# just make it +make build BINS=sealos +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json new file mode 100644 index 000000000000..950b839be0aa --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 3 +} diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md new file mode 100644 index 000000000000..d861e1f7ed0e --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 3 +--- + +# image-cri-shim User Guide + +## Overview + +image-cri-shim is a gRPC (Google Remote Procedure Call) shim based on CRI (Container Runtime Interface) and kubelet. CRI is the interface used in Kubernetes to interact with container runtimes, while kubelet is the Kubernetes component responsible for maintaining container runtime status and node-level resource management. + +The main functionality of image-cri-shim is automatic image name recognition, which eliminates the need for users to manually specify the image name when deploying containers with Kubernetes. This simplifies the container image deployment process and improves the convenience for users. + +In practical usage, image-cri-shim serves as middleware that receives requests from kubelet and forwards them to the container runtime. By automatically recognizing the image name, image-cri-shim streamlines the deployment process of container images and reduces user burden. + +``` ++------------+ +----------------+ +-------------------+ +| User | | Kubelet | | image-cri-shim | +| (Kubernetes| | (Node agent) | | (Middleware) | +| Manifest) | | | | | ++-----+------+ +-------+--------+ +-------+-----------+ + | | | + | YAML Manifest | | + |---------------> | | + | | | + | | | + | | CRI Request | + | |------------------------> | + | | | + | | Image Name | + | | Auto-Recognition | + | | | + | | | + | | CRI Response | + | | <------------------------+ + | | | + | | | + | Container | | + | Deployment | | + | <----------------------| | + | | | + | | | ++------------+ +-------+--------+ +-------+-----------+ + +``` + +From the above flowchart, it can be seen that a user creates a Kubernetes YAML manifest containing container information and submits the manifest to kubelet. kubelet, which acts as an agent on Kubernetes nodes, is responsible for managing containers. +Next, kubelet sends a CRI request to the image-cri-shim middleware. The main task of image-cri-shim is to automatically recognize the image name. It processes the CRI request and retrieves relevant image information. Once image-cri-shim identifies the image name, it returns the CRI response to kubelet. + +Finally, kubelet deploys the container using the image name obtained from image-cri-shim. This process is transparent to the user, as they do not need to manually specify the image name, simplifying the container deployment process and improving convenience. + +## Architecture + +The architecture of image-cri-shim is illustrated in the following diagram: + +![](images/image-cri-shim.png) + +## Usage + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://sealos.hub:5000 +force: true +debug: true +timeout: 15m +auth: admin:passw0rd + +registries: +- address: http://172.18.1.38:5000 + auth: admin:passw0rd +``` + +The above configuration is a YAML file used to set the parameters of image-cri-shim. It includes several key parameters, and the explanation for each parameter is as follows: + +1. shim: Specifies the UNIX socket file path + +for image-cri-shim. This path is used for communication with kubelet. +2. cri: Specifies the UNIX socket file path for the container runtime (e.g., containerd). image-cri-shim uses this path to communicate with the container runtime. +3. address: Defines the address of the image registry. In this example, the registry address is http://sealos.hub:5000. +4. force: When set to true, image-cri-shim forcefully starts the shim without waiting for the cri to start. +5. debug: When set to true, enables debug mode and outputs more logging information. +6. timeout: Defines the timeout for image operations. In this example, the timeout is set to 15 minutes (15m). +7. auth: Defines the authentication credentials for accessing the image registry. In this example, the username is admin and the password is passw0rd. + +Additionally, the configuration file includes a list of registries, which defines other image registries and their authentication credentials. In this example, there is only one additional registry: +- address: The address of the registry is http://172.18.1.38:5000. +- auth: The authentication credentials for accessing the registry. In this example, the username is admin and the password is passw0rd. + +This configuration file provides image-cri-shim with the necessary information to communicate with kubelet, the container runtime (such as containerd), and access and manage the image registry. + +Note: image-cri-shim is compatible with both CRI API v1alpha2 and v1. + +### Service Management + +image-cri-shim is typically run as a system service. To manage image-cri-shim, you can use system service management tools (such as systemctl) to start, stop, restart, or view the status of the service. First, make sure you have correctly installed image-cri-shim and configured it as a system service. + +1. Start the service: `systemctl start image-cri-shim` +2. Stop the service: `systemctl stop image-cri-shim` +3. Restart the service: `systemctl restart image-cri-shim` +4. View the service status: `systemctl status image-cri-shim` + +### Log Management + +To view the logs of the image-cri-shim service, you can use the journalctl command. journalctl is a tool used to query and display system logs, and it is used in conjunction with the systemd service manager. + +Here are the commands to view the logs of the image-cri-shim service using journalctl: + +```shell +journalctl -u image-cri-shim +``` + +This will display all logs of the image-cri-shim service. If you want to view the logs in real-time, you can add the -f option: + +```shell +journalctl -u image-cri-shim -f +``` + +Additionally, you can filter the logs by time. For example, if you only want to view the logs from the past hour, you can use the following command: + +```shell +journalctl -u image-cri-shim --since "1 hour ago" +``` + +These commands should help you view and analyze the logs of the image-cri-shim service, allowing you to better understand the service's operation status and potential issues. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png new file mode 100644 index 000000000000..5c8d5f81b23a Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json new file mode 100644 index 000000000000..e47d9e07a6d5 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 99 +} \ No newline at end of file diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/images/01.webp differ diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md new file mode 100644 index 000000000000..33bb00cc6c5e --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/lvscare/lvscare.md @@ -0,0 +1,74 @@ +# LVScare User Guide + +## Introduction + +LVScare is a lightweight load balancing and health checking tool based on IPVS technology. It provides real-time monitoring and management of backend services. Sealos, a cloud operating system, is a Kubernetes HA installation tool based on kubeadm that can be used to install and upgrade high availability Kubernetes clusters on any Linux system. When combined, Sealos and LVScare enable high availability of Kubernetes master nodes. Leveraging the lifecycle management capabilities of Sealos and the lightweight, zero-dependency, and high availability features of LVScare, it ensures the stability and reliability of Kubernetes clusters effectively. + +### How LVScare Works and Its Features + +LVScare monitors the health status of backend services (real servers) in real-time using IPVS. If a service becomes unavailable, LVScare immediately sets its weight to 0 (for graceful TCP termination) and removes it from the service list during the next check. When the service recovers, LVScare automatically adds it back to the service list. This design of LVScare makes it lightweight, zero-dependency, and highly available. It occupies fewer resources, is stable and reliable, and similar to the implementation of kube-proxy, it can ensure the continuous availability of services through IPVS-based local load balancing. + +## Integration of Sealos and LVScare + +In Sealos, we utilize the recommended approach of static pods to automatically configure and manage LVScare for achieving high availability of Kubernetes clusters. During the installation process of a Kubernetes cluster, Sealos automatically uses LVScare for health checking and load balancing of the master nodes. This means that even if one of the master nodes fails, it does not affect the overall functionality of the Kubernetes cluster. + +![](./images/01.webp) + +### Running LVScare as a Static Pod + +First, use the `sealctl static-pod` command to generate the configuration for LVScare and place it in the `/etc/kubernetes/manifests` directory. This allows all nodes in the Kubernetes cluster to access this configuration. + +```bash +lvscare care --vs 10.103.97.12:6443 --rs 192.168.0.2:6443 --rs 192.168.0.3:6443 --rs 192.168.0.4:6443 --interval 5 --mode route +``` + +### Generating and Adjusting Static Pod Configuration + +To start LVScare on each node, we can use the following command to generate the configuration for the static pod: + +```bash +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +When there are changes in the master nodes, Sealos only needs to rerun the `sealctl static-pod` command to adjust the master nodes, simplifying the logic of maintaining static pods. Before joining the cluster nodes, we need to manually start the IPVS rules by calling `sealctl ipvs` (which directly calls the LVScare SDK) to maintain the IPVS cluster. After the node joins successfully, Kubernetes static pods can take over the IPVS rules. + +## Advantages of Using LVScare + +### High Availability + +By combining LVScare with Sealos, high availability of Kubernetes cluster master nodes can be achieved. + +### Health Checking Mechanism + +The health checking mechanism of LVScare can promptly detect and handle issues, preventing them from causing larger problems due to the failure of a single node. + +### Seamless Integration + +As a static pod in Seal + +os, LVScare can seamlessly integrate with other parts of the Kubernetes cluster. + +### Simplified Operations + +Sealos automatically configures and manages LVScare, greatly simplifying the operational tasks of the Kubernetes cluster. + +## LVScare Usage Example + +Please note that all real servers need to listen on the same host and be set in "route" mode. Then, you can run LVScare in the foreground. For example: + +```bash +docker run -p 8081:80 --name echoserver1 -d cilium/echoserver +docker run -p 8082:80 --name echoserver2 -d cilium/echoserver +docker run -p 8083:80 --name echoserver3 -d cilium/echoserver +lvscare care --vs 169.254.0.1:80 --rs 127.0.0.1:8081 --rs 127.0.0.1:8082 --rs 127.0.0.1:8083 --logger DEBG --health-schem http --health-path / +``` + +## Cleanup + +Finally, you can use the following command to clean up: + +```bash +lvscare care --vs 169.254.0.1:80 --logger DEBG -C +``` + +Conclusion: LVScare is a lightweight load balancing and health checking tool based on IPVS. When seamlessly integrated with Sealos, it greatly improves the availability and performance of Kubernetes clusters. Give it a try and see how LVScare can help you better manage your Kubernetes cluster! diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json new file mode 100644 index 000000000000..a788e786e6c5 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 2 +} diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md new file mode 100644 index 000000000000..fe2b676aed32 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cert.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 1 +--- + + +# Certificate Management with `cert` + +The `cert` command is used to generate the necessary certificate files for a Kubernetes cluster. In a Kubernetes cluster, certificates are used to ensure secure communication between components such as the API server, kubelet, and etcd. Certificates provide encryption using the Transport Layer Security (TLS) protocol to ensure the confidentiality and integrity of data during transit. + +The `sealctl cert` command generates certificates automatically based on the provided parameters. These parameters include node IP, node name, service CIDR, DNS domain, and optional additional alternate names. By generating and configuring these certificates, you can ensure secure communication within your Kubernetes cluster. + + +``` +The `cert` command is used to generate Kubernetes certificates. + +Options: + --alt-names Alternate names, such as sealos.io or 10.103.97.2. Can specify multiple alternate names. + --node-name Node name, such as master0. + --service-cidr Service CIDR, such as 10.103.97.2/24. + --node-ip IP address of the node, such as 10.103.97.2. + --dns-domain DNS domain for the cluster. Default value is cluster.local. + --cert-path Path to Kubernetes certificate files. Default value is /etc/kubernetes/pki. + --cert-etcd-path Path to Kubernetes etcd certificate files. Default value is /etc/kubernetes/pki/etcd. + +Examples: + sealctl cert --alt-names sealos.io --alt-names 10.103.97.2 \ + --node-name master0 --service-cidr 10.103.97.2/24 \ + --node-ip 10.103.97.2 --dns-domain cluster.local + +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md new file mode 100644 index 000000000000..5bae776b35d0 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/cri.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 2 +--- + +# CRI Container Management + +The `cri` command is used to manage and inspect the Container Runtime Interface (CRI) environment in a Kubernetes cluster. The container runtime is the underlying technology responsible for running containers, such as Docker, containerd, or CRI-O. In Kubernetes, the container runtime is used to start, stop, and manage containers to support workloads in the cluster. + +The `sealctl cri` command provides a set of subcommands that allow you to perform various operations related to the container runtime, such as checking if the runtime is Docker, if it is running, listing Kubernetes containers, deleting containers, pulling images, checking image existence, and retrieving CGroup driver information. + +By using the `sealctl cri` command, you can easily manage and inspect the container runtime environment in your Kubernetes cluster to ensure proper configuration and smooth operation. + + +```shell +sealctl cri [flags] +``` + + +Subcommands: + +1. `socket`: Check the CRI socket. + +```shell +sealctl cri socket +``` + +2. `cgroup-driver`: Get the cgroup driver of the container runtime. + +```shell +sealctl cri cgroup-driver [--short] +``` + +- `--short`: Print only the result. + +Global flags: + +- `--socket-path`: Path to the CRI socket. +- `--config`: Path to the CRI configuration file. + +Examples: + +```shell +sealctl cri socket +sealctl cri cgroup-driver --short +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md new file mode 100644 index 000000000000..82bb133846af --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hostname.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 3 +--- + +# Get Hostname + +Get the hostname of the operating system: + +```shell +sealctl hostname +``` + +Example: + +```shell +sealctl hostname +``` + +Executing this command will return the hostname of the operating system. No additional parameters need to be passed. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md new file mode 100644 index 000000000000..a218a94e18f7 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/hosts.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 4 +--- + +# Manage Hosts + +The `hosts` command is used to manage the hosts file of the operating system. The hosts file is a file used for domain name resolution to IP addresses and is typically used locally to override DNS resolution. By modifying the hosts file, you can assign a custom IP address to a specific domain name without relying on a DNS server. + +`sealctl hosts` provides the following three subcommands to manage the hosts file: + +1. `list`: List all entries in the current hosts file. +2. `add`: Add a new domain-to-IP mapping to the hosts file. +3. `delete`: Delete a specified domain-to-IP mapping from the hosts file. + +With these subcommands, you can conveniently view, add, and delete mappings in the hosts file, allowing you better control over domain name resolution to IP addresses. + +1. `sealctl hosts list`: List all entries in the current hosts file. + + Example: + + ```shell + sealctl hosts list + ``` + +2. `sealctl hosts add`: Add a new entry to the hosts file. + + Parameters: + + - `--ip`: IP address (required) + - `--domain`: Domain name (required) + + Example: + + ```shell + sealctl hosts add --ip 192.168.1.100 --domain example.com + ``` + +3. `sealctl hosts delete`: Delete an entry from the hosts file. + + Parameters: + + - `--domain`: Domain name to delete (required) + + Example: + + ```shell + sealctl hosts delete --domain example.com + ``` + +Note: You can specify the path of the hosts file by adding the `--path` parameter after any `hosts` subcommand. The default path is `/etc/hosts` (Linux systems). + +Example: + +```shell +sealctl hosts list --path /custom/path/hosts +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md new file mode 100644 index 000000000000..c5ca9a4c7b49 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/ipvs.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 6 +--- + +# Manage IPVS + +The `ipvs` command is used to create and manage local IPVS (IP Virtual Server) load balancing. IPVS is a module in the Linux kernel that allows high-performance load balancing to be implemented in the kernel space. The `ipvs` command achieves load balancing of services by managing the mapping between virtual servers and real servers. + +`sealctl ipvs` supports the following features: + +1. Creation and management of mappings between virtual servers and real servers. +2. Health-check functionality to periodically check the health status of real servers and perform necessary online/offline operations. +3. Support for two proxy modes: `route` and `link`. +4. Support for configuring proxy scheduling algorithms (e.g., round-robin, weighted round-robin, etc.). +5. Support for one-time creation of proxy rules (`--run-once` flag) or continuous operation and management of proxy rules. +6. Support for cleanup: Existing IPVS rules can be cleared and the command will exit using the `-C` or `--clean` flag. + +With the `sealctl ipvs` command, users can easily create and manage high-performance load balancing services locally. + +**Usage** + +```shell +sealctl ipvs [flags] +``` + +**Options** + +- `-C`, `--clean`: Clear existing rules and then exit. +- `--health-insecure-skip-verify`: Skip verification of insecure requests (default is true). +- `--health-path string`: URL path for probing (default is "/healthz"). +- `--health-req-body string`: Request body sent by the health checker. +- `--health-req-headers stringToString`: HTTP request headers (default is []). +- `--health-req-method string`: HTTP request method (default is "GET"). +- `--health-schem string`: HTTP scheme for the probe (default is "https"). +- `--health-status ints`: Valid status codes. +- `-h`, `--help`: Help for ipvs. +- `-i`, `--iface string`: Name of the virtual interface to create, behaving the same as kube-proxy (default is "lvscare"). Enabled only in mode=link. +- `--interval durationOrSecond`: Health check interval (default is 0s). +- `--ip ip`: Target IP as the routing gateway, used together with mode=route. +- `--logger string`: Log level: DEBG/INFO (default is "INFO"). +- `--masqueradebit int`: IPTables masquerade bit. Enabled only in mode=link. +- `--mode string`: Proxy mode: route/link (default is "route"). +- `--rs strings`: Real server addresses, e.g., 192.168.0.2:6443. +- `--run-once`: Create proxy rules and then exit. +- `--scheduler string`: Proxy scheduler (default is "rr"). +- `--vs string`: Virtual server address, e.g., 169.254.0.1:6443. + +**Global Options** + +- `--debug`: Enable debug logging. +- `--show-path`: Enable displaying code path. + +**Documentation** + +To use the `sealctl ipvs` command, follow these steps: + +1. Provide the necessary options and parameters for the command. +2. Execute the command, which will create or manage local IPVS load balancing. + +**Examples** + +Create proxy rules and then exit: + +```shell +sealctl ipvs --vs 169.254.0.1:6443 --rs 192.168.0.2:6443 --run-once +``` + +Clear existing IPVS rules: + +```shell +sealctl ipvs --clean diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md new file mode 100644 index 000000000000..91d03d6133a6 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/registry.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 5 +--- + +# Registry Image Repository + +## Sealos: Detailed Guide and Usage of `sealctl registry serve` Command + +Sealos provides the `sealctl registry serve` command to facilitate the construction and management of Docker image repositories. This document provides a detailed guide and usage examples for the `sealctl registry serve` command. + +### Introduction + +The `sealctl registry serve` command is primarily used to start a Docker distribution image repository server. It supports two modes: `filesystem` and `inmem`. + +1. **Filesystem Mode**: In this mode, `sealctl` runs a Docker distribution image repository server for a specified directory. The image data is stored on disk in this mode. **This command is also used by Sealos for incremental image synchronization**. + +2. **In-memory Mode**: In this mode, `sealctl` runs an in-memory Docker distribution image repository server. The image data is only stored in memory, and the data will be lost when the process exits. + +### Command Options + +The `sealctl registry serve filesystem` command supports the following options: + +- `--disable-logging`: Disable logging output (default is false). +- `--log-level`: Configure the log level (default is 'error'). +- `-p, --port`: The port the server listens on (default is a randomly unused port). + +### Usage Examples + +Here are some usage examples of the `sealctl registry serve` command: + +#### Start a Filesystem Image Repository Server + +```bash +sealctl registry serve filesystem --port=5000 +``` + +The above command starts a filesystem image repository server on port 5000. + +#### Start an In-memory Image Repository Server + +```bash +sealctl registry serve inmem +``` + +The above command starts an in-memory image repository server. The server will lose stored data when the process exits. + +With the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. It is a powerful and user-friendly tool for both development and production environments. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md new file mode 100644 index 000000000000..882ab560cb05 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/sealctl.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 0 +--- + +# Sealctl User Guide + +Sealos provides `sealctl`, a command-line tool for operating with Sealos and cluster nodes. It includes the following subcommands: + +1. `cert`: Manages certificates for generating, viewing, and updating TLS certificates. +2. `cri`: Manages Container Runtime Interface (CRI) configuration, such as Docker or containerd. +3. `hostname`: Views or sets the system hostname. +4. `hosts`: Manages the system's hosts file, which defines static hostname-to-IP address mappings. +5. `ipvs`: Manages IP Virtual Server (IPVS) rules for load balancing and proxying. +6. `registry`: Manages image repositories for storing container images in container repository format and repository management. +7. `static_pod`: Manages static Pods and creates static Pod configurations. +8. `token`: Generates and manages access tokens for authorizing access to Kubernetes clusters. + +With these subcommands, you can conveniently manage and configure your Sealos system, enabling control over containers, image repositories, networks, and other aspects. + +# Sealos Dependent Commands + +1. **Add Hosts** + + Adds a new hosts record on the node with the specified IP address. The parameters include the IP address, hostname, and domain name. Use the `sealctl hosts add` command. + +2. **Delete Hosts** + + Deletes a hosts record on the node with the specified IP address. The parameters include the IP address and domain name. Use the `sealctl hosts delete` command. + +3. **Hostname** + + Retrieves the hostname of the node with the specified IP address. Use the `sealctl hostname` command. + +4. **IPVS Load Balancing** + + Configures IPVS for load balancing on the node with the specified IP address. The parameters include the node IP address, virtual IP address, and a list of master node IP addresses. Use the `sealctl ipvs` command. + +5. **Clear IPVS Rules** + + Clears the IPVS configuration on the node with the specified IP address. The parameters include the node IP address and virtual IP address. Use the `sealctl ipvs` command. + +6. **Generate Static Pods** + + Deploys a static Pod (lvscare) on the node with the specified IP address. The parameters include the node IP address, virtual IP address, Pod name, image name, and a list of master node IP addresses. Use the `sealctl static-pod lvscare` command. + +7. **Manage Cluster Interact Authentication Token** + + Generates a token for the node with the specified IP address. The parameters include the node IP address, configuration file, and certificate key. Use the `sealctl token` command. + +8. **Get Node's CGroup Information** + + Retrieves the CRI CGroup information of the node with the specified IP address. Use the `sealctl cri cgroup` command. + +9. **Get Node's CRI Socket Information** + + Retrieves the CRI socket information of the node with the specified IP address. Use the `sealctl cri socket` command. + +10. **Generate Self-signed HTTPS Certificates on Node** + + Generates certificates for the node with the specified IP address. The parameters include the node IP address, a list of alternate names, host IP address, hostname, service CIDR, and DNS domain. Use the `sealctl cert` command. + +11. **Start Registry on Node** + + Starts the registry on the specified node for incremental image synchronization. Use the `sealctl registry serve` command. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md new file mode 100644 index 000000000000..e82cbecad743 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/static-pod.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 7 +--- + +# Static Pod Configuration + +The `static-pod` command is used to generate static Pods that are managed directly by kubelet instead of the API server. Static Pods are useful in certain scenarios, such as setting up and managing control plane components in a Kubernetes cluster. + +The `sealctl static-pod` command provides a convenient way to generate static Pod configuration files for specific purposes. Currently, it mainly supports generating the `lvscare` static Pod, which is a tool for managing IPVS rules. + +Using `sealctl static-pod lvscare`, you can generate the `lvscare` static Pod YAML file based on specified parameters such as VIP, master node addresses, and image name. This file can then be stored in the static Pod path of kubelet, and kubelet will automatically create and manage the corresponding Pod. + +**Usage** + +```shell +sealctl static-pod lvscare [flags] +``` + +**Options** + +- `--vip`: Default VIP IP (default is "10.103.97.2:6443"). +- `--name`: Name of the generated lvscare static Pod. +- `--image`: Image for the generated lvscare static Pod (default is `sealos.hub:5000/sealos/lvscare:latest`). +- `--masters`: List of master addresses for the generated static Pod. +- `--print`: Whether to print the YAML. + +**Examples** + +Generate the lvscare static Pod file and print the YAML: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +If the `--print` option is not used, the configuration file will be directly generated in `/etc/kubernetes/manifests` and the static Pod will be enabled: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md new file mode 100644 index 000000000000..40e57478c431 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 9 +--- + +# Tar and Untar in Sealos + +Sealos provides the `sealctl tar` and `sealctl untar` commands for compressing and decompressing files or directories. This guide explains how to use these two commands in detail. + +## The `sealctl tar` Command + +The `sealctl tar` command is used to compress a specified directory path into an archive file. Note that it will strip the parent directory. + +**Command Options:** + +- `--clear`: Whether to delete the source files after compression, default is false. +- `--compression`: Compression algorithm, available options are tar/gzip/zstd/disable, default is disable. +- `-o, --output`: Path of the archive file. + +**Basic Usage:** + +```bash +sealctl tar [flags] [options] +``` + +## The `sealctl untar` Command + +The `sealctl untar` command is used to search for archive files that match a glob pattern in the specified source path (`src`) and extract them to the destination path (`dst`). + +**Command Options:** + +- `--clear`: Whether to delete the source files after extraction, default is false. +- `-o, --output`: Path to extract the archive file. + +**Basic Usage:** + +```bash +sealctl untar [flags] [options] +``` + +## Usage Examples + +Here are some examples of using the `sealctl tar` and `sealctl untar` commands: + +**Create a compressed file:** + +```bash +sealctl tar --output=/path/to/archive.tar /path/to/source +``` + +The above command compresses the `source` directory into the `archive.tar` file. + +**Extract a compressed file:** + +```bash +sealctl untar --output=/path/to/destination /path/to/archive.tar +``` + +The above command extracts the `archive.tar` file to the `destination` directory. + +With the `sealctl tar` and `sealctl untar` commands, users can easily compress and decompress files or directories. These commands are useful tools for file management, particularly in backup and file migration scenarios. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md new file mode 100644 index 000000000000..311856c7ebc8 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealctl/token.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 8 +--- + +# Token Management + +The `sealctl token` command is primarily used to generate a token for connecting master and worker nodes in a Kubernetes cluster. In a Kubernetes cluster, when you want to add a new worker node, you typically need to provide a token for authentication. This token ensures that only worker nodes with the correct token can join the cluster. + +The `sealctl token` command generates a token for authentication by accepting a configuration file (optional) and a certificate key (optional) as parameters. By default, if no configuration file and certificate key are provided, the command uses built-in default settings to generate the token. + +In summary, the `sealctl token` command is used to generate a token for authentication, allowing worker nodes to securely join a Kubernetes cluster. Using this command simplifies the process of adding nodes to the cluster and ensures the security of the cluster. + +**Usage** + +```shell +sealctl token [config] [certificateKey] +``` + +**Parameters** + +- `config`: Configuration file (optional). +- `certificateKey`: Certificate key (optional). + +**Examples** + +Generate a token with default parameters: + +```shell +sealctl token +``` + +Generate a token with a custom configuration file and certificate key: + +```shell +sealctl token my-config my-certificate-key +``` diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json new file mode 100644 index 000000000000..c9cb98244ebc --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 1 +} diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md new file mode 100644 index 000000000000..5f05248f881e --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 11 +--- + +# Application Cluster Image Usage Guide + +Sealos provides a repository called [cluster-image](https://github.com/labring-actions/cluster-image) on GitHub for building and publishing application images for Kubernetes clusters. These images can be created by submitting code to this repository and can be published to `docker.io/labring/` as official application images. It supports building Docker container images as well as cluster images for applications. + +## Types of Images + +The repository supports three types of image builds: + +- **Application Cluster Images**: These are primarily for building application images using GitHub Actions and support both amd64 and arm64 architectures. +- **Configuration Cluster Images**: These are mainly for building configuration images using GitHub Actions. They are not container images and are not architecture-specific. They typically contain configuration scripts or customizations to default configurations. +- **Docker Images**: These are mainly for building container images using GitHub Actions and support both amd64 and arm64 architectures. + +## Workflow for Image Builds + +You can trigger image builds directly in the GitHub repository by creating an issue. Here are a few examples: + +- `/imagebuild_dockerimages helm v3.8.2 Key1=Value1,Key2=Value2` +- `/imagebuild_configs coredns v0.0.1` +- `/imagebuild_apps helm v3.8.2` + +The format of the image build commands for each type is `/imagebuild_ [Key=Value,...]`, where `` can be `dockerimages`, `configs`, or `apps`, `` and `` represent the application name and version respectively, and `[Key=Value,...]` is optional buildArg parameters used only for the `dockerimages` type. + +## Location of Image Configurations + +You can place your configuration files in the `applications///` directory, including Dockerfiles, Kubefiles, and init.sh scripts, among others. The init.sh script is typically used for downloading dependencies such as Helm and kubectl-minio. You can choose to use either a Dockerfile or Kubefile to define your image build logic. + +## Image Build Rules + +The build rules vary slightly for each type of image. Generally, you need to create different subdirectories under the application directory and place different types of files in them, which Sealos will use to build the images. The specific rules are as follows: + +1. `charts` directory: Place the Helm charts required for the cluster images. Kubernetes will scan the charts and fetch the images for building, and the registry directory will be placed at the same level as the Kubefile. +2. `manifests` directory: Place the Kubernetes yaml configurations directly. Kubernetes will scan all the images in the manifests directory and build the registry directory, which will be placed at the same level as the Kubefile. +3. `images/shim` directory: Store additional image lists and build the registry directory, which will be placed at the same level as the Kubefile. +4. If templates are required, place files with the `.tmpl` extension in `etc`, `charts`, or `manifests`. These files can be rendered by the `sealos run` command with environment variables and the `.tmpl` extension will be removed. For example, a file named `aa.yaml.tmpl` will be rendered as `aa.yaml`. Please ensure that the file names do not conflict with existing files. +5. The `registry` directory must be placed at the same level as the Kubefile. Otherwise, it will not be copied to the private repository of master0. Also, ensure that the registry is not stored in a chart, as it may cause slow scanning by Helm and potentially lead to OOM (out-of-memory) issues during image builds. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md new file mode 100644 index 000000000000..74bee99631c1 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/add.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 4 +--- + +# Adding Cluster Nodes with `sealos add` + +`sealos add` is a command in the Sealos command-line tool used to add nodes to a cluster. This guide provides detailed instructions on how to use the command and its options. + +**Note: Make sure the number of control nodes is odd to ensure proper etcd leader election.** + +## Basic Usage + +### Adding Nodes + +To add nodes to the cluster, you can use the `--nodes` option: + +```bash +sealos add --nodes x.x.x.x +``` + +In the above command, replace `x.x.x.x` with the IP address of the node you want to add. + +### Adding Control Nodes + +To add control nodes to the cluster, you can use the `--masters` option: + +```bash +sealos add --masters x.x.x.x +``` + +### Adding Control and Regular Nodes Together + +If you want to add both control nodes and regular nodes to the cluster, you can use the `--masters` and `--nodes` options together: + +```bash +sealos add --masters x.x.x.x --nodes x.x.x.x +sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## Options + +The `sealos add` command provides the following options: + +- `--cluster='default'`: The name of the cluster to perform the add operation. Defaults to `default`. + +- `--masters=''`: The control nodes to be added. + +- `--nodes=''`: The nodes to be added. + +Each option can be followed by an argument. + +## Usage Example + +Here's an example usage that adds a node with the IP address `192.168.0.2` to the cluster: + +```bash +sealos add --nodes 192.168.0.2 +``` + +That's it for the usage guide of the `sealos add` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md new file mode 100644 index 000000000000..d6e657705cf1 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/apply.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 1 +--- + +# Starting a Cluster with `sealos apply` + +`sealos apply` is an important command in the Sealos command-line tool used to run cluster images in a Kubernetes cluster. This guide provides detailed instructions on how to use the command and its options. + +## Basic Usage + +The basic usage of the `sealos apply` command is as follows: + +```shell +$ sealos apply -f Clusterfile +``` + +Clusterfile content: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: default +spec: + # Server IP addresses and roles + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +``` + +This command will run cluster images in the Kubernetes cluster based on the specified `Clusterfile`. + + +## Options + +The `sealos apply` command provides several options to customize its behavior: + +- `-f, --Clusterfile='Clusterfile'`: Specifies the Clusterfile to apply. Defaults to `Clusterfile`. +- `--config-file=[]`: Specifies the path to a custom config file to replace or modify resources. +- `--env=[]`: Sets environment variables to be used during command execution. +- `--set=[]`: Sets values on the command line, usually for replacing template values. +- `--values=[]`: Specifies values files to be applied to the `Clusterfile`, usually used for templating. + +Each option can be followed by one or more parameters. Multiple parameters are separated by commas. + +For example, you can use the `--set` option to set values on the command line: + +```shell +sealos apply -f Clusterfile --set key1=value1,key2=value2 +``` + +This command will set the values of `key1` and `key2` to `value1` and `value2`, and then apply the `Clusterfile`. + +Similarly, you can use the `--values` option to specify a values file: + +```shell +sealos apply -f Clusterfile --values values.yaml +``` + +This command will apply the `Clusterfile` based on the values in the `values.yaml` file. + +**For more examples, please refer to the [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster/.md) section.** + +That's it for the usage guide of the `sealos apply` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md new file mode 100644 index 000000000000..d2cb207046dc --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/build.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 6 +--- + +# Building Images with `sealos build` + +The `build` command in Sealos is used to build OCI images using instructions from Sealfiles, Kubefiles, Dockerfiles, or Containerfiles. This is the fundamental command in Sealos for building cluster images. + +If no parameters are specified, Sealos will use the current working directory as the build context and look for instruction files. If no Sealfile, Kubefile, Dockerfile, or Containerfile is found, the build will fail. + +Here are some key options for the `build` command: + +1. `--all-platforms`: Attempts to build the image for all supported base image platforms. +2. `--authfile`: Path to the authentication file. +3. `--build-arg`: Provides an `argument=value` to the builder. +4. `--build-context`: Provides additional build context to the builder as `argument=value`. +5. `--creds`: Credentials to access the registry as `[username[:password]]`. +6. `-D, --disable-compression`: Disables layer compression by default. +7. `--env`: Sets environment variables for the image. +8. `-f, --file`: Pathname or URL of the Dockerfile. +9. `--force-rm`: Always removes intermediate containers after a build, even if the build fails. +10. `--format`: Format for the manifest and metadata of the built image. +11. `--from`: Replaces the value of the first FROM instruction in the Containerfile with the specified image name. +12. `--http-proxy`: Passes the HTTP Proxy environment variable. +13. `--isolation`: Process isolation `type` to use, can be 'oci' or 'chroot'. +14. `--max-pull-procs`: Maximum number of goroutines to use for pulling images. +15. `--platform`: Sets the OS/ARCH/VARIANT for the image to the provided value instead of the host's current operating system and architecture. +16. `--pull`: Pulls the image from the registry, if new or not present in the store. Can be set to false, always, or never. +17. `-q, --quiet`: Suppresses the build output and image read/write progress. +18. `--retry`: Number of times to retry on push/pull failure. +19. `--retry-delay`: Delay in seconds between retries on push/pull failure. +20. `--rm`: Removes intermediate containers after a successful build. +21. `--save-image`: Saves resolved images from a specific directory in the registry format. +22. `--sign-by`: Signs the image with the GPG key of the specified `FINGERPRINT`. +23. `-t, --tag`: Name and optionally a tag in the 'name:tag' format to apply to the built image. +24. `--target`: Sets the target build stage to build. +25. `--timestamp`: Sets the created timestamp to the specified epoch seconds for reproducible builds. Default is the current time. + +These options provide flexibility for various build requirements, including platform-specific builds, environment variable settings, build context management, image signing, and more. With the `--save-image` option, Sealos can automatically recognize and save the required images (including those resolved from image lists, Helm charts, and manifests) in the Docker Registry format. + +The process isolation mode `--isolation` supports two parameters: 'oci' and 'chroot'. Choose 'oci' mode if OCI is supported locally and 'chroot' mode if OCI is not supported. + +The `--save-image` option is used in Sealos build commands to automatically find and save the required images during the build process. In Sealos, building an image may involve other dependent images. These dependent + +images can come from image lists, Helm charts, or cluster manifests. When using the `--save-image` option, Sealos will automatically resolve these dependencies based on the build context and save them in the Docker Registry format. + +For example, here is an example using the `--save-image` option: + +```bash +sealos build -t myapp:v1.0.0 -f Dockerfile . +``` + +In this example, Sealos will use the current directory as the build context, read the build instructions from the Dockerfile, and attempt to build an image tagged as `myapp:v1.0.0`. Additionally, Sealos will resolve all base images referenced in the Dockerfile's `FROM` instructions and save those images as well. These images will be saved in the Docker Registry format and can be pushed directly to a Docker Registry. + +If your build context also includes Helm charts or cluster manifests, Sealos will also resolve the images referenced in those files and save them accordingly. + +Overall, the `--save-image` option provides a convenient way for Sealos to handle image dependencies during the build process, greatly improving the convenience and efficiency of building images. + +Here are some detailed examples: + +- [Build with Image Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-image_list.md) +- [Build with Deploy Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md) +- [Build with Helm Charts](/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [Build with Binary](/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md) +- [Build with go-template](/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md) +- [Build with exec and scp](/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md) + +With the `build` command in Sealos, you can build OCI images based on various instruction files to provide the required images for Sealos. This process includes handling various instructions in Dockerfiles or other instruction files, such as `FROM`, `RUN`, `ADD`, etc., as well as managing image layers, labels, and more. The build process also involves pulling base images, running commands, saving the results, and more. Each step can be finely controlled and customized using the options mentioned above to accommodate different build requirements. + +That's the usage guide for the `sealos build` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md new file mode 100644 index 000000000000..8a595e99bd44 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/cert.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 3 +--- + +# Updating Cluster Certificates with `sealos cert` + +The `cert` command in Sealos is used to update the API server certificates in a cluster. This guide provides detailed instructions on how to use this command and its options. + +## Basic Usage + +To add domain names or IP addresses to the certificate, you can use the `--alt-names` option: + +```bash +sealos cert --alt-names sealos.io,10.103.97.2,127.0.0.1,localhost +``` + +In the above command, replace `sealos.io,10.103.97.2,127.0.0.1,localhost` with the domain names and IP addresses you want to add. + +**Note**: It is recommended to back up the old certificates before performing this operation. + +After executing the `sealos cert` command, the API server certificates in the cluster will be updated. You don't need to manually restart the API server as Sealos will automatically handle the restart. + +## Options + +The `cert` command provides the following options: + +- `--alt-names='`': Adds domain names or IP addresses to the certificate, e.g., `sealos.io` or `10.103.97.2`. + +- `-c, --cluster='default'`: Specifies the name of the cluster on which to perform the exec operation. Default is `default`. + +Each option can be followed by an argument. + +## Certificate Verification + +After updating the certificates, you can use the following commands for verification: + +```bash +kubectl -n kube-system get cm kubeadm-config -o yaml +openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text +``` + +The above commands retrieve the kubeadm-config ConfigMap in the kube-system namespace and display detailed information about the apiserver.crt certificate. + +That concludes the usage guide for the `sealos cert` command. We hope this helps you. If you have any questions or encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md new file mode 100644 index 000000000000..7753c45d0f93 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/commands.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 0 +--- + +# Sealos Command Reference + +## Cluster Management Commands + +- `apply`: Runs cluster images within a Kubernetes cluster using Clusterfile. +- `cert`: Updates the certificates of the Kubernetes API server. +- `run`: Easily runs cloud-native applications. +- `reset`: Resets all content in the cluster. +- `status`: Views the status of the Sealos cluster. + +## Node Management Commands + +- `add`: Adds nodes to the cluster. +- `delete`: Removes nodes from the cluster. + +## Remote Operation Commands + +- `exec`: Executes shell commands or scripts on the specified node. +- `scp`: Copies files to the remote location of the specified node. + +## Experimental Commands + +- `registry`: Commands related to the image registry. + +## Container and Image Commands + +- `build`: Builds images using instructions from Sealfile or Kubefile. +- `create`: Creates a cluster but does not run CMD, used for image inspection. +- `inspect`: Inspects the configuration of containers or images. +- `images`: Lists images in local storage. +- `load`: Loads images from a file. +- `login`: Logs into a container registry. +- `logout`: Logs out of a container registry. +- `manifest`: Operates on manifest lists and image indexes. +- `merge`: Merges multiple images into one. +- `pull`: Pulls images from a specified location. +- `push`: Pushes images to the specified destination. +- `rmi`: Removes one or more images from local storage. +- `save`: Saves images to an archive file. +- `tag`: Adds an additional name to a local image. + +## Other Commands + +- `completion`: Generates autocompletion scripts for the specified shell. +- `docs`: Generates API reference documentation. +- `env`: Prints all environment information used by Sealos. +- `gen`: Generates a Clusterfile with all default settings. +- `version`: Prints version information. + +The `--debug` flag in Sealos is a global flag used to enable debug mode for more detailed information about the system's operation when issues occur. + +For installation instructions, please refer to the [Sealos Installation Guide](/self-hosting/lifecycle-management/quick-start/installation); for a quick start guide, please refer to the [Quick Start Guide](/self-hosting/lifecycle-management/quick-start/.md). diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md new file mode 100644 index 000000000000..004a4d8dd4ca --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/create.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 6 +--- + +# Create Working Directory + +`sealos create` is a command in the Sealos command line tool, mainly used for creating the cluster working directory without executing the CMD, so as to review the image. This guide will detail its usage and options. + +## Basic Usage + +The `sealos create` command is used to create a cluster working directory but does not actually run, mainly used for debugging or testing. It can output the address of the cluster image, you can check whether the content of the cluster image is consistent with the expectation. + +```bash +sealos create docker.io/labring/kubernetes:v1.24.0 +``` + +In the above command, `clustername` represents the name of the cluster you want to create. + +## Options + +The `sealos create` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster to be created but not actually run. The default is `default`. + +- `--platform='linux/arm64/v8'`: Set the operating system/architecture/version of the image to the provided value, rather than the current operating system and architecture of the host (for example, `linux/arm`). + +- `--short=false`: If true, only print the mount path. + +- `-e, --env=[]`: Specify environment variables used during the rendering of template files. + +Each option can be followed by an argument. + +## Example + +For example, you can use the following command to create a cluster named `mycluster`, but do not actually run it: + +```bash +sealos create -e registryPort=8443 docker.io/labring/kubernetes:v1.24.0 +``` + +This command will create a cluster working directory with an image name of `docker.io/labring/kubernetes:v1.24.0` and output the address of the cluster image. The `-e registryPort=8443` option specifies the environment variable used during the rendering of template files, where `registryPort` is set to `8443`. Please note that in this example, the cluster is not actually run. + +The above is the usage guide for the `sealos create` command, hope it helps. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md new file mode 100644 index 000000000000..6750281c9c5f --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/delete.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 4 +--- + +# Delete Cluster Node Removal + +`sealos delete` is a command in the Sealos command line tool, mainly used to remove nodes from the cluster. This guide will detail its usage and options. + +**Note to ensure the number of control nodes is odd to ensure normal election of etcd** + +## Basic Usage + +### Delete Node + +To remove a node from the cluster, you can use the `--nodes` option: + +```bash +sealos delete --nodes x.x.x.x +``` + +In the above command, `x.x.x.x` should be replaced with the IP address of the node you want to delete. If you accidentally delete the wrong node, you can use the `sealos add` command to recover it: + +```bash +sealos add --nodes x.x.x.x +``` + +### Delete Control Node + +To remove a control node from the cluster, you can use the `--masters` option: + +```bash +sealos delete --masters x.x.x.x +``` + +Please note, if the `--masters` parameter is specified, sealos will delete your control node. + +### Delete Control Node and Node + +If you want to delete both control nodes and nodes at the same time, you can use the `--masters` and `--nodes` options at the same time: + +```bash +sealos delete --masters x.x.x.x --nodes x.x.x.x +sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## Options + +The `sealos delete` command provides the following options: + +- `--cluster='default'`: The name of the cluster to which the deletion operation applies. The default is `default`. + +- `--force=false`: You can enter a `--force` flag to force delete nodes. + +- `--masters=''`: The control nodes to be removed. + +- `--nodes=''`: The nodes to be removed. + +Each option can be followed by an argument. + +## Usage Example + +Here is a usage example that deletes a node with the IP address of `192.168.0.2`: + +```bash +sealos delete --nodes 192.168.0.2 +``` + +The above is the usage guide for the `sealos delete` command, hope it helps. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md new file mode 100644 index 000000000000..4331432abf1c --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/env.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 7 +--- + +# Environment Variables + +`sealos env` is a command in the Sealos command-line tool, used to display the current environment variables supported by sealos and their current values. + +## Basic Usage + +### Viewing Environment Variables + +To view the environment variables, you can use the `sealos env` command: + +```bash +sealos env +``` + +### Viewing Environment Variables and Descriptions + +To view the environment variables and their descriptions, you can use the `sealos env -v` command: + +```bash +sealos env -v +``` + + +## How to Set Environment Variables + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t xxx . +``` + + +That's the usage guide for the `sealos env` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md new file mode 100644 index 000000000000..f7ce53e19ffd --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/exec.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# Execute Command + +`sealos exec` is a command in the Sealos command-line tool, used to execute Shell commands or scripts on specified cluster nodes. This guide will detail its usage and options. + +## Basic Usage + +The basic `sealos exec` command format is as follows: + +```bash +sealos exec "shell command or script" +``` + +In the above command, `shell command or script` is the Shell command or script you want to execute on the cluster nodes. + +## Options + +The `sealos exec` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster on which the command will be executed. The default is `default`. + +- `--ips=[]`: Run commands on nodes with specified IP addresses. + +- `-r, --roles='':` Run commands on nodes with specified roles. Currently supports master,node,registry. + +Each option can be followed by one or more parameters. + +## Examples + +For example, you can use the following command to view the contents of the `/etc/hosts` file on all nodes of the default cluster: + +```bash +sealos exec "cat /etc/hosts" +``` + +If you want to view the contents of the `/etc/hosts` file on nodes with `master` and `node` roles in a cluster named `my-cluster`, you can use the following command: + +```bash +sealos exec -c my-cluster -r master,node "cat /etc/hosts" +``` + +If you only want to view the contents of the `/etc/hosts` file on a node with the IP address `172.16.1.38`, you can use the following command: + +```bash +sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" +``` + +That's the usage guide for the `sealos exec` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md new file mode 100644 index 000000000000..7601c36f62ce --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/gen.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 3 +--- + +# Generate Cluster Configuration + +Sealos' `gen` command is used to generate a Kubernetes cluster configuration file (Clusterfile), which can then be applied using the `sealos apply` command. The `gen` command can help users quickly generate a basic configuration file, which can then be modified and adjusted according to their needs. + +Here are the basic usage of `sealos gen` command and some common examples: + +1. Generate a single-node cluster with default configuration: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 + ``` + +Notice: labring/helm should be set before labring/calico. + +2. Generate a cluster that includes multiple images and specifies the master and worker nodes: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' + ``` + +Notice: labring/helm should be set before labring/calico. + +3. Specify SSH port, for servers using the same SSH port: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' + ``` + + For servers using different SSH ports: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ + --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd 'xxx' + ``` + +After the Clusterfile is generated, users can modify this file according to their needs. Add or modify environment variables; modify the cluster cidr configuration. Once the modifications are done, users can use the `sealos apply` command to create or update the cluster based on this configuration file. + +Example explanations: + +- [Custom Configuration Installation](/self-hosting/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) + +That's the usage guide for the `sealos gen` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md new file mode 100644 index 000000000000..c3d79a4d6f4f --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/images.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +--- + +# Image List + +Sealos' `images` command is primarily used to view locally stored images. Users can use it to view all local images or to filter and view specific images. The command supports various parameters to help users view and manage images more conveniently. + +## Basic Usage + +The basic `sealos images` command will display all non-intermediate stage local images, for example: + +```bash +sealos images +``` + +This will display all final stage images stored locally. + +## Examples + +Here are some common examples of the `sealos images` command: + +1. Display all images, including intermediate images built: + + ```bash + sealos images --all + ``` + +2. Display a specific image: + + ```bash + sealos images [imageName] + ``` + +3. Display images in a specified Go template format: + + ```bash + sealos images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}' + ``` + +## Parameters + +Here are some common parameters for the `sealos images` command: + +- `-a, --all`: Display all images, including intermediate images built. + +- `--digests`: Display the digests of images. + +- `-f, --filter`: Filter output based on provided conditions. + +- `--format`: Beautify the images printout using a Go template. + +- `--history`: Display the naming history of images. + +- `--json`: Output in JSON format. + +- `--no-trunc`: Do not truncate the output. + +- `-n, --noheading`: Do not print column headings. + +- `-q, --quiet`: Only display image IDs. + +By combining these parameters, users can easily retrieve and manage locally stored images. For example, using the `--all` parameter displays all images, including intermediate ones; using the `--filter` parameter filters images based on specific conditions; using the `--json` parameter outputs image information in JSON format, facilitating programmatic processing, etc. + +That's the usage guide for the `sealos images` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md new file mode 100644 index 000000000000..7ff8dfff3e87 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 6 +--- + +# Inspect Details + +Sealos' `inspect` command is primarily used to view the configuration information of build containers or built images. The command supports viewing detailed information about an image or container, including its metadata, environment variables, startup commands, etc. + +## Basic Usage + +Use the `sealos inspect` command to view the configuration information of a specified container or image. For example, to view the configuration of a specified container: + +```bash +sealos inspect containerID +``` + +Or to view the configuration of a specified image: + +```bash +sealos inspect --type image imageWithTag +``` + +## Examples + +Here are some common examples of the `sealos inspect` command: + +1. View container configuration: + + ```bash + sealos inspect containerID + ``` + +2. View image configuration: + + ```bash + sealos inspect --type image imageWithTag + ``` + +3. View configuration information of an image ID: + + ```bash + sealos inspect --type image @imageID # Or just input the imageID, '@' is optional + ``` + +4. View configuration information of a remote image repository: + + ```bash + sealos inspect --type image docker://alpine:latest + ``` + +5. View configuration information of an image in a local OCI archive file: + + ```bash + sealos inspect --type image oci-archive:/abs/path/of/oci/tarfile.tar + ``` + +6. View configuration information of an image in a local Docker archive file: + + ```bash + sealos inspect --type image docker-archive:/abs/path/of/docker/tarfile.tar + ``` + +7. Display image environment variables in Go template format: + + ```bash + sealos inspect --format '{{.OCIv1.Config.Env}}' alpine + ``` + +## Parameters + +Here are some common parameters for the `sealos inspect` command: + +- `-f, --format`: Display output results in Go template format. **Template structure code [InspectOutput](https://github.com/labring/sealos/blob/f8a17787822714c5fdf21f2a75cc86fadb88adfa/pkg/buildah/inspect.go#L189)** + +- `-t, --type`: Specify the type to view, which can be a container (`container`) or an image (`image`). + +Depending on your needs, you can combine these parameters to get specific configuration information. For example, using the `-t` parameter can specify whether you want to view the configuration information of the container or the image; using the `-f` parameter, you can define a specific output format, which is convenient for processing or parsing the output results. + +That's the usage guide for the `sealos inspect` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md new file mode 100644 index 000000000000..49a9f84a3069 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/load.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 6 +--- + +# Load Image + +`sealos load` is a command used to load images from archive files. This is very useful when you need to import images from existing archive files, especially in environments without a network connection. + +## Usage: + +`sealos load [flags] [options]` + +## Parameters: + +Here are the parameters for the `sealos load` command: + +- `-i, --input=''`: Load image from a tar archive file. + +## Examples: + +- Load an image from an archive file: `sealos load -i myimage.tar` + +Note that when using the `sealos load` command, you need to ensure that the specified archive file exists and is correctly formatted. If you encounter problems when importing images, you may need to check your archive files to ensure they have not been corrupted or incorrectly formatted. + +That's the usage guide for the `sealos load` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md new file mode 100644 index 000000000000..b755c25099a6 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/login.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 6 +--- + +# Login to Repository + +The `sealos login` command is used to log into the container registry on the specified server. After logging into the registry, you can pull and push images. + +## Usage: + +`sealos login [flags] [options] registryName` + +## Parameters: + +Here are the parameters for the `sealos login` command: + +- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment variable. + +- `--cert-dir=''`: Use certificates at the specified path to access the image repository. + +- `--get-login=true`: Return the current login user for the registry. + +- `-k, --kubeconfig=''`: Log into the sealos image repository hub.sealos.io using kubeconfig. + +- `-p, --password=''`: Password for the registry. + +- `--password-stdin=false`: Take the password from standard input. + +- `-u, --username=''`: Username for the registry. + +- `-v, --verbose=false`: Write more detailed information to standard output. + +## Examples: + +- Log into the quay.io registry: `sealos login -u myusername -p mypassword quay.io` + +Please note that when using the `sealos login` command, you need to make sure that you provide the correct username and password, otherwise the login process might fail. If you encounter problems during the login process, you might need to check your username and password to ensure they have not been entered incorrectly or forgotten. + +That's the usage guide for the `sealos login` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md new file mode 100644 index 000000000000..8584054cb52b --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/logout.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 6 +--- + +# Logout from Repository + +The `sealos logout` command is used to remove the locally cached account and password of the image repository on the specified server. + +## Usage: + +`sealos logout [flags] [options] registryName` + +## Parameters: + +Here are the parameters for the `sealos logout` command: + +- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment variable. + +- `-a, --all=false`: Delete all authentication information. + + +## Examples: + +- Logout from the quay.io image repository: `sealos logout quay.io` + +That's the usage guide for the `sealos logout` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md new file mode 100644 index 000000000000..fa8e9160c617 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 6 +--- + +# Image Manifest + +The `manifest` command of Sealos is used to create, modify, and push manifest lists and image indexes. These functions are mainly used to handle multi-architecture support of images. In Docker and OCI image specifications, manifest lists (also known as "fat manifests") or image indexes allow an image tag (like `myimage:latest`) to be used on various hardware architectures (like amd64, arm64, ppc64le, etc.). + +Here are some of the major `manifest` subcommands: + +1. `create`: Creates a new manifest list or image index. Example: `sealos manifest create localhost/list` +2. `add`: Adds an image to the manifest list or image index. Example: `sealos manifest add localhost/list localhost/image` +3. `annotate`: Adds or updates information in entries of the manifest list or image index. Example: `sealos manifest annotate --annotation A=B localhost/list localhost/image` +4. `inspect`: Displays the content of the manifest list or image index. Example: `sealos manifest inspect localhost/list` +5. `push`: Pushes the manifest list or image index to the registry. Example: `sealos manifest push localhost/list transport:destination` +6. `remove` and `rm`: Removes entries from the manifest list or image index, or completely deletes the manifest list or image index. Example: `sealos manifest remove localhost/list sha256:entryManifestDigest` or `sealos manifest rm localhost/list` + +With the `sealos manifest` command, you can flexibly manage manifest lists or image indexes, providing support for multi-architecture Docker or OCI images. Users can create custom manifest lists according to their needs, making it convenient to deploy and run Docker images on different hardware architectures. + +Users who want to build multi-architecture images through the manifest command can refer to the document [Building Cluster Images that Support Multiple Architectures](/self-hosting/lifecycle-management/operations/build-image/build-multi-arch-image.md). diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md new file mode 100644 index 000000000000..74db43c175d1 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/merge.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 6 +--- + +# Merge Image Merging + +The primary function of the `merge` command in Sealos is to merge multiple images into one. It does this by reading the Dockerfiles of each input image, merging the commands and layer structures into a new image. The running logic of this command is very similar to the `build` command, and many of the parameters are the same. + +This function is very useful when multiple images share layers, as it can reduce the size of the image and save storage space. Moreover, since the merged image contains all the functionalities of multiple images, it can help simplify application deployment. + +Here is a basic usage example of `sealos merge`: + +```bash +sealos merge -t new:0.1.0 kubernetes:v1.19.9 mysql:5.7.0 redis:6.0.0 +``` + +In this example, the three images `kubernetes:v1.19.9`, `mysql:5.7.0`, and `redis:6.0.0` are merged into a new image `new:0.1.0`. + +The `sealos merge` command provides rich options to customize the merging process, such as `--all-platforms` to attempt to build images for all base image platforms, `--build-arg` to provide parameters to the builder, `--no-cache` to disable existing cached images, and so on. + +Please note that the `sealos merge` command builds a new image based on the Dockerfiles of each input image. Therefore, if the Dockerfiles of the input images are incompatible, or there are any build errors, this command may fail. When using the `sealos merge` command, make sure you understand the Dockerfile of each input image and adjust as needed. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md new file mode 100644 index 000000000000..74ae19b2b566 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/pull.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 6 +--- + +# Pull Image + +The `sealos pull` command is a very useful one that allows you to download images from the container image repository and store them locally. Users can obtain images by their tags or digests. If no tag is specified, the image with the 'latest' tag (if it exists) will be downloaded by default. + +By using this command, users can easily download the required images from remote repositories, greatly improving work efficiency. + +## Usage: + +`sealos pull [flags] [options] imageName` + +## Parameters: + +The following are the parameters of the `sealos pull` command: + +- `-a, --all-tags=false`: Download all tagged images in the repository. + +- `--authfile=''`: The path to the authentication file. The REGISTRY_AUTH_FILE environment variable can be used to override it. + +- `--cert-dir=''`: The specified path to the certificate for accessing the image repository. + +- `--creds=''`: Use `[username[:password]]` to access the image repository. + +- `--decryption-key=[]`: The key needed to decrypt the image. + +- `--platform=[linux/arm64/v8]`: When choosing an image, prioritize the specified OS/ARCH over the current operating system and architecture. + +- `--policy='missing'`: Set the policy, the optional values include 'missing', 'always', 'never'. + +- `-q, --quiet=false`: Do not output progress information when pulling images. + +- `--remove-signatures=false`: Do not copy signatures when pulling images. + +- `--retry=3`: The number of retries when the pull fails. + +- `--retry-delay=2s`: The delay between retries when the pull fails. + +## Examples: + +- Pull an image: `sealos pull my-image:latest` + +- Pull an image from the Docker daemon: `sealos pull docker-daemon:my-image:tag` + +- Pull an image from a specific repository: `sealos pull myregistry/myrepository/my-image:tag` + +- Pull multiple images: `sealos pull imageID1 imageID2 imageID3` + +The above is a usage guide for the `sealos push` command, hoping to help you. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md new file mode 100644 index 000000000000..6b9b2a5677a0 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/push.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 6 +--- + +# Push Image + +`sealos push` is a command in the Sealos command-line tool, used to push images to a specified location. This command is particularly useful when you need to push local Docker images to a remote image repository. This guide will provide a detailed introduction to its usage. + +## Basic Usage + +The basic format of the `sealos push` command is as follows: + +```bash +sealos push IMAGE_ID DESTINATION +``` + +In the above command, `IMAGE_ID` is the ID of the image you want to push, and `DESTINATION` is the location where you want to push it. The `DESTINATION` uses the "transport:details" format. If not specified, the source IMAGE is reused as the DESTINATION. + +In Sealos, the transport defines the format and location of the source image and the target image during the copying process. Here are the various transports supported by Sealos: + +1. `containers-storage`: This transport is used for storing and managing containers running locally, such as images of containers created using Podman or CRI-O. + +2. `dir`: This transport stores the image in a directory in the local file system, the structure of which conforms to the OCI layout. + +3. `docker`: This transport is used for interacting with Docker registries, such as Docker Hub or any other compatible private registry. + +4. `docker-archive`: This transport stores the image as a local Docker tar file (`.tar`), which is Docker's native format. + +5. `docker-daemon`: This transport is used for interacting with the local Docker daemon. It can extract images from the Docker daemon or push images to the Docker daemon. + +6. `oci`: This transport stores the image in a directory that conforms to the OCI layout, which is an open container image format. + +7. `oci-archive`: This transport stores the image as a local OCI tar file (`.tar`). + +8. `ostree`: This transport stores the image in an OSTree repository, which is a file system that supports atomic upgrades and rollbacks. + +9. `sif`: This is the Singularity SIF format, mainly used for high-performance computing and data-intensive applications. + +Examples: + +- Push an image to a Docker registry: `sealos push my-image:latest docker://my-registry.example.com/my-image:latest` + +- Export an image from the Docker daemon: `sealos push docker-daemon:my-image:latest dir:/path/to/save/` + +- Push an image to local container storage: `sealos push my-image:latest containers-storage:my-new-image:latest` + +## Example + +For example, you can use the following command to push an image to the `registry.example.com` repository: + +```bash +sealos push my_image_id docker://registry.example.com/my_repository:my_tag +``` + +## Optional Parameters + +- `--all`: This parameter is used to push all images referred by the manifest list. + +- `--authfile`: This parameter is used to specify the path to the authentication file. The REGISTRY_AUTH_FILE environment variable can be overridden. + +- `--cert-dir`: This parameter is used to specify the path to the certificate required to access the registry. + +- `--compression-format`: This parameter is used to specify the compression format to be used. + +- `--compression-level`: This parameter is used to specify the compression level to be used. + +- `--cr-option`: This parameter is used to control whether the image's Custom Resources (CR) are pushed to the target image repository. + + Specifically, the optional values for this parameter include: + + - "yes": The image and its associated CR will be pushed to the target image repository. + + - "no": Only push the image, do not push any CR. + + - "only": Only push the CR, do not push the image itself. + + - "auto": Automatically decide whether to push based on the actual status of the image and CR. For example, if the CR has changed or does not exist in the target repository, it will be pushed. + + Please note, this parameter is mainly used when dealing with images containing custom resources (such as Kubernetes CRD objects), allowing you to more flexibly control the push process of images and CRs. + +- `--creds`: This parameter is used to access the registry, in the form of `[username[:password]]`. + +- `--digestfile`: This parameter, after copying the image, writes the digest of the resulting image to a file. + +- `-D`, `--disable-compression`: This parameter is used to not compress layers. + +- `--encrypt-layer`: This parameter is used to specify the layer to be encrypted. 0-index layer index supports negative index (for example, 0 is the first layer, -1 is the last layer). If not defined, all layers will be encrypted when the encryption-key flag is specified. + +- `--encryption-key`: This parameter is used to specify the key needed to encrypt the image, used together with the encryption protocol (for example, jwe:/path/to/key.pem). + +- `-f`, `--format`: This parameter is used to specify the type of manifest to use in the target (oci, v2s1, or v2s2) (default is the source's + +manifest type, with fallback). + +- `-q`, `--quiet`: This parameter is used to not output progress information when pushing the image. + +- `--remove-signatures`: This parameter is used to not copy signatures when pushing the image. + +- `--retry`: This parameter is used to specify the number of retries when the push/pull fails. + +- `--retry-delay`: This parameter is used to specify the delay between retries when the push/pull fails. + +- `--rm`: This parameter is used to delete the manifest list after the push is successful. + +- `--sign-by`: This parameter is used to sign the image using a GPG key with the specified `FINGERPRINT`. + +That's the guide to using the `sealos push` command, and I hope it's helpful to you. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md new file mode 100644 index 000000000000..dc886345c6c2 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/registry.md @@ -0,0 +1,233 @@ +--- +sidebar_position: 8 +--- + +# Registry Image Repository Commands + +## Sealos: Detailed Explanation and User Guide for the `sealos registry save` Command + +The `registry save` command is used to pull remote Docker images to the local and save them in a specified directory. This is particularly useful for deploying container images in offline or intranet environments. + +When executing the `registry save` command, it will automatically obtain the `sealos login` authentication information for repository authentication. + +**Usage Guide** + +1. Use context to automatically retrieve images + + Pull and save images in the default manner. This mode will automatically parse the `charts` directory, `manifests` directory, and `images` directory to get the image list. + + **Usage Example** + + ```shell + sealos registry save --registry-dir=/tmp/registry1 my-context + ``` + + +2. Specified image list mode + + Pass the image list using arguments + + **Usage Example** + + ```shell + sealos registry save --registry-dir=/tmp/registry2 --images=docker.io/library/busybox:latest + ``` + +**Options** + +The following options apply to the `save` command and its subcommands: + +- `--max-procs`: The maximum number of parallel processes used to pull images. +- `--registry-dir`: The local directory to save images. +- `--arch`: The target architecture of the image, such as: `amd64`, `arm64`, etc. +- `--images`: The image list to be pulled and saved, separated by commas. For example: "my-image1:latest,my-image2:v1.0". + +## Sealos: Detailed Explanation and User Guide for the `sealos registry serve` Command + +In the process of managing Docker image repositories, Sealos provides the `sealos registry serve` command to facilitate user operations. This article will detail the usage methods and examples of the `sealos registry serve` command. + +### Basic Introduction + +The main function of the `sealos registry serve` command is to start a Docker distribution image repository server, supporting two modes: `filesystem` and `inmem`. + +1. **Filesystem mode**: In this mode, sealctl will run a Docker distribution image repository server for the specified directory. In this mode, the image data will be stored on the hard drive. + +2. **In-memory mode**: In this mode, sealctl will run a Docker distribution image repository server in memory. In this mode, the image data is only stored in memory, and the data will be lost after the process exits. + +### Command Parameters + +The `sealos registry serve filesystem ` command supports the following parameters: + +- `--disable-logging`: Disable log output, default is false. +- `--log-level`: Configure log level, default is 'error'. +- `-p, --port`: The port the server listens to, default is a random unused port. + +### Usage Examples + +Here are some usage examples of the `sealos registry serve` command: + +#### Start an image repository server in the filesystem + +```bash +sealos registry serve filesystem --port=5000 +``` + +The above command will start a filesystem image repository server on port 5000. + +#### Start an image repository server in memory + +```bash +sealos registry serve inmem +``` + +The above command will start an in-memory image repository server. The stored data of this server will be lost when the process exits. + +Through the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. Whether in the development environment or in the production environment, it is a powerful and easy-to-use tool. + +## Sealos: Detailed Explanation and User Guide of the `sealos registry passwd` Command + +In the process of managing Docker image repositories, Sealos provides the `sealos registry passwd` command to facilitate users in modifying the password for the cluster registry. It offers a convenient method to help users change the password of the registry. + +### Basic Usage + +Use the `sealos registry passwd` command to modify the password of the registry. + +```bash +sealos registry passwd +``` + +### Parameters + +Below are the parameters of the `sealos registry passwd` command: + +- `-c, --cluster-name`: Cluster name, the default is 'default'. + +- `-f, --cri-shim-file-path`: Image cri shim file path, if null it will not update the image cri shim file. The default path is '/etc/image-cri-shim.yaml'. + +- `-p, --htpasswd-path`: Registry password file path. The default path is '/etc/registry/registry_htpasswd'. + +### Usage Steps + +1. Execute the `sealos registry passwd` command, you can specify parameters according to the needs. + +2. According to the command prompt, input the new password. + +3. After the command is successfully executed, the registry's password will be changed to the new password. + +### Demo Explanation + +[![asciicast](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ.svg)](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ) + +**In the usage process, it will let the user choose the registry type** + +- registry: Binary startup, execute `systemctl restart registry` to restart the image repository. + +- containerd: Containerd startup, execute "nerdctl restart sealos-registry" to restart the image repository. + +- docker: Docker startup, execute "docker restart sealos-registry" to restart the image repository. + +### Notice + +**After changing the registry password, modify the registry password in the Clusterfile** +After changing the registry password, all nodes and services using this registry need to update their configurations to use the new password for authentication. Otherwise, they will not be able to pull or push images from this registry. + +If you are unsure about how to update the configuration of nodes and services, it is recommended to consult related documentation or seek professional technical support before changing the registry password. + +## Sealos: Detailed Explanation and User Guide of the `sealos registry sync` Command + +Sealos' `registry sync` command can help you synchronize all images between two registries. This can be used not only for image migration but also for backing up your images. + +### Basic Command Usage + +Execute the `sealos registry sync` command for image synchronization: + +```bash +sealos registry sync source dst +``` + +Here `source` represents the address of the source registry, and `dst` is the address of the target registry. + +For example, if you want to synchronize all images in the registry with the address of 127.0.0.1:41669 to the registry with the address of sealos.hub:5000, you should execute the following command: + +```bash +sealos registry sync 127.0.0.1:41669 sealos.hub:5000 +``` + +### Authentication and Permissions + +Before executing the `sealos registry sync` command, please ensure that you have permissions to access the source registry and the target registry. You can use `sealos login` to authenticate the registry. + +### Synchronization Process + +Please note that image synchronization may take some time, depending on the number and size of images, as well as the speed of the network. During the synchronization process, please keep the network connected and ensure not to interrupt the execution of the command before synchronization is complete. + +Importantly, the `sealos registry sync` command supports incremental synchronization. Images that already exist in the target registry will not be re-synchronized. + +### Parameter Options + +The `sealos registry sync` command also provides some parameter options, allowing you to control the synchronization process more finely: + +- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images. + +- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images. + +- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select images. + +- `-a` or `--all`: If the source image is a list, synchronize all images. This is particularly useful in heterogeneous environments because by default, only images of the current architecture will be synchronized. + +For example, if you want to synchronize all architecture images, you can add the `-a` parameter: + +```bash +sealos registry sync -a 127.0.0.1:41669 sealos.hub:5000 +``` + +The above is a detailed explanation and usage guide for the `sealos registry sync` command. We hope this information helps you better understand and use this command. If you encounter any problems during use, feel free to ask at any time. + + +## Sealos: Detailed Explanation and User Guide of the `sealos registry copy` Command + +The `registry copy` command in Sealos is used to copy a specified image from one registry to another registry. This can assist you in migrating or backing up images between different registries. + +### Basic Command Usage + +Use the `sealos registry copy` command for image copying: + +```bash +sealos registry copy source-image dst +``` + +Here `source-image` represents the full name of the source image (including the address and image name), and `dst` is the address of the target registry. + +For example, to copy an image named `127.0.0.1:41669/my-image:tag` to a registry with the address `sealos.hub:5000`, you can execute the following command: + +```bash +sealos registry copy 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +### Authentication and Permissions + +Before executing the `sealos registry copy` command, please ensure that you have permissions to access the source image and the target registry. You can use `sealos login` to authenticate the registry. + +### Copying Process + +Please note that image copying may take some time, depending on the size of the image and the speed of the network. During the copying process, please keep the network connected and ensure not to interrupt the execution of the command before the copying is complete. + +### Parameter Options + +The `sealos registry copy` command provides some parameter options, allowing you to control the copying process more finely: + +- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images. + +- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images. + +- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select images. + +- `-a` or `--all`: If the source image is a list, copy all images. This is particularly useful in heterogeneous environments because, by default, only images of the current architecture will be copied. + +For example, if you want to copy all architecture images, you can add the `-a` parameter: + +```bash +sealos registry copy -a 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +The above is the usage guide for the `sealos registry copy` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md new file mode 100644 index 000000000000..e25b3f3742fa --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/reset.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 2 +--- + +# Reset Cluster + +The `sealos reset` is a command in the Sealos command-line tool for resetting the entire cluster. This command is particularly useful when you want to completely clear cluster data or rebuild the cluster. This guide will detail how to use it. + +## Basic Usage + +The basic format of the `sealos reset` command is as follows: + +```bash +sealos reset --cluster cluster_name +``` + +In the above command, `cluster_name` is the name of the cluster you want to reset. + +## Example + +For instance, you can use the following command to reset a cluster named `mycluster`: + +```bash +sealos reset --cluster mycluster +``` + +## Optional Parameters + +- `--force`: This parameter is used to force the reset of the cluster, even if the cluster reset operation has not been successfully completed. + +```bash +sealos reset --cluster mycluster --force +``` + +- `--masters`: This parameter is used to specify the master nodes to be reset. + +```bash +sealos reset --cluster mycluster --masters master1 +``` + +- `--nodes`: This parameter is used to specify the worker nodes to be reset. + +```bash +sealos reset --cluster mycluster --nodes node1 node2 +``` + +- `-p`, `--passwd`: This parameter is used to provide a password for authentication. + +- `-i`, `--pk`: This parameter specifies the file to read the identity (private key) used for public key authentication. + +- `--pk-passwd`: This parameter is used for the passphrase to decrypt the PEM-encoded private key. + +- `--port`: This parameter is used to specify the port of the remote host to connect to. + +- `-u`, `--user`: This parameter is used to specify the username for authentication. + +```bash +sealos reset --cluster mycluster --user username --pk /root/.ssh/id_rsa --pk-passwd yourpassword +``` + +The above is the usage guide for the `sealos reset` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md new file mode 100644 index 000000000000..8bcfd1e6bf6b --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +--- + +# Rmi: Delete Local Images + +`sealos rmi` is a command in the Sealos command-line tool that is used to delete one or more images stored locally. This command can help you clean up useless or outdated images and save storage space. This guide will detail how to use it. + +## Basic Usage + +The basic format of the `sealos rmi` command is as follows: + +```bash +sealos rmi imageID +``` + +In the above command, `imageID` is the ID of the image you want to delete. + +## Example + +For instance, you can use the following command to delete an image with ID `imageID`: + +```bash +sealos rmi imageID +``` + +If you want to delete multiple images, just list all the image IDs in the command line, for example: + +```bash +sealos rmi imageID1 imageID2 imageID3 +``` + +## Optional Parameters + +- `-a`, `--all`: This parameter is used to delete all images. When using this option, the command will not accept any image IDs. + +```bash +sealos rmi --all +``` + +- `-f`, `--force`: This parameter is used to forcefully delete an image and any containers using that image. + +```bash +sealos rmi --force imageID +``` + +- `-p`, `--prune`: This parameter is used to prune dangling images (images without a tag and not referenced by any containers). + +```bash +sealos rmi --prune +``` + +The above is the usage guide for the `sealos rmi` command. We hope it is helpful to you. If you encounter any problems during use, feel free to ask us any questions. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md new file mode 100644 index 000000000000..ac9e72d36c03 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/run.md @@ -0,0 +1,84 @@ +--- +sidebar_position: 1 +--- + +# Run: Execute Cluster Images + +The `run` command of Sealos is a powerful and flexible tool that supports cluster initialization, application installation, multi-image execution, single-node clusters, and more. Below is a detailed explanation and some usage examples of the `sealos run` command and its parameters. + +## Command Overview + +``` +sealos run --masters [arg] --nodes [arg] [Options] +``` + +The `` parameter is the name and version of the Docker image you want to run in the cluster. `--masters` and `--nodes` are the IP lists of the master and node nodes where you want to run this image. + +### Option Explanation + +- `--cluster='default'`: The name of the cluster where the operation is to be run. + +- `--cmd=[]`: Overwrite the CMD instruction in the image. + +- `--config-file=[]`: The path to the custom configuration file, used to replace resources. + +- `-e, --env=[]`: The environment variables set during command execution. + +- `-f, --force=false`: Forcefully overwrite the application in this cluster. + +- `--masters=''`: The master nodes to be run. + +- `--nodes=''`: The node nodes to be run. + +- `-p, --passwd=''`: Authenticate using the provided password. + +- `-i, --pk='/root/.ssh/id_rsa'`: Choose the private key file from which to read the public key authentication identity. + +- `--pk-passwd=''`: The password to decrypt the PEM-encoded private key. + +- `--port=22`: The connection port of the remote host. + +- `-t, --transport='oci-archive'`: Load image transport from a tar archive file. (Optional values: oci-archive, docker-archive) + +- `-u, --user=''`: The username for authentication. + +## Examples + +1. Create a cluster on your bare-metal servers, specifying the IP list: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +2. Run multiple images: +``` +sealos run labring/kubernetes:v1.24.0 labring/helm:v3.11.3 calico:v3.24.6 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 +``` + +3. Specify the InfraSSH port of the server: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' +``` + +4. Customize a VIP Kubernetes cluster: +``` +sealos run -e defaultVIP=10.103.97.2 labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +5. Create a single-node Kubernetes cluster: +``` +sealos run labring/kubernetes:v1.24.0 +``` + +6. Create a cluster using custom environment variables: +``` +sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +These examples demonstrate the power and flexibility of the `sealos run` command, which can be customized and adjusted according to your needs. + +For more examples, please refer to [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster.md). diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md new file mode 100644 index 000000000000..ab6e80274ba0 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/save.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 6 +--- + +# Save: Store Images + +`sealos save` is a command in the Sealos command-line tool used to save images to archive files. This command can help you conveniently back up and migrate your images. This guide will detail its usage. + +## Basic Usage + +The basic `sealos save` command format is as follows: + +```bash +sealos save -o outputFilename imageName +``` + +In the above command, `outputFilename` is the name of the archive file you want to save, and `imageName` is the name of the image you want to save. + +## Example + +For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file named `kubernetes.tar`: + +```bash +sealos save -o kubernetes.tar labring/kubernetes:v1.24.0 +``` + +## Optional Parameters + +- `--format`: This parameter is used to specify the transport format for saving the image. The currently available options are `oci-archive`, `docker-archive`, `oci-dir`, and `docker-dir`. The default value is `oci-archive`. +- `-m`: This parameter can be used to save multiple images at the same time, but it is only applicable to the `docker-archive` format. + + +For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file named `kubernetes.tar` in the `docker-archive` method: + +```bash +sealos save -o kubernetes.tar --format docker-archive labring/kubernetes:v1.24.0 +sealos save -o kubernetes.tar -m --format docker-archive labring/kubernetes:v1.24.0 labring/helm:v3.5.0 +``` + +The above is the usage guide of the `sealos save` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md new file mode 100644 index 000000000000..d31cf5e91892 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/scp.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# Scp: Copy Files + +`sealos scp` is a command in the Sealos command-line tool, used for copying files to specified cluster nodes. This guide will detail its usage and options. + +## Basic Usage + +The basic `sealos scp` command format is as follows: + +```bash +sealos scp "source file path" "destination file path" +``` + +In the above command, `source file path` is the local path of the file you want to copy, and `destination file path` is the remote node path you want to copy the file to. + +## Options + +The `sealos scp` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster to which the files should be copied. The default is `default`. + +- `--ips=[]`: Copies the files to nodes with the specified IP addresses. + +- `-r, --roles='':`: Copies the files to nodes with specified roles. + +Each option can be followed by one or more arguments. + +## Examples + +For example, you can use the following command to copy the local file `/root/aa.txt` to `/root/dd.txt` on all nodes in the default cluster: + +```bash +sealos scp "/root/aa.txt" "/root/dd.txt" +``` + +If you want to copy files on the nodes with the `master` and `node` roles in the cluster named `my-cluster`, you can use the following command: + +```bash +sealos scp -c my-cluster -r master,node "/root/aa.txt" "/root/dd.txt" +``` + +If you only want to copy files on the node with the IP address `172.16.1.38`, you can use the following command: + +```bash +sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" +``` + +The above is the usage guide for the `sealos scp` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md new file mode 100644 index 000000000000..752cc7e57229 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/commands/tag.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 6 +--- + +# Tag: Add Image Names + +`sealos tag` is a command in the Sealos command-line tool, used to add one or more additional names to the images stored locally. This command can help you better manage your images. This guide will detail its usage. + +## Basic Usage + +The basic `sealos tag` command format is as follows: + +```bash +sealos tag imageName newName +``` + +In the above command, `imageName` is the name of the image you want to operate on, and `newName` is the new tag you want to add. + +## Examples + +For example, you can use the following command to add a new name `firstNewName` to an image named `imageName`: + +```bash +sealos tag imageName firstNewName +``` + +You can also add multiple names at once, for example, add two names `firstNewName` and `SecondNewName`: + +```bash +sealos tag imageName firstNewName SecondNewName +``` + +The above is the usage guide for the `sealos tag` command, and we hope it is helpful to you. If you encounter any problems during use, feel free to ask us. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md new file mode 100644 index 000000000000..f00cafdd02bb --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 10 +--- + +# Kubernetes Cluster Image Documentation + +This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, types, and versions. + +## Image Names + +The official cluster images provided by Sealos include the following: + +1. kubernetes: Kubernetes image with containerd as the container runtime interface (CRI). +2. kubernetes-docker: Kubernetes image with Docker as the CRI. +3. kubernetes-crio: Kubernetes image with Crio as the CRI. + +Currently, Sealos primarily provides Kubernetes-related images and has not yet provided other types of cluster images such as k3s or k0s. + +## Image Types + +Sealos offers different types of Kubernetes cluster images based on the container runtime interface (CRI): + +1. Kubernetes image with containerd as the CRI. +2. Kubernetes image with Docker as the CRI. +3. Kubernetes image with Crio as the CRI. + +Users can choose the appropriate image type based on their requirements and preferences. + +## Image Versions + +Sealos offers multiple versions of cluster images. Examples include: + +### 1. Development Version + +This version is suitable for users who want to try out the latest features of the project. The development version may contain new features and improvements that have not been thoroughly tested and may not be as stable. + +Example: `v1.26(v1.26-amd64/v1.26-arm64)` + +### 2. Latest Version + +The latest version is typically more stable than the development version but may not include all the new features. This version is recommended for most users. + +Example: `v1.26.0(v1.26.0-amd64/v1.26.0-arm64)` + +### 3. Release Version (Including Historical Versions) + +The release version includes historical versions. Release versions have usually undergone rigorous testing and are considered stable. + +Example: `v1.26.0-4.1.5(v1.26.0-4.1.5-amd64/v1.26.0-4.1.5-arm64)` `4.1.5` is the corresponding version number for Sealos. + +When choosing an image version, users should consider their requirements and preferences. Additionally, Sealos provides sub-versions for different processor architectures to meet the needs of users on different hardware platforms. + +## Summary + +This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, types, and versions. Users can select the appropriate image type and version based on their requirements and preferences to run containers in a Kubernetes cluster. diff --git a/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md new file mode 100644 index 000000000000..c5f8bcefd174 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/lifecycle-management/reference/sealos/sealos.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 0 +--- + +# Sealos User Guide + +Sealos is a unified cloud operating system designed to manage cloud-native applications. It provides a set of command-line tools to help users manage Kubernetes clusters, nodes, perform remote operations, manage containers and images, and perform other functions. Below are detailed introductions to these commands: + +This chapter provides a user guide for using Sealos, as well as information related to cluster images. + +- Sealos User Guide: For detailed information about each command, including all supported parameters and subcommands, please refer to the [sealos](/self-hosting/lifecycle-management/reference/sealos/commands.md) reference documentation. +- Cluster Images: + - [Rootfs Cluster Images](/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application Cluster Images](/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md) diff --git a/docs/archived/4.0/docs/self-hosting/sealos/QA.md b/docs/archived/4.0/docs/self-hosting/sealos/QA.md new file mode 100644 index 000000000000..a7bbc88787de --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/sealos/QA.md @@ -0,0 +1,113 @@ +--- +sidebar_position: 3 +--- + +# Q&A + +Encountering issues during the deployment and use of Sealos Cloud is not uncommon. To assist you effectively, we have compiled a list of frequently encountered problems along with comprehensive solutions. + +## Deployment Related Issues + +This section details the problems you may face during the deployment phase and their respective solutions. For issues not covered here, please consult with us at the [Sealos Community](https://github.com/labring/sealos/discussions). + +### Q1: iptables / ip_forward Concerns + +**Problem Overview**: In some operating systems, such as older versions of Centos and RHEL, iptables or IPv4 IP forwarding is not enabled by default. This can hinder the creation of iptables rules or the forwarding of packets, potentially preventing the cluster from starting correctly. + +**Resolution Strategy**: To address this, execute the following commands on each node to activate iptables and IP forwarding: + +```shell +$ modprobe br_netfilter +$ echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables +$ echo 1 > /proc/sys/net/ipv4/ip_forward +``` + +### Q2: Issues with System Kernel + +- **Problem Overview**: An outdated system kernel can impede the proper startup of the cluster. Also, certain applications, especially those dependent on MongoDB 5.0, might not function with an older kernel. +- **Resolution Strategy**: Ensure your system's kernel version is at least 5.4 or higher before commencing the deployment. + +### Q3: System Resource Constraints + +- **Problem Overview**: Limited system resources can lead to deployment delays or even halts. If you encounter prolonged wait times, it's likely due to insufficient system resources. +- **Resolution Strategy**: Check the resource status of your nodes using `kubectl describe nodes`, focusing on CPU, memory, and storage availability. + +### Q4: Networking Issues + +- **Problem Overview**: Incorrect server configuration can lead to various network issues during deployment. Common areas of concern include: + 1. Misconfiguration of http_proxy / https_proxy environment variables; + 2. Inadequate server firewall settings; + 3. Improper server routing configurations; +- **Resolution Strategy**: Troubleshoot network issues by verifying the correctness of these configurations. + +## Certificate and Domain Name Issues + +### Certificate Renewal Process + +Certificates are crucial for the security of your Sealos cluster. Follow these steps to update your certificates, especially as they approach their expiration date: + +1. **Backup Existing Certificate**: + + On the `master0` node, backup your current certificate. This step is crucial to prevent loss of the certificate during the update process. Use this command: + + ```shell + $ kubectl get secret -n sealos-system wildcard-cert -o yaml > cert-backup.yaml + ``` + + This will save the `wildcard-cert` certificate in YAML format to `cert-backup.yaml`. + +2. **Storing the New Certificate**: + + Place your new certificate files (.crt and .key) on the `master0` node. + +3. **Updating the Certificate**: + + To update, use the script below, replacing `` and `` with the actual paths of your new certificate and key files. + + ```shell + #!/bin/bash + # Set Variables + CRT_FILE= + KEY_FILE= + + # Base64 encode the certificate and key files + CRT_BASE64=$(cat $CRT_FILE | base64 -w 0) + KEY_BASE64=$(cat $KEY_FILE | base64 -w 0) + + # Create JSON for update + PATCH_JSON='{"data":{"tls.crt":"'$CRT_BASE64'","tls.key":"'$KEY_BASE64'"}}' + + # Update the Secret using kubectl patch + kubectl patch secret wildcard-cert -n sealos-system -p $PATCH_JSON + ``` + + This script encodes the new certificate in Base64 and updates the Kubernetes cluster's Secret object using `kubectl patch`. + +### Changing the Domain Name + +Changing a domain name in a Sealos cluster is complex, often requiring adjustments in multiple components and services. We do not currently provide a comprehensive guide for this process in our documentation. However, future plans include the release of a Sealos Cluster Management Panel for easier domain name and certificate replacement. + +It's important to note that domain name changes demand deep knowledge of the cluster's network setup and may involve intricate DNS settings and service discovery. We recommend undertaking such changes only if you have the requisite expertise or with guidance from a professional. + +### user registration switch + +disabled user register: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: true/signUpEnabled: false/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + +enabled user register: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: false/signUpEnabled: true/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + + +## Database Issues + +### Dify installation + +The Sealos offline package does not include the vector database by default (and the related image is not packaged), so it is currently not possible to install the Dify application in a privately deployed Sealos cluster. \ No newline at end of file diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg new file mode 100644 index 000000000000..680d572141b3 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-1.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg new file mode 100644 index 000000000000..8b6a354702c0 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/chrome-certificate-2.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png new file mode 100644 index 000000000000..6e2c46692cf6 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-1.png differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg new file mode 100644 index 000000000000..a99aaeaf5a0a Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-2.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg new file mode 100644 index 000000000000..8a42e6c08c8e Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/firefox-export-certificate-3.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg new file mode 100644 index 000000000000..3a211ccd3e13 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-1.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg new file mode 100644 index 000000000000..357d5880dbe4 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-2.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg new file mode 100644 index 000000000000..9c33c03204f5 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/safari-export-certificate-3.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg b/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg new file mode 100644 index 000000000000..243d0d2d5583 Binary files /dev/null and b/docs/archived/4.0/docs/self-hosting/sealos/images/windows-trust-certificate.jpg differ diff --git a/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md b/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md new file mode 100644 index 000000000000..ab7a4a33e1d0 --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/sealos/install-object-storage.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 2 +--- + +# Install Object Storage + +Download the Helm charts. + +```bash +curl -O https://raw.githubusercontent.com/minio/operator/master/helm-releases/operator-5.0.6.tgz +``` + +Install Operator. + +```bash +helm install --namespace minio-system --create-namespace minio-operator operator-5.0.6.tgz +``` + +Install Minio, Controller, etc. + +```bash +# DOMAIN is the domain name for the Sealos cluster +# Set environment variables for Minio admin account (default Minio admin account is username/passw0rd) +# -e minioAdminUser={16-character random alphanumeric string} -e minioAdminPassword={32-character random alphanumeric string} +sealos run ghcr.io/labring/sealos-cloud-objectstorage:latest -e cloudDomain={DOMAIN} +``` + +# Uninstall Object Storage + +Installation of the Object Storage failed, you can use the script to clean up the remaining resources. Uninstalling the +Object Storage can also be done using the script to clean up. + +```bash +#!/usr/bin/env bash +set +e + +kubectl delete app objectstorage -n app-system +kubectl delete ns objectstorage-system objectstorage-frontend +helm uninstall minio-operator -n minio-system +kubectl delete ns minio-system +kubectl delete crd objectstoragebuckets.objectstorage.sealos.io objectstorageusers.objectstorage.sealos.io +kubectl delete clusterrole objectstorage-manager-role objectstorage-metrics-reader objectstorage-proxy-role +kubectl delete clusterrolebinding objectstorage-manager-rolebinding objectstorage-proxy-rolebinding +``` \ No newline at end of file diff --git a/docs/archived/4.0/docs/self-hosting/sealos/installation.md b/docs/archived/4.0/docs/self-hosting/sealos/installation.md new file mode 100644 index 000000000000..6f146822581b --- /dev/null +++ b/docs/archived/4.0/docs/self-hosting/sealos/installation.md @@ -0,0 +1,362 @@ +--- +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Install Sealos Cluster + +export const Highlight = ({children, color}) => ( + + {children} + +); + +:::tip + +In the context of large-scale clusters and corporate production environments, [the enterprise or bespoke editions of the Sealos private cloud](/self-hosting) come highly recommended. + +::: + +## Before You Begin + +### Hardware + +- Unique hostnames for each cluster node. +- Time synchronization across all nodes. +- Preferably use a fresh operating system for setting up the cluster. **Refrain from installing Docker yourself!** +- Compatible with numerous Linux versions like Ubuntu, Debian, CentOS, Rocky Linux. +- **Ensure a system kernel version of 5.4 or higher**. + +Recommended System Configuration: + +We advise using Ubuntu 22.04 LTS with a kernel version of 5.4 or higher. The specifications are as follows: + +| Operating System | Kernel Version | CPU | Memory | Storage | Masters | Nodes | +|------------------|----------------|-----|--------|---------|------------|-------| +| Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | Odd Number | Any | + +:::info +Kubernetes and Sealos Cloud Operating system require roughly 2 cores (2c) and 2GB of memory (2g) per Master node, and about 1 core (1c) and 1GB of memory (1g) per Node node. Ensure each node in your cluster is well-equipped for these system components. +::: + +### Network Considerations + +- All nodes must be interconnected; +- Installation scripts should be run on the **first Master node** of the Kubernetes cluster; **nodes outside the cluster aren't set up for installations yet**; +- Make sure all nodes are capable of mutual communication. + +### Domain Name Usage + +- A domain name is necessary for accessing Sealos and its services; +- If you lack a domain name, `nip.io` offers a free domain name service. + +### Certificate Requirements + +Sealos uses certificates for secure communication. Without your certificates, [cert-manager](https://cert-manager.io/docs/) will be employed for auto-certification. + +If you are providing your certificate, it should support the following domains (assuming your domain is cloud.example.io): + +- `*.cloud.example.io` +- `cloud.example.io` + +## Installation Steps + +We offer a one-click installation script to streamline the deployment process. This script is capable of setting up a Sealos cluster from scratch or integrating it into an existing Kubernetes cluster (use with caution on pre-existing clusters). + +:::info +This script is specifically designed for deploying Sealos clusters on "Kubernetes clusters installed via Sealos," and is not compatible with Kubernetes clusters set up in other ways. + +For instructions on deploying Kubernetes clusters using Sealos, refer to: [Installing Kubernetes Clusters](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md). This method is compatible with most Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags), except for version 1.28 and higher. +::: + +If your system is new to Kubernetes, **we advise using this script to install both Kubernetes and the Sealos cluster simultaneously.** + +There are different installation options based on your domain name needs: + +### 1. No public domain, don't want custom domains + +If you don't have a public domain or need custom domains, you can use the free `nip.io` wildcard DNS service. `nip.io` maps dynamic IP addresses to fixed subdomains, useful for local dev environments. + +It works by taking any IP address as part of a `nip.io` subdomain, and resolving requests back to that IP. For example, if your intranet IP is `192.168.1.10`, you can use the domain `192.168.1.10.nip.io`. Requests to this domain will resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up intranet DNS. + +To use nip.io for Sealos, run the below on the first master node and enter prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh +``` + +When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP. + +After installation, the terminal will display the Sealos access domain and login credentials, as shown below: + +```shell +Sealos cloud login info: +Cloud Version: latest +URL: https://10.214.210.102.nip.io +admin Username: admin +admin Password: sealos2023 +``` + +### 2. Have public domain, want public access + +If you have a public domain and want public Sealos access, you'll need a trusted public SSL/TLS certificate. You can use acme.sh to automatically issue certs, or get free certs from your domain provider or purchase commercial certificates. + +Place the certificate files in a directory on the first master, like `/root/certs/`. + +:::info + +You'll also need to configure DNS records at your domain provider: + +``` +cloud.example.io A +*.cloud.example.io A +``` + +This maps your domain and subdomains to the first master's public IP. + +::: + +Then run below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= \ +--cert-path= \ +--key-path= +``` + ++ `` is your public domain ++ `` is the certificate file path (`.crt` or `.pem`) e.g. `/root/certs/example.crt` ++ `` is the private key file path (`.key` or `.pem`) e.g. `/root/certs/example.key` + +### 3. Have public domain, want internal access + +If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS records resolving to the first master's internal IP: + +``` +cloud.example.io A +*.cloud.example.io A +``` + +Then run the below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` + +Where `` is your public domain. + +The installer will use [cert-manager](https://cert-manager.io/docs/) to automatically sign certificates. + +### 4. No public domain, want custom domain + +If you don't have a public domain but need a custom domain, set up internal DNS resolving a custom domain to the first master's internal IP. + +:::note + +Assuming the first master internal IP is `192.168.1.10`, and your custom domain is `cloud.example.io`. + +::: + +You can use CoreDNS, Reference configuration: + +``` +(global_cache) { + cache { + # [5, 60] + success 65536 3600 300 + # [1, 10] + denial 8192 600 60 + prefetch 1 60m 10% + } +} + +.:53 { + errors + health + ready + + import global_cache + + template IN A cloud.example.io { + answer "{{ .Name }} 60 IN A 192.168.1.10" + fallthrough + } + + forward . 223.5.5.5 + + log + loop + + reload 6s +} +``` + +This resolves `cloud.example.io` and subdomains to the first master internal IP. + +Then run below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` + +Where `` is your custom domain. + +The installer uses [cert-manager](https://cert-manager.io/docs/) for certificates. + +## Ensuring Browsers Trust Your Self-Signed Certificate + +When using installation options 1, 3, or 4, as highlighted earlier, your browser won't automatically recognize and trust your certificate. This leads to a warning message when you try to enter Sealos Cloud: + +![](images/chrome-certificate-1.jpg) + +Continuing to Sealos Cloud despite the warning results in issues like the App icons not appearing correctly, and Apps failing to launch. + +![](images/chrome-certificate-2.jpg) + +To address this, you'll need to export the self-signed certificate and set up your system to trust it. Follow these steps to do so. + +### Exporting Self-Signed Certificates + +The process of exporting self-signed certificates differs slightly in various browsers. Here's a guide for exporting these certificates in some of the most commonly used browsers: + +#### Chrome (includes browsers based on Chromium, like the updated Edge and Brave) + +1. In the address bar, click the "Not Secure" warning on the left side. +2. Select "Certificate is invalid" to bring up a window with certificate information. +3. In this window, go to the "Details" tab. +4. Within "Details", find and click on the "Export" option. +5. Choose a file name and location for saving, then complete the exporting procedure. + +#### Firefox + +1. On the webpage, click on "Advanced". + + ![Firefox Certificate Export Step 1](images/firefox-export-certificate-1.png) + +2. Next, click "View Certificate". + + ![Firefox Certificate Export Step 2](images/firefox-export-certificate-2.jpg) + +3. On the certificate page, select "PEM (certificate)". + + ![Firefox Certificate Export Step 3](images/firefox-export-certificate-3.jpg) + +#### Safari + +1. Select "Show Details" on the page. + + ![Safari Certificate Export Step 1](images/safari-export-certificate-1.jpg) + +2. Click on "View this Certificate". + + ![Safari Certificate Export Step 2](images/safari-export-certificate-2.jpg) + +3. You'll see a window displaying the certificate chain. + + ![Safari Certificate Export Step 3](images/safari-export-certificate-3.jpg) + +4. To export the certificate, drag the one highlighted in red to either your desktop or a specific folder. + + + +### Trusting Self-Signed Certificates + +#### macOS + +To trust a self-signed certificate on macOS, use the following command: + +```bash +# Assuming the certificate is stored at ~/Downloads/10.214.210.102.nip.io.cer +$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/10.214.210.102.nip.io.cer +``` + +You can also trust the certificate through these steps: + +##### 1. Adding the Certificate to Keychain + +1. Open **Keychain Access**, click on the "System" category under "System Keychains." +2. Navigate to the certificate's location in "Finder" and **double-click the certificate file** (usually `.cer`, `.crt`, or `.pem`). +3. Authenticate with your username and password to allow changes. +4. The certificate is now added to your keychain. + +##### 2. Trusting the Certificate + +1. In Keychain Access, locate and double-click the newly added self-signed certificate in the "System" category. +2. In the certificate window, expand the "Trust" section. +3. Change the setting next to "When using this certificate" from "Use System Defaults" to "Always Trust." +4. Close the window and authenticate if prompted to confirm the changes. +5. The certificate is now trusted. + +:::info Note + +Self-signed certificates, not issued by a third-party Certificate Authority (CA), won't be trusted by other devices. Manually perform these steps on each device within your company or organization that needs to trust the certificate. + +::: + +#### Windows + +1. Right-click the root certificate file and select "Install Certificate." +2. Choose "Current User" or "Local Machine," then "Next." +3. Select "Place all certificates in the following store," click "Browse," choose "Trusted Root Certification Authorities," "OK," then "Next." +4. Click "Finish," "Yes," then "OK." + +![](images/windows-trust-certificate.jpg) + +#### Linux + +Linux distributions vary in updating the root certificate store and paths for private certificates. First, copy the self-signed CA certificate to the designated path, then run the command to update the root certificate store. + + + + + ```bash + # Debian/Ubuntu/Gentoo + $ sudo cp root_ca.crt /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates + $ sudo rm /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates --fresh + ``` + + + + + ```bash + # CentOS/Fedora/RHEL + $ yum install ca-certificates + $ update-ca-trust force-enable + $ cp root_ca.crt /etc/pki/ca-trust/source/anchors/ + $ update-ca-trust + ``` + + + + + ```bash + # Alpine + $ apk update && apk add --no-cache ca-certificates + $ cp root_ca.crt /usr/local/share/ca-certificates/ + $ update-ca-certificates + ``` + + + + + ```bash + # OpenSUSE/SLES + $ cp root_ca.crt /etc/pki/trust/anchors/ + $ update-ca-certificates + ``` + + + diff --git a/docs/archived/4.0/i18n/zh-Hans/Community.md b/docs/archived/4.0/i18n/zh-Hans/Community.md new file mode 100644 index 000000000000..c015bb6aee20 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/Community.md @@ -0,0 +1,11 @@ +# 社区 + +Sealos 是一个由用户和贡献者参与推动的开源项目,如果您对产品使用存在疑问和建议,可尝试以下方式寻求支持。我们的团队与社区会竭尽所能为您提供帮助。 + ++ 📱 扫码加入社区微信交流群👇 + + ![](https://oss.laf.run/htr4n1-images/sealos-qr-code.jpg) + ++ 💬 加入我们的 [Discord 服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。 ++ 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。 ++ 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/Intro.md b/docs/archived/4.0/i18n/zh-Hans/Intro.md new file mode 100644 index 000000000000..c4ac398826ef --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/Intro.md @@ -0,0 +1,53 @@ +import Highlight from '@site/src/components/Highlight' + +# 什么是 Sealos? + + + +云原生生态之庞大与复杂,无疑让许多企业感到手足无措,这个生态依然缺乏好用的、开箱即用的发行版。在云原生市场还急需一款云操作系统以进一步降低云原生门槛与成本。 + +Sealos 的问世为企业开启了一扇新窗。它为企业与开发者提供了一种全新的选择,无论在公有云还是私有云中,都只需安装一个云操作系统。它能让各类应用在操作系统上稳定安全地运行,一并解决了应用所需的各种依赖问题。 + +Sealos 坚守的理念是:**云可以像 Linux 一样简单**。你可以像使用 Linux 一样安装后立即投入使用,使用过程无需过多纠结。唯一的差别仅在于,Linux 是安装在单一服务器上,而 Sealos 则是安装在整个数据中心。 + +在 Sealos 的架构中,**公有云与私有云并无本质上的差异**,他们应是同一套代码的不同实例,唯一的区别仅在于配置与所安装应用的差异。在内网安装即是私有云,而在公网对外提供服务则成为公有云。 + +我们坚信,随着 Sealos 的不断发展与进步,未来的云将更加开放,更加简单。云计算的美好将属于所有算力的提供者,云的价值将由所有云计算的参与者共享。**他们将能够在多元化的市场环境中,以更经济的方式享受云计算所带来的种种便利。** + +## 概述 + +Sealos 是一款以 Kubernetes 为内核的**云操作系统发行版**。它以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,使企业能够**像使用个人电脑一样**简单地使用云。 + +用户将可以像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用,几乎不需要任何专业的交付和运维成本。同时,利用独特的集群镜像能力,用户可将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用,轻松订制所需的云。通过强大且灵活的应用商店功能,可满足各类用户的多样化需求。 + +![](./images/sealos-desktop-zh.webp) + +## 适用场景 & 优势 + +Sealos 是一款强大的业务运行平台,它能完美支持 Java、Go、Python、PHP 等各类应用,不受编程语言限制。该平台为应用提供稳定的运行环境,并解决后端依赖问题,例如数据库、对象存储和消息队列等。更进一步,它还能灵活处理应用配置管理、服务发现、公网暴露,以及自动伸缩等问题。 + +### 公有云 + +如果您的业务需要运行在公有云环境中,那么您可以直接使用 [Sealos 提供的公有云服务](https://cloud.sealos.io)。 + +#### Sealos 的能力 + +- 🚀 **应用管理**:在应用商店中轻松管理并快速发布可公网访问的分布式应用。 +- 🗄️ **数据库管理**:秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。 +- 🌥️ **公私一致**:即是公有云也是私有云,支持传统应用无缝迁移到云环境。 + +#### Sealos 的优势 + +- 💰 **高效 & 经济**:仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。 +- 🌐 **通用性强,无心智负担**:专注于业务本身,无需担心复杂性,几乎没有学习成本。 +- 🛡️ **灵活 & 安全**:多租户共享机制在确保安全的同时,实现资源隔离与高效协作。 + +### 私有云 + +Sealos 云操作系统 **100% 开源**,您可在 [GitHub](https://github.com/labring/sealos) 上找到所有源代码,因此 **Sealos 公有云**的全部功能也可以部署在私有云环境中,以满足您更多样化的业务需求。 + +### 强大的应用交付能力 + +- Sealos 具备优秀的 Kubernetes 生命周期管理能力,还可以自由定制 Kubernetes 环境。 +- Sealos 可以将整个集群打包,并一键交付到客户环境中。所有服务和业务都可以整体交付。 +- 不同于 Docker 主要关注单机镜像,Sealos 更进一步,可以将**整个集群**或者某个分布式应用进行打包。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md new file mode 100644 index 000000000000..cfa4b5c9d89c --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/Architecture.md @@ -0,0 +1,22 @@ +# 架构说明 + +Sealos 采用应用分离与应用互联的方式构建,不同应用可以独立部署,也可以与其他应用联合起来提供服务。 +所有服务全部通过 `kubeconfig` 作为应用身份验证,使得不管是在浏览器,还是本地使用 `sealos` 命令行,甚至是本地的第三方客户端,都能够获得一致的使用体验。 + +## 整体架构 + +![Architecture](./images/architecture_light.png#gh-light-mode-only)![Architecture](./images/architecture_dark.png#gh-dark-mode-only) + +## 应用架构 + +Sealos 上的应用采用前后端分离的架构,同时前端能够提供 `SSR` 能力,使得应用也可以单独对外提供服务,而不需要绑定在 Sealos 单体上。 + +以下图为例: + +**应用管理 App Launchpad** 是 Sealos 提供的一个单镜像部署工具, **终端 Terminal** 是 Sealos 云操作作系统的终端应用,提供和单机操作系统终端一致的命令行服务。 +对于 Sealos 上的各种应用而言,它们可以直接与 `Kubernetes` 的服务进行交互,也可以与各种 CRD Controller 比如 Terminal Controller 和 Ingress-Nginx Controller 等等进行交互。 +应用之间也可以互相调用,比如用户在**数据库**应用中新建的数据库,可以直接唤起**终端**应用并自动连接上数据库进行调试。 + +详细信息可以点击 [链接](../../guides) 文档了解更多。 + +![Application](./images/application_light.png#gh-light-mode-only)![Application](./images/application_dark.png#gh-dark-mode-only) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png new file mode 100644 index 000000000000..403e9223d16c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_dark.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png new file mode 100644 index 000000000000..500f67bbe589 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/application_light.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png new file mode 100644 index 000000000000..6d14555ef4d6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_dark.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png new file mode 100644 index 000000000000..17bf8cf5d6ae Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/Architecture/images/architecture_light.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md new file mode 100644 index 000000000000..c47e344a0b80 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/how-to-deploy-the-application-to-desktop.md @@ -0,0 +1,39 @@ +# 如何部署应用到 desktop + +## 使用 Terminal 编写 app.yaml + +### 关键信息 + +- type: iframe,标识为一个 web 应用 +- spec name 为桌面显示的名称 +- url 应用地址 +- icon 是在 desktop 上显示的图标 + +```yaml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: app-sample +spec: + name: Image Hub Demo + icon: + type: iframe + data: + url: https://hub.sealos.io/ + desc: + icon: https://cloud.sealos.io/logo.svg + menuData: + displayType: normal +``` + +![appyaml.png](./images/app-yaml.png) + +### apply yaml + +``` +kubectl apply -f app.yaml +``` + +![apply](./images/app-apply-command.png) + +### 刷新浏览器,桌面即可看到 App diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png new file mode 100644 index 000000000000..f8a5fe3e636f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-apply-command.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png new file mode 100644 index 000000000000..e9ae045a5134 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/advanced-guide/images/app-yaml.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png new file mode 100644 index 000000000000..7313d36f5487 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png new file mode 100644 index 000000000000..32f0fb67f319 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/imgs/sealos2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md new file mode 100644 index 000000000000..011060f9c9c7 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/ai-applications/install-fastgpt-on-desktop.md @@ -0,0 +1,15 @@ +# 快速安装 FastGpt AI 知识库 + +无需服务器、无需魔法、无需域名,点击即可部署 👇 + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt) + +由于需要部署数据库,部署完后需要等待 2~4 分钟才能正常访问。默认用了最低配置,首次访问时会有些慢。 + +![](./imgs/sealos1.png) + +## 运行 + +点击 sealos 提供的【外网地址】即可使用。登录用户名为: root,密码是刚设置的环境变量,上图中设置了: 1234 + +![](./imgs/sealos2.png) diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md b/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md new file mode 100644 index 000000000000..891248b8de3e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/baas/install-fireboom.md @@ -0,0 +1,106 @@ +# 快速安装 Fireboom + +[Fireboom](https://fireboom.io) ⾯向开发者的可视化 API 开发平台,为开发者提供**声明式开发框架**、**可视化编辑套件**和**钩子机制**。 + +![fireboom introduction](../images/fireboom/fireboom-intro.jpeg) + +官网地址: https://fireboom.io + +**功能特性** + +- 分钟级交付: 将传统模式下 2天才能完成的接⼝缩短⾄ 2分钟 + +- 前端变全栈: 可视化开发,显著降低接⼝开发⻔槛,让前端搞定前后端秒变全栈 + +- 后端不搬砖: 声明式编程,避免复制粘贴,减少bug概率,让后端专注于业务开发 + +## 步骤 1:新建应用 + +- 在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/appsmith-1.png) + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/appsmith-2.png) + + +## 步骤 2:应用部署 + +- 基础配置: + + - 应用名称(自定义):fireboom + + - 镜像名(默认最新版本):fireboomapi/fireboom_server:latest + + - CPU(推荐):2 Core ( 最低1 Core) + + - 内存(推荐):4 G (最低 512 M) + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/fireboom/2-1.png) + +- 网络配置: + + - 控制面板:9123 + - API端点:9991,开启外网 + +![](../images/fireboom/2-2.png) + +- 高级配置: + + - 命令设置: + - 参数设置: dev (dev表示开发模式,start表示生产模式) + + - 持久化存储: + + - 元数据:/fbserver/store + - 上传文件:/fbserver/upload + + +![](../images/fireboom/2-3.png) + +## 步骤 3:部署应用 + +1, 点击「部署应用」开始部署应用。 + +![](../images/fireboom/3-1.png) + +2, 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +![](../images/fireboom/3-2.png) + +## 步骤 4:访问应用 + +- 访问控制面板:9123对应的域名 + +![](../images/fireboom/4-1.png) + +- 访问API端点:9991对应的域名 + +``` +status ok +``` + +## 步骤 5:配置Fireboom(可选) + +设置后,可方便使用swagger文档~ + +1, 查看API外网地址: 设置-> 系统 + +![](../images/fireboom/5-1.png) + +2, 修改为:API端点地址,9991对应的公网地址 + + - 静态值:选择静态值,设置为 **API端点** 公网域名 + - 环境变量:前往 环境变量 ,找到 FB_API_PUBLIC_URL 设置为 **API端点** 公网域名 + +![](../images/fireboom/5-2.png) + + +3, 使用swagger文档测试API + +![](../images/fireboom/5-3.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md new file mode 100644 index 000000000000..120af78ff064 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-halo.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 2 +--- + +# 快速安装 Halo + +在 Sealos 上快速部署 Halo 博客平台 + +![](../images/halo_img-1.png) + +[Halo](https://github.com/halo-dev/halo) 是一款强大易用的开源建站工具,本文将介绍如何在 Sealos 上部署 Halo,同时在 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) 可以帮助你轻松地管理和维护数据库,以满足不同的业务需求。 + +## 步骤 1:准备 PostgreSQL + +### 使用 Sealos 数据库安装 PostgreSQL + +参考 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) ,一分钟即可成功安装和使用 PostgreSQL。 + +### 查看数据库配置: + +保存这里生成的配置中包含数据库的用户名密码以及数据库连接方式,用于下文 Halo 中配置: + +![](../images/halo_img-6.png) + +> 图中可以看到 PostgreSQL 连接配置为: +> +> Username: postgres +> +> Password: rcx7f47m +> +> Connection: postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432 + +## 步骤 2: 在 Sealos 中部署 Halo应用 + +### 打开 应用管理 + +![](../images/halo_img-7.png) + +![](../images/halo_img-8.png) + +### 填写配置 + +- 自定义应用名称 + +- 镜像名称设置为 `halohub/halo:2.5` + +- CPU 和存储应根据实际情况进行配置,memory 需要 `500Mi` 的内存才能启动 Halo,这里可以参考官方 prepare:https://docs.halo.run/getting-started/prepare + +- 暴露端口应为 `8090`。同时,设置为外网访问将自动为应用配置一个出口域名用于外网访问,同时需要在 Halo 中进行配置该地址 + +- 环境变量包括数据库配置和其他相关配置。需要注意的是,你需要根据你在 [步骤 1](查看数据库配置:) 中设置的用户名、密码和 DNS 来配置数据库连接信息。同时,你还需要为 Halo 配置超级管理员的用户名和密码。 + +配置 Halo 环境变量 + +```Bash +spring.sql.init.platform=postgresql +spring.r2dbc.url=r2dbc:pool:postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432/halo +spring.r2dbc.username=postgres +spring.r2dbc.password=rcx7f47m +halo.external-url=tmtdvfjiyyfh.cloud.sealos.io +halo.security.initializer.superadminusername=root +halo.security.initializer.superadminpassword=sealos +``` + +环境变量配置详解: + +| 参数名 | 描述 | +| -------------------------------------------- | -------------------------------------------------------- | +| spring.r2dbc.url | 数据库连接地址,详细可查阅下方的 数据库链接格式 | +| spring.r2dbc.username | 数据库用户名 | +| spring.r2dbc.password | 数据库密码 | +| spring.sql.init.platform | 数据库平台名称,支持 postgresql、mysql、h2 | +| halo.external-url | 外部访问链接,如果需要在公网访问,需要配置为实际访问地址 | +| halo.security.initializer.superadminusername | 初始超级管理员用户名 | +| halo.security.initializer.superadminpassword | 初始超级管理员密码 | + +数据库的链接格式(这里我们使用 postgresql 的格式): + +| 链接方式 | 链接地址格式 | spring.sql.init.platform | +| ----------- | ---------------------------------------------------------------------------------- | ------------------------ | +| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql | +| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 | + +### 网络配置 + +![](../images/halo_img-9.png) + +### 环境变量配置 + +![](../images/halo_img-10.png) + +### 配置持久化存储卷 + +挂载 `/root/.halo2` 目录来持久化 halo 数据 : + +![](../images/halo_img-11.png) + +## 步骤 3: 使用外网访问 Halo + +成功启动应用后,即可通过外网访问地址访问 Halo 进行配置: + +![](../images/halo_img-12.png) + +![](../images/halo_img-13.png) + +### 初始化 Halo + +![](../images/halo_img-14.png) + +![](../images/halo_img-15.png) + +### First Halo + +![](../images/halo_img-16.png) + +![](../images/halo_img-17.png) + +![](../images/halo_img-18.png) + +## FAQ + +### 忘记密码 + +#### 进入 「应用管理」,点击日志查看 + +![](../images/halo_img-19.png) + +![](../images/halo_img-20.png) + +可以看到这里的日志中:用户名:`admin` 密码:`QTu2J2xgUQ3ngqUo` + +#### 或终端执行如下命令查看日志 + +![](../images/halo_img-21.png) + +``` +root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1 +2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin === +``` + +![](../images/halo_img-22.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md new file mode 100644 index 000000000000..41f1c465897f --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/blog-platform/install-wordpress.md @@ -0,0 +1,103 @@ +--- +sidebar_position: 1 +--- + +# 快速安装 WordPress + +[WordPress](https://github.com/WordPress/WordPress) 是一款功能强大、易用、开源的内容管理系统 (CMS), 可以用于在互联网上发布网站、新闻、博客等内容。它提供了便捷的内容编辑、发布和更新方法 , 同时也可以通过主题和插件进行高度自定义。WordPress 已经成为非常成功和普及的 CMS 系统 , 在全球有超过 3 千万个网站使用 WordPress 搭建。 + +> 使用 Sealos 安装 WordPress,可以大幅度减少安装时间,极速开启用户的内容输出之旅。 + +WordPress 需要搭配 MySQL 数据库一同安装使用。作为使用 Sealos 的最佳实践,建议使用**数据库**应用独立安装 MySQL 数据库,依托独立数据库,快速安装 WordPress。 + +## 新建 MySQL 实例 + +进入 [Sealos](https://cloud.sealos.io) 桌面环境,在「数据库」中,快速配置需要的数据库实例,等待数据库部署完成。 + +![](../images/wordpress_15.png) + +![](../images/wordpress_16.png) + +![](../images/wordpress_17.png) + +**注:此时的 MySQL 实例中并不存在用户自定义数据库,需要在终端中手动创建** + +## 新建数据库 + +通过**数据库**应用安装的 MySQL 实例时,并不会自动创建用户自定义数据库。在使用应用程序连接 MySQL 数据库时,您需要在 MySQL 实例中手动创建相应的数据库。 + +可以通过如下方式进入数据库的终端命令行 + +![](../images/wordpress_12.png) +![](../images/wordpress_13.png) + +也可以在终端中自行输入命令 + +`mysql -u username -p -h yourhost` + +![](../images/wordpress_14.png) + +数据库连接成功后,可以按照如下命令创建数据库 + +`CREATE DATABASE yourdb;` + +至此,您已经在 MySQL 实例中手动创建了一个用户自定义数据库。 + +## 应用管理 + +点击应用管理,进入安装界面。 + +![](../images/wordpress_1.png) + +## 基础配置 + +首先设置自定义的应用名称,选择需要安装的镜像。Sealos 既支持公有镜像源,也支持私有镜像源。本示例选用的官方镜像源,不指定镜像版本时,默认拉取 latest 版本。 + +其次,为 WordPress 配置 CPU 与内存资源,WordPress 十分的轻量级,较少的资源就可以保证程序的正常运行。 + +![](../images/wordpress_8.png) + +## 网络配置 + +网络配置方面,打开外网访问,Sealos 会为用户自动分配出口域名用作外网访问,如果用户已经注册了自己的自定义域名,那么可以将自定义域名 cname 到该出口域名,这样就可以使用该自定义域名访问用户安装的应用。 + +注:后续演示,使用的是 Sealos 随机生成的出口域名。 + +![](../images/wordpress_9.png) + +## 高级配置 + +在高级配置中,需要根据数据库的实际情况配置 WordPress 的环境变量。 + +高级配置中,需要为 WordPress 配置环境变量与本地存储。 + +WordPress 常见环境变量如下所示: + +```Plain +WORDPRESS_DB_HOST 数据库实例的ip地址 +WORDPRESS_DB_USER 连接数据库的用户 +WORDPRESS_DB_PASSWORD 连接库所需的用户密码 +WORDPRESS_DB_NAME 连接的数据库名称 +``` + +根据之前安装的 MySQL 示例,可以这样配置环境变量: + +```Plain +WORDPRESS_DB_HOST=mysql-host:3306 +WORDPRESS_DB_USER=root +WORDPRESS_DB_PASSWORD=root_passwd +WORDPRESS_DB_NAME=yourdb +``` + +WordPress 官方镜像的存储卷挂载地址为:`/var/www/html`,用户根据需求为应用分配存储空间。 + +这样,WordPress 也一并安装成功,点击详情,通过外网地址就可以成功访问。 + +![](../images/wordpress_10.png) + +点击该链接,如下页面,证明安装成功 + +![](../images/wordpress_11.png) + +**注:如部署失败 , 优先排查 WordPress 访问的用户自定义数据库是否存在于 MySQL 实例中** + diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md new file mode 100644 index 000000000000..293547d7a0df --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/dial-testing-system/install-uptime-kuma.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 1 +--- + +# 快速安装 Uptime Kuma + +![](../images/uptimekuma_img-0.png) + +[Uptime Kuma](https://github.com/louislam/uptime-kuma) 是一款开源的、易于使用的服务器监控工具。它可以帮助你实时监控服务器的运行状态、响应时间以及其他关键指标,以确保你的服务器始终保持最佳状态。如果你想快速安装 Uptime Kuma,可以按照以下步骤进行操作: + +### 步骤 1:首先进入 [Sealos](https://cloud.sealos.io) 桌面环境并打开「应用管理」 + +![](../images/uptimekuma_img-1.png) + +### 步骤 2: 新建应用 + +在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/uptimekuma_img-2.png) + +### 步骤 3: 设置启动参数 + +在启动参数中,按照以下方式进行设置: + +- 容器暴露端口指定为 3001。 +- 设置为外网访问即可通过给出的域名访问。 + +![](../images/uptimekuma_img-3.png) + +![](../images/uptimekuma_img-4.png) + +### 步骤 4: 设置存储卷 + +在高级配置中,添加存储卷挂载来持久化 Uptime Kuma 数据目录 `/app/data`。这样可以确保应用数据在容器重启后不会丢失。 + +![](../images/uptimekuma_img-5.png) + +### 步骤五:部署应用 + +点击「部署应用」后即可启动应用: + +![](../images/uptimekuma_img-6.png) + +### 步骤 6: 访问应用 + +一旦应用启动成功,你就可以使用应用的外网地址进行访问了。在浏览器中输入应用的域名,即可访问应用的主界面。 + +![](../images/uptimekuma_img-7.png) + +### 步骤 7: 创建管理员账户 + +为了保护 Uptime Kuma 的安全性,首次进入你需要创建一个管理员账户,填写管理员账户的基本信息,包括用户名、密码。填写完毕后,点击创建按钮进行注册。 + +![](../images/uptimekuma_img-8.png) + +### 步骤 8: 添加自定义监控项 + +![](../images/uptimekuma_img-9.png) + +![](../images/uptimekuma_img-10.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md b/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md new file mode 100644 index 000000000000..28808a705686 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/gitea/install-gitea.md @@ -0,0 +1,113 @@ +# 快速安装 Gitea + +喝杯茶吧!无痛自托管多合一软件开发服务,包括 Git 托管、代码审查、团队协作、包注册和 CI/CD。 + +## 步骤 1:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](../images/gitea-0.png) + +## 步骤 2:打开 「应用管理」 应用 + +![](../images/gitea-1.png) + +## 步骤 3:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/gitea-2.png) + +## 步骤 4:应用部署 + +- 基础配置: + + - 应用名称(自定义):gitea + - 镜像名(默认最新版本):gitea/gitea:latest-rootless + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + + ![](../images/gitea-3.png) + +- 网络配置: + + - 容器暴露端口:3000 + - 外网访问:开启 + + ![](../images/gitea-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 Gitea 的数据(推荐 1 G)。 + +![](../images/gitea-5.png) + +## 步骤 5:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/gitea-6.png) + +## 步骤6:配置数据库 + +- 通过 Sealos 为 Gitea 配置 MySQL 数据库 +- 进入 Database + +![](../images/gitea-9.png) + +- 新建数据库 + +![](../images/gitea-10.png) + +- 部署集群 + +- 基础配置: + + - 集群类型:mysql + - 数据库版本:ac-mysql-8.0.30 + - CPU(推荐):1 Core + - 内存(推荐):1 G + + +![](../images/gitea-11.png) + +- 部署成功后进入详情页面查看 MySQL 连接信息 + +![](../images/gitea-12.png) + +- 点击一键连接进入 MySQL 终端连接 + +![](../images/gitea-13.png) + +- 执行 + +```sql +CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +``` + +创建 Gitea 所需要的数据库 + +![](../images/gitea-14.png) + +## 步骤 7:访问应用 + +- 点击「App Launchpad」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/gitea-7.png) + +- 访问外网地址后进入到配置页面,根据刚刚创建的 MySQL 的详情页面进行配 + +![](../images/gitea-15.png) + +配置好后点击安装,等待一会便可进入到登陆页面,部署成功! + +![](../images/gitea-16.png) + +![](../images/gitea-17.png) + diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png new file mode 100644 index 000000000000..025a0c4e45a9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-10-52-H2ZWUZ.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png new file mode 100644 index 000000000000..1ca1cd0ed53a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-31-vrNHJU.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png new file mode 100644 index 000000000000..5ba9b3ad8784 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-1iudM0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png new file mode 100644 index 000000000000..ac8de099a1ae Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2022-04-10-14-32-ADfk8T.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg new file mode 100644 index 000000000000..1b1fe554ea55 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-54-EIVahX.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg new file mode 100644 index 000000000000..a773666c4ff2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-55-NDkuEg.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png new file mode 100644 index 000000000000..fdc343a80edc Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-11-59-FxJE12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png new file mode 100644 index 000000000000..b4d2db4792a7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-01-DKect7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png new file mode 100644 index 000000000000..d512ad14195a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-05-CWczxm.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png new file mode 100644 index 000000000000..28a241ade20c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-06-lvv6ms.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png new file mode 100644 index 000000000000..aa8e415fb5b7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-07-s8W7iu.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png new file mode 100644 index 000000000000..0935a645d999 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-09-RslDGj.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png new file mode 100644 index 000000000000..a446015a5c59 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-24-QHYKZt.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png new file mode 100644 index 000000000000..6996e718eb53 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-26-HYOaBJ.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png new file mode 100644 index 000000000000..c76f2e0535ae Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-28-ccnUOj.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png new file mode 100644 index 000000000000..ba4d166331c6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-29-z5E9gi.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg new file mode 100644 index 000000000000..7ca839b52b2f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png new file mode 100644 index 000000000000..ac7134504f86 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-FwsbfW.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png new file mode 100644 index 000000000000..59460d087f22 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-Vs9ccy.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png new file mode 100644 index 000000000000..0fc1e2d152b9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-YFHPYc.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png new file mode 100644 index 000000000000..00e7aa10391a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-hdHfxP.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png new file mode 100644 index 000000000000..08ff96c0c126 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/2023-06-26-13-09-nwrxrv.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png new file mode 100644 index 000000000000..01876eeff7a0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png new file mode 100644 index 000000000000..fad0d90a71df Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png new file mode 100644 index 000000000000..d6567da245ae Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png new file mode 100644 index 000000000000..78b505e75298 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png new file mode 100644 index 000000000000..9771645376a2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png new file mode 100644 index 000000000000..ca7f7e62e60c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/appsmith-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png new file mode 100644 index 000000000000..5fb4c0ec51f5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png new file mode 100644 index 000000000000..714ad3891845 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png new file mode 100644 index 000000000000..e0d5a1a605d4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/2-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png new file mode 100644 index 000000000000..10d9ef1b793a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png new file mode 100644 index 000000000000..eef62cdfc5f9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/3-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png new file mode 100644 index 000000000000..880e86b32ce3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/4-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png new file mode 100644 index 000000000000..b8f83e07b0d6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png new file mode 100644 index 000000000000..b9b2404dc9ca Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png new file mode 100644 index 000000000000..5e22398cdfcd Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/5-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg new file mode 100644 index 000000000000..7f9660c847fa Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-architecture.jpeg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg new file mode 100644 index 000000000000..848bd2b5d97d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/fireboom/fireboom-intro.jpeg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png new file mode 100644 index 000000000000..51d1b8564725 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png new file mode 100644 index 000000000000..403270869164 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png new file mode 100644 index 000000000000..ebcad36657b9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png new file mode 100644 index 000000000000..7de1f375aa42 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png new file mode 100644 index 000000000000..32fea7a571b3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png new file mode 100644 index 000000000000..da74e2ae6af6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png new file mode 100644 index 000000000000..70d6971d9906 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png new file mode 100644 index 000000000000..cd370d79a7bf Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png new file mode 100644 index 000000000000..1c2de6f7d4c2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-16.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png new file mode 100644 index 000000000000..a41d1df04ff1 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-17.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png new file mode 100644 index 000000000000..7cbc77cebe6d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png new file mode 100644 index 000000000000..7011b7769934 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png new file mode 100644 index 000000000000..edadcc1ae00b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png new file mode 100644 index 000000000000..4044b3367783 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png new file mode 100644 index 000000000000..e2cd50bc191b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png new file mode 100644 index 000000000000..9db3b3c06cf8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png new file mode 100644 index 000000000000..e8dffac2625c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/gitea-9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png new file mode 100644 index 000000000000..51d7a08dd355 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png new file mode 100644 index 000000000000..47ac9bd3b6ac Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png new file mode 100644 index 000000000000..986e50a61e62 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png new file mode 100644 index 000000000000..21e55e93ae98 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png new file mode 100644 index 000000000000..7911c018cf43 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png new file mode 100644 index 000000000000..c3d2cc3bde08 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png new file mode 100644 index 000000000000..11197b975ece Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png new file mode 100644 index 000000000000..d443d3d8dfe7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-16.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png new file mode 100644 index 000000000000..1e88fe4f3c51 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-17.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png new file mode 100644 index 000000000000..b93a0dcfc9f5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-18.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png new file mode 100644 index 000000000000..17c863592f83 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-19.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png new file mode 100644 index 000000000000..5a9d401f2631 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png new file mode 100644 index 000000000000..9ed9ed3435f6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-20.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png new file mode 100644 index 000000000000..6a1c4a896f58 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-21.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png new file mode 100644 index 000000000000..d5e70681e57e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-22.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png new file mode 100644 index 000000000000..29ba87c9cc57 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png new file mode 100644 index 000000000000..0790b5ce2e43 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png new file mode 100644 index 000000000000..f35eebc16cda Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png new file mode 100644 index 000000000000..933d0efde05f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png new file mode 100644 index 000000000000..93a5e88a6e85 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png new file mode 100644 index 000000000000..9b41cf4dee07 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png new file mode 100644 index 000000000000..609c7c0959c2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/halo_img-9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png new file mode 100644 index 000000000000..0aa0f2546ce3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png new file mode 100644 index 000000000000..ef702aadb7e3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png new file mode 100644 index 000000000000..00edfb9382b2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png new file mode 100644 index 000000000000..c947ad852ca0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png new file mode 100644 index 000000000000..289789fd639f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png new file mode 100644 index 000000000000..a58fbc16537b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png new file mode 100644 index 000000000000..414ff44e067b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png new file mode 100644 index 000000000000..dc7bb21bd4eb Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png new file mode 100644 index 000000000000..0011841a7d14 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/pageplug-9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png new file mode 100644 index 000000000000..3b8766799280 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png new file mode 100644 index 000000000000..8594460ab60b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png new file mode 100644 index 000000000000..a8785255d657 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png new file mode 100644 index 000000000000..4582e3ffe3f2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png new file mode 100644 index 000000000000..1eca4f3c1aba Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png new file mode 100644 index 000000000000..1c592128b98b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png new file mode 100644 index 000000000000..d5ab998613d2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png new file mode 100644 index 000000000000..d8ae3e93589d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png new file mode 100644 index 000000000000..ab8def218f71 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/16.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png new file mode 100644 index 000000000000..6dedc0883d31 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/17.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png new file mode 100644 index 000000000000..0cbffe89e7bc Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png new file mode 100644 index 000000000000..d08b062b0a44 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png new file mode 100644 index 000000000000..97b8d6a7c99e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png new file mode 100644 index 000000000000..3568a104c8c7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png new file mode 100644 index 000000000000..6a25d0523ce2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png new file mode 100644 index 000000000000..05553d347357 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png new file mode 100644 index 000000000000..50dbf2e31bc0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png new file mode 100644 index 000000000000..5b117a1c33f6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/tailchat/9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png new file mode 100644 index 000000000000..2281aaa41234 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png new file mode 100644 index 000000000000..a19a6b255b39 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png new file mode 100644 index 000000000000..6a7fb98ddb46 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png new file mode 100644 index 000000000000..0cbffe89e7bc Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png new file mode 100644 index 000000000000..56f6c056be69 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png new file mode 100644 index 000000000000..abda6fa2d152 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png new file mode 100644 index 000000000000..8c9b87c88d9e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png new file mode 100644 index 000000000000..8aa71909987b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png new file mode 100644 index 000000000000..810d8e793df6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png new file mode 100644 index 000000000000..1a657270d137 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png new file mode 100644 index 000000000000..6aeebd4b6de1 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/uptimekuma_img-9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png new file mode 100644 index 000000000000..d641378d4201 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png new file mode 100644 index 000000000000..d1cd7993028d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png new file mode 100644 index 000000000000..103bf474654c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png new file mode 100644 index 000000000000..80c69d185806 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png new file mode 100644 index 000000000000..250114b79ef9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png new file mode 100644 index 000000000000..d180432b02c4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png new file mode 100644 index 000000000000..877112038094 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png new file mode 100644 index 000000000000..b490fade878c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_16.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png new file mode 100644 index 000000000000..9d0bc0391604 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_17.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png new file mode 100644 index 000000000000..83254bdb6cd1 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png new file mode 100644 index 000000000000..1b0a1f926bb8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png new file mode 100644 index 000000000000..86ad0a605c7d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png new file mode 100644 index 000000000000..1a9a1566ae1c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png new file mode 100644 index 000000000000..52b0dc908d42 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png new file mode 100644 index 000000000000..a18bfa6d1703 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png new file mode 100644 index 000000000000..484684f79ae4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png new file mode 100644 index 000000000000..24db5cadb900 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/images/wordpress_9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md new file mode 100644 index 000000000000..ff4f000b6d71 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-appsmith.md @@ -0,0 +1,65 @@ +# 快速安装 Appsmith + +[Appsmith](https://github.com/appsmithorg/appsmith) 是一个用于构建、部署和维护内部应用程序的开源平台。您可以构建任何东西,从简单的 CRUD 应用程序、管理面板、仪表板到自定义业务应用程序和复杂的多步骤工作流。 + +## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/appsmith-1.png) + +## 步骤 2:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/appsmith-2.png) + +## 步骤 3:应用部署 + +- 基础配置: + + - 应用名称(自定义):appsmith + + - 镜像名(默认最新版本):appsmith/appsmith-ce + + - CPU(推荐):2 Core + + - 内存(推荐):4 G + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/appsmith-3.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](../images/appsmith-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 Appsmith 的数据(推荐 15 G)。 + +![](../images/appsmith-5.png) + +## 步骤 4:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/appsmith-6.png) + +## 步骤 5:访问应用 + +- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/appsmith-7.png) + +- 访问成功! + +![](../images/appsmith-8.png) + + diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md new file mode 100644 index 000000000000..2e80833180ba --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/low-code-platform/install-pageplug.md @@ -0,0 +1,71 @@ +# 快速安装 PagePlug + +[PagePlug](https://github.com/cloudtogo/pageplug) 是 [Appsmith](https://github.com/appsmithorg/appsmith) 的中国化项目,基于 Appsmith 做了整体性能优化及汉化,也集合了特色表单解决方案 Formily 组件、图表解决方案 Echarts 组件、低代码小程序开发等,是面向研发使用的一个开源的、声明式的、可视化的、符合开发者直觉的前端低代码框架。 + +## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/pageplug-1.png) + +## 步骤 2:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/pageplug-2.png) + +## 步骤 3:应用部署 + +- 基础配置: + + - 应用名称(自定义):pageplug + + - 镜像名(默认最新版本):cloudtogouser/pageplug-ce + + - CPU(推荐):2 Core + + - 内存(推荐):4 G + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/pageplug-3.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](../images/pageplug-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 PagePlug 的数据。 + +![](../images/pageplug-5.png) + +## 步骤 4:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/pageplug-6.png) + +## 步骤 5:访问应用 + +- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +![](../images/pageplug-7.png) + +![](../images/pageplug-8.png) + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/pageplug-9.png) + +- 若访问出现 503 异常,等待一会重试即可。 + +![](../images/pageplug-10.png) + +- 访问成功! + +![](../images/pageplug-11.png) diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md b/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md new file mode 100644 index 000000000000..2c5a619a4405 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/others/install-anki.md @@ -0,0 +1,142 @@ +# 快速安装 Anki 自定义同步服务器 + +Anki 是一个辅助记忆软件,其本质是一个卡片排序工具--**即依据使用者对卡片上的自定义内容进行主动测试、自我评判后,其内部算法根据评判结果更改每张卡片下次测试时间的排序工具。** + +所谓的卡片,专业说法叫 Flash Card(抽认卡或闪卡),是一小块纸片,分为正反两面,将问题和提示写在一面,将答案写在另一面。使用方法就是先看正面的问题与提示,在脑中回想答案,然后翻出反面进行对照验证。 + +闪卡的核心制作原则就是:**一个知识点一张卡**。因此非常适合用来学习英文,也可以用来记忆历史事件时间、公式等等。 + +Anki 的同步服务器在国外,还是一个个人项目,带宽很小,同步速度很慢,如果我们想在多个客户端之间同步学习进度和新增的知识点,那将非常痛苦。 + +为了解决这个问题,我们需要部署一个自定义的同步服务器,然后让客户端去使用这个同步服务器。 + +## Anki 同步服务器部署 + +自从 2023 年 2 月份,Anki 发布了 PC 端 2.1.57 版本以后,Anki 的 PC 端,安卓端,iOS 端用户都可以自定义同步服务器了,并且不再需要安装插件。从此 Anki 小伙伴再也不用担心 Anki 同步的问题了,困扰 Anki 用户多年的同步问题终于得到彻底解决。 + +社区目前只有 [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs) 这个项目支持最新的 Anki 版本,其他的同步服务器项目基本上都失效了。这个项目是用 Rust 写的,追踪 [Anki 官方](https://github.com/ankitects/anki) 同步服务端的进度,它们都是基于sqlite c 作为数据存储后端。 + +下面我们将在 Sealos 中部署并配置 anki-sync-server-rs。 + +首先在浏览器地址栏输入网址 [https://cloud.sealos.io/](https://cloud.sealos.io/) 进入 Sealos 桌面。然后打开「应用管理」: + +![](../images/2023-06-26-11-54-EIVahX.jpg) + +点击「新建应用」: + +![](../images/2023-06-26-11-55-NDkuEg.jpg) + +依次输入应用名和镜像名,容器暴露端口是 `27701`,并开启外网访问: + +![](../images/2023-06-26-11-59-FxJE12.png) + +继续向下,展开「高级配置」,点击「编辑环境变量」: + +![](../images/2023-06-26-12-01-DKect7.png) + +将以下内容粘贴到环境变量输入框中: + +```bash +ANKISYNCD_USERNAME= +ANKISYNCD_PASSWORD= +``` + +请将 `` 替换为你自己的用户名,将 `` 替换为你自己的密码。 + +![](../images/2023-06-26-12-05-CWczxm.png) + +点击「新增存储卷」: + +![](../images/2023-06-26-12-06-lvv6ms.png) + +挂载路径填入 `/app`,然后点击确认: + +![](../images/2023-06-26-12-07-s8W7iu.png) + +最后点击右上角的部署应用即可。 + +部署完成后点击「详情」进入应用详情界面。 + +![](../images/2023-06-26-12-09-RslDGj.png) + +这里可以看到实例的运行状态,一定要等到状态是 running 才算是部署成功。如果一段时间以后状态还不是 running,可以点击「详情」查看故障原因: + +![](../images/2023-06-26-13-09-Vs9ccy.png) + +部署成功后,可以看到应用的运行情况,包括 CPU 占用、内存占用等。点击外网地址即可通过外网域名直接打开同步服务器的 Web 界面。 + +![](../images/2023-06-26-13-09-YFHPYc.png) + +如果出现以下的界面,则表示部署成功: + +![](../images/2023-06-26-13-09-FwsbfW.png) + +查看日志的方法也很简单,直接点击实例右侧的「三个点」,然后点击「日志」即可查看日志: + +![](../images/2023-06-26-13-09-hdHfxP.png) + +![](../images/2023-06-26-13-09-nwrxrv.png) + +## 客户端设置 + +### 桌面端 + +桌面客户端(macOS/Windows/Linux)配置方法如下: + +1. 先打开「首选项」 + + ![](../images/2023-06-26-12-24-QHYKZt.png) + +2. 点击「**网络**」,往下看,可以看到标有 `self-hosted sync server(自定义同步服务器)` 的方框,在里面填写您的服务端的地址: + + ![](../images/2023-06-26-12-26-HYOaBJ.png) + +3. 重启 Anki,然后点击「**同步**」: + + ![](../images/2023-06-26-12-28-ccnUOj.png) + +4. 这时候会弹出一个输入框让你输入用户名和密码,你需要将你之前设置的用户名和密码输入进去: + + ![](../images/2023-06-26-12-29-z5E9gi.png) + +5. 点击确认后,就会开始同步了。 + + +### 安卓端 + +安卓端也是直接配置即可,我的 AnkiDroid 版本是 `2.15.6`。你可以通过「设置 -> 高级设置 -> 自定义同步服务器」找到配置页面。 + +![](../images/2022-04-10-14-31-vrNHJU.png) + +再填写用户名和密码: + +> 设置 -> 常用设置 -> AnkiWeb账户 + +这样就算配置完成了,所有的牌组都同步过来了。 + +![](../images/2022-04-10-14-32-ADfk8T.png) +![](../images/2022-04-10-14-32-1iudM0.png) + +官方的版本比较旧,如果你想使用更激进的社区版本,可以到这个页面下载最新的 Beta 版: + ++ [https://github.com/ankidroid/Anki-Android/releases](https://github.com/ankidroid/Anki-Android/releases) + +建议下载 **arm64-v8a** 版本。 + +安装完成后,可以通过「设置 -> 同步 -> 自定义同步服务器」找到配置页面: + +![](../images/2023-06-26-12-39-1jsF0t.jpeg) + +再填写用户名和密码: + +> 设置 -> 同步 -> AnkiWeb账户 + +### iOS 端 + +AnkiMobile 也已经支持和自建的同步服务器同步了。至少对于版本 Ankimobile 2.0.90(20090.2) 来说,似乎是可行的,这是一位 iOS 系统用户[在 Anki 论坛报告的](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862)。 + +如果设置完成后发现不能同步可以参考下面的内容再试一次: + +> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and then on again. + +上面的内容摘自 [ANki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md new file mode 100644 index 000000000000..6d6a4f1adb0f --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Go Apps.md @@ -0,0 +1,151 @@ +# 快速安装 Go 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Go 程序 + +```go +package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello,Wolrd!") + }) + + http.ListenAndServe(":8080", nil) +} +``` + +## 步骤2:创建Docker镜像 + +- 在与`main.go`相同的目录中,创建一个名为`Dockerfile`的文件,包含以下内容: + +``` +FROM golang:1.17 as builder + +WORKDIR /app +COPY . . + +RUN go mod init myapp +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /app/main /app/ + +EXPOSE 8080 +ENTRYPOINT ["/app/main"] +``` + +这个`Dockerfile`定义了一个多阶段构建过程。在第一阶段,我们使用官方的`golang`镜像作为基础镜像,然后编译Go应用程序。在第二阶段,我们使用轻量级的`alpine`镜像,将编译好的二进制文件复制到`/app`目录,并将8080端口暴露。 + +- 在`Dockerfile`所在的目录运行以下命令,为Go应用程序构建Docker镜像: + +``` +docker build -t your_image_name . +``` + +将`your_image_name`替换为您的镜像名称和标签。 + + + +## 步骤3:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag demo damager6666/demo:latest + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/demo:latest + ``` + +## 步骤4:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤5:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 6:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤7:应用部署 + +- 基础配置: + + - 应用名称(自定义):go-demo + - 镜像名(**步骤5**中推送的镜像):damager6666/demo:latest + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/java-example-5.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤8:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤9:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/hello +``` + +![](images/java-example-9.png) + +- 页面上显示 ”Hello,World! “ ,表示你的 Go 应用程序已经在 Sealos 上运行 + diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md new file mode 100644 index 000000000000..9a036f53487a --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Java Apps.md @@ -0,0 +1,272 @@ +# 快速安装 Java 程序 + +首先,确保你已经安装了以下工具: + +- Docker +- Maven + +## 步骤1:编写你的 Java 程序 + +- 这里直接通过 [Spring Initializr](https://start.spring.io/) 生成生成一个基本的 Spring Boot 项目。 + +![](images/java-example-1.png) + +- 打开项目的 `pom.xml` 文件,在 `` 部分添加以下内容,以包含MySQL JDBC驱动: + + ``` + + mysql + mysql-connector-java + + ``` + +- 接下来,在 `src/main/resources` 目录下的 `application.properties` 文件中添加以下内容,以配置数据库连接: + + ``` + spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 + spring.datasource.username=root + spring.datasource.password=your_password + spring.jpa.hibernate.ddl-auto=update + ``` + +## 步骤2:创建实体类和存储类 + +在项目中创建一个新的Java类,如 `Person.java`,用于表示数据库中的实体。添加以下内容: + +```java +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + // Getters and setters + // ... +} +``` +- 然后创建一个存储库接口,如 `PersonRepository.java`: + + ```java + import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.stereotype.Repository; + + @Repository + public interface PersonRepository extends JpaRepository { + } + ``` + +## 步骤3:创建 Web 控制器 + +- 在项目的 `src/main/java` 目录下,创建一个简单的 Web 控制器。例如,创建一个名为 `PersonController.java` 的文件,并添加以下内容: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class PersonController { + @Autowired + private PersonRepository personRepository; + + @GetMapping("/getPersons") + public String getAllPersons() { + List persons = personRepository.findAll(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < persons.size(); i++) { + sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n"); + } + System.out.println(sb.toString()); + return sb.toString(); + } + + @PostMapping(value = "/addPerson") + public Person addPerson(@RequestBody Person person) { + return personRepository.save(person); + } + +} +``` + +## 步骤4:构建应用程序 + +- 使用maven构建项目,运行以下命令 + + ``` + mvn clean install + ``` + + ​ ![](images/java-example-2.png) + +## 步骤5:创建Docker镜像 + +- 在 Java 项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile: + + ``` + # 使用官方的 OpenJDK 镜像作为基础镜像 + FROM openjdk:8-jre-slim + + # 设置工作目录 + WORKDIR /app + + # 复制构建好的 JAR 文件到镜像中 + COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar + + # 暴露应用程序的端口 + EXPOSE 8080 + + # 设置启动命令 + CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"] + ``` + +- 在项目根目录下,运行以下命令构建 Docker 镜像: + +``` +docker build -t java-demo . +``` + +## 步骤6:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag java-demo damager6666/demo:v2 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/java-demo:v2 + ``` + +## 步骤7:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤8:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤9:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤10:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + +## 步骤11:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 12:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤13:应用部署 + +- 基础配置: + + - 应用名称(自定义):java-demo + - 镜像名:damager6666/java-demo:v2 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/java-example-5.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤14:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤15:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/java-example-13.png) + +- 页面上显示之间插入到数据库的数据 ,表示你的 Java 应用程序已经在 Sealos 上运行 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md new file mode 100644 index 000000000000..410c2a5ba93e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Node.js Apps.md @@ -0,0 +1,227 @@ +# 快速安装 Node.js 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Node.js 程序 + +- 在目录下创建一个名为 `app.js` 的文件,并添加以下代码: + + ```js + const express = require('express'); + const mysql = require('mysql'); + const app = express(); + + const db = mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + + db.connect((err) => { + if (err) throw err; + console.log('Connected to the database.'); + }); + + app.get('/', (req, res) => { + db.query('SELECT id, name FROM users', (err, results) => { + if (err) throw err; + res.send(JSON.stringify(results)); + }); + }); + + app.listen(8080, () => { + console.log('Server is running on port 8080'); + }); + + ``` + +- 在项目根目录下创建一个名为 `package.json` 的文件,并包含以下内容: + + ```json + { + "name": "nodejs-web-app-mysql", + "version": "1.0.0", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "^4.17.1", + "mysql": "^2.18.1" + } + } + ``` + +## 步骤2:创建 Docker 镜像 + +- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile: + +```dockerfile +FROM node:16 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "start"] +``` + +## 步骤3:构建 Docker 镜像 + +- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像: + + ``` + docker build -t nodejs-demo . + ``` + +## 步骤4:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag nodejs-demo damager6666/nodejs-demo:v1 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/nodejs-demo:v1 + ``` + + + +## 步骤5:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤6:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤7:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤8:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + + + +## 步骤9:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 10:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤11:应用部署 + +- 基础配置: + + - 应用名称(自定义):nodejs-demo + - 镜像名:damager6666/nodejs-demo:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/nodejs-example-0.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤12:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤13:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/nodejs-example-1.png) + +- 页面上显示之间插入到数据库的数据 ,表示你的 Node.js应用程序已经在 Sealos 上运行 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md new file mode 100644 index 000000000000..dcaae39c6e39 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Python Apps.md @@ -0,0 +1,247 @@ +# 快速安装 Python 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Python 程序 + +- 在目录下创建一个名为 `app.py` 的文件,并添加以下代码: + + ```python + from flask import Flask, render_template + import mysql.connector + + app = Flask(__name__) + + def get_data_from_database(): + cnx = mysql.connector.connect( + host="java-demo-db-mysql.ns-7otl3mb2.svc", + user="root", + password="l9h8f24b", + database="test_db" + ) + + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + data = cursor.fetchall() + cursor.close() + cnx.close() + + return data + + @app.route('/') + def index(): + data = get_data_from_database() + return render_template('index.html', data=data) + + if __name__ == '__main__': + app.run(host='0.0.0.0', debug=True) + ``` + +- 在项目目录下创建一个名为 `requirements.txt` 的文件,以列出应用程序所需的依赖。 + +``` +Flask +mysql-connector-python +``` + +- 在项目目录中创建一个名为 `templates` 的文件夹,并在其中创建一个名为 `index.html` 的文件。将以下内容添加到 `index.html`: + + ~~~html + + + + + Data from MySQL + + +

Data from MySQL

+ + + + + + + {% for row in data %} + + + + + + {% endfor %} +
Column1Column2Column3
{{ row[0] }}{{ row[1] }}{{ row[2] }}
+ + + ``` + + 请将 `Column1`、`Column2` 和 `Column3` 替换为您要显示的实际列名。 + ~~~ + +## 步骤2:创建 Docker 镜像 + +- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 Dockerfile: + + ```dockerfile + FROM python:3.8 + + WORKDIR /app + + COPY requirements.txt . + + RUN pip install --no-cache-dir -r requirements.txt + + COPY . . + + EXPOSE 5000 + + CMD ["python", "app.py"] + ``` + +## 步骤3:构建 Docker 镜像 + +- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像: + + ``` + docker build -t python-demo . + ``` + +## 步骤4:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag python-demo damager6666/python-demo:v1 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/python-demo:v1 + ``` + + + +## 步骤5:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤6:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤7:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤8:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + + + +## 步骤9:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 10:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤11:应用部署 + +- 基础配置: + + - 应用名称(自定义):python-demo + - 镜像名:damager6666/python-demo:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/python-example-0.png) + +- 网络配置: + + - 容器暴露端口:5000 + + + - 外网访问:开启 + +![](images/python-example-1.png) + +## 步骤12:部署应用 + +![](images/python-example-2.png) + +## 步骤13:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/python-example-3.png) + +- 在浏览器上输入外网地址,可以访问到刚刚存入到 MySQL 中的数据,表示程序已经成功 Sealos 上 + + ```http + https://mkqreaqfgwac.cloud.sealos.io/ + ``` + +![](images/python-example-4.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md new file mode 100644 index 000000000000..1b9873b42e09 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/Quick installation of Vue Apps.md @@ -0,0 +1,144 @@ +# 快速安装 Vue 程序 + +首先,确保你已经安装了以下工具: + +- Docker +- Node.js +- npm + +## 步骤1:安装 Vue CLI + +```bash +npm install -g @vue/cli +``` + +## 步骤2:使用 Vue CLI 创建一个新项目 + +```bash +vue create my-vue-app +``` + +## 步骤3:使用 Vue CLI 构建项目的生产版本 + +```bash +cd my-vue-app +npm run build +``` + +## 步骤4:创建Docker镜像 + +- 在与my-vue-app目录中,创建一个名为`Dockerfile`的文件,包含以下内容: + +```dockerfile +# 使用 nginx 镜像作为基础镜像 +FROM nginx:1.21-alpine + +# 复制构建的前端文件到 nginx 容器中 +COPY dist/ /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] +``` + +- 在`Dockerfile`所在的目录运行以下命令,为 Vue 应用程序构建 Docker 镜像: + +```bash +docker build -t my-vue-app . +``` + +## 步骤5:推送Docker镜像 + +将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + +- 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag my-vue-app damager6666/my-vue-app:v1 + ``` + +- 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/my-vue-app:v1 + ``` + + +## 步骤6:登录 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤7:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 8:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤9:应用部署 + +- 基础配置: + + - 应用名称(自定义):my-vue-app + - 镜像名(**步骤5**中推送的镜像):damager6666/my-vue-app:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/vue-example-0.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](images/vue-example-1.png) + +## 步骤10:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/vue-example-2.png) + +## 步骤11:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/vue-example-3.png) + +- 在浏览器上输入 + +``` +https://mvpztqzczudy.cloud.sealos.io +``` + +![](images/vue-example-4.png) + +- 页面上显示 ”Hello,Vue.js! “ ,表示你的 Vue 应用程序已经在 Sealos 上运行 + diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png new file mode 100644 index 000000000000..f856ec873a65 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png new file mode 100644 index 000000000000..83b3f85e4441 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png new file mode 100644 index 000000000000..7164ba6a31fc Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png new file mode 100644 index 000000000000..b1cf562ca8c0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png new file mode 100644 index 000000000000..6eedee90f87c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png new file mode 100644 index 000000000000..3548b505e15f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png new file mode 100644 index 000000000000..179f4b85a5fb Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png new file mode 100644 index 000000000000..07db46ce9e6f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png new file mode 100644 index 000000000000..670338bb9b00 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png new file mode 100644 index 000000000000..a79b84e285a8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png new file mode 100644 index 000000000000..d5f28559a335 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png new file mode 100644 index 000000000000..4a14b632edd4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png new file mode 100644 index 000000000000..19a4f0f3276e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png new file mode 100644 index 000000000000..b5582f255cd6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/java-example-9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png new file mode 100644 index 000000000000..629be8f5f144 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png new file mode 100644 index 000000000000..83d4acd117f7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/nodejs-example-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png new file mode 100644 index 000000000000..86d53a8b85c7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png new file mode 100644 index 000000000000..2f03cf92b397 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png new file mode 100644 index 000000000000..19dcda99374a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png new file mode 100644 index 000000000000..54ceb5547f34 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png new file mode 100644 index 000000000000..3ffbe07560ba Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/python-example-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png new file mode 100644 index 000000000000..648276d14120 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-0.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png new file mode 100644 index 000000000000..e47a1b27115d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png new file mode 100644 index 000000000000..dab8d56621b8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png new file mode 100644 index 000000000000..9ac654f24398 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png new file mode 100644 index 000000000000..b00209521090 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/examples/programming-languages/images/vue-example-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md b/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md new file mode 100644 index 000000000000..b2c53ab1004d --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/examples/social-communication/install-tailchat.md @@ -0,0 +1,177 @@ +# 快速安装 Tailchat + +[Tailchat](https://tailchat.msgbyte.com/) 是一款插件化易拓展的开源 IM 应用。可拓展架构赋予 Tailchat 无限可能性。 + +前端微内核架构 + 后端微服务架构 使得 Tailchat 能够驾驭任何定制化/私有化的场景 + +面向企业与私域用户打造,高度自由的群组管理与定制化的面板展示可以让私域主能够更好的展示自己的作品,管理用户,打造自己的品牌与圈子。 + +![](../images/tailchat/0.png) + + +## 首先在 [Sealos](https://cloud.sealos.io) 桌面环境中打开「应用管理」 + +![](../images/tailchat/1.png) + +## 新建应用 + +![](../images/tailchat/2.png) + +### 创建依赖 + +`tailchat` 作为企业级的应用,最小依赖: `mongodb`, `redis`, `minio`. + +接下来让我们来一一创建。 + +#### mongodb + +为了方便起见我们固定一个实例,并且为数据库绑定本地存储。 + +使用的镜像是 `mongo:4` + +需要注意的是因为我没有给数据库设置密码,因此不要对外网提供网络服务。容器暴露端口填数据库默认服务端口 `27017` 即可 + +内容如下: + +![](../images/tailchat/3.png) + +点击部署应用提交部署 + +耐心等待一会,就可以看到应用已经启动起来了 + +![](../images/tailchat/4.png) + +> 需要注意的是初始分配的64m对于mongodb来说实在太小了,所以通过变更应用改为了128m。可以随时分配资源大小这也是sealos/k8s很方便的一点 + +#### minio + +接下来我们创建minio, minio是一个开源的对象存储服务。我们同样可以通过`sealos`的点点点来快速创建 + +使用的镜像是: `minio/minio` + +需要注意的是我们要进行一些调整: + +- 暴露端口: 9000 +- 运行命令改为: `minio` +- 命令参数改为: `server /data` +- 设置环境变量: + - MINIO_ROOT_USER: tailchat + - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat +- 本地存储: `/data` + +最终结果如下: + +![](../images/tailchat/5.png) + +点击部署按钮同样看到服务已经正常启动起来了。 + +#### redis + +最后我们需要部署redis作为内容缓存与信息转发。 + +使用镜像: `redis:alpine` + +暴露端口: `6379` + +最终结果如下: + +![](../images/tailchat/6.png) + + +### 创建 Tailchat 本体 + +此时Tailchat所需要的依赖均已部署完毕,如下: + +![](../images/tailchat/7.png) + +现在我们来部署 `Tailchat` 本体。 + +`Tailchat` 的本体会相对复杂一点,不过因为`sealos`纯UI操作也不会太过复杂。 + +- 使用镜像: `moonrailgun/tailchat` +- 暴露端口: `11000`(记得要打开外网访问) +- 配置环境变量如下: + ``` + SERVICEDIR=services,plugins + TRANSPORTER=redis://redis:6379 + REDIS_URL=redis://redis:6379 + MONGO_URL=mongodb://mongo/tailchat + MINIO_URL=minio:9000 + MINIO_USER=tailchat + MINIO_PASS=com.msgbyte.tailchat + ``` + +最终效果如下: + +![](../images/tailchat/8.png) + +耐心等待一段时间后可以看到`Tailchat` 服务已经启动起来了 + +![](../images/tailchat/9.png) + +## 预览服务 + +首先我们可以先检查一下`Tailchat`服务的可用性,可以通过外网地址提供的服务后面加上 `/health` 来检查服务可用性, 如: `https://.cloud.sealos.io/health` + +当启动完毕后,Tailchat服务会返回如下内容: + +![](../images/tailchat/10.png) + +这段json字符串中包含了使用的镜像版本,节点名称,系统占用,微服务加载情况。 + +这里我们可以看到我的常见的服务, 如`user`/`chat.message`以及一些带有插件前缀的服务如`plugin.registry`都已经正常启动起来了,说明我们的服务端是正常运行的。 + +现在我们可以直接访问我们的外网地址,可以看到经过短暂的加载后,页面正常打开自动跳转到了登录界面。 + +![](../images/tailchat/11.png) + +随便注册一个账号,可以看到我们可以正常进入Tailchat的主界面, 如下图: + +![](../images/tailchat/12.png) + +至此我们的服务已经成功在 sealos 中落地了。 + +## 扩容服务 + +当然,作为一个分布式架构的系统,`Tailchat` 天然是支持水平扩容的。而在 `sealos` 想要实现扩容也非常简单, 只需要通过变更操作修改实例数即可: + +![](../images/tailchat/13.png) + +![](../images/tailchat/14.png) + +![](../images/tailchat/15.png) + +此时当我们访问 `https://.cloud.sealos.io/health` 可以看到我们可以访问到不同的节点 + +![](../images/tailchat/16.png) + +## 添加 Tailchat 入口到桌面 + +打开 Terminal, 输入`vim app.yml`创建并编辑一个配置文件 + +输入以下内容,注意url要换成自己部署的网址 + +```yml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: tailchat-app-entry +spec: + name: Tailchat + icon: + type: iframe + data: + url: + desc: + icon: https://tailchat.msgbyte.com/img/logo.svg + menuData: + displayType: normal +``` + +按`esc`退出编辑模式, 按`:wq`保存并退出vim + +输入`kubectl apply -f app.yml`启动配置。 + +完毕后刷新页面,此时我们可以看到我们的入口就出现在`sealos`的桌面上了 + +![](../images/tailchat/17.png) diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md new file mode 100644 index 000000000000..564035e57741 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/add-domain.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +--- + +# 自定义域名 + +想让您的用户能够享受与您的品牌一致的专属体验?只需要在 [Sealos](https://cloud.sealos.io) 中为你的项目指定一个自定义域名。 + +## 部署时接入 + +如果您在部署应用时接入自定义域名,只需要在部署应用时打开「外网访问」,Sealos 会给您的应用自动分配一个外网域名。 + +![](./images/app-launchpad-domain.png) + +然后需要到您的域名服务商处,添加该域名的 `CNAME` 解析到上面分配的外网域名。以阿里云为例: + +![](./images/aliyun-cname.png) + +等待解析生效后即可回到 Sealos 中绑定自定义域名,直接点击右侧的「自定义域名」: + +![](./images/app-launchpad-domain2.png) + +在弹出的界面中输入您的自定义域名,然后点击确认即可。 + +![](./images/app-launchpad-domain3.png) + +最终点击右上角的「部署」开始部署应用,部署完成后点击外网地址即可通过自定义域名访问应用。 + +## 部署完成后接入 + +已经部署完成的应用可以在应用详情页面的右上角点击「变更」,然后参考之前的步骤接入自定义域名即可。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md new file mode 100644 index 000000000000..558d9e3c362d --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/applaunchpad.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 1 +--- + +# 应用管理 + +**应用管理** 是 Sealos 内置的单镜像部署工具,主要用于简化和加速应用程序的部署过程,可以帮助您在 5 分钟内完成应用的部署和上线。 + +目前「应用管理」具备以下功能: + +- 支持使用私有镜像部署应用; +- 支持根据应用需求,自定义所需的 CPU 和内存资源; +- 支持多副本; +- 弹性伸缩 (HPA); +- 提供外网访问地址,便于公网访问; +- 允许用户为应用配置自定义域名,提高品牌识别度和用户体验; +- ConfigMap 配置文件; +- 应用数据的持久化存储,保障数据的安全性和持续性; +- 提供应用和 Pod 的实时监控,帮助用户及时发现并解决问题; +- 记录和管理应用日志,便于问题追踪和性能分析; +- 分析系统事件(Events),提供关键信息帮助优化应用性能; +- 一键进入容器终端,方便管理和调试; +- 支持将应用的多个端口暴露到外网。 + +## [快速开始](/quick-start/use-app-launchpad.md) + +快速安装一些比较常见的应用。 + +## [更新应用](/guides/applaunchpad/update-app.md) + +应用部署完成后修改应用配置。 + +## [自定义域名](/guides/applaunchpad/add-domain.md) + +为应用接入自定义域名。 + +## [暴露多端口](/guides/applaunchpad/expose-multi-ports.md) + +将应用的多个端口暴露到外网中。 + +## [环境变量](/guides/applaunchpad/environment.md) + +通过环境变量为应用提供配置信息。 + +## [配置文件](/guides/applaunchpad/configmap.md) + +通过配置文件为应用提供配置信息。 + +## [弹性伸缩](/guides/applaunchpad/autoscale.md) + +通过弹性伸缩来根据负载自动调整应用的实例数量。 + +## [持久化存储](/guides/applaunchpad/persistent-volume.md) + +使用持久化存储来保障数据的持久化。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md new file mode 100644 index 000000000000..8675aa2553a4 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/autoscale.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 6 +--- + +# 弹性伸缩 + +「[应用管理](/guides/applaunchpad/applaunchpad.md)」可以在 Sealos 集群中自动调整应用的实例数量,以适应不同的负载条件。这个功能叫「**弹性伸缩**(Horizontal Pod Autoscaler,简称 HPA)」。 + +「弹性伸缩」通过监测特定的度量指标(如 CPU 使用率或内存使用率)来动态增加或减少应用的实例数量,以确保应用程序运行效率和资源利用的最优化。 + +> 注意:在弹性伸缩的上下文中,提到的“使用率”通常是指应用实例的平均使用率。例如,如果一个应用有两个实例,那么其 CPU 平均使用率将是这两个实例的 CPU 使用率的平均值。 + +弹性伸缩的工作原理: + +1. **监控**:持续监控应用的关键性能指标,如 CPU 使用率或内存使用量。 +2. **决策**:根据设定的目标(例如 CPU 使用率不超过 50%),系统会计算出为了达到这一目标所需增加或减少的应用实例数量。 +3. **调整**:基于计算结果,弹性伸缩会自动指示 Sealos 的控制器增加或减少应用实例的数量,以保持资源使用率在预定目标范围内。 + +假设我们为一个应用设置了如下规则:CPU 使用率不得超过 50%,实例数量可在 1 至 5 之间变动。在这种设定下,弹性伸缩将会: + +- 当应用的平均 CPU 使用率超过 50% 时,系统会增加实例数量,最多增至 5 个。 +- 当应用的平均 CPU 使用率低于 50% 时,系统会减少实例数量,但至少保持 1 个实例运行。 + +![](./images/autoscale.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md new file mode 100644 index 000000000000..df100c517418 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/configmap.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 5 +--- + +# 配置文件 + +在 [Sealos](https://cloud.sealos.io) 应用程序中,配置文件的使用非常重要,尤其是当应用程序需要处理大量或复杂的配置信息时。与环境变量相比,配置文件可以提供一种更灵活、更可靠的配置管理方式。环境变量更适合用于传递少量、简单的配置数据。 + +配置文件的主要优势在于其能够存储和管理复杂的配置数据,这些配置数据可以包含配置文件、命令行参数、环境变量等信息。这些数据可以在应用容器启动时注入到容器中,从而允许你对应用程序的行为进行调整,而无需重新构建镜像。 + +下面以 Nginx 容器为例,说明如何使用配置文件来调整应用的行为: + +- **文件名**: 对应 Nginx 容器里的文件,可以参考镜像提供者给出的说明。 +- **文件值**: 文件对应的内容。如果内容过于复杂,建议在本地编辑完成后,再复制粘贴到相应位置。 +- **注意事项**:配置文件是挂载单个文件,而不是挂载目录。你需要明确指定要挂载的具体文件,而非仅仅指定一个目录路径。 + +![](./images/applaunchpad13.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md new file mode 100644 index 000000000000..3c9c97ba0d10 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/environment.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 4 +--- + +# 环境变量 + +在 [Sealos](https://cloud.sealos.io) 中,环境变量扮演着至关重要的角色,它们是管理容器应用配置数据的关键工具。通过使用环境变量,可以为应用提供必要的配置信息,而无需直接修改应用的代码或其镜像,从而提高应用的可维护性和可扩展性。 + +在「[应用管理](/guides/applaunchpad/applaunchpad.md)」界面,环境变量是通过批量输入的方式来定义的。用户可以通过按行输入的方式来定义多个环境变量,其中每个环境变量由键(key)和值(value)组成,两者之间可以使用等号(=)或冒号(:)作为分隔符,「[应用管理](/guides/applaunchpad/applaunchpad.md)」会自动清除键(key)中的无效字符,以确保环境变量的正确性和有效性。 + +![](./images/applaunchpad12.png) + +**可正常解析的环境变量格式:** + +```bash +host=127.0.0.1 +port:3000 +name: sealos +- username=123 +- password:123 +# 这一行的注释会被忽略。因为不包含等号(=)或冒号(:)这两种标记字符。 +``` + +**不可解析的环境变量格式:** + +```bash +host=127.0.0.1 # 这一行会被解析,因为包含了等号(=)或冒号(:)这两种标记字符。这里的注释也会被解析,因为前面已经有等号(=)了。 +``` \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md new file mode 100644 index 000000000000..7983429dcbbd --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/expose-multi-ports.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 3 +--- + +# 暴露多端口 + +在复杂的应用环境中,经常需要应用服务同时暴露多个端口以满足不同的需求。这种需求可能出现在多种场景下: + ++ **多协议支持**:例如,一个应用可能同时支持 HTTP 和 HTTPS 协议,需要暴露 80 和 443 端口。 ++ **应用的多功能**:例如,一个应用可能有一个 Web 服务和一个 Admin 服务,它们分别监听不同的端口。 ++ **兼容性考虑**:为了与旧版本或其他服务兼容,可能需要同时暴露新旧两种接口的端口。 ++ **Prometheus 监控和应用服务**:如果你的应用既有业务端口,又有一个用于 Prometheus 监控的 `/metrics` 端口,你可能需要同时暴露两者。 ++ **GRPC 和 RESTful 服务并存**:如果你的应用同时提供 GRPC 和 RESTful 服务,你可能需要为每种服务类型暴露不同的端口。 + +[Sealos](https://cloud.sealos.io) 在使用「[应用管理](/guides/applaunchpad/applaunchpad.md)」部署应用时,可以轻松地选择暴露多个端口。在部署应用的过程中,用户只需要点击「网络配置」选项,然后选择「添加端口」,即可实现多端口的配置。 + +![](./images/multi-ports1.png) + +此外,Sealos 平台还提供了端口的外网访问功能。一旦暴露到公网,每一个暴露的端口都会被分配一个独立的二级域名,使得用户可以更方便地进行远程访问和管理。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png new file mode 100644 index 000000000000..a4ba01524001 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png new file mode 100644 index 000000000000..54651df59314 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/aliyun-cname.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png new file mode 100644 index 000000000000..9e88bb76dc3d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..b5a9dd12dc40 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..5eb0377da0e8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/app-launchpad-domain3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png new file mode 100644 index 000000000000..0e2d87f96122 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png new file mode 100644 index 000000000000..37bbf7ab57d5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png new file mode 100644 index 000000000000..ceeb631f3120 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png new file mode 100644 index 000000000000..a0189b569ab8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png new file mode 100644 index 000000000000..9317aa9f5c5d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png new file mode 100644 index 000000000000..a7551be28826 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png new file mode 100644 index 000000000000..016395059908 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png new file mode 100644 index 000000000000..fd28e1f9fb6b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png new file mode 100644 index 000000000000..82b4603f4ca3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png new file mode 100644 index 000000000000..c4f8f7c33d95 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png new file mode 100644 index 000000000000..6019b61ca10f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png new file mode 100644 index 000000000000..73fcdcc39cf3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png new file mode 100644 index 000000000000..227d5f1b8cff Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png new file mode 100644 index 000000000000..77eb0919f867 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/applaunchpad9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png new file mode 100644 index 000000000000..e8b4907448f6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/autoscale.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png new file mode 100644 index 000000000000..ab08b16299b9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/multi-ports1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png new file mode 100644 index 000000000000..b904addd338a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/persistent-volume.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png new file mode 100644 index 000000000000..373e56b2bee4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png new file mode 100644 index 000000000000..b7ea59eed18a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png new file mode 100644 index 000000000000..0fddf4574890 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png new file mode 100644 index 000000000000..aa4fc627bc11 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/images/set-cert4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md new file mode 100644 index 000000000000..6c9377648120 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/persistent-volume.md @@ -0,0 +1,13 @@ +--- +sidebar_position: 7 +--- + +# 持久化存储 + +在 [Sealos](https://cloud.sealos.io) 中,容器可以轻松地被创建和销毁。虽然这为部署和管理应用带来了灵活性,但它也带来了数据持久性的挑战。如果应用在容器内部存储数据,那么当容器被销毁时,所有存储在容器内的数据也会丢失。 + +持久化存储可以确保数据存储在容器外部,即使在容器重启或重新部署的情况下也能保证数据不会丢失。**如果你的应用需要保留数据(例如数据库、文件存储系统或任何存储用户数据的服务),一定要使用持久化存储!** + +假设你要部署一个 Nextcloud,Nextcloud 容器的所有数据都存储在 `/var/www/html` 这个目录下,因此我们需要将该目录下的数据通过外挂存储进行持久化。 + +![](./images/persistent-volume.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md new file mode 100644 index 000000000000..f865fd7aa47e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/applaunchpad/set-cert.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 8 +--- + +# 设置自定义域名证书 + +在「应用管理」中成功设置自定义域名,但是无法访问域名并显示证书不安全。 这是因为 cert-manager 没有成功签发证书。为了解决这个问题,我们可以手动设置证书。 + +首先,确认在云厂商中成功设置 CNAME,并且下载域名对应的证书。 + +打开「应用管理」,设置自定义域名。 + +![](./images/set-cert1.png) + +打开「终端」,依次执行以下命令。 + +```bash +# 用证书文件信息创建 tls.crt(使用证书文件信息替换掉 xxxx) +cat > tls.crt < tls.key <的最大连接数 | +| max_wal_size | 设置WAL文件的最大大小 | +| min_wal_size | 设置WAL文件的最小大小 | +| max_worker_processes | 设置PostgreSQL可以启动
的最大后台进程数 | +| shared_buffers | 设置PostgreSQL可以启动
的最大后台进程数 | + +**常见的mysql参数**: + +| 参数名 | 描述 | +|-------------------------|-----------------------------------| +| innodb_buffer_pool_size | 设置InnoDB缓冲池的大小 | +| max_connections | 允许的最大并发连接数 | +| query_cache_size | 查询缓存的大小 | +| thread_cache_size | 线程缓存的大小 | +| max_allowed_packet | 最大数据包大小 | +| innodb_log_file_size | InnoDB日志文件的大小 | + +**常见的mongo参数**: + +| 参数名 | 描述 | +|-------------------------|------------------------------------| +| storage.dbPath | 数据文件存放路径 | +| storage.journal.enabled | 启用日志 | +| net.port | 服务器端口 | +| net.bindIp | 绑定的IP地址 | + +**常见的redis参数**: + +| 参数名 | 描述 | +|------------------------|-------------------------------------| +| maxclients | 最大客户端连接数 | +| maxmemory | 最大内存使用量 | +| maxmemory-policy | 内存淘汰策略 | +| appendonly | AOF持久化开关 | +| appendfsync | AOF文件刷新频率 | + + +4. **应用pg-config.yaml**: + +```bash +$ kubectl apply -f pg-config.yaml +``` +![config_4](./imgs/config_4.png) + +5. **检查pg-config.yaml是否应用成功**: +```bash +# OpsRequest中对应的状态为Succeed且pod对应的状态为Running则说明配置应用成功 +$ kubectl get OpsRequest +$ kubectl get pod +``` +![config_5](./imgs/config_5.png) + +6. **进入数据库查看配置是否生效**: +```bash +$ show max_connections; +``` +![config_6](./imgs/config_6.png) +![config_7](./imgs/config_7.png) + diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png new file mode 100644 index 000000000000..d8c8c62be3a6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png new file mode 100644 index 000000000000..4bd234485bf3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png new file mode 100644 index 000000000000..1f6d2ff3383b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png new file mode 100644 index 000000000000..02f9a61edddf Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png new file mode 100644 index 000000000000..412e0bbe7fc7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png new file mode 100644 index 000000000000..5f49dd84f12e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png new file mode 100644 index 000000000000..88f930a876f0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/config-docs/imgs/config_7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md new file mode 100644 index 000000000000..16de7edf2d5e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/dbprovider.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 2 +--- + +# 数据库 + +**数据库** 是一个应用的核心组成,Sealos 提供了一个数据库集群部署管理工具,可以帮助你快速部署数据库集群。目前支持 MySQL、PostgreSQL、MongoDB、Redis、Kafka等。目前**数据库** 支持如下特性。 + ++ 弹性伸缩 ++ 支持多版本、多类型 数据库 ++ 数据库 连接:一键连接、外网访问 ++ 数据库 连接:手动备份、自动备份 ++ 数据库 监控:资源、状态、性能监控 ++ 数据库 迁移:公网迁移、文件迁移 ++ 数据库 高可用:支持多个 数据库 实例 ++ 可视化 数据库 管理:新建、删除、更新 、暂停、重启 数据库 + + +## 快速开始 + +以部署一个 PostgreSQL 数据库为例,体验 **数据库** 的便捷性。 + +只需 6 步即可完成部署和访问: + +1. 在 [Sealos](https://cloud.sealos.io) 桌面进入 **数据库** 应用: + +![start_1](./imgs/start_1.png) + +2. 点击新建集群: + +![start_2](./imgs/start_2.png) + +3. 填写数据库名并选择对应的数据库和参数: + +![start_3](./imgs/start_3.png) + +4. 点击部署: + +![start_4](./imgs/start_4.png) + +5. 查看数据库详情信息: + +![start_5](./imgs/start_5.png) + +6. 数据库详情界面: + +![start_6](./imgs/start_6.png) + +![start_7](./imgs/start_7.png) + +![start_8](./imgs/start_8.png) + +![start_9](./imgs/start_9.png) + +![start_10](./imgs/start_10.png) + +![start_11](./imgs/start_11.png) + +![start_12](./imgs/start_12.png) + + +## 数据库连接 + +### 一键连接 + +1. 进入 **数据库** ,点击连接: + +![connect_1](./imgs/connect_1.png) + +2. 在终端操作数据库: + +![connect_2](./imgs/connect_2.png) + + +### 外网访问 + +1. 进入 **数据库** ,开启外网访问: + +![connect_3](./imgs/connect_3.png) + +2. 确认开启: + +![connect_4](./imgs/connect_4.png) + +3. 复制数据库连接信息: + +![connect_5](./imgs/connect_5.png) + +4. 在数据库连接工具中连接数据库: + +![connect_6](./imgs/connect_6.png) + +![connect_7](./imgs/connect_7.png) + + +## 数据库备份 + +### 手动备份 +1. 进入数据库备份界面,点击备份: + +![backup_1](./imgs/backup_1.png) + +2. 填写备份信息,开始备份: + +![backup_2](./imgs/backup_2.png) + +3. 查看备份状态: + +![backup_3](./imgs/backup_3.png) + +![backup_4](./imgs/backup_4.png) + + +### 自动备份 +1. 进入数据库备份界面,点击备份: + +![backup_5](./imgs/backup_5.png) + +2. 开启自动备份,填写备份信息: + +![backup_6](./imgs/backup_6.png) + + +## 数据库迁移 + +以 MySQL 数据库为例,介绍 数据库迁移 的过程。 + +### 公网迁移 + +公网迁移涉及两个数据库:源数据库、目标数据库。源数据库是迁移的数据来源,目标数据库是迁移的数据目的地。以下介绍中以本地数据库做为源数据库进行演示。 + +1. 进入目标数据库,连接目标数据库: + +![migration_1](./imgs/migration_1.png) + +2. 在终端界面中创建对应的database(如果已经存在对应的database,则跳过这一步): + +![migration_2](./imgs/migration_2.png) +```bash +# 创建数据库sql语句,示例: +$ create database testmysql; +``` + +3. 进入目标数据库公网迁移界面,点击迁移按钮: + +![migration_3](./imgs/migration_3.png) + +4. 查看迁移配置信息: + +![migration_4](./imgs/migration_4.png) + +复制迁移配置信息,如下: +```bash +# 设置配置信息sql语句,示例: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` + +5. 在源数据库中执行设置配置信息(MySQL、Postgres需要手动配置信息,Mongo不需要进行配置): +```bash +# 设置配置信息sql语句,示例: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` +![migration_5](./imgs/migration_5.png) + +6. 进入目标数据库,填写源数据库的迁移信息,要迁移哪些表,并填写要迁移到目标数据库中哪个database: + +![migration_6](./imgs/migration_6.png) + +如果需要持续从源数据库迁移数据,可开启高级配置中的持续迁移,如下: + +![migration_7](./imgs/migration_7.png) + +7. 查看迁移任务信息: + +![migration_8](./imgs/migration_8.png) + +8. 进入目标数据库,连接目标数据库,检查迁移数据是否完整: + +![migration_9](./imgs/migration_9.png) + +### 文件迁移 + +1. 进入目标数据库,连接目标数据库: + +![migration_10](./imgs/migration_10.png) + +2. 在终端界面中创建对应的database(如果已经存在对应的database,则跳过这一步): +```bash +# 创建数据库sql语句,示例: +$ create database testmysql; +``` +![migration_11](./imgs/migration_11.png) + +3. 上传迁移文件、填写数据库名,开始迁移: + +![migration_12](./imgs/migration_12.png) + +4. 文件迁移中,等待迁移结果: + +![migration_13](./imgs/migration_13.png) + +![migration_14](./imgs/migration_14.png) + +5. 连接数据库,检查迁移数据是否完整: + +![migration_15](./imgs/migration_15.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png new file mode 100644 index 000000000000..8c9e8c50867d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png new file mode 100644 index 000000000000..02f239da516c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png new file mode 100644 index 000000000000..e0d5dca8c2ef Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png new file mode 100644 index 000000000000..58c37d7c5d2a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png new file mode 100644 index 000000000000..8c9e8c50867d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png new file mode 100644 index 000000000000..48b20cf2b8ca Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/backup_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png new file mode 100644 index 000000000000..52c6879b4d29 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png new file mode 100644 index 000000000000..76215ce1c9ab Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png new file mode 100644 index 000000000000..0a205d4a3626 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png new file mode 100644 index 000000000000..a1a4366d15ce Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png new file mode 100644 index 000000000000..0b4a3b398f73 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png new file mode 100644 index 000000000000..3125cbb7bf12 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png new file mode 100644 index 000000000000..e8bce07c9cde Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/connect_7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png new file mode 100644 index 000000000000..d5527ccf5116 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png new file mode 100644 index 000000000000..d5527ccf5116 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png new file mode 100644 index 000000000000..895255296854 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png new file mode 100644 index 000000000000..12561fb4eba5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png new file mode 100644 index 000000000000..074b4882b952 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png new file mode 100644 index 000000000000..409e77e907a6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png new file mode 100644 index 000000000000..54589726b673 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png new file mode 100644 index 000000000000..895255296854 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png new file mode 100644 index 000000000000..2bc1d55549e8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png new file mode 100644 index 000000000000..9a49e93f1b62 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png new file mode 100644 index 000000000000..e9b0d128758b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png new file mode 100644 index 000000000000..271c5bafd8b5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png new file mode 100644 index 000000000000..778411659966 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png new file mode 100644 index 000000000000..3202d5031fb1 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png new file mode 100644 index 000000000000..54589726b673 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/migration_9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png new file mode 100644 index 000000000000..2b87eb60a94f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png new file mode 100644 index 000000000000..6e10d9cedd0e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png new file mode 100644 index 000000000000..633705ae905c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png new file mode 100644 index 000000000000..f08ae8c081df Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png new file mode 100644 index 000000000000..016fe962470c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png new file mode 100644 index 000000000000..f687bb3cda4f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png new file mode 100644 index 000000000000..175419decad9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png new file mode 100644 index 000000000000..da9edd9ca8cc Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png new file mode 100644 index 000000000000..e61a8d5befba Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png new file mode 100644 index 000000000000..63aeada4ce95 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png new file mode 100644 index 000000000000..8e2d489183a4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png new file mode 100644 index 000000000000..cbb8d4a36ce9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/dbprovider/imgs/start_9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png new file mode 100644 index 000000000000..3ff6dfa9b024 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png new file mode 100644 index 000000000000..e636144a8eeb Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/10.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png new file mode 100644 index 000000000000..12624610f67d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/11.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png new file mode 100644 index 000000000000..4ae45dedc78c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/12.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png new file mode 100644 index 000000000000..e3cc905cd04f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/13.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png new file mode 100644 index 000000000000..66dff9b0eecd Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/14.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png new file mode 100644 index 000000000000..0022dce6edd9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/15.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png new file mode 100644 index 000000000000..442bdb7e067a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/16.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png new file mode 100644 index 000000000000..0e768c7adef3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/17.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png new file mode 100644 index 000000000000..386fb07680d8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png new file mode 100644 index 000000000000..3e17d0f68633 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png new file mode 100644 index 000000000000..17ffbdd8d582 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png new file mode 100644 index 000000000000..61c1098e18af Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png new file mode 100644 index 000000000000..4f8e59ec7250 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png new file mode 100644 index 000000000000..65a4e96dbcaa Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/7.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png new file mode 100644 index 000000000000..827eb00383d8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/8.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png new file mode 100644 index 000000000000..6ba91e30a63b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/images/9.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md new file mode 100644 index 000000000000..7409d90e89db --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/objectstorage/objectstorage.md @@ -0,0 +1,188 @@ +--- +sidebar_position: 0 +--- + +# 对象存储 + +「对象存储」是 Sealos 内置的对象存储服务,主要用于存储和管理非结构化数据。 + +目前「对象存储」具备以下功能: + +- 上传文件到「存储桶」 +- 从「存储桶」下载文件 +- 公开「存储桶」的访问权限 +- 使用 SDK 访问「存储桶」 +- 监控「存储桶」资源指标 +- 静态托管 + +## 快速开始 + +### 上传文件 + +进入「对象存储」 +![](./images/1.png) + +创建「存储桶」 +![](./images/2.png) + +设置「存储桶」名字为 test,权限为 private +![](./images/3.png) + +「存储桶」创建成功 +![](./images/4.png) + +上传文件 +![](./images/5.png) + +上传文件成功 +![](./images/6.png) + +### 公开「存储桶」的访问权限 + +点击「编辑」 +![](./images/7.png) + +设置「存储桶权限」为 publicRead,点击「应用」 +![](./images/8.png) + +复制文件链接 +![](./images/9.png) + +粘贴到浏览器地址栏访问文件 +![](./images/10.png) + +### 查看访问密钥配置 + +对象存储用户由唯一的 Access Key(用户名)和对应的 Secret Key(密码)组成。Internal 为对象存储的内部访问地址,External +为对象存储的外部访问地址。 +![](./images/11.png) + +### 使用 SDK 访问「存储桶」 + +SDK 访问「存储桶」需要三个参数:AccessKey、SecretKey、Endpoint。参数都在访问密钥中,Internal 是内网地址 Endpoint,External 是外网地址 +Endpoint。如果需要使用 Region 参数,默认使用 us-east-1。 + +#### Go Client SDK + +详细文档参考:https://min.io/docs/minio/linux/developers/go/API.html + +例子:使用 Go Client SDK 上传 style.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前 +K8s 集群内,可以将 Endpoint 改为内网地址 Internal。 + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" +) +import "github.com/minio/minio-go/v7" +import "github.com/minio/minio-go/v7/pkg/credentials" + +func main() { + endpoint := "objectstorageapi.xxx.xxx.xxx" + accessKey := "xxxxxxxx" + secretKey := "xxxxxxxxxxxxxxxx" + // init minio client + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + }) + if err != nil { + log.Fatalln(err) + } + // get local file + file, err := os.Open("./style.css") + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + fmt.Println(err) + return + } + // put object + uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"}) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Successfully uploaded bytes: ", uploadInfo) +} +``` + +文件上传成功 +![](./images/12.png) + +#### Java Client SDK + +详细文档参考:https://min.io/docs/minio/linux/developers/java/API.html + +例子:使用 Java Client SDK 上传 style1.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前 +K8s 集群内,可以将 Endpoint 改为内网地址 Internal。 + +```xml + + + io.minio + minio + 8.5.9 + +``` + +```javascript +package org.example; + +import io.minio.MinioClient; +import io.minio.UploadObjectArgs; + +public class FileUploader { + public static void main(String[] args) throws Exception { + + MinioClient minioClient = + MinioClient.builder() + .endpoint("https://objectstorageapi.xxx.xxx.xxx") + .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx") + .build(); + + + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket("sv3dd7u4-test") + .object("style1.css") + .filename("src/main/java/org/example/style1.css") + .build()); + + System.out.println("Successfully uploaded bytes."); + } +} +``` + +文件上传成功 +![](./images/13.png) + +#### 其他语言 SDK 略 + +详细文档参考:https://min.io/docs/minio/linux/developers/minio-drivers.html + +### 静态托管 + +创建一个权限为 publicRead/publicReadwrite 的「存储桶」,点击「打开托管」 +![](./images/14.png) + +点击地址跳转访问 +![](./images/15.png) + +点击「自定义域名」 +![](./images/16.png) + +跳转到「应用管理」的「变更」中,可以自定义域名 +![](./images/17.png) + + + + diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png new file mode 100644 index 000000000000..4e7734a645f3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/templates.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg new file mode 100644 index 000000000000..c842edc94e1e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/templates/images/wechat-qr-code.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md b/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md new file mode 100644 index 000000000000..cbf9222823f9 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/templates/submit-template.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import wechat from './images/wechat-qr-code.jpg'; + +# 提交模板 + +:::tip + +提交模板是可以拿奖金💰的!奖励规则如下: + +| 类型 | 奖金 | +| ------------------------------------------------------------ | ---- | +| 提交模板到 Sealos 模板市场 | 50¥ | +| 同时将模板的一键部署 PR 提交到模板应用的官方文档或者 README 中 | 150¥ | + +::: + +## 模板提交流程 + +Sealos 模板市场的所有模板都是实时从 [Sealos 模板仓库](https://github.com/labring-actions/templates) 同步过来的。如果您想要为这个仓库贡献新的模板,可以通过提交 PR(Pull Request)的方式来实现。 + +要创建一个新模板,您可以参考这里的 [template.yaml](https://github.com/labring-actions/templates/blob/main/template.yaml) 文件。系统已经内置了许多通用的环境变量和函数,这些都可以在编写模板时使用。这些内置的功能允许您使用类似于 `GitHub Actions` 的语法来编写模板,例如,您可以使用 `${{ SEALOS_NAMESPACE }}` 这样的环境变量来设置模板参数。关于这些内置环境变量的具体信息,可以参阅[模板说明文档](https://github.com/labring-actions/templates/blob/main/example_zh.md)。 + +## 领取奖金 + +首先填写并提交以下表单: + + + +然后扫码添加 Sealos 小助理微信领取奖金: + + \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md b/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md new file mode 100644 index 000000000000..f5dff84a1cca --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/templates/templates.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 0 +--- + +# 模板市场 + +import Highlight from '@site/src/components/Highlight' + + + +Sealos 的[模板市场](https://template.cloud.sealos.io/)提供了一系列预制的模板,这些模板可用于快速创建和部署网站和各种应用程序。你可以在模板市场中找到各种类型的模板,比如博客、AI 应用、低代码应用、网盘、IM 应用、中间件等等。这些模板旨在简化开发过程,使开发者能够快速启动和部署项目,而无需从零开始构建整个网站,也无需关心应用之间的各种依赖关系。 + +与 Vercel 的模板市场相比,Sealos 的模板市场在应用范围和功能上有所不同。Vercel 主要专注于前端项目的部署,它的模板主要是为了支持网站的界面和用户交互部分,但它不支持数据的持久化存储,所有的数据都是临时的,一旦应用重启,数据就会丢失。 + +Sealos 应用模板不仅支持前端项目,还支持后端和其他各类应用的部署。更重要的是,Sealos 支持挂载持久化存储,这对于需要存储大量数据或者保持数据持久化的应用来说至关重要。例如,对于电商网站、大型社交媒体应用或企业级应用来说,数据的持久化是必不可少的功能。 + +![](./images/templates.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png new file mode 100644 index 000000000000..f1fcc577c7f2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png new file mode 100644 index 000000000000..01dba426f6b0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/database-terminal.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png new file mode 100644 index 000000000000..ecb9443c9621 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png new file mode 100644 index 000000000000..c8fa6b4ae9f9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/nginx-terminal.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png new file mode 100644 index 000000000000..9e649dac3a11 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal-pod.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp new file mode 100644 index 000000000000..d1daf72539d7 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/images/terminal.webp differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md new file mode 100644 index 000000000000..7a622ffe046c --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/terminal/terminal.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 4 +--- + +# 终端 + +终端是 [Sealos](https://cloud.sealos.io) 云操作系统内置的应用,它是一款非常强大的应用,与单机操作系统的终端一样,允许用户与操作系统进行直接交互,用户可以通过键入命令来执行各种操作。 + +![](./images/terminal.webp) + +终端的价值在于其高效,灵活和强大的特性。对于许多任务来说,使用终端比使用图形用户界面(GUI)更快更简洁。对于开发者而言,终端是必不可少的工具。使用终端,可以让用户深入理解和控制 Sealos 云操作系统。 + +**每一个用户的终端实际上都运行在 Kubernetes 的一个 Pod 中**,可以在终端中通过以下命令查看: + +![](./images/terminal-pod.png) + +## 进入应用容器的终端 + +也可以直接通过终端 App 进入每个应用所在容器的终端。假设你在应用管理中部署了一个应用 Nginx,可以直接进入 Nginx 应用的详情页面,依次点击详情右侧的三个点,再点击「终端」,便进入了 Nginx 应用的终端。 + +![](./images/nginx-terminal.png) + +![](./images/nginx-terminal-1.png) + +## 终端一键直连数据库 + +还可以在终端中一键直连[数据库 App](../dbprovider/dbprovider.md) 中创建的数据库。 + +进入数据库详情页面,点击左侧的「一键连接」: + +![](./images/database-terminal.png) + +就会直接跳转到终端 App 并连接数据库: + +![](./images/database-terminal-1.png) + +## 终端直接操作 Kubernetes 资源 + +:::danger + +以下操作需要一定的 Kubernetes 和容器相关知识基础,否则不要轻易操作。 + +::: + +资深 Kubernetes 用户可以直接使用终端来操作 Kubernetes 资源,对于资深用户而言,终端的优势有很多: + ++ 可以批量操作和自动化 ++ 高度灵活性 ++ 可以进行深入的故障排查 + +以下是一些常见的终端中操作 Kubernetes 资源的示例: + +### 查看用户的 Pod 列表 + +```bash +kubectl get pods +``` + +### 创建和管理资源对象 + +```bash +# 使用YAML或JSON文件创建资源 +$ kubectl create -f + +# 使用YAML或JSON文件创建或更新资源 +$ kubectl apply -f + +# 删除指定类型和名称的资源 +$ kubectl delete + +# 查看指定 Deployment 的滚动更新状态 +$ kubectl rollout status deployment/ +``` + +### 故障排查 + +```bash +# 获取指定资源的详细信息,包括事件和状态 +$ kubectl describe + +# 获取指定 Pod 中指定容器的日志 +$ kubectl logs -c + +# 在指定 Pod 中打开一个终端,以便进行调试 +$ kubectl exec -it +``` \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/accept-invite.gif b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/accept-invite.gif new file mode 100644 index 000000000000..571b21e9363a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/accept-invite.gif differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/create-workspace.gif b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/create-workspace.gif new file mode 100644 index 000000000000..87e2684db91d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/create-workspace.gif differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/invite-user.gif b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/invite-user.gif new file mode 100644 index 000000000000..3da872468328 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/invite-user.gif differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/switch-workspace.gif b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/switch-workspace.gif new file mode 100644 index 000000000000..ae1df35f3353 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/images/switch-workspace.gif differ diff --git a/docs/archived/4.0/i18n/zh-Hans/guides/workspace/worksapce.md b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/worksapce.md new file mode 100644 index 000000000000..d8cbe67692cd --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/guides/workspace/worksapce.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 5 +--- + +# 团队协作与工作空间 + +## 名词解释 + +### 工作空间 + +工作空间(workspace)是 Sealos 云操作系统的核心功能,基于kubernetes +namespace实现,是一种多租户的资源隔离机制,可以将一个kubernetes集群划分为多个工作空间,每个工作空间都有自己的资源配额和权限,可以将不同的用户划分到不同的工作空间中,实现资源隔离和权限控制。 + +个人空间(personal +workspace)是工作空间的一种特殊形式,每个用户都有一个个人空间,个人空间的名称与用户的用户名相同,个人空间的资源配额和权限与普通工作空间相同,但是个人空间无法删除,也无法将其他用户加入到个人空间中。 + +### 角色与权限 + +工作空间中的用户可以拥有不同的角色,不同的角色拥有不同的权限,目前 Sealos 云操作系统中的角色有以下几种: +所有者(owner)、管理员(manager)、开发者(developer)。 + ++ 所有者(owner):拥有工作空间的所有权限,包括删除工作空间、查看/创建/修改工作空间中的所有资源、邀请用户以管理员/开发者身份加入工作空间等。 ++ 管理员(manager):拥有工作空间的管理权限,包括查看/创建/修改工作空间中的所有资源、邀请用户以开发者身份加入工作空间等。 ++ 开发者(developer):拥有工作空间的开发权限,包括查看工作空间中的资源状态。 + +## 快速开始 + +### 创建工作空间 + +![create-workspace.gif](images%2Fcreate-workspace.gif) + +### 切换工作空间 + +![switch-workspace.gif](images%2Fswitch-workspace.gif) + +### 邀请用户加入工作空间 + +通过用户id邀请用户加入工作空间,被邀请的用户可以选择接受或拒绝邀请,如果接受邀请,被邀请的用户将成为工作空间的成员,拥有工作空间中的资源权限。 + +![invite-user.gif](images%2Finvite-user.gif) + +### 接受邀请 + +当用户被邀请加入工作空间时,可以在管理面板查看到邀请消息,点击邀请消息可以查看邀请详情,点击接受邀请按钮可以接受邀请,接受邀请后,用户将成为工作空间的成员,拥有工作空间中的资源权限。 + +![accept-invite.gif](images%2Faccept-invite.gif) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/images/sealos-desktop-zh.webp b/docs/archived/4.0/i18n/zh-Hans/images/sealos-desktop-zh.webp new file mode 100644 index 000000000000..b82a41f1c549 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/images/sealos-desktop-zh.webp differ diff --git a/docs/archived/4.0/i18n/zh-Hans/images/sealos-qr-code.jpg b/docs/archived/4.0/i18n/zh-Hans/images/sealos-qr-code.jpg new file mode 100644 index 000000000000..506286e82d7d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/images/sealos-qr-code.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/msa/privacy-policy.md b/docs/archived/4.0/i18n/zh-Hans/msa/privacy-policy.md new file mode 100644 index 000000000000..95e40288140e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/msa/privacy-policy.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 2 +--- + +# Sealos 云服务隐私政策 + +我们非常重视您的隐私保护,在您使用本服务时,我们将按照以下政策收集、使用、披露和保护您的个人信息。请您仔细阅读并充分理解本隐私政策。 + +**信息收集** + +1. 在您注册或使用本服务时,我们可能收集您的姓名、电话号码、电子邮件地址、地址等个人信息。 +2. 在您使用本服务过程中产生的信息,如操作日志、访问IP地址、设备型号等。 +3. 我们可能会通过 Cookies 或其他技术收集和存储您访问本服务的相关信息,以便为您提供更好的用户体验。 + +**信息使用** + +1. 我们会根据法律法规规定以及与用户之间的约定来处理用户的个人信息。 +2. 我们可能会将收集到的信息用于改进服务质量、开发新产品或功能等目的。 +3. 我们可能会将收集到的信息用于向您推送与本服务相关的通知或广告。 + +**信息披露** + +1. 我们不会向任何第三方披露您的个人信息,除非: + + 1. 您事先同意; + + 2. 法律法规要求; + + 3. 为维护我们或其他用户的合法权益。 + +2. 我们可能与关联公司、合作伙伴分享您的个人信息,但我们会采取相应的保密措施,确保信息安全。 + +**信息保护** + +1. 我们采取各种安全措施,包括加密、访问控制等技术手段,以保护您的个人信息免受未经授权的访问、使用或泄露。 +2. 我们会定期对收集、存储和处理的个人信息进行安全评估,以确保个人信息安全。 +3. 在发生个人信息泄露等安全事件时,我们会立即启动应急预案,并在法律法规规定的范围内向您及时告知。 + +**用户权利** + +1. 您有权随时查阅、更正或删除您的个人信息。 +2. 您有权拒绝我们收集您的个人信息,但这可能导致您无法使用本服务的部分功能。 +3. 您有权要求我们停止处理您的个人信息,但这可能导致您无法继续使用本服务。 + +**隐私政策更新** + +1. 我们可能会对本隐私政策进行修改。如本隐私政策发生变更,我们将在本服务页面上发布修改后的隐私政策。如您继续使用本服务,则视为同意修改后的隐私政策。 +2. 我们鼓励您定期查阅本隐私政策,以了解我们如何保护您的个人信息。 + +**未成年人保护** + +我们非常重视对未成年人个人信息的保护,如您为未成年人,请在监护人指导下使用本服务,并请监护人帮助您在使用本服务过程中正确处理个人信息。 + +**跨境数据传输** + +由于我们的服务器可能位于不同国家或地区,您同意我们可能需要将您的个人信息传输至其他国家或地区,并在该等国家或地区存储和处理以向您提供服务。我们会采取适当措施确保跨境传输的数据仍然受到适当保护。 + +**联系我们** + +1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系(微信): + [fangnux] +2. 我们将尽快回复并解决您提出的问题。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/msa/terms-of-service.md b/docs/archived/4.0/i18n/zh-Hans/msa/terms-of-service.md new file mode 100644 index 000000000000..3d30bde57f34 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/msa/terms-of-service.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 1 +--- + +# Sealos 云服务服务协议 + +Sealos 云服务服务协议是您与珠海环界云计算有限公司(以下简称“我们”或“本公司”)之间就云服务(以下简称“本服务”)的使用等相关事项所订立的协议。请您仔细阅读并充分理解本协议各条款,特别是免除或者限制我们责任的条款、对您权益的限制条款、争议解决和法律适用条款等。如您不同意本协议任一内容,请勿注册或使用本服务。 + +**第1条 服务内容** + +1. 我们将向您提供存储、计算、网络传输等基于互联网的信息技术服务。 +2. 我们将为您提供相关技术支持和客户服务,帮助您更好地使用本服务。 +3. 您同意我们有权自行决定变更、中断、终止或暂停本服务的全部或部分功能,且无需对您承担任何责任。 + +**第2条 用户注册与账户管理** + +1. 您在使用本服务前需要注册一个账户。您保证在注册时提供的信息真实、准确、完整,并及时更新。 +2. 您应妥善保管账户名和密码,对由此产生的全部行为负责。如发现他人使用您的账户,应立即通知我们。 +3. 我们有权对您的账户进行审查,如发现您的账户存在异常情况,我们有权暂停或终止向您提供服务。 + +**第3条 使用规则** + +1. 您不得利用本服务从事任何违法活动或侵犯他人合法权益的行为,包括但不限于侵犯知识产权、泄露他人商业机密等。 +2. 您不得通过任何手段恶意注册账户,包括但不限于以牟利、炒作、套现等目的。 +3. 您不得利用本服务传播任何违法、有害、恶意软件等信息。 +4. 您应遵守相关法律法规及本协议的规定,对在本服务中发布的信息及使用本服务所产生的结果承担全部责任。 + +**第4条 费用及支付** + +1. 您同意支付与本服务相关的费用,具体费用标准以我们公布的价格为准。 +2. 我们可能会根据市场情况调整费用标准,并提前通知您。您如继续使用本服务,则视为同意调整后的费用标准。 +3. 如您未按时支付费用,我们有权暂停或终止向您提供本服务,并保留追讨欠款及要求赔偿的权利。 + +**第5条 服务免责与责任限制** + +1. 本服务按照现有技术和条件所能达到的水平提供。我们不能保证本服务无故障、安全完全可靠或满足您的所有需求。 +2. 如因不可抗力导致本服务发生故障或无法正常使用,我们不承担责任。 +3. 对于因您自身原因导致的数据丢失、损坏等情况,我们不承担责任。 + +**第6条 隐私政策** + +您同意接受并遵守我们制定的隐私政策。隐私政策为本协议的一部分,具体内容请参见下文“隐私政策”。 + +**第7条 知识产权** + +1. 我们对本服务及相关软件、技术、文档等拥有全部知识产权,除非经我们明确许可,您不得进行复制、分发、出租、反向工程等行为。 +2. 您在使用本服务过程中产生的数据和内容(包括但不限于文件、图片等)之知识产权归您所有,但为了向您提供服务,您同意授权我们在必要范围内对其进行存储、备份等操作。 + +**第8条 协议终止** + +1. 如您违反本协议规定,我们有权立即终止向您提供服务,并保留追究法律责任的权利。 +2. 如您决定停止使用本服务,可以申请注销账户,本协议自账户注销之日起终止。 + +**第9条 争议解决和法律适用** + +1. 本协议的签订、履行、解释和争议解决均适用中华人民共和国法律。 +2. 若您与我们发生争议,应先友好协商解决;协商不成的,任何一方均有权将争议提交至有管辖权的人民法院诉讼解决。 + +**第10条 网络安全** + +1. 如果利用平台服务提供违反国家法律的服务,您需要自行承担后果。 +2. 您对外提供的服务需要做好监管职责,如有用户在您的服务上从事违反国家法律的行为,您需自行承担后果。 +3. 我们如果发现您存在违法行为,将会对相关部门发起诉讼,并为有关部门提供证据。 + +**第11条 其他条款** + +1. 如本协议中部分条款因违反法律法规而被视为无效,不影响其他条款的效力。 +2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-sokx.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-sokx.png new file mode 100644 index 000000000000..834cb08f40d3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-sokx.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tlpi.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tlpi.png new file mode 100644 index 000000000000..b904addd338a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tlpi.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tpsc.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tpsc.jpg new file mode 100644 index 000000000000..8e477e550a4d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-tpsc.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ualx.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ualx.jpg new file mode 100644 index 000000000000..49ab8b95ae32 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ualx.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ubqj.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ubqj.jpg new file mode 100644 index 000000000000..5f1da8a47b4b Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/SCR-20230529-ubqj.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/aliyun-cname.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/aliyun-cname.png new file mode 100644 index 000000000000..54651df59314 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/aliyun-cname.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-2.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-2.png new file mode 100644 index 000000000000..7154f775b5d2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-3.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-3.png new file mode 100644 index 000000000000..2c57fd60b2e2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment.png new file mode 100644 index 000000000000..2f2429e965d2 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-deployment.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-1.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-1.jpg new file mode 100644 index 000000000000..1b6652464f9a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-1.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain.png new file mode 100644 index 000000000000..9e88bb76dc3d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain2.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..b5a9dd12dc40 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain3.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..5eb0377da0e8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-domain3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-zh.webp b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-zh.webp new file mode 100644 index 000000000000..e9651614238f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad-zh.webp differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad.jpg new file mode 100644 index 000000000000..1a64b79192e3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-launchpad.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list-2.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list-2.png new file mode 100644 index 000000000000..355cd126eec3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list.png new file mode 100644 index 000000000000..a9461aae6b7e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-list.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-nginx.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-nginx.png new file mode 100644 index 000000000000..8948883dd408 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-nginx.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-url.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-url.png new file mode 100644 index 000000000000..7e36ad3b964e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/app-url.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-configure.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-configure.png new file mode 100644 index 000000000000..2e3953ab44e0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-configure.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-connect.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-connect.png new file mode 100644 index 000000000000..c7d6e3a3b018 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-connect.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-creating.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-creating.png new file mode 100644 index 000000000000..d4c091e355e0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-creating.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-launch.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-launch.jpg new file mode 100644 index 000000000000..ecbf33885d31 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-launch.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-more.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-more.png new file mode 100644 index 000000000000..2e38550cc547 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-more.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-select.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-select.png new file mode 100644 index 000000000000..12c77075f415 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/database-select.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/deploy-template.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/deploy-template.png new file mode 100644 index 000000000000..8dbd671ca11d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/deploy-template.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/multi-ports1.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/multi-ports1.png new file mode 100644 index 000000000000..ab08b16299b9 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/multi-ports1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/nginx.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/nginx.png new file mode 100644 index 000000000000..14adf1fcc771 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/nginx.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/template-detail.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/template-detail.png new file mode 100644 index 000000000000..d17b41b7094f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/template-detail.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates-2.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates-2.png new file mode 100644 index 000000000000..f2df81fe93e8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates.jpg b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates.jpg new file mode 100644 index 000000000000..4bb9dc03c3e6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/templates.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/images/terminal.png b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/terminal.png new file mode 100644 index 000000000000..b0f260da9972 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/quick-start/images/terminal.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/next-steps.md b/docs/archived/4.0/i18n/zh-Hans/quick-start/next-steps.md new file mode 100644 index 000000000000..c8e1833bb2b5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/quick-start/next-steps.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 5 +--- + +# 下一步 + +恭喜您成功进入了 Sealos 的世界,现在您可以选择深入了解 Sealos 的更多功能: \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/use-app-launchpad.md b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-app-launchpad.md new file mode 100644 index 000000000000..858b66532303 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-app-launchpad.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 2 +--- + +# 使用应用管理 + +Sealos 的桌面环境类似于单机操作系统 macOS 或 Windows 的桌面环境,桌面上的每个图标都是一个 App,类似于 Windows 的快捷方式。与单机操作系统一样,Sealos 也有系统自带的 App,要想在这个云操作系统中快速安装应用,就需要用到系统自带的 App:**应用管理**。 + +> 关于应用管理的详细信息请参阅 [应用管理介绍](/guides/applaunchpad/applaunchpad.md) + +下面将演示如何在 Sealos 云操作系统中快速安装一些比较常见的应用。 + +## 快速安装 Nginx + +首先在 [Sealos](https://cloud.sealos.run) 桌面环境中打开**应用管理**。 + +![](./images/app-launchpad-zh.webp) + +点击「新建应用」。 + +![](./images/app-list.png) + +依次输入应用名和镜像名,并开启外网访问,最后点击「部署应用」即可。 + +![](./images/app-deployment.png) + +部署完成后点击「详情」进入应用详情界面。 + +![](./images/app-list-2.png) + +可以看到 Nginx 应用的运行情况,包括 CPU 占用、内存占用等。点击外网地址即可通过外网域名直接打开 Nginx 界面。 + +![](./images/app-nginx.png) + +![](./images/SCR-20230529-sokx.png) + +## 快速安装 Nextcloud + +再来看一个例子,在**应用管理**中快速安装开源的私人网盘应用 Nextcloud。 + +与之前的例子一样,在**应用管理**中点击「新建应用」。依次输入应用名和镜像名,选择合适的 CPU 和内存,并开启外网访问。 + +![](./images/app-deployment-2.png) + +Nextcloud 容器的所有数据都存储在 `/var/www/html` 这个目录下,因此我们需要将该目录下的数据通过外挂存储进行持久化。 + +继续往下,展开「高级配置」,在「本地存储」中点击「新增存储卷」。 + +![](./images/app-deployment-3.png) + +选择外挂存储的容量,并填入存储挂载到应用中的路径,然后点击「确认」。 + +![](./images/SCR-20230529-tlpi.png) + +最后再点击右上角的「部署应用」,应用就安装完成了。 + +点击「应用详情」进入 Nextcloud 应用的详情页面,确认应用处于 Running 状态,然后点击外网地址直接打开 Nextcloud 的 Web 界面。 + +![](./images/SCR-20230529-tpsc.jpg) + +设置用户名和密码之后点击「Install」开始安装。稍等片刻安装完成后直接刷新页面。 + +![](./images/SCR-20230529-ualx.jpg) + +输入用户名密码即可登录成功。 + +![](./images/SCR-20230529-ubqj.jpg) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/use-database.md b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-database.md new file mode 100644 index 000000000000..133a02e1be35 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-database.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 4 +--- + +# 使用数据库 + +数据库是数据管理的重要手段 , 可用于高效地存储和访问数据。Sealos 提供了一个简单易用的数据库前端应用,屏蔽了繁琐的命令行操作,帮助你管理关系数据库、NoSQL、向量数据库和流数据库等各种类型的数据库。你只需通过「[数据库](/guides/dbprovider/dbprovider.md)」应用就能轻松创建各种数据库,包括 MySQL、PostgreSQL、MongoDB、Redis 等多种类型的数据库。 + +下面将通过示例来演示 Sealos 如何快速安装 MySQL。 + +## 快速安装 MySQL + +1. 在 [Sealos](https://cloud.sealos.run) 桌面环境中打开数据库。 + +![](./images/database-launch.jpg) + +2. 点击新建数据库,完成基础配置设置。 + +![](./images/database-configure.png) + +3. 点击「部署」,然后点击「确认」等待数据库创建成功。 + +![](./images/database-more.png) + +4. 点击一键连接,就可以直接打开「[终端](../../guides/terminal/terminal.md)」应用进入数据库的命令行终端。 + +![](./images/database-connect.png) + +## 快速安装 PostgreSQL 和 MongoDB + +PostgreSQL 和 MongoDB 数据库的安装和 MySQL 类似,需要在基础配置界面选择相应的数据库类型。 + +![](./images/database-select.png) diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/use-template.md b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-template.md new file mode 100644 index 000000000000..aa8ce69c84bb --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-template.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 1 +--- + +# 使用模板 + +这篇文档将会向您展示如何使用应用模板来快速安装各种分布式应用,并最大限度地发挥 Sealos 的功能。 + +## 1. 选择模板 + +首先在 [Sealos](https://cloud.sealos.run) 桌面环境中打开「模板市场」。 + +![](./images/templates.jpg) + +选择自己想要部署的应用模板。 + +![查看模板市场](./images/templates-2.png) + +不知道部署哪个应用?可以试试 [FastGPT AI 知识库](/examples/ai-applications/install-fastgpt-on-desktop.md) + +## 2. 将模板应用部署到 Sealos + +选择模板之后,设置好相应的参数值,点击「部署应用」按钮开始部署。 + +![将模板部署到 Sealos](./images/deploy-template.png) + +部署完成后,点击「确认」跳转到应用详情。 + +![](./images/template-detail.png) + +等待应用的状态变成 `running` 之后,点击外网地址即可通过外网域名直接打开应用的 Web 界面。 + +![](./images/app-url.png) + diff --git a/docs/archived/4.0/i18n/zh-Hans/quick-start/use-terminal.md b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-terminal.md new file mode 100644 index 000000000000..8600624654be --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/quick-start/use-terminal.md @@ -0,0 +1,161 @@ +--- +sidebar_position: 3 +--- + +# 使用终端 + +熟悉 Kubernetes 和容器基础知识的用户可以选择使用「[终端](/guides/terminal/terminal.md)」命令行来部署应用。本文以 Nginx 为例,介绍如何使用终端来部署应用。 + +首先在 [Sealos](https://cloud.sealos.run) 桌面环境中打开终端 App。 + +![](./images/app-launchpad-1.jpg) + +打开终端 App 之后,会默认给您分配一个终端窗口,您可以在终端窗口中进行各种命令行操作。 + +![](./images/terminal.png) + +① 先创建一个 Nginx Deployment 的部署清单: + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + resources: + requests: + cpu: 0.2 + memory: 32Mi + limits: + cpu: 0.2 + memory: 32Mi +``` + +② 再创建一个 Nginx Service 的部署清单: + +```yaml +# service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + app: nginx +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + + selector: + app: nginx +``` + +③ 执行以下命令通过部署清单创建 Deployment 和 Service: + +```bash +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml +``` + +测试是否能正常访问 Nginx: + +```bash +$ kubectl get pod -l app=nginx +NAME READY STATUS RESTARTS AGE +nginx-7546c75fb4-572x9 1/1 Running 0 5m37s + +$ kubectl get svc -l app=nginx +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +nginx ClusterIP 10.64.204.173 80/TCP 5m36s + +$ curl 10.64.204.173 + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +④ 如果你想把 Nginx 暴露到外网,并通过外网域名进行访问,还需要创建一个 Ingress 资源。先创建一个部署清单: + +```yaml +# ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/backend-protocol: HTTP + nginx.ingress.kubernetes.io/client-body-buffer-size: 64k + nginx.ingress.kubernetes.io/proxy-body-size: 32m + nginx.ingress.kubernetes.io/proxy-buffer-size: 64k + nginx.ingress.kubernetes.io/server-snippet: | + client_header_buffer_size 64k; + large_client_header_buffers 4 128k; + nginx.ingress.kubernetes.io/ssl-redirect: "false" + name: nginx +spec: + rules: + - host: nginx.cloud.sealos.run + http: + paths: + - backend: + service: + name: nginx + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - nginx.cloud.sealos.run + secretName: wildcard-cloud-sealos-io-cert +``` + +⑤ 再执行以下命令通过部署清单创建 Ingress 资源: + +```bash +$ kubectl apply -f ingress.yaml +``` + +⑥ 测试是否能通过外网域名正常访问 Nginx: + +![](./images/nginx.png) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/QA.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/QA.md new file mode 100644 index 000000000000..97aab794c1b2 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/QA.md @@ -0,0 +1,224 @@ +--- +sidebar_position: 1 +--- + +# 常见问题 + +使用Sealos时,您可能会遇到一些问题。以下是一些常见问题的答案和解决方法。 + +## 镜像构建问题 + +### Q1: 在构建阶段如何设置代理服务? + +在执行构建命令时,可以通过设置HTTP_PROXY环境变量来配置代理服务。 + +```shell +HTTP_PROXY=socket5://127.0.0.1:7890 sealos build xxxxx +``` + +### Q2:如何启用buildah的调试日志? + +若需要查看buildah的调试日志,可以通过设定`BUILDAH_LOG_LEVEL`环境变量实现。 + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +### Q3:如何在Pod中执行Sealos构建? + +若在Pod中执行Sealos构建,请按以下步骤操作: + +1. 在Pod中构建镜像,可用以下YAML配置创建Deployment。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sealoscli + name: sealoscli +spec: + replicas: 1 + selector: + matchLabels: + app: sealoscli + strategy: {} + template: + metadata: + labels: + app: sealoscli + spec: + containers: + - image: #用你的sealos镜像替换 + name: sealoscli + stdin: true + stdinOnce: true + securityContext: + privileged: true +``` + +2. 创建Dockerfile。以下是一个例子,根据需要进行修改。 + +```dockerfile +FROM bitnami/minideb:buster + +ARG TARGETOS +ARG TARGETARCH + +LABEL from=bitnami/minideb:buster platform=rootcloud team=oam tag=buster name=base + +RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && sed -i "s@http://security.debian.org@http://mirrors.aliyun.com/debian-security@g" /etc/apt/sources.list +RUN install_packages curl iputils-ping net-tools telnet procps vim wget jq + +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV TZ=Asia/Shanghai +``` + +3. 在Pod中执行构建命令。 + +```shell +sealos build --arch arm64 --build-arg TARGETOS=linux --build-arg TARGETARCH=arm64 -t test -f Dockerfile . +``` + +### Q4:如何使用其他构建工具构建集群镜像? + +如果您想使用 Docker 或 Podman 等其他容器工具来构建集群镜像,您可以借助 [sreg](https://github.com/labring/sreg) 来缓存镜像。 + +以下是操作步骤: + +1. 安装 sreg: + ```shell + wget https://github.com/labring/sreg/releases/download/v0.1.1/sreg_0.1.1_linux_amd64.tar.gz + tar -xzf sreg_0.1.1_linux_amd64.tar.gz sreg + mv sreg /usr/bin/ + ``` +2. 缓存镜像: + ```shell + sreg save --registry-dir=registry . + ``` +3. 构建集群镜像: + ```shell + docker build -t xxxx -f Sealfile . + ``` + +### Q5:执行Sealos构建时遇到“lgetxattr /var/lib/containers/storage/overlay/0c2afe770ec7870ad4639f18a1b50b3a84718f95c8907f3d54e14dbf0a01d50d/merged/dev/ptmx: no such device”错误? + +这个问题可能与`fuse-overlayfs`的版本有关。建议您从[这里](https://github.com/containers/fuse-overlayfs/releases)下载最新版本下载并替换`/bin/fuse-overlayfs`。 + +## 运行时选择问题 + +### Q1:如何选择Kubernetes运行时? + +Sealos会根据您选择的镜像决定使用哪种运行时。如果选择了kubernetes-docker镜像,Sealos将使用Docker作为运行时;如果选择了kubernetes-crio镜像,Sealos将使用CRI-O作为运行时。 + +## 版本兼容性问题 + +### Q1:报错:"Applied to cluster error: failed to init exec auth.sh failed exit status 127"? + +此问题常因您使用的sealos版本和镜像版本不匹配造成。请确认您的镜像版本和sealos的版本是匹配的。 +例如,若您正使用形如kubernetes:v1.xx.x的版本,可能需要升级sealos,特别是在使用较老版本的sealos,而sealos集群镜像则使用了最新版时。 +另一种解决方法是选择对应版本的sealos镜像。比如,如果您的sealos版本是4.1.3,那么集群镜像应选择形如kuberntes:v1.24.0-4.1.3的版本。 +确保镜像版本和sealos版本的匹配,可以帮助避免此类问题。 + +### Q2: 如果您在集群中新增了其他域名,或者修改了 service 的 CIDR,并且在添加 master 时出现了错误 + +为了解决这个问题,Sealos 团队在 4.2.0 版本进行了相应的修复。具体的修复内容和讨论可以在这个 pull request 中查看:https://github.com/labring/sealos/pull/2943 。 + +所以,如果您遇到了这个问题,我们建议您升级到 Sealos 4.2.0 版本。更新后的版本应该能够正确处理这些变更,并且在添加 master 时不会出现错误。 + +## 文件和目录位置问题 + +### Q1:如何修改`/root/.sealos`默认目录的存储位置? + +若需修改默认的存储位置,可以设置SEALOS_RUNTIME_ROOT环境变量,然后运行sealos命令。建议您将这个环境变量设置为全局的,这样在其他命令或场景中也可以方便使用。 + +```shell +export SEALOS_RUNTIME_ROOT=/data/.sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q2:如何修改`/var/lib/sealos`默认目录的存储位置? + +若需修改默认的存储位置,可以设置SEALOS_DATA_ROOT环境变量,然后运行sealos命令。同样,建议您将这个环境变量设置为全局的。 + +```shell +export SEALOS_DATA_ROOT=/data/sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q3: 如何修改 Sealos 镜像数据和状态的存储路径? + +> 在使用 Sealos 集群时,可能需要改变默认的镜像数据存储路径和状态数据的存储路径。默认情况下,这些数据被存储在 `/etc/containers/storage.conf` 文件定义的位置。 + +1. **查看当前存储配置** + 首先,我们可以使用下面的命令来查看当前的镜像存储配置: + ``` + sealos images --debug + ``` + 这个命令会打印出包含当前存储配置的文件,例如: + ``` + 2023-06-07T16:27:02 debug using file /etc/containers/storage.conf as container storage config + REPOSITORY TAG IMAGE ID CREATED SIZE + ``` +2. **修改镜像数据存储路径** + 如果你希望更改镜像数据的存储路径,你可以编辑 `/etc/containers/storage.conf` 文件。在这个文件中,找到并修改 `graphroot` 字段设置为新的路径。例如: + ``` + vim /etc/containers/storage.conf + ``` + 在编辑器中,将 `graphroot` 字段的值修改为你希望的新路径。 +3. **修改状态数据存储路径** + 参考 Buildah 的设计,Sealos 同样提供了状态数据存储路径的设置。在同样的配置文件 `/etc/containers/storage.conf` 中,找到并修改 `runroot` 字段为新的路径。 + +通过以上步骤,你可以将 Sealos 集群的镜像数据和状态数据保存到新的地址。每次运行 Sealos 命令时,它都将使用你在 `graphroot` 和 `runroot` 中设置的新路径来分别存储镜像数据和状态数据。 + +### Q4:ssh传输文件时,如何禁止检查文件的md5? + +在网络环境良好时,禁用md5检查可以极大提升传输速度。若不想在ssh传输文件时检查文件的md5,可将SEALOS_SCP_CHECKSUM环境变量设置为false以禁用此功能。建议将此环境变量设为全局,以便在多场景下使用。 + +```shell +export SEALOS_SCP_CHECKSUM=false +sealos run labring/kubernetes:v1.24.0 +``` + + +## 其他问题 + +### Q1:image-cri-shim导致端口大量占用,耗尽服务器socket资源? + +出现此问题时,可通过以下命令解决: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0/sealos_4.2.0_linux_amd64.tar.gz && tar xvf sealos_4.2.0_linux_amd64.tar.gz image-cri-shim +sealos exec -r master,node "systemctl stop image-cri-shim" +sealos scp "./image-cri-shim" "/usr/bin/image-cri-shim" +sealos exec -r master,node "systemctl start image-cri-shim" +sealos exec -r master,node "image-cri-shim -v" +``` + +### Q2:报错"[ERROR FileAvailable--etc-kubernetes-kubelet.conf]: /etc/kubernetes/kubelet.conf already exists" + +此问题可通过升级至Sealos 4.1.7+来解决。 + +### Q3:报错:"function "semverCompare" not defined" + +此问题可通过升级至Sealos 4.1.4+来解决。 + +我们希望这些解答能帮助您解决在使用Sealos过程中遇到的问题。如果还有其他问题,欢迎随时提问。 + +## 使用技巧 + +### Q1: 如何清理 Sealos 集群的缓存文件 + +> 在使用 Sealos 集群时,安装过程中可能会在本地存储一些缓存文件,这些文件会重复占用磁盘空间。那么,如何清理这些缓存文件以释放磁盘空间呢? + +我们提供了一个非常简单的解决方案,只需要执行以下命令: + +```shell +sealos unmount --all && sealos rm --all +``` +这个命令的作用是移除所有缓存的 Sealos 集群镜像文件,以及所有的相关挂载点。--all 选项表示处理所有相关文件和挂载点。 +执行这个命令后,所有 Sealos 集群的缓存文件就会被清理掉,从而释放出被它们占用的磁盘空间。 +这是一个非常有用的技巧,特别是对于在磁盘空间有限的环境中运行 Sealos 集群的用户来说。在你感觉磁盘空间被占用过多时,不妨尝试执行这个命令来释放一些空间。 +请注意,这个命令只会删除缓存文件,不会影响已经运行的集群。也就是说,执行这个命令后,你的集群仍然可以正常运行。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md new file mode 100644 index 000000000000..12d630e18348 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/build-image-using-registry-sync.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 2 +--- + +# 镜像构建改进指南 + +## 深入理解Sealos镜像构建 + +为了了解Sealos镜像构建的背后所做的工作,我们将首先揭示它在底层究竟进行了哪些操作。以下是一个直观的架构图: + +![](images/build.png) + +Sealos在构建镜像过程中涵盖了以下几个核心步骤: + +- **缓存镜像**:解析构建执行时的工作目录(在这里我们称之为"context"目录),将缓存镜像保存到registry结构并存储在./registry目录下。 +- **构建镜像**:在context目录中进行镜像构建,生成新的镜像。(请注意,构建镜像时需要将./registry目录进行复制。) + +## 提升镜像构建效率 + +当前项目中,我们借用了`github.com/distribution/distribution`的源代码,在执行缓存镜像的过程中直接调用了registry的sdk并启动了registry-proxy功能。借助于镜像仓库的缓存能力,我们将镜像缓存并存入context/registry目录。 + +这个过程的关键就在于调用了distribution仓库的方法进行保存镜像: + +- 启动 registry-proxy 功能。 +- 保存镜像摘要及索引相关数据(通过调用saveManifestAndGetDigest方法)。 +- 保存镜像文件数据(通过调用saveBlobs方法)。 + +这种方法确实具有一些显著的优点: + +- 轻量化:无需依赖其他组件即可保存镜像。 +- 自由控制:可以自由控制保存逻辑,无需依赖第三方组件。 + +然而,我们也注意到了一些潜在的问题: + +- 对新手来说,代码理解难度较高,不易清晰了解这里的逻辑。 +- 无法缓存使用token认证的方式。 +- 需要依赖一些临时存储空间,对空间有要求。 + +考虑到这些问题,我们决定尝试一种新的模式:在本地启动一个轻量的registry,使用`skopeo copy`的sdk进行代码复用。这一改变直接解决了之前所有的问题。 + +![](images/registry-build.png) + +**所以,新的构建方式 ✨镜像仓库同步✨ 优雅登场 🎉🎉** + +官方仓库中的[#3154](https://github.com/labring/sealos/pull/3154)这个PR已经完成了这个功能的实现。目前,Sealos支持这两种方式进行镜像构建。接下来,我会介绍如何启动新功能(如果新功能表现稳定,我们可能会废弃旧的构建方式)。 + +## 如何启动新功能 + +> Sealos v4.3.0 以上版本默认支持此功能。 + +启动新功能非常简单,只需在你构建镜像之前添加一个环境变量即可。这个功能同时支持build和merge两个命令。 + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +``` + +以下是执行上述命令后的预期输出: + +```tex +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +Getting image source signatures +Copying blob fee740108510 done +Copying config f92f3ea6e4 done +Writing manifest to image destination +Storing signatures +Getting image source signatures +Copying blob 08409d417260 done +Copying config 44dd6f2230 done +Writing manifest to image destination +Storing signatures +2023-06-01T13:16:07 info saving images busybox, alpine +STEP 1/2: FROM scratch +STEP 2/2: COPY registry ./registry +COMMIT test +Getting image source signatures +Copying blob 13ab73c881c8 done +Copying config 4e22d16b36 done +Writing manifest to image destination +Storing signatures +--> 4e22d16b366 +Successfully tagged localhost/test:latest +4e22d16b366e9fec25641522a74cbd73a7db67dc0516b8f8e00200c4d0551592 +``` + +希望以上内容可以帮助您更好地理解并使用Sealos的新镜像构建方式。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md new file mode 100644 index 000000000000..1b4f1b3d016d --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/dual-stack-cluster.md @@ -0,0 +1,792 @@ +--- +sidebar_position: 5 + +--- + +# 使用calico安装双栈集群 + +1. 前置条件 + - sealos 版本 >=4.3.0 + - 每个主机都有一个IPv4主机和IPv6地址,并且可以互通通过IPv4和IPv6地址 + - calico采用vxlan模式, 内核版本必须大于 3.12。 可以参考[官方文档](https://github.com/cyclinder/kubespray/blob/042c960c6617f8a360a8281464ff63f99ee2471c/docs/calico.md) +2. 运行 `sealos gen` 生成一个 Clusterfile,例如: + +```shell +$ sealos gen labring/kubernetes:v1.26.1 labring/helm:v3.10.3 labring/calico:v3.25.0 --masters 192.168.0.10 --nodes 192.168.0.11 --passwd "xxx" >Clusterfile +``` + +注意:labring/helm 应当在 labring/calico 之前。 + +生成的 Clusterfile 如下: + +
+Clusterfile + + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + ExtraArgs: + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10 + ServiceSubnet: 10.96.0.0/22 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +``` + +
+ +3. 生成 Clusterfile 后,编辑Clusterfile,然后添加IPv6 的 pod 和svc 的 CIDR 范围。这里使用fd85:ee78:d8a6:8607::1:0000/112、fd85:ee78:d8a6:8607::1000/116作为参考示例。主要修改以下信息。 + +
+Clusterfile + + +```yaml +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 #增加pod IPv6地址段 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #增加svc IPv6地址段 +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a #增加控制节点的ipv6地址,如果你需要使用此IP访问apiserver + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #增加svc IPv6地址段 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 #默认为64 + node-cidr-mask-size-ipv4: 24 #默认为24 +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" #增加pod IPv6地址段 +--- +# 添加Calico双栈配置 +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 #增加pod IPv6地址段 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +最终的Clusterfile会是这样。 + +
+Clusterfile + + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 + node-cidr-mask-size-ipv4: 24 + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +4. 运行 `sealos apply -f Clusterfile` 部署集群。 + +5. 更多参考[Calico官网](https://docs.tigera.io/calico/latest/networking/ipam/ipv6) 和 [k8s官方文档](https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/dual-stack-support/) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md new file mode 100644 index 000000000000..3767c5df2317 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/image-build-standardized.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 1 +--- + +# 镜像构建与标准化目录配置 + +在开展 Sealos 镜像构建任务前,我们建议先构建一个符合规范的目录结构。这样能够使构建过程更加规范,易于管理,同时也能降低出错率。这篇文章将详细指导你如何创建这样一个目录结构,并解释每个目录的用途。 + +## 目录结构示例 + +一个完整的、符合规范的目录结构示例如下: + +```shell +. +├── charts +│   └── nginx +│   ├── Chart.lock +│   ├── charts +│   ├── Chart.yaml +│   ├── README.md +│   ├── templates +│   ├── values.schema.json +│   └── values.yaml +├── images +│   └── shim +│   └── nginxImages +├── init.sh +├── Kubefile +├── manifests +│   └── nginx +│   ├── deployment.yaml +│   ├── ingress.yaml +│   └── service.yaml +├── opt +│   └── helm +└── registry +``` + +## 目录描述 + +每个目录在构建过程中都扮演着特定的角色,以下是他们的详细描述: + +- `Kubefile` (必需):这个文件类似于 Dockerfile,是构建镜像的核心文件。它定义了构建过程中的各个步骤,如基础镜像选择、环境变量设置、文件复制等。 +- `manifests`:这个目录用于存放 Kubernetes 的 yaml 文件,这些文件描述了你的应用的配置信息,如 Pod、Service、Deployment 的配置。 +- `charts`:这个目录用于存放 Helm chart 文件,Helm chart 是 Kubernetes 的一个包管理工具,可以简化 Kubernetes 应用的部署和管理。 +- `images/shim`:这个目录用于存放无法从 yaml 文件或 Helm chart 中自动提取的镜像。在构建过程中,sealos 将自动拉取这些镜像。 +- `opt`:二进制文件存储在这里。 +- `registry`:这个目录用于存放构建过程中拉取到本地的镜像。在构建过程中,该目录将自动生成,无需手动创建。 +- `init.sh`:这个脚本在构建过程中由 GitHub Action 自动运行,你可以在这个脚本中编写一些自动化的工作,如初始化环境、预处理数据等。([cluster-image](https://github.com/labring-actions/cluster-image)的规则) + +## Kubefile 参数 + +`Kubefile` 文件是镜像构建的核心,它支持多种参数,以下是这些参数的详细解析: + +```shell +FROM labring/kubernetes:v1.24.0 +ENV version v1.1.0 +COPY manifests ./manifests +COPY registry ./registry +ENTRYPOINT ["kubectl apply -f manifests/tigera-operator.yaml"] +CMD ["kubectl apply -f manifests/custom-resources.yaml"] +``` + +各个参数的描述: + +- `FROM`:这个指令用于设置构建的基础镜像,所有的构建步骤都基于这个镜像进行。 +- `LABEL`: `LABEL`定义一些sealos集群镜像的内部配置。 + + - `check` 集群镜像运行前的一些检查脚本操作 + - `clean` 集群reset或者节点删除的清理脚本 + - `clean-registry` 集群reset时候的清理镜像仓库的脚本 + - `image` 集群的lvscare镜像地址(sealos的IPVS镜像) + - `init` 集群初始化的脚本 + - `init-registry` 集群初始化时启动容器镜像仓库的脚本 + - `sealos.io.type` 集群镜像类型,目前主要是rootfs、application和patch。 + - rootfs 是运行集群的基础镜像,比如kubernetes、kubernetes-docker这种包含镜像、二进制等集群所需的。(**每个节点都需要存在**) + - application 是应用镜像,比如calico、helm、istio等应用服务的镜像。(**只存储到master0节点**) + - patch是在rootfs镜像后需要调整的,是另一种修改rootfs镜像的方式(**还有一种方式是Config方式**),它会覆盖默认的集群运行的第一个镜像。 + + - `sealos.io.version` 镜像的版本号,目前开启的是v1beta1 + - `version` 集群的版本号,当前是kubernetes的版本号 + - `vip` 是VIP的地址,为修改IPVS的虚IP使用 + +- `ENV`:`ENV`指令将环境变量``设置为值``。(rootfs中默认会有一些默认的环境变量,可以修改rootfs中一些默认参数,比如镜像仓库的账号密码、docker、containerd的存储目录等等) + + 具体的集群镜像需要具体查看,`sealos inspect`镜像看一下对应的环境变量,不同版本的镜像略有不同。 + + - SEALOS_SYS_CRI_ENDPOINT: 当前集群镜像的criSocket (不同类型集群镜像可能不同) + - criData: cri的数据目录 + - defaultVIP: 默认的VIP地址 + - disableApparmor: 是否禁用apparmor (containerd有这个问题) + - registryConfig: 容器镜像仓库的配置目录 + - registryData: 容器镜像仓库的数据目录(因为是目录进行了挂载,其实这个配置没有实际意义,它实际还是存储在/var/lib/sealos下面) + - registryDomain: 默认镜像仓库的域名 + - registryPassword: 默认镜像仓库的密码 + - registryPort: 默认镜像仓库的端口 + - registryUsername: 默认镜像仓库的账户 + - sandboxImage: 默认cri启动的sandbox_image。(无需写repo只需要写镜像名称,eg: pasue:3.7) +- `COPY`:`COPY`指令从``复制新的文件或目录,并将它们添加到容器的文件系统路径``上。(**注意,需要把registry目录进行拷贝,否则集群没有容器镜像**) +- `ENTRYPOINT`:这个指令用于设置镜像的启动命令,当镜像启动时,这条命令会被执行。 +- `CMD`:这个指令也用于设置镜像的启动命令,但它与 ENTRYPOINT 指令的区别在于,如果用户在运行镜像时(`sealos run --cmd`)提供了启动命令,CMD 指令中的命令将会被覆盖。 + +在构建过程中,Sealos 还会自动设置一些内置的环境变量,包括(前缀为'SEALOS_SYS'的环境变量无法被修改): + +- SEALOS_SYS_KUBE_VERSION:Kubernetes的版本号,例如 v1.26.0 +- SEALOS_SYS_SEALOS_VERSION:Sealos的版本号,例如 4.1.3 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/build.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/build.png new file mode 100644 index 000000000000..d1ae7d6aeadd Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/build.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png new file mode 100644 index 000000000000..485b531d28f8 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/registry-build.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png new file mode 100644 index 000000000000..70ef65ffa13f Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/images/sealos-run.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/sealos-run.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/sealos-run.md new file mode 100644 index 000000000000..6946a3bbc93e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/sealos-run.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 2 +--- + +# Sealos Run 的工作原理 + +以下是 `sealos run` 命令的详细工作原理。为了帮助您更好地理解,我们将其分解为九个主要步骤。 + +## 1. 执行命令 + +首先,用户通过执行以下命令启动 Sealos: + +```bash +sealos run kubernetes:v1.23.8 +``` + +## 2. 执行 Pipeline + +命令执行后,Sealos 开始执行一系列相关的操作步骤,我们称之为 "Pipeline"。这个过程包括以下几个子步骤: + +### 2.1 节点检查 + +Sealos 会对所有节点进行检查,以确保它们都是可用的。例如,检查是否存在重复的主机名,以及所有节点的时间是否已经同步。 + +### 2.2 拉取镜像 + +接着,Sealos 从远程仓库拉取 Kubernetes 的镜像,并将其加载到本地存储。 + +### 2.3 渲染配置 + +然后,Sealos 会对拉取到的镜像的 rootfs(文件系统)进行配置渲染,这是为了准备后续的节点分发。 + +### 2.4 分发镜像文件 + +配置渲染完成后,Sealos 会将渲染后的 rootfs 文件分发到各个节点。 + +### 2.5 分发 Registry 目录 + +此外,Sealos 也会将镜像中的 registry 目录分发到对应的 registry 角色节点。Sealos 支持两种模式: + +- **scp模式**:直接通过 scp 将目录复制到每个节点,这种方式会占用大量的网络带宽。 +- **镜像同步模式**:使用 skopeo sdk 的镜像同步机制,能够实现增量镜像同步,从而节省网络带宽。该功能可通过环境变量 `SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true` 来开启。 + +### 2.6 执行 Bootstrap + +Bootstrap 是关键步骤,包括以下操作: + +#### 2.6.1 添加 Host 解析 + +Sealos 会在每个节点上添加 registry 的 host 解析。 + +#### 2.6.2 执行 Registry-Init 脚本 + +在添加完 host 解析后,Sealos 会执行 registry-init 脚本。 + +#### 2.6.3 执行 Init 脚本 + +最后,Sealos 会执行 init 脚本,启动 Kubernetes 服务。 + +## 3. 执行 InitMaster0 + +Pipeline 执行完成后,Sealos 进入 InitMaster0 阶段。在此阶段,Sealos 会执行以下操作: + +### 3.1 生成 Kubeadm 配置 + +首先,Sealos 生成 Kubeadm 的初始化配置文件。 + +### 3.2 生成并分发证书文件 + +然后,Sealos 生成集群的所有 + +证书文件,并将它们分发到各个节点。 + +### 3.3 初始化 Master0 + +最后,Sealos 执行`kubeadm init`初始化 Master0 节点。 + +## 4. 执行 JoinMaster + +初始化完 Master0 节点后,Sealos 进入 JoinMaster 阶段。在此阶段,Sealos 会执行以下操作: + +### 4.1 同步 Kubeconfig + +首先,Sealos 将 Kubeconfig 文件同步到各个节点。 + +### 4.2 同步证书文件 + +接下来,Sealos 同步证书文件到各个节点。 + +### 4.3 生成 Join 配置 + +然后,Sealos 生成 JoinMaster 配置文件。 + +### 4.4 生成 Join 令牌 + +接着,Sealos 生成 Join 令牌。 + +### 4.5 执行 Join Master 流程 + +最后,Sealos 执行`kubeadm join`流程,让其他 Master 节点加入集群。 + +## 5. 执行 JoinNode + +所有 Master 节点加入集群后,Sealos 进入 JoinNode 阶段,执行以下操作: + +### 5.1 生成 Join 令牌 + +首先,Sealos 再次生成 Join 令牌。 + +### 5.2 生成 IPVS 规则 + +然后,Sealos 生成 IPVS 规则。 + +### 5.3 生成 Join 配置 + +接下来,Sealos 再次生成 JoinNode 配置。 + +### 5.4 执行 Join Node 流程 + +最后,Sealos 执行 Join Node 流程,让 Worker 节点加入集群。 + +## 6. 同步 IPVS + +所有节点加入集群后,Sealos 将 Master 的 IPVS 规则同步到所有节点。这里主要是通过 lvscare 的静态 pod,根据 Master 节点的数量同步静态 pod 的配置。 + +## 7. 执行 Guest 阶段 + +IPVS 规则同步完成后,Sealos 执行 Guest 阶段。这是执行集群镜像命令的阶段。 + +## 8. 写入 Clusterfile + +最后,Sealos 将最终的执行结果写入 Clusterfile。 + +这就是 `sealos run` 命令的工作原理。希望通过这篇文章,您能对 `sealos run` 命令有更深入的理解。以下是我们提到的全景图,以帮助您更好地理解整个流程。 + +![](images/sealos-run.png) diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/template-function.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/template-function.md new file mode 100644 index 000000000000..93676da3b7e4 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/advanced-guide/template-function.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 9 +--- + +# 模板引入与函数增强 + +在 Sealos 的镜像构建过程中,"tmpl" 后缀的文件在 "etc"、"scripts" 和 "manifests" 这三个目录下的文件会被特殊处理。 + +1. "etc" 目录:这个目录通常用于存放配置文件。在构建过程中,Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,渲染完成后的文件将被拷贝到集群镜像的相应目录下。 + +2. "scripts" 目录:这个目录通常用于存放执行脚本。Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,生成的脚本将在集群镜像构建过程中被执行。 + +3. "manifests" 目录:这个目录通常用于存放 Kubernetes 资源清单文件。Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,生成的清单文件将在集群镜像构建过程中被应用到 Kubernetes 集群中。 + +总的来说,这三个目录下的 "tmpl" 后缀文件在 Sealos 的镜像构建过程中都会被视为模板文件进行处理。这种处理方式提供了更多的灵活性,让我们可以在构建过程中动态生成配置、脚本或者 Kubernetes 资源清单。 + +在构建镜像时,我们支持 `template` 来让维护者完全控制生成的配置文件(模块渲染)。例如: + +```yaml +service: + name: {{ .serviceName }} + port: + {{- if typeIs "string" .servicePort }} + name: {{ .servicePort }} + {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} + number: {{ .servicePort | int }} + {{- end }} +``` + +请参考 Golang 的 [text/template](https://pkg.go.dev/text/template) 了解基础介绍和更多细节。 + +## 模板函数增强 + +此外,我们支持 `templateFunc` 来增强模板函数。例如: + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://{{ .registryDomain }}:{{ .registryPort }} +force: true +debug: false +image: /var/lib/image-cri-shim +{{ if and (ne .SEALOS_SYS_KUBE_VERSION "") (semverCompare "^1.26.0" .SEALOS_SYS_KUBE_VERSION) }}version: v1{{ else }}version: v1alpha2{{ end }} +timeout: 15m +auth: {{ .registryUsername }}:{{ .registryPassword }} +``` + +在这里我们使用 `semverCompare` 来检查用户是否运行在 k8s 版本 v1.26.0 或以上,如果是,生成 `version: v1`,否则生成 `version: v1alpha2`。 +有了这个支持,我们可以很轻松地用一个集群镜像文件管理多版本的 Kubernetes 支持。 + +### 一些最常用的模板函数 + +* [semverCompare](http://masterminds.github.io/sprig/semver.html) 比较语义版本,而不是字符串比较。 +* [default](http://masterminds.github.io/sprig/defaults.html) default 可以在值为空或未定义时提供一个默认值。 +* [toYaml](https://github.com/labring/sealos/blob/main/lifecycle/pkg/template/funcmap.go#L66) 将当前值(对象,映射,数组)显示为 yaml 格式的字符串。 + +完整的支持函数列表,请[点击这里](http://masterminds.github.io/sprig/)。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/images/sealos.webp b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/images/sealos.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/images/sealos.webp differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/lifecycle-management.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/lifecycle-management.md new file mode 100644 index 000000000000..24b9a6001815 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/lifecycle-management.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 0 +--- + +# K8s 集群生命周期管理 + +Sealos 提供一套强大的工具,使得用户可以便利地管理整个集群的生命周期。 + +## 功能介绍 + +使用 Sealos,您可以安装一个不包含任何组件的裸 Kubernetes 集群。此外,Sealos 还可以在 Kubernetes 之上,通过集群镜像能力组装各种上层分布式应用,如数据库、消息队列等。 + +Sealos 不仅可以安装一个单节点的 Kubernetes 开发环境,还能构建数千节点的生产高可用集群。 + +Sealos 具有自由伸缩集群、备份恢复、释放集群等功能,即使在离线环境中,Sealos 也能提供出色的 Kubernetes 运行体验。 + +## 主要特性 + +- 支持 ARM,v1.20 以上版本离线包支持 containerd 与 docker 集成 +- 提供 99 年证书,支持集群备份,升级 +- 不依赖 ansible、haproxy、keepalived,一个二进制工具,零依赖 +- 提供离线安装,不同 Kubernetes 版本只需使用不同的集群镜像 +- 高可用性由 ipvs 实现的 localLB 提供,占用资源少,稳定可靠,类似 kube-proxy 的实现 +- 使用 image-cri-shim 自动识别镜像名称,使离线交付更方便 +- 几乎兼容所有支持 systemd 的 x86_64 架构的环境 +- 轻松实现集群节点的增加/删除 +- 已有数万用户在线上环境使用 Sealos,稳定可靠 +- 支持集群镜像,自由组合定制你需要的集群,如 openebs 存储+数据库+minio 对象存储 +- 使用 buildah 的 sdk 实现对镜像标准统一,完全兼容 OCI 的标准 + +## 使用 Sealos 运行 Kubernetes 集群 + +使用 Sealos 运行一个 Kubernetes 集群非常简单,只需以下步骤: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.3.0/scripts/install.sh \ + | sh -s v4.3.0 labring/sealos +# 创建一个集群 +$ sealos run labring/kubernetes:v1.25.0-4.2.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +[![asciicast](https://asciinema.org/a/519263.svg)](https://asciinema.org/a/519263?speed=3) + +## 在集群上运行分布式应用 + +通过 `sealos run` 命令,您可以在集群上运行各种分布式应用,如数据库、消息队列、AI 能力,甚至企业级 SaaS 软件。例如: + +```shell +# MySQL 集群 +$ sealos run labring/mysql-operator:8.0.23-14.1 + +# Clickhouse 集群 +$ sealos run labring/clickhouse:0.18.4 + +# Redis 集群 +$ sealos run labring/redis-operator:3.1.4 +``` + +## 自定义集群 + +对于 Sealos 生态没有的集群镜像,用户可以方便地自己构建和定制属于自己的集群镜像。例如: + +[构建一个 ingress 集群镜像](/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md) + +您还可以定制一个完全属于自己的 Kubernetes: + +Sealfile: + +```shell +FROM kubernetes:v1.25.0 +COPY flannel-chart . +COPY mysql-chart . +CMD ["helm install flannel flannel-chart", "helm install mysql mysql-chart"] +``` + +```shell +sealos build -t my-kuberentes:v1.25.0 . +sealos run my-kuberentes:v1.25.0 ... +``` + +## 常见问题 + +**Sealos 是 Kubernetes 安装工具吗?** + +安装部署只是 Sealos 的一个基本功能,如同单机操作系统有 Boot 模块一样,Sealos 的 Boot 模块可以很好地管理 Kubernetes 在任何场景下的生命周期。 + +**Sealos 和 Rancher、KubeSphere 有什么区别?** + +Sealos 的设计理念是 "化整为零,自由组装,大道至简"。Sealos 利用 Kubernetes 的能力,以简单的方式提供给用户真正需要的东西。用户需要的不一定是 Kubernetes,用户需要的是具体的能力。 + +Sealos 是极其灵活的,不会给用户带来额外负担。它的形态取决于用户的需求和安装的应用。Sealos 的核心是分布式应用,所有应用都是一等公民。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md new file mode 100644 index 000000000000..6433a19d0589 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 4 +--- + +# 构建基于二进制文件的集群镜像 + +此文档主要介绍了如何使用 `sealos` 工具将单一二进制文件(例如 `helm` 或 `kustomize`)打包为集群镜像,并将它们通过在主节点上部署集群镜像进行安装。以 `helm` 为例,我们将详细介绍如何将二进制文件打包成集群镜像。 + +## 创建构建工作空间 + +首先,创建一个基础目录作为构建工作空间: + +```shell +$ mkdir ~/cluster-images +``` + +在工作空间中,创建一个 `opt` 目录用于存储二进制文件: + +```shell +$ cd cluster-images +$ mkdir opt/ +``` + +## 准备二进制文件 + +接下来,我们准备 `helm` 二进制文件。在此,我们从 [github release](https://github.com/helm/helm/releases) 中下载: + +```shell +wget https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz +tar -zxvf helm-v3.10.1-linux-amd64.tar.gz +chmod a+x linux-amd64/helm +mv linux-amd64/helm opt/ +``` + +## 创建构建镜像所需的 `Sealfile` 文件 + +创建一个名为 `Sealfile` 的文件,内容如下: + +```shell +FROM scratch +COPY opt ./opt +CMD ["cp opt/helm /usr/bin/"] +``` + +目前的目录结构如下: + +``` +. +├── Sealfile +└── opt + └── helm +``` + +## 构建集群镜像 + +现在,一切准备就绪,你可以开始构建集群镜像了: + +```shell +sealos build -t labring/helm:v3.10.1 . +``` + +**注意:** 首先你需要在本地主机上安装 `sealos` 命令。 + +你可以查看构建日志来了解构建过程。 + +```shell +root@ubuntu:~/cluster-images# sealos build -t labring/helm:v3.10.1 . +... +``` + +查看构建的镜像,现在所有依赖的二进制文件都已经构建进集群镜像中: + +```shell +root@ubuntu:~/cluster-images# sealos images +labring/helm v3.10.1 19ed4a24f0fe 3 minutes ago 45.1 MB +``` + +## 推送镜像 + +你可以将镜像推送至任何 Docker 镜像仓库,下面的命令将镜像推送到 dockerhub: + +```shell +sealos push labring/helm:v3.10.1 +``` + +**注意:** 请使用 `sealos` 命令来操作集群镜像,不支持 Docker 命令。 + +如果你使用的是私有镜像仓库,可以使用 `sealos login` 命令登录你的镜像仓库,然后再推送或者拉取镜像 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md new file mode 100644 index 000000000000..135698980bff --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 5 +--- + +# 构建基于 go-template 的集群镜像 + +在构建集群镜像的过程中,我们可以使用 `--env` 选项通过sealos命令行传递一些变量。这些环境变量可以被Kubefile的 `CMD` 命令或者yaml文件模板所使用。 + +## 在 Kubefile 中使用环境变量 + +这个示例定义了一个 `SERVICE_TYPE` 变量,它允许用户在安装应用程序时自定义服务暴露类型,并将参数传递给CMD中的helm命令。 + +Kubefile 示例: + +```shell +FROM scratch +ENV SERVICE_TYPE "NodePort" +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=$(SERVICE_TYPE)"] +``` + +运行集群应用并设置一个自定义的 `SERVICE_TYPE=LoadBalancer`,如果不设置,它将默认为 NodePort。 + +```shell +sealos run labring/nginx:v1.23.1 --env SERVICE_TYPE=LoadBalancer +``` + +## 在Yaml文件中使用环境变量 + +准备一个简单的nginx服务的yaml文件,这个文件必须是 `*.tmpl` 扩展名,以便在运行 `sealos run --env` 命令时渲染。 + +```shell +$ cat manifests/service.yaml.tmpl +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: {{ .serviceType }} + ports: + - port: 80 + nodePort: {{ .http_NodePort }} + name: http + - port: 443 + nodePort: {{ .https_NodePort }} + name: https + selector: + name: nginx +``` + +下面是一个Kubefile样例,你可以在这里设置默认的环境变量。 + +```shell +FROM scratch +ENV serviceType NodePort +ENV http_NodePort 30080 +ENV https_NodePort 30443 + +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/service.yaml"] +``` + +当你构建镜像时,什么都不会发生,只有在运行应用程序时,它才会渲染。如果没有设置 `--env`,它将使用 Kubefile 的默认 ENV。 + +```shell +sealos run labring/nginx:1.23.1 --env serviceType=LoadBalancer --env http_NodePort=30080 --env https_NodePort=30443 +``` + +你会发现 sealos 会在主节点的本地路径上基于 `service.yaml.tmpl` 渲染一个新的yaml文件 `service.yaml`。 + +**注意** 新版本的应用的rootfs放到了`/var/lib/sealos/data/default/applications`目录,每个应用都有独立的目录。 + +```shell +root@node1:~# ls /var/lib/sealos/data/default/rootfs/manifests |grep service +service.yaml +service.yaml.tmpl +``` + +检查 yaml 内容: + +```shell +root@node1:~# cat /var/lib/sealos/data/default/rootfs/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30080 + name: http + - port: 443 + nodePort: 30443 + name: https + selector: + name: nginx +``` + +**注意:**所有类型的文件都支持这个特性(文件名后缀是.tmpl且构建目录在etc、scripts和manifests),你可以自己尝试一下。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md new file mode 100644 index 000000000000..dfcfc668aa8e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 3 +--- + +# 构建基于 Helm Charts 的集群镜像 + +让我们以最简单的 nginx 应用程序为例,介绍如何基于 Helm Charts 构建一个基于 nginx 的集群镜像。 + +## 一、准备工作 + +创建一个用于构建工作的基础目录。 + +```shell +$ mkdir ~/cloud-images +``` + +创建一个 `charts` 目录来存储 Kubernetes nginx Helm Charts 文件。 + +```shell +$ cd cloud-images +$ mkdir charts +``` + +## 二、准备Helm Charts + +准备 nginx Helm Charts,这里我们使用 [bitnami 官方的 nginx Helm Charts](https://bitnami.com/stack/nginx),让我们将 Helm Chart 文件拉取到本地并解压到 `charts` 目录中。 + +```shell +helm repo add bitnami https://charts.bitnami.com/bitnami +helm search repo bitnami/nginx +helm pull bitnami/nginx --version=13.2.13 -d charts/ --untar +``` + +**注意:** 首先你应该安装 Helm 命令工具到本地主机。 + +现在,charts 目录的结构如下所示。 + +``` +charts/ +└── nginx + ├── Chart.lock + ├── charts + ├── Chart.yaml + ├── README.md + ├── templates + ├── values.schema.json + └── values.yaml +``` + +## 三、创建Kubefile + +创建一个名为 `Kubefile` 的文件用于镜像构建: + +```shell +$ cat Kubefile +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm install nginx charts/nginx --namespace=nginx --create-namespace"] +``` + +建议使用 `helm upgrade --install` 而不是 `helm install`,这样可以在以后更新应用程序时重复运行相同的命令。 + +你可以根据需要添加其他选项,例如通过 NodePort 暴露服务。 + +```shell +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +``` + +## 四、构建集群镜像 + +现在一切准备就绪,你可以开始构建集群镜像。 + +```shell +sealos build -t labring/nginx:v1.23.2 . +``` + +**注意:** 你应该首先将 sealos 命令安装到本地主机。 + +你可以查看构建日志。 + +```shell +root@ubuntu:~/cloud-images# sealos build -t labring/nginx:v1.23.2 . +2022-11-06T15:58:33 info lookup in path charts +2022-11-06T15:58:33 info sub chart is nginx +2022-11-06T15:58:33 warn if you access private registry,you must be 'sealos login' or 'buildah login' +2022-11-06T15:58:33 info pull images [docker.io/bitnami/nginx:1.23.2-debian-11-r29] for platform is linux/amd64 +Pulling image: docker.io/bitnami/nginx:1.23.2-debian-11-r29 +1d8866550bdd: Download complete +cbbfe6232a5b: Download complete +ed342369e859: Download complete +Status: images save success +2022-11-06T15:58:43 info output images [docker.io/bitnami/nginx:1.23.2-debian-11-r29] for platform is linux/amd64 +STEP 1/3: FROM scratch +STEP 2/3: COPY . . +STEP 3/3: CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +COMMIT labring/nginx:v1.23.2 +Getting image source signatures +Copying blob 9f5a861e0f8d done +Copying config 1b89695273 done +Writing manifest to image destination +Storing signatures +--> 1b896952734 +Successfully tagged localhost/labring/nginx:v1.23.2 +1b8969527343939d60859469708e5420758f7419a421304f81b5132669982de7 +2022-11-06T15:58:44 info + ___ ___ ___ ___ ___ ___ + /\ \ /\ \ /\ \ /\__\ /\ \ /\ \ + /::\ \ /::\ \ /::\ \ /:/ / /::\ \ /::\ \ + /:/\ \ \ /:/\:\ \ /:/\:\ \ /:/ / /:/\:\ \ /:/\ \ \ + _\:\~\ \ \ /::\~\:\ \ /::\~\:\ \ /:/ / /:/ \:\ \ _\:\~\ \ \ + /\ \:\ \ \__\ /:/\:\ \:\__\ /:/\:\ \:\__\ /:/__/ /:/__/ \:\__\ /\ \:\ \ \__\ + \:\ \:\ \/__/ \:\~\:\ \/__/ \/__\:\/:/ / \:\ \ \:\ \ /:/ / \:\ \:\ \/__/ + \:\ \:\__\ \:\ \:\__\ \::/ / \:\ \ \:\ /:/ / \:\ \:\__\ + \:\/:/ / \:\ \/__/ /:/ / \:\ \ \:\/:/ / \:\/:/ / + \::/ / \:\__\ /:/ / \:\__\ \::/ / \::/ / + \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ + + Website :https://www.sealos.io/ + Address :github.com/labring/sealos +``` + +sealos 将自动从 charts 目录中提取镜像,将其拉取到本地并存储在 registry 目录中。 + +现在的目录结构如下所示: + +```shell +. +├── charts +│ └── nginx +│ ├── Chart.lock +│ ├── charts +│ ├── Chart.yaml +│ ├── README.md +│ ├── templates +│ ├── values.schema.json +│ └── values.yaml +├── Kubefile +└── registry + └── docker + └── registry +``` + +在本地查看构建的镜像,现在所有依赖的部署清单和镜像缓存都构建到了集群镜像中。 + +```shell +root@ubuntu:~/cloud-images# + + sealos images +labring/nginx v1.23.2 521c85942ee4 4 minutes ago 56.8 MB +``` + +你可以将镜像推送到任何 Docker 镜像仓库,下面的命令将其推送到 Docker Hub。 + +```shell +sealos push labring/nginx:v1.23.2 +``` + +**注意:** 请使用 sealos 命令操作集群镜像,不支持 Docker 命令。 + +如果你使用私有镜像仓库,只需在拉取或推送镜像之前使用 `sealos login` 命令登录到注册表。 + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +## 五、安装集群镜像 + +然后你可以在你的集群中运行集群镜像。 + +```shell +sealos run labring/nginx:v1.23.2 +``` + +helm 二进制命令将安装到你的 Kubernetes 集群的主节点上。 + +```shell +root@ubuntu:~# helm -n nginx ls +``` + +## 六、说明 + +默认情况下,在构建镜像时,sealos 只解析默认的 values.yml 文件,但是你也可以为 sealos 提供自定义的 values.yaml 文件。 + +**自定义 values 文件必须放在与你的 Chart 相同的目录中,并且必须以 `.values.yaml` 的形式命名,例如 `loki-stack.values.yaml`。** + +```shell +. +├── charts +│ ├── loki-stack +│ │ ├── charts +│ │ ├── Chart.yaml +│ │ ├── README.md +│ │ ├── requirements.lock +│ │ ├── requirements.yaml +│ │ ├── templates +│ │ └── values.yaml +│ └── loki-stack.values.yaml +├── init.sh +├── Kubefile +``` + +`loki-stack.values.yaml` 文件内容如下: + +```shell +$ cat charts/loki-stack.values.yaml +promtail: + enabled: false +fluent-bit: + enabled: true +grafana: + enabled: true +``` + +不同的 values 文件可能会输出不同的镜像列表,以使 sealos 能够在 `sealos build` 过程中自动解析镜像。 + +```shell +$ helm template charts/loki-stack/ -f charts/loki-stack/values.yaml|grep image: + image: "grafana/promtail:2.0.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + +$ helm template charts/loki-stack/ -f charts/loki-stack.values.yaml|grep image: + image: "grafana/fluent-bit-plugin-loki:1.6.0-amd64" + image: "kiwigrid/k8s-sidecar:0.1.209" + image: "grafana/grafana:6.7.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + image: bats/bats:v1.1.0 +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md new file mode 100644 index 000000000000..c9e33edaf746 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-image-list.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 1 +--- + +# 构建基于镜像清单的集群镜像 + +本文将指导你如何使用镜像列表构建集群镜像,或使用现有的docker存储的tar包进行构建应用镜像。 + +## 镜像列表 构建 + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── shim +│   └── CalicoImageList +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM labring/kubernetes:v1.24.0 +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +说明: + +CalicoImageList 中的镜像列表将被拉取到本地,然后使用 `kubectl apply -f` 命令将其应用到集群中。 + +镜像列表目前支持: +- docker.io/calico/cni:v3.20.0 这种远程的镜像 +- containers-storage:docker.io/labring/coredns:v0.0.1 这种本地的OCI容器镜像 +- docker-daemon:docker.io/library/nginx:latest 这种本地的docker容器镜像 + + +## 镜像tar包 构建 + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── skopeo +│   ├── calico.tar +│   └── tar.txt +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM scratch +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +说明: + +tar.txt 中的配置会被拉取到本地并重定向镜像列表,然后使用 `kubectl apply -f` 命令将其应用到集群中。 +配置文件格式: + +``` +docker-archive:calico.tar@calico/cni:v3.20.0 +``` + +镜像列表目前支持: +- docker-archive 这种docker存储的镜像,仅支持单个镜像 +- oci-archive 这种oci存储的镜像,仅支持单个镜像 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md new file mode 100644 index 000000000000..2618ff743b6d --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 2 +--- + +# 构建基于部署清单的集群镜像 + +本文档将详细介绍如何构建基于部署清单(Deployment Manifest)的集群镜像。我们将以一个简单的nginx应用为例来进行说明。 + +## 一、准备工作 + +1. 首先,创建一个基础目录作为构建工作区。 + +```shell +$ mkdir ~/cloud-images +``` + +2. 创建一个名为 `manifests` 的目录来存储 kubernetes nginx 部署 yaml 文件。 + +```shell +$ cd cloud-images +$ mkdir manifests +``` + +## 二、准备清单文件 + +在这个阶段,我们将准备一个简单的nginx kubernetes yaml文件。 + +```shell +$ cat manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.23.1 + ports: + - containerPort: 80 +``` + +## 三、创建Kubefile + +在这个阶段,我们需要创建一个Kubefile文件,该文件将用于构建镜像。 + +```shell +FROM scratch +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/deployment.yaml"] +``` + +## 四、构建集群镜像 + +在准备好所有必需的文件和目录后,我们可以开始构建集群镜像。 + +```shell +sealos build -t labring/nginx:v1.23.1 . +``` + +**注意:** 在开始构建前,您需要先在本地主机上安装 sealos 命令。 + +构建过程中,您可以查看构建日志。 + +## 五、验证镜像 + +在构建完毕后,可以通过下列命令查看构建的镜像: + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.1 521c85942ee4 4 minutes ago 56.8 MB +``` + +## 六、推送镜像 + +最后,我们可以将构建好的镜像推送至任何Docker镜像仓库,以下命令将其推送至DockerHub。 + +```shell +sealos push labring/nginx:v1.23.1 +``` + +**注意:** 请使用 sealos 命令来操作集群镜像,Docker 命令不受支持。 + +如果你使用的是私有镜像仓库,只需要在拉取或推送镜像前使用 `sealos login` 登录仓库即可。 + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +至此,基于部署清单的集群镜像已经构建完成。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md new file mode 100644 index 000000000000..3426fd485712 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md @@ -0,0 +1,127 @@ +--- +sidebar_position: 6 +--- + +# 使用 exec 和 scp 命令构建集群镜像 + +默认情况下,`sealos run xx` 只会在第一个主节点上运行命令和复制文件。当你希望在特定节点或所有节点上运行命令或复制文件时,你可以在构建集群镜像时使用 `sealos exec` 或 `sealos scp` 命令。 + +- sealos exec: 连接到一个或多个节点并运行任何 shell 命令; +- sealos scp: 连接到一个或多个节点并将本地文件复制到远程节点。 + +虽然你可以直接在宿主机上使用这些命令,但本文主要描述的是在使用 sealos build 构建集群镜像时如何使用这两个命令。 + +## sealos exec 示例 + +以下是构建一个 openebs 集群镜像的示例。在安装 openebs maystor 之前,需要在节点上执行一些初始化操作,你可以使用 sealos exec 来实现这一点。 + +首先,创建一个用于构建工作的基础目录。 + +```shell +$ mkdir ~/cloud-images +``` + +创建一个 `charts` 目录,用来存储 kubernetes nginx helm charts 文件。 + +```shell +$ cd cloud-images +``` + +创建一个名为 `Kubefile` 的文件,用于镜像构建: + +```shell +$ cat Kubefile +FROM scratch +COPY manifests manifests +COPY registry registry +COPY opt opt +COPY mayastor.sh mayastor.sh +CMD ["bash mayastor.sh"] +``` + +创建一个名为 `mayastor.sh` 的脚本文件,sealos exec 后面的 shell 命令将在所有节点上执行(在所有节点上创建 hugepage、加载内核模块),但其他命令只会在主节点上运行。 + +```shell +$ cat mayastor.sh +#!/usr/bin/env bash +set -e + +sealos exec " +echo vm.nr_hugepages = 1024 | sudo tee -a /etc/sysctl.d/mayastor.conf +sysctl -p +sudo modprobe -- nbd +sudo modprobe -- nvmet +sudo modprobe -- nvmet_rdma +sudo modprobe -- nvme_fabrics +sudo modprobe -- nvme_tcp +sudo modprobe -- nvme_rdma +sudo modprobe -- nvme_loop +cat < /tmp/buildimage/kubeadm.yml < /tmp/buildimage/Kubefile < +Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.24.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +``` + + + +2. 生成 Clusterfile 后,然后更新集群配置。例如,要修改 pods 的 CIDR 范围,就可以修改 `networking.podSubnet` 和 `spec.data.spec.calicoNetwork.ipPools.cidr` 字段。最终的 Clusterfile 会像是这样: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +--- +apiVersion: kubeadm.k8s.io/v1beta2 +kind: ClusterConfiguration +networking: + podSubnet: 10.160.0.0/12 +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + ipPools: + - blockSize: 26 + cidr: 10.160.0.0/12 + encapsulation: IPIP + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*" +``` + +
+ +3. 运行 `sealos apply -f Clusterfile` 启动集群。集群运行成功后会把 Clusterfile 保存到 `.sealos/default/Clusterfile` 文件中,可以修改其中字段来重新 apply 对集群进行变更。 + +**注意:** + +- 可以参考[官方文档](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/)或运行 `kubeadm config print init-defaults` 命令来打印 kubeadm 配置。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md new file mode 100644 index 000000000000..30235afd65e2 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/run-cluster.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 0 +--- + +# Sealos 集群镜像运行 + +在这个目录中,我们提供了一系列详细的教程和资源,帮助您有效地运行和管理 Sealos 集群。以下是每个文件或子目录的简短概述: + +1. **集群初始化**:这部分提供了所有必需的步骤,以启动并初始化一个新的 Sealos 集群。它包括如何配置 Sealos,以及如何运行初始化命令等信息。 +2. **节点管理**:这个部分包括如何添加和移除集群节点,以及如何维护和管理集群节点的详细信息。 +3. **服务管理**:这部分包含了如何在 Sealos 集群上部署和管理各种服务的指南。它包括如何使用 Sealos 的命令行工具来管理服务,以及如何配置服务以满足你的特定需求。 +4. **实战示例**:这部分包含了各种实战示例,包括如何运行和管理各种应用,以及如何处理特定的运维问题。 + +我们希望这个目录可以为您在运行和管理 Sealos 集群时提供全方位的帮助。如果有任何问题或建议,欢迎随时联系我们。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md new file mode 100644 index 000000000000..ae6bf478ad4c --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/template-apply-cluster.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 2 +--- + +# 模板配置安装 + +我们也可以使用 Go 模版语法来编写 Clusterfile(就像 [Helm](https://helm.sh/) 一样,但暂不支持部分模版函数,如 `include`/`tpl`/`require`/`lookup`)。 例如,创建 `Clusterfile.yaml` 如下: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: {{ .Values.clusterName }} +spec: + hosts: + - ips: {{ .Values.masters | toYaml | nindent 8 }} + roles: ["master", "amd64"] + {{- with .Values.nodes }} + - ips: {{ . | toYaml | nindent 8 }} + roles: ["node", "amd64"] + {{- end }} + image: {{ .Values.images | toYaml | nindent 4 }} + ssh: + passwd: {{ env "SSH_PASSWORD" .Values.ssh.passwd }} + pk: {{ default "~/.ssh/id_rsa" .Values.ssh.pk }} + port: {{ default 22 .Values.ssh.port | int }} + user: {{ default "root" .Values.ssh.user }} +--- +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +networking: + dnsDomain: {{ default "cluster.local" .Values.networking.dnsDomain }} + serviceSubnet: {{ default "10.96.0.0/18" .Values.networking.serviceSubnet }} + podSubnet: {{ default "100.64.0.0/17" .Values.networking.podSubnet }} +``` + +随后,创建一个自定义的 values 文件 `example.values.yaml`: + +```yaml +clusterName: default +images: + - dockerhub.tencentcloudcr.com/labring/kubernetes:v1.23.8 + - dockerhub.tencentcloudcr.com/labring/calico:v3.24.1 +masters: + - 10.74.16.27:22 + - 10.74.16.140:22 + - 10.74.16.101:22 +nodes: [] +ssh: + # passwd: notSetYet + pk: /path/to/private/key/file + port: 22 + user: root +networking: + dnsDomain: cluster.local + serviceSubnet: 10.96.0.0/18 + podSubnet: 100.64.0.0/17 +``` + +然后就可以像这样部署集群了: + +```shell +$ sealos apply -f Clusterfile.yaml --values example.values.yaml --set clusterName=testlocal --env SSH_PASSWORD=s3cret +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md new file mode 100644 index 000000000000..1a2cbe082765 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/operations/run-cluster/upgrade-cluster.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 3 +--- + +# 如何升级集群 + +如果你想要升级你的 Kubernetes 集群,你只需要运行以下命令: + +```sh +sealos run labring/kubernetes:<新版本号> +``` + +确保你已经建立了集群。 + +## 实例说明 + +1. 假设你已经运行过以下命令: + +```sh +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.8 --nodes 192.168.64.7 +``` + +2. 现在你想要升级集群到 v1.25.0,你可以这样操作: + +```sh +sealos run labring/kubernetes:v1.25.0 +``` + +在运行到 'kubeadm upgrade v1.25.0' 的时候,你将看到: + +```txt +[upgrade/version] You have chosen to change the cluster version to "v1.25.0" +[upgrade/versions] Cluster version: v1.24.0 +[upgrade/versions] kubeadm version: v1.25.0 +[upgrade] Are you sure you want to proceed? [y/N]: +``` + +输入 'y' 来继续升级。 + +如果**出现错误**,你可以再次运行命令 'sealos run labring/kubernetes:v1.25.0'。即使失败,它也能保证得到相同的结果。 + +## 注意事项 + +1. **升级不能跨过次版本号**。比如从 'v1.23.0' 升级到 'v1.25.0' 是不允许的。如果你确实需要从 'v1.23.0' 升级到 'v1.25.0',你可以分成两步来操作,比如先从 'v1.23.0' 升级到 'v1.24.0',然后再从 'v1.24.0' 升级到 'v1.25.0'。 + +2. 一旦升级成功,集群挂载的旧版本镜像就会被替换。添加主节点或工作节点将会应用新版本。 + +这就是升级 Kubernetes 集群的整个过程。如果你在升级过程中遇到任何问题,不要犹豫,尽快查阅相关文档或者寻求帮助。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md new file mode 100644 index 000000000000..30d2cfa8dc46 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 3 +--- + +# 构建一个 Ingress 集群镜像 + +这里展示了如何用 helm 构建一个 nginx-ingress 集群镜像。 + +## 下载 helm chart + +```shell +$ mkdir ingress-nginx && cd ingress-nginx +$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +$ helm pull ingress-nginx/ingress-nginx +``` + +随后就能找到下载的 chart: + +```shell +$ ls +ingress-nginx-4.1.0.tgz +``` + +## 添加镜像列表 + +sealos 会下载镜像列表中的镜像并缓存到 registry 目录。 + +目录必须形如 `images/shim/[your image list filename]`: + +```shell +$ cat images/shim/nginxImages +k8s.gcr.io/ingress-nginx/controller:v1.2.0 +k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1 +``` + +## 编写 Dockerfile + +```Dockerfile +FROM scratch +COPY ../examples . +CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"] +``` + +## 构建集群镜像 + +```shell +$ sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 . +``` + +sealos 在构建的时候会自动添加镜像列表中的镜像依赖到集群镜像中,通过神奇的方式保存了里面依赖的 Docker 镜像。 +并且在到别的环境中运行的时候更神奇的自动检测集群中是否有 Docker 镜像,有的话自动下载,没有的话才会去 k8s.gcr.io 下载。 +用户无需修改 helm chart 中的 docker 镜像地址,这里用到了镜像缓存代理的黑科技。 + +## 推送到镜像 registry + +```shell +$ sealos login docker.io +$ sealos push docker.io/fanux/ingress-nginx:v1.2.0 +``` + +## 运行集群镜像 + +```shell +$ sealos run docker.io/fanux/ingress-nginx:v1.2.0 +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md new file mode 100644 index 000000000000..6d900af995a5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md @@ -0,0 +1,192 @@ +--- +sidebar_position: 2 +keywords: [K8s, K8s 安装, K8s 部署, K8s 离线安装, K8s 离线部署] +--- + +# 安装 K8s 集群 + +Sealos 支持安装 `amd64` 和 `arm64` 架构的 K8s 集群。 + +## 先决条件 + +首先需要[下载 Sealos 命令行工具](/self-hosting/lifecycle-management/quick-start/install-cli.md),sealos 是一个简单的 Golang 二进制文件,可以安装在大多数 Linux 操作系统中。 + +以下是一些基本的安装要求: + ++ 每个集群节点应该有不同的主机名。主机名不要带下划线。 ++ 所有节点的时间需要同步。 ++ 需要在 K8s 集群的**第一个 master 节点**上运行 `sealos run` 命令,目前**集群外的节点不支持集群安装**。 ++ 建议使用干净的操作系统来创建集群。**不要自己装 Docker!** ++ 支持大多数 Linux 发行版,例如:Ubuntu、CentOS、Rocky linux。 ++ 支持 [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags) 中的所有 Kubernetes 版本。 ++ 支持使用 Containerd 作为容器运行时。 ++ 在公有云上安装请使用**私有 IP**。 + +## 查看集群镜像 + +Sealos 所有的集群镜像都可以在 [cluster-image-docs](https://github.com/labring-actions/cluster-image-docs) 仓库里找到。除了推送到 Docker Hub 之外,这些镜像还被同步到了阿里云的镜像仓库。 + +Docker Hub 上可以通过以下链接查看 Sealos 所有的集群镜像:[https://hub.docker.com/u/labring](https://hub.docker.com/u/labring). + +使用 [Registry Explorer](https://explore.ggcr.dev/) 可以查看 K8s 集群镜像的所有版本,直接输入 `registry.cn-shanghai.aliyuncs.com/labring/kubernetes`,然后点击 “Submit Query”: + +![](images/registry-explorer.png) + +就会看到这个集群镜像的所有 tag。 + +Docker Hub 同理,输入 `docker.io/labring/kubernetes` 即可查看所有 tag。 + +:::info注意 + +K8s 的小版本号越高,集群越稳定。例如 v1.28.x,其中的 x 就是小版本号。建议使用小版本号比较高的 K8s 版本。到本文截止时间为止,v1.27 最高的版本号是 v1.27.7,而 v1.28 最高的版本号是 v1.28.3,所以**建议使用 v1.27.7**。你需要根据实际情况来选择最佳的 K8s 版本 + +::: + +## 安装 K8s 单机版 + +```shell +# sealos version must >= v4.1.0 +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 registry.cn-shanghai.aliyuncs.com/labring/cilium:v1.13.4 --single +``` + +## 安装 K8s 集群 + +```shell +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 registry.cn-shanghai.aliyuncs.com/labring/cilium:v1.13.4 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +注意:labring/helm 应当在 labring/cilium 之前。 + +参数说明: + +| 参数名 | 参数值示例 | 参数说明 | +| --- | --- | --- | +| --masters | 192.168.0.2 | K8s master 节点地址列表 | +| --nodes | 192.168.0.3 | K8s node 节点地址列表 | +| --ssh-passwd | [your-ssh-passwd] | ssh 登录密码 | +|kubernetes | labring/kubernetes:v1.25.0 | K8s 集群镜像 | + +在干净的服务器上直接执行上面命令,不要做任何多余操作即可安装一个高可用 K8s 集群。 + +## 安装各种分布式应用 + +```shell +sealos run registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 # install helm +sealos run registry.cn-shanghai.aliyuncs.com/labring/openebs:v3.9.0 # install openebs +sealos run registry.cn-shanghai.aliyuncs.com/labring/minio-operator:v4.5.5 registry.cn-shanghai.aliyuncs.com/labring/ingress-nginx:4.1.0 +``` + +这样高可用的 Minio 等应用都有了,不用关心所有的依赖问题。 + +## 增加 K8s 节点 + +增加 node 节点: + +```shell +$ sealos add --nodes 192.168.64.21,192.168.64.19 +``` + +增加 master 节点: + +```shell +$ sealos add --masters 192.168.64.21,192.168.64.19 +``` + +## 删除 K8s 节点 + +删除 node 节点: + +```shell +$ sealos delete --nodes 192.168.64.21,192.168.64.19 +``` + +删除 master 节点: + +```shell +$ sealos delete --masters 192.168.64.21,192.168.64.19 +``` + +## 清理 K8s 集群 + +```shell +$ sealos reset +``` + + + +## 离线安装 K8s + +离线环境只需要提前导入镜像,其它步骤与在线安装一致。 + +首先在有网络的环境中导出集群镜像: + +```shell +$ sealos pull registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +$ sealos save -o kubernetes.tar registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +``` + +### 导入镜像并安装 + +将 kubernetes.tar 拷贝到离线环境, 使用 load 命令导入镜像即可: + +```shell +$ sealos load -i kubernetes.tar +``` + +剩下的安装方式与在线安装的步骤一致: + +```shell +$ sealos images # 查看集群镜像是否导入成功 +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 # 单机安装,集群安装同理 +``` + +### 快速启动 K8s 集群 + +也可以不用 load 命令导入镜像,直接使用以下命令即可安装 K8s: + +```shell +$ sealos run kubernetes.tar # 单机安装,集群安装同理 +``` + +## 集群镜像版本支持说明 + +### 支持 Containerd 的 K8s + +推荐使用 Containerd 作为容器运行时 (CRI) 的集群镜像版本,Containerd 是一种轻量级、高性能的容器运行时,与 Docker 兼容。使用 Containerd 的 Kubernetes 镜像可以提供更高的性能和资源利用率。以下是支持 Containerd 的集群镜像版本支持说明: + +| K8s 版本 | Sealos 版本 | CRI 版本 | 集群镜像版本 | +| -------- | ----------------- | -------- | -------------------------- | +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes:v1.28.0 | + +根据 Kubernetes 版本的不同,您可以选择不同的 Sealos 版本和 CRI 版本。例如,如果您要使用 Kubernetes v1.26.0 版本,您可以选择 sealos v4.1.4-rc3 及更高版本,并使用 v1 CRI 版本。 + +### 支持 Docker 的 K8s + +当然,你也可以选择使用 Docker 作为容器运行时,以下是支持 Docker 的集群镜像版本支持说明: + +| K8s 版本 | Sealos 版本 | CRI 版本 | 集群镜像版本 | +| -------- | ----------------- | -------- | --------------------------------- | +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes-docker:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes-docker:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes-docker:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes-docker:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes-docker:v1.28.0 | + + +与支持 Containerd 的 Kubernetes 镜像类似,您可以根据 Kubernetes 版本的不同选择不同的 Sealos 版本和 CRI 版本。例如,如果您要使用 Kubernetes v1.26.0 版本,您可以选择 sealos v4.1.4-rc3 及更高版本,并使用 v1 CRI 版本。 + +### 支持 Containerd 的 k3s + +| K3s 版本 | Sealos 版本 | 集群镜像版本 | +|----------|------------|---------------------| +| `>=1.24` | `>=v5.0.0` | labring/k3s:v1.24.0 | + +## 总结 + +您可以根据自己的需求和偏好,在不同的镜像类型和版本中进行选择。同时,不要忘记查看 [更新日志](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md),以了解各个版本的更新内容和修复问题。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png new file mode 100644 index 000000000000..a9e28004026c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/images/registry-explorer.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/install-cli.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/install-cli.md new file mode 100644 index 000000000000..99c91f281acd --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/quick-start/install-cli.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 1 +keywords: [sealos, sealos 命令行, sealos 下载] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 下载 Sealos 命令行工具 + +你可以通过运行命令来获取版本列表: + +```shell +$ curl --silent "https://api.github.com/repos/labring/sealos/releases" | jq -r '.[].tag_name' +``` + +> 注意:在选择版本时,建议使用稳定版本例如 `v4.3.0`。像 `v4.3.0-rc1`、`v4.3.0-alpha1` 这样的版本是预发布版,请谨慎使用。 + +设置 `VERSION` 环境变量为 latest 版本号,或者将 `VERSION` 替换为您要安装的 Sealos 版本: + +```shell +$ VERSION=`curl -s https://api.github.com/repos/labring/sealos/releases/latest | grep -oE '"tag_name": "[^"]+"' | head -n1 | cut -d'"' -f4` +``` + +## 二进制自动下载 + +```shell + +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/main/scripts/install.sh | PROXY_PREFIX=https://mirror.ghproxy.com sh -s ${VERSION} labring/sealos + +``` + +## 二进制手动下载 + + + + +```shell +$ wget https://mirror.ghproxy.com/https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_amd64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +```shell +$ wget https://mirror.ghproxy.com/https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_arm64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +## 包管理工具安装 + +### DEB 源 + +```shell +$ echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | sudo tee /etc/apt/sources.list.d/labring.list +$ sudo apt update +$ sudo apt install sealos +``` + +### RPM 源 + +```shell +$ sudo cat > /etc/yum.repos.d/labring.repo << EOF +[fury] +name=labring Yum Repo +baseurl=https://yum.fury.io/labring/ +enabled=1 +gpgcheck=0 +EOF +$ sudo yum clean all +$ sudo yum install sealos +``` + +## 源码安装 + +### 前置依赖 +1. `linux` +2. `git` +3. `golang` 1.20+ +4. `libgpgme-dev libbtrfs-dev libdevmapper-dev` + +如果在 `arm64` 环境下需要添加 `:arm64` 后缀。 + +### 构建 + +```shell +# git clone the repo +$ git clone https://github.com/labring/sealos.git +# just make it +$ make build BINS=sealos +``` + +## 下一步 + +[安装 K8s 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md)。 + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json new file mode 100644 index 000000000000..950b839be0aa --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 3 +} diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md new file mode 100644 index 000000000000..2a2fb7892ca8 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 3 +--- + +# image-cri-shim 使用指南 + +## 工作原理 + +image-cri-shim 是一个基于 CRI (Container Runtime Interface) 和 kubelet 的 gRPC (Google Remote Procedure Call) shim。CRI 是 Kubernetes 中用于与容器运行时进行交互的接口,而 kubelet 是负责维护容器运行状态和节点级别的资源管理的 Kubernetes 组件。 + +image-cri-shim 的主要功能是自动识别镜像名称,让用户在使用 Kubernetes 部署容器时无需手动指定镜像名称。这样可以降低用户的操作难度,提高部署容器的便利性。 + +在实际使用中,image-cri-shim 可以作为一个中间件,接收来自 kubelet 的请求,然后将请求转发给容器运行时。通过自动识别镜像名称,image-cri-shim 可以简化容器镜像的部署流程,减轻用户的操作负担。 + +``` ++------------+ +----------------+ +-------------------+ +| User | | Kubelet | | image-cri-shim | +| (Kubernetes| | (Node agent) | | (Middleware) | +| Manifest) | | | | | ++-----+------+ +-------+--------+ +-------+-----------+ + | | | + | YAML Manifest | | + |---------------> | | + | | | + | | | + | | CRI Request | + | |------------------------> | + | | | + | | Image Name | + | | Auto-Recognition | + | | | + | | | + | | CRI Response | + | | <------------------------+ + | | | + | | | + | Container | | + | Deployment | | + | <----------------------| | + | | | + | | | ++------------+ +-------+--------+ +-------+-----------+ + +``` + +从上述流程图可以看出,用户创建一个包含容器信息的 Kubernetes YAML 清单,然后将该清单提交给 kubelet。kubelet 是 Kubernetes 节点上的代理,负责管理容器。 +接着,kubelet 将 CRI 请求发送给 image-cri-shim 中间件。image-cri-shim 的主要任务是自动识别镜像名称,它会处理这个 CRI 请求并获取相关的镜像信息。当 image-cri-shim 识别到镜像名称后,它会将 CRI 响应返回给 kubelet。 + +最后,kubelet 使用从 image-cri-shim 获取的镜像名称来部署容器。这个过程对用户是透明的,用户无需手动指定镜像名称,从而简化了容器部署流程并提高了便利性。 + +## 架构图 + +image-cri-shim 的架构如下图所示: + +![](images/image-cri-shim.png) + + +## 使用说明 + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://sealos.hub:5000 +force: true +debug: true +timeout: 15m +auth: admin:passw0rd + +registries: +- address: http://172.18.1.38:5000 + auth: admin:passw0rd +``` +这段配置文件是一个用于设置 image-cri-shim 的 YAML 格式文件。配置文件中包含了一些关键的参数,以下是每个参数的解释: + +1. shim: 指定 image-cri-shim 的 UNIX 套接字文件路径。这个路径用于与 kubelet 之间的通信。 +2. cri: 指定容器运行时(如 containerd)的 UNIX 套接字文件路径。image-cri-shim 会使用这个路径与容器运行时进行通信。 +3. address: 定义镜像仓库的地址。在本例中,镜像仓库地址为 http://sealos.hub:5000。 +4. force: 设置为 true 时,image-cri-shim 会在强制启动shim,无需等待cri启动后启动。 +5. debug: 设置为 true 时,启用调试模式,输出更多的日志信息。 +6. timeout: 定义镜像操作的超时时间。在本例中,超时时间为 15 分钟(15m)。 +7. auth: 定义用于访问镜像仓库的身份验证凭据。在本例中,用户名为 admin,密码为 passw0rd。 + +此外,配置文件还包含了一个 registries 列表,用于定义其他镜像仓库及其身份验证凭据。在这个例子中,只有一个其他仓库: +- address: 该仓库的地址为 http://172.18.1.38:5000。 +- auth: 用于访问该仓库的身份验证凭据。在本例中,用户名为 admin,密码为 passw0rd。 +这个配置文件为 image-cri-shim 提供了所需的信息,以便正确地与 kubelet 和容器运行时(如 containerd)进行通信,以及访问和管理镜像仓库。 + +注意: image-cri-shim 能够同时兼容 CRI API v1alpha2 和 v1。 + +### 管理服务 + +image-cri-shim 通常作为一个系统服务运行。要管理 image-cri-shim,您可以使用系统服务管理工具(如 systemctl)来启动、停止、重启或查看服务状态。首先,确保您已经正确地安装了 image-cri-shim 并将其配置为一个系统服务。 + +1. 启动服务: `systemctl start image-cri-shim` +2. 停止服务: `systemctl stop image-cri-shim` +3. 重启服务: `systemctl restart image-cri-shim` +4. 查看服务状态: `systemctl status image-cri-shim` + +### 日志管理 + +要查看 image-cri-shim 服务的日志,您可以使用 journalctl 命令。journalctl 是一个用于查询和显示系统日志的工具,它与 systemd 服务管理器一起使用。 + +以下是使用 journalctl 查看 image-cri-shim 服务日志的命令: + +```shell +journalctl -u image-cri-shim +``` + +这将显示 image-cri-shim 服务的全部日志。如果您希望实时查看日志,可以添加 -f 参数: + +```shell +journalctl -u image-cri-shim -f +``` + +此外,您还可以根据时间过滤日志。例如,如果您只想查看过去一小时的日志,可以使用以下命令: + +```shell +journalctl -u image-cri-shim --since "1 hour ago" +``` + +这些命令应该能帮助您查看和分析 image-cri-shim 服务的日志,从而更好地了解服务的运行状态和可能出现的问题。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png new file mode 100644 index 000000000000..5c8d5f81b23a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/_category_.json b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/_category_.json new file mode 100644 index 000000000000..e47d9e07a6d5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 99 +} \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/images/01.webp b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/images/01.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/images/01.webp differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/lvscare.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/lvscare.md new file mode 100644 index 000000000000..683793f00493 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/lvscare/lvscare.md @@ -0,0 +1,74 @@ +# LVScare 使用指南 + +## 介绍 + +LVScare是一款基于IPVS技术的轻量级负载均衡和健康检查工具,可以实时监控和管理后端服务。Sealos,一个云操作系统,它的生命周期管理是一个基于kubeadm的Kubernetes HA安装工具,适用于在任何Linux系统中安装和升级高可用Kubernetes集群。两者结合能实现Kubernetes master节点的高可用性。Sealos利用其生命周期管理能力,通过配合LVScare的轻量级,0依赖,高可用的特性,可以非常有效地保证Kubernetes集群的稳定性和可靠性。 + +### LVScare的工作原理与特点 + +LVScare通过IPVS实时监控后端服务(real servers)的健康状态。如果某个服务变得不可用,LVScare会立即将其权重设为0(用于TCP优雅终止),并在下一次检查期间从服务列表中移除。服务恢复正常后,LVScare会自动将其重新加入到服务列表。LVScare的这种设计使得它具备轻量级,0依赖,高可用的特性。占用资源少,稳定可靠,类似于kube-proxy的实现,可以通过ipvs实现的localLB保证服务的持续可用。 + +## Sealos与LVScare的集成 + +在Sealos中,我们利用了官方推荐的静态Pod的方式,自动配置和管理LVScare,以实现Kubernetes集群的高可用性。Sealos会在安装Kubernetes集群的过程中自动使用LVScare进行master节点的健康检查和负载均衡。这意味着,即使某个master节点出现故障,也不会影响Kubernetes集群的整体功能。 + +![](./images/01.webp) + +### 如何运行LVScare静态Pod + +首先,通过`sealctl static-pod`命令生成LVScare的配置,并将其放入`/etc/kubernetes/manifests`目录。这样,Kubernetes集群中的所有节点都能获取到这份配置。 + +```bash +lvscare care --vs 10.103.97.12:6443 --rs 192.168.0.2:6443 --rs 192.168.0.3:6443 --rs 192.168.0.4:6443 --interval 5 --mode route +``` + +### 生成和调整静态Pod配置 + +在需要在每个节点上启动LVScare时,我们可以使用以下命令生成静态Pod的配置: + +```bash +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168 + +.0.3:6443 --print +``` + +当master节点有变化时,Sealos只需重新执行`sealctl static-pod`命令即可调整master节点,简化了维护静态Pod的逻辑。在集群join节点之前,我们需要调用`sealctl ipvs`(该命令直接调用LVScare sdk)手动启动IPVS规则,维护好IPVS集群。节点join成功后,Kubernetes的静态Pod就可以接管IPVS规则了。 + +## 使用LVScare的优势 + +### 高可用性 + +结合LVScare,Sealos可以实现Kubernetes集群master节点的高可用性。 + +### 健康检查机制 + +LVScare的健康检查机制可以及时发现并处理问题,防止单节点故障引发的更大问题。 + +### 无缝集成 + +作为Sealos中的静态Pod,LVScare可以与Kubernetes集群其他部分无缝集成。 + +### 简化运维 + +Sealos自动配置和管理LVScare,大大简化了Kubernetes集群的运维工作。 + +## LVScare使用示例 + +请注意,所有的real server需要在同一主机上监听,并设置为`route`模式。然后,你可以在前台运行LVScare。例如: + +```bash +docker run -p 8081:80 --name echoserver1 -d cilium/echoserver +docker run -p 8082:80 --name echoserver2 -d cilium/echoserver +docker run -p 8083:80 --name echoserver3 -d cilium/echoserver +lvscare care --vs 169.254.0.1:80 --rs 127.0.0.1:8081 --rs 127.0.0.1:8082 --rs 127.0.0.1:8083 --logger DEBG --health-schem http --health-path / +``` + +## 清理 + +最后,你可以使用以下命令进行清理: + +```bash +lvscare care --vs 169.254.0.1:80 --logger DEBG -C +``` + +结论:LVScare是一款基于IPVS的轻量级负载均衡和健康检查工具,它能与Sealos无缝集成,极大地提高了Kubernetes集群的可用性和性能。试一试,看看LVScare如何帮助你更好地管理你的Kubernetes集群! diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/_category_.json b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/_category_.json new file mode 100644 index 000000000000..a788e786e6c5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 2 +} diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cert.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cert.md new file mode 100644 index 000000000000..9de055627c86 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cert.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 1 +--- + + +# cert 证书管理 + +`cert` 命令用于生成 Kubernetes 集群所需的证书文件。在 Kubernetes 集群中,证书用于确保组件之间的通信安全,例如 API server、kubelet 和 etcd 等。证书通过 TLS(Transport Layer Security)协议实现加密,以确保数据在传输过程中的保密性和完整性。 + +`sealctl cert` 命令可以根据提供的参数自动生成证书。这些参数包括节点 IP、节点名称、服务 CIDR、DNS 域以及可选的其他备用名称。通过生成并配置这些证书,您可以确保 Kubernetes 集群的安全通信。 + + + +``` +cert 命令用于生成 Kubernetes 证书。 + +参数: + --alt-names 备用名称,例如 sealos.io 或 10.103.97.2。可以包含多个备用名称。 + --node-name 节点名称,例如 master0。 + --service-cidr 服务网段,例如 10.103.97.2/24。 + --node-ip 节点的 IP 地址,例如 10.103.97.2。 + --dns-domain 集群 DNS 域,默认值为 cluster.local。 + --cert-path Kubernetes 证书文件路径,默认值为 /etc/kubernetes/pki。 + --cert-etcd-path Kubernetes etcd 证书文件路径,默认值为 /etc/kubernetes/pki/etcd。 + +示例: + sealctl cert --alt-names sealos.io --alt-names 10.103.97.2 \ + --node-name master0 --service-cidr 10.103.97.2/24 \ + --node-ip 10.103.97.2 --dns-domain cluster.local + +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cri.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cri.md new file mode 100644 index 000000000000..cd36074f7b68 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/cri.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 2 +--- + +# CRI 容器管理 + +`cri` 命令是用于管理和检查 Kubernetes 集群中的容器运行时(Container Runtime Interface,CRI)环境。容器运行时是负责运行容器的底层技术,如 Docker、containerd 或者 CRI-O 等。在 Kubernetes 中,容器运行时用于启动、停止和管理容器,以支持集群中的工作负载。 + +`sealctl cri` 命令提供了一组子命令,使您能够执行与容器运行时相关的各种操作,例如检查运行时是否是 Docker、是否正在运行,列出 Kubernetes 容器,删除容器,拉取镜像,检查镜像是否存在以及获取 CGroup 驱动信息等。 + +通过使用 `sealctl cri` 命令,您可以轻松地管理和检查 Kubernetes 集群中的容器运行时环境,确保其正确配置和正常运行。 + + + +```shell +sealctl cri [flags] +``` + + + +子命令: + +1. `socket`:检测 CRI 套接字。 + +```shell +sealctl cri socket +``` + +2. `cgroup-driver`:获取容器运行时的 cgroup 驱动。 + +```shell +sealctl cri cgroup-driver [--short] +``` + +- `--short`:仅打印结果。 + +全局参数: + +- `--socket-path`:CRI 套接字路径。 +- `--config`:CRI 配置文件。 + +示例: + +```shell +sealctl cri socket +sealctl cri cgroup-driver --short + +``` \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hostname.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hostname.md new file mode 100644 index 000000000000..a6d4451d0713 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hostname.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 3 +--- + + +# hostname 获取主机名 + +获取操作系统的主机名: + +```shell +sealctl hostname +``` + +示例: + +```shell +sealctl hostname +``` + +执行此命令将返回操作系统的主机名。无需传递任何参数。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hosts.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hosts.md new file mode 100644 index 000000000000..df7431ec4697 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/hosts.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 4 +--- + +# hosts 管理 + +`hosts` 命令是用于管理操作系统的 hosts 文件。hosts 文件是一个用于解析域名到 IP 地址的文件,通常在本地系统中用于覆盖 DNS 解析。通过修改 hosts 文件,您可以为一个特定的域名分配一个自定义的 IP 地址,而不必依赖 DNS 服务器。 + +`sealctl hosts` 提供了以下三个子命令来实现 hosts 文件的管理: + +1. `list`:列出当前 hosts 文件中的所有条目。 +2. `add`:向 hosts 文件中添加一个新的域名与 IP 地址映射。 +3. `delete`:从 hosts 文件中删除一个指定的域名与 IP 地址映射。 + +通过这些子命令,您可以方便地查看、添加和删除 hosts 文件中的映射,从而更好地控制域名到 IP 地址的解析。 + +1. `sealctl hosts list`:列出当前 hosts 文件中的条目。 + + 示例: + + ```shell + sealctl hosts list + ``` + + + +2. `sealctl hosts add`:向 hosts 文件中添加一个新条目。 + + 参数: + + - `--ip`:IP 地址(必填) + - `--domain`:域名(必填) + + 示例: + + ```shell + sealctl hosts add --ip 192.168.1.100 --domain example.com + ``` + +3. `sealctl hosts delete`:从 hosts 文件中删除一个条目。 + + 参数: + + - `--domain`:要删除的域名(必填) + + 示例: + + ```shell + sealctl hosts delete --domain example.com + ``` + +注意:您可以在任何 `hosts` 子命令后面添加 `--path` 参数来指定 hosts 文件的路径。默认路径为 `/etc/hosts`(Linux 系统)。 + +示例: + +```shell +sealctl hosts list --path /custom/path/hosts +``` + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/ipvs.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/ipvs.md new file mode 100644 index 000000000000..b903d23e6483 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/ipvs.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 6 +--- + +# ipvs 管理 + +`ipvs` 命令用于创建和管理本地的 IPVS 负载均衡。IPVS(IP Virtual Server)是 Linux 内核中的一个模块,它允许在内核空间实现高性能的负载均衡。`ipvs` 命令通过管理虚拟服务器和真实服务器的映射关系,实现对服务的负载均衡。 + +`sealctl ipvs` 支持以下功能: + +1. 创建和管理虚拟服务器 (virtual server) 和真实服务器 (real server) 的映射关系。 +2. 提供健康检查功能,定期检查真实服务器的健康状态,并根据需要对其进行上下线操作。 +3. 支持两种代理模式:`route` 和 `link`。 +4. 支持配置代理调度算法(如轮询、加权轮询等)。 +5. 支持一次性创建代理规则(`--run-once` 标志)或持续运行并管理代理规则。 +6. 支持清理功能:通过 `-C` 或 `--clean` 标志,可以清除现有的 IPVS 规则并退出。 + +通过 `sealctl ipvs` 命令,用户可以轻松地在本地创建和管理高性能的负载均衡服务。 + +**用法** + +```shell +sealctl ipvs [flags] +``` + +**选项** + +- `-C`, `--clean`: 清除现有规则,然后退出。 +- `--health-insecure-skip-verify`: 跳过不安全请求的验证(默认为 true)。 +- `--health-path string`: 用于探测的 URL 路径(默认为 "/healthz")。 +- `--health-req-body string`: 健康检查器发送的请求体。 +- `--health-req-headers stringToString`: HTTP 请求头(默认为 [])。 +- `--health-req-method string`: HTTP 请求方法(默认为 "GET")。 +- `--health-schem string`: 探测器的 HTTP 方案(默认为 "https")。 +- `--health-status ints`: 有效状态码。 +- `-h`, `--help`: ipvs 帮助。 +- `-i`, `--iface string`: 要创建的虚拟接口的名称,与 kube-proxy 的行为相同(默认为 "lvscare")。仅在 mode=link 时启用。 +- `--interval durationOrSecond`: 健康检查间隔(默认为 0s)。 +- `--ip ip`: 作为路由网关的目标 IP,与 route 模式一起使用。 +- `--logger string`: 日志级别:DEBG/INFO(默认为 "INFO")。 +- `--masqueradebit int`: IPTables masquerade 位。仅在 mode=link 时启用。 +- `--mode string`: 代理模式:route/link(默认为 "route")。 +- `--rs strings`: 真实服务器地址,例如 192.168.0.2:6443。 +- `--run-once`: 创建代理规则并退出。 +- `--scheduler string`: 代理调度器(默认为 "rr")。 +- `--vs string`: 虚拟服务器地址,例如 169.254.0.1:6443。 + +**全局选项** + +- `--debug`: 启用调试日志。 +- `--show-path`: 启用显示代码路径。 + +**使用文档** + +要使用 `sealctl ipvs` 命令,请按照以下步骤操作: + +1. 为命令提供必要的选项和参数。 +2. 执行命令,将创建或管理本地 IPVS 负载均衡。 + +**示例** + +创建代理规则并退出: + +```shell +sealctl ipvs --vs 169.254.0.1:6443 --rs 192.168.0.2:6443 --run-once +``` + +清除现有 IPVS 规则: + +```shell +sealctl ipvs --clean +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/registry.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/registry.md new file mode 100644 index 000000000000..dbe1b40640e0 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/registry.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 5 +--- + +# registry 镜像仓库 + +## Sealos:sealctl registry serve 命令详解与使用指南 + +在构建并管理 Docker 镜像仓库过程中,Sealos 提供了 `sealctl registry serve` 命令以方便用户进行相关操作。本文将详细介绍 `sealctl registry serve` 命令的使用方法和示例。 + +### 基本介绍 + +`sealctl registry serve` 命令的主要作用是启动一个 Docker 分发镜像仓库服务器,支持两种模式:`filesystem` 和 `inmem`。 + +1. **Filesystem 模式**:在此模式下,sealctl 将运行一个针对指定目录的 Docker 分发镜像仓库服务器。该模式下,镜像数据将存储在硬盘上。**该命令还用于sealos做增量镜像同步** + +2. **In-memory 模式**:在此模式下,sealctl 将运行一个内存中的 Docker 分发镜像仓库服务器。该模式下,镜像数据仅保存在内存中,进程退出后数据将丢失。 + +### 命令参数 + +`sealctl registry serve filesystem ` 命令支持以下参数: + +- `--disable-logging`: 禁用日志输出,默认为 false。 +- `--log-level`: 配置日志级别,默认为 'error'。 +- `-p, --port`: 服务器监听的端口,默认为随机未使用的端口。 + +### 使用示例 + +以下是一些 `sealctl registry serve` 命令的使用示例: + +#### 在文件系统中启动镜像仓库服务器 + +```bash +sealctl registry serve filesystem --port=5000 +``` + +以上命令将在端口5000上启动一个文件系统镜像仓库服务器。 + +#### 在内存中启动镜像仓库服务器 + +```bash +sealctl registry serve inmem +``` + +以上命令将启动一个内存镜像仓库服务器。该服务器在进程退出后,存储的数据将丢失。 + +通过 `sealctl registry serve` 命令,用户可以轻松地管理和操作 Docker 镜像仓库。无论是在开发环境,还是在生产环境中,它都是一个强大且易用的工具。 + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/sealctl.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/sealctl.md new file mode 100644 index 000000000000..8ee151bd6dc1 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/sealctl.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 0 +--- + +# Sealctl 使用指南 + +Sealos 提供 sealctl 是使用 Sealos 与 集群节点进行操作的命令行工具。它包括以下几个子命令: + +1. `cert`:管理证书,用于生成、查看和更新TLS证书。 +2. `cri`:管理容器运行时接口(CRI)配置,例如Docker或containerd。 +3. `hostname`:查看或设置系统主机名。 +4. `hosts`:管理系统的hosts文件,用于定义静态主机名到IP地址映射。 +5. `ipvs`:管理IP虚拟服务器(IPVS)规则,用于负载均衡和代理。 +6. `registry`:管理镜像仓库,用于存储容器镜像仓库格式镜像以及镜像仓库管理。 +7. `static_pod`:管理静态Pod,可以创建静态Pod的配置。 +8. `token`:生成和管理访问令牌,用于授权访问Kubernetes集群。 + +通过这些子命令,您可以方便地管理和配置您的Sealos系统,实现对容器、镜像仓库、网络等各个方面的控制。 + + + + +# sealos 依赖命令 + +1. **添加Hosts** + + 在指定 IP 地址的节点上添加一个新的 hosts 记录。参数包括 IP 地址、主机名和域名。使用`sealctl hosts add `命令 + +2. **删除Hosts** + + 删除指定 IP 地址节点上的一个 hosts 记录。参数包括 IP 地址和域名。使用`sealctl hosts delete`命令 + +3. **hostname** + + 获取指定 IP 地址节点的主机名。 使用`sealctl hostname`命令 + +4. **IPVS负载均衡** + + 在指定 IP 地址的节点上配置 IPVS,实现负载均衡。参数包括节点 IP 地址、虚拟 IP 地址和主节点 IP 地址列表。 使用`sealctl ipvs`命令 + +5. **清空IPVS规则** + + 清除指定 IP 地址节点上的 IPVS 配置。参数包括节点 IP 地址和虚拟 IP 地址。 使用`sealctl ipvs`命令 + +6. **静态POD生成** + + 在指定 IP 地址的节点上部署一个静态 Pod(lvscare)。参数包括节点 IP 地址、虚拟 IP 地址、Pod 名称、镜像名称和主节点 IP 地址列表。使用`sealctl static-pod lvscare`命令 + +7. **处理集群交互认证的token** + + 为指定 IP 地址的节点生成一个 token。参数包括节点 IP 地址、配置文件和证书密钥。使用`sealctl token`命令 + +8. **获取节点的cgroup** + + 获取指定 IP 地址节点的cri CGroup 信息。 使用`sealctl cri cgroup`命令 + +9. **获取节点的cri-socket** + + 获取指定 IP 地址节点的 cri Socket 信息。 使用`sealctl cri socket`命令 + +10. **在节点生成https自签名证书** + + 为指定 IP 地址的节点生成证书。参数包括节点 IP 地址、备用名称列表、主机 IP 地址、主机名、服务 CIDR 和 DNS 域名。 使用`sealctl cert` 命令 + +11. **节点启动registry** + +​ 在指定节点启动regsitry,为进行增量镜像同步。使用`sealctl registry serve`命令 + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/static-pod.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/static-pod.md new file mode 100644 index 000000000000..0b6e8450802d --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/static-pod.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 7 +--- + +# static-pod 配置 + +`static-pod` 命令用于生成静态 Pod,这些 Pod 是由 kubelet 直接管理的,而不是通过 API 服务器。静态 Pod 在某些场景下非常有用,比如设置和管理 Kubernetes 集群中的控制平面组件。 + +`sealctl static-pod` 命令提供了一种简便的方法,用于生成用于特定目的的静态 Pod 配置文件。目前,它主要支持生成 `lvscare` 静态 Pod,`lvscare` 是一种用于管理 IPVS 规则的工具。 + +使用 `sealctl static-pod lvscare`,您可以根据指定的参数(如 VIP、主节点地址、镜像名称等)生成 `lvscare` 静态 Pod YAML 文件。然后,该文件可以存储在 kubelet 的静态 Pod 路径下,kubelet 将自动创建和管理相应的 Pod。 + + + +**用法** + +```shell +sealctl static-pod lvscare [flags] +``` + +**选项** + +- `--vip`: 默认 VIP IP(默认为 "10.103.97.2:6443")。 +- `--name`: 生成 lvscare 静态 Pod 名称。 +- `--image`: 生成 lvscare 静态 Pod 镜像(默认为 `sealos.hub:5000/sealos/lvscare:latest`)。 +- `--masters`: 生成 master 地址列表。 +- `--print`: 是否打印 YAML。 + +**示例** + +生成 lvscare 静态 Pod 文件并打印 YAML: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +如果没有使用 `--print` 选项,将直接生成配置文件到 `/etc/kubernetes/manifests` 并启用静态 Pod: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 +``` diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md new file mode 100644 index 000000000000..a37f10ccb649 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/tar&&untar.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 9 +--- + +# tar 与 untar 详解 + +Sealos 提供了 `sealctl tar` 和 `sealctl untar` 命令以便于用户进行文件或文件夹的压缩和解压。本文将详细介绍这两个命令的使用方法。 + +## sealctl tar 命令 + +`sealctl tar` 命令的主要作用是将指定路径的目录压缩成归档文件。注意,这将剥离父目录。 + +**命令参数:** + +- `--clear`:是否在压缩完成后删除源文件,默认为 false。 +- `--compression`:压缩算法,可用选项有 tar/gzip/zstd/disable,默认为 disable。 +- `-o, --output`:归档文件的路径。 + +**基本用法:** + +```bash +sealctl tar [flags] [options] +``` + +## sealctl untar 命令 + +`sealctl untar` 命令的主要作用是在指定路径 `src` 查找匹配 glob 模式的归档文件,并在 `dst` 路径进行解压。 + +**命令参数:** + +- `--clear`:是否在解压完成后删除源文件,默认为 false。 +- `-o, --output`:解压归档文件的路径。 + +**基本用法:** + +```bash +sealctl untar [flags] [options] +``` + +## 使用示例 + +以下是一些 `sealctl tar` 和 `sealctl untar` 命令的使用示例: + +**创建一个压缩文件:** + +```bash +sealctl tar --output=/path/to/archive.tar /path/to/source +``` + +以上命令将 `source` 目录压缩为 `archive.tar` 文件。 + +**解压一个压缩文件:** + +```bash +sealctl untar --output=/path/to/destination /path/to/archive.tar +``` + +以上命令将 `archive.tar` 文件解压到 `destination` 目录。 + +通过 `sealctl tar` 和 `sealctl untar` 命令,用户可以轻松地进行文件或文件夹的压缩和解压操作。这两个命令在文件管理中,尤其是在备份和迁移文件时,都是非常有用的工具。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/token.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/token.md new file mode 100644 index 000000000000..8a9c08571ae6 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealctl/token.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 8 +--- + +# token 管理 + +`sealctl token` 命令的主要目的是为了生成一个用于连接主节点(master)和工作节点(node)的 token。在 Kubernetes 集群中,当您想要将一个新的工作节点加入到集群时,通常需要提供一个 token 作为身份验证。这个 token 确保只有拥有正确 token 的工作节点才能加入到集群中。 + +`sealctl token` 命令通过接收配置文件(可选)和证书密钥(可选)作为参数,生成一个用于身份验证的 token。在默认情况下,如果不提供配置文件和证书密钥,命令会使用内置的默认设置来生成 token。 + +总之,`sealctl token` 命令用于生成一个用于身份验证的 token,允许工作节点安全地加入到 Kubernetes 集群中。使用这个命令可以简化节点加入集群的过程,确保集群的安全性。 + + +**用法** + +```shell +sealctl token [config] [certificateKey] +``` + +**参数** + +- `config`: 配置文件(可选)。 +- `certificateKey`: 证书密钥(可选)。 + +**示例** + +使用默认参数生成 token: + +```shell +sealctl token +``` + +使用自定义配置文件和证书密钥生成 token: + +```shell +sealctl token my-config my-certificate-key +``` + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/_category_.json b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/_category_.json new file mode 100644 index 000000000000..c9cb98244ebc --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 1 +} diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md new file mode 100644 index 000000000000..ec858e07d4cd --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 11 +--- + +# 应用集群镜像使用指南 + +Sealos 提供了一个名为 [cluster-image](https://github.com/labring-actions/cluster-image) 的仓库,该仓库在 GitHub 上用于构建并发布 Kubernetes 集群的应用镜像。这些镜像可以通过提交代码到这个仓库来创建,并可以发布到 `docker.io/labring/` 作为官方的应用镜像。它支持构建 Docker 容器镜像以及应用的集群镜像。 + +## 镜像的类型 + +仓库支持三种类型的镜像构建: + +- **APP集群镜像**:主要是构建应用镜像,使用 GitHub Action,会同时支持 amd64 和 arm64 架构。 +- **配置集群镜像**:主要是构建配置镜像,使用 GitHub Action,没有容器镜像不区分架构,一般是一些脚本相关的配置或者覆盖默认的配置镜像。 +- **Docker镜像**:主要是构建容器镜像,使用 GitHub Action,会同时支持 amd64 和 arm64 架构。 + +## 镜像构建的工作流程 + +你可以直接在 GitHub 仓库中创建 Issue 来触发镜像的构建。这里提供了几个示例可以参考: + +- `/imagebuild_dockerimages helm v3.8.2 Key1=Value1,Key2=Value2` +- `/imagebuild_configs coredns v0.0.1` +- `/imagebuild_apps helm v3.8.2` + +每种类型的镜像构建命令的格式为 `/imagebuild_<类型> <应用名称> <版本> [Key=Value,...]`,其中 `<类型>` 是 `dockerimages`、`configs` 或 `apps`, `<应用名称>` 和 `<版本>` 分别代表应用的名称和版本,`[Key=Value,...]` 是可选的buildArg参数,仅用于 `dockerimages` 类型。 + +## 镜像配置的存放位置 + +你可以在 `applications/<应用名称>/<版本>/` 目录下放置你的配置文件,包括 Dockerfile、Kubefile 和 init.sh 等。init.sh 脚本通常用于下载一些依赖的二进制文件,如 helm、kubectl-minio 等。你可以选择使用 Dockerfile 或 Kubefile 来编写你的镜像构建逻辑。 + +## 镜像构建规则 + +对于每种类型的镜像,构建规则略有不同。通常,你需要在应用的目录下创建不同的子目录并放置不同类型的文件,然后 Sealos 会根据这些文件来构建镜像。具体的规则如下: + +1. `charts` 目录:放置一些集群镜像需要的 Helm chart,Kubernetes 会根据扫描的 chart 获取镜像并构建 + +出 registry 目录放到与 Kubefile 同级的目录。 +2. `manifests` 目录:直接放置一些 Kubernetes yaml 配置,Kubernetes 会扫描 manifests 目录所有的镜像并构建出 registry 目录放到与 Kubefile 同级的目录。 +3. `images/shim` 目录:主要存储一些额外的镜像列表并构建出 registry 目录放到与 Kubefile 同级的目录。 +4. 如果需要模板,在 `etc`、`charts`、`manifests` 放置一些以 `.tmpl` 结尾的文件可以被 `sealos run` 环境变量渲染后去掉 `.tmpl`,比如渲染之前是 `aa.yaml.tmpl`,渲染后为 `aa.yaml`。请注意文件名不要与现有的文件冲突。 +5. `registry` 必须放在与 Kubefile 同级的目录,否则无法拷贝到 master0 的私有仓库。制作镜像时也需要注意这一点。不要把 registry 存放到 chart 里,否则 helm 扫描可能会很慢,可能导致 OOM。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/add.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/add.md new file mode 100644 index 000000000000..f5f6e79bfc9e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/add.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 4 +--- + +# add 集群节点添加 + +`sealos add` 是 Sealos 命令行工具中的一个命令,主要用于向集群中添加节点。本指南将详细介绍其使用方法和选项。 + +**注意要保证控制节点的个数为奇数个以保证etcd可以正常选举** + +## 基本用法 + +### 添加节点 + +要向集群中添加节点,可以使用 `--nodes` 选项: + +```bash +sealos add --nodes x.x.x.x +``` + +在上述命令中,`x.x.x.x` 应替换为你想要添加的节点的 IP 地址。 + +### 添加控制节点 + +要向集群中添加控制节点,可以使用 `--masters` 选项: + +```bash +sealos add --masters x.x.x.x +``` + +### 同时添加控制节点和节点 + +如果你想同时向集群中添加控制节点和节点,可以同时使用 `--masters` 和 `--nodes` 选项: + +```bash +sealos add --masters x.x.x.x --nodes x.x.x.x +sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## 选项 + +`sealos add` 命令提供了以下选项: + +- `--cluster='default'`: 要执行加入操作的集群的名称。默认为 `default`。 + +- `--masters=''`: 要加入的主节点。 + +- `--nodes=''`: 要加入的节点。 + +每个选项后都可以跟随一个参数。 + +## 使用示例 + +以下是一个使用示例,该示例向集群中添加了 IP 地址为 `192.168.0.2` 的节点: + +```bash +sealos add --nodes 192.168.0.2 +``` + +以上就是 `sealos add` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/apply.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/apply.md new file mode 100644 index 000000000000..defa546ea78b --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/apply.md @@ -0,0 +1,84 @@ +--- +sidebar_position: 1 +--- + +# apply 启动集群 + +`sealos apply` 是 Sealos 命令行工具中的一个重要命令,用于在 Kubernetes 集群中运行集群镜像。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +`sealos apply` 命令的基本用法如下: + +```shell +$ sealos apply -f Clusterfile +``` + +Clusterfile 内容: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: default +spec: + # 服务器 IP 地址列表和角色 + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +``` + +这条命令会根据指定的 `Clusterfile` 文件在 Kubernetes 集群中运行集群镜像。 + +## 选项 + +`sealos apply` 命令提供了多种选项,用于定制命令的行为: + +- `-f, --Clusterfile='Clusterfile'`: 指定要应用的集群文件。默认为 `Clusterfile`。 +- `--config-file=[]`: 指定自定义Config文件的路径,用于替换或者修改资源。 +- `--env=[]`: 设置在命令执行过程中要使用的环境变量。 +- `--set=[]`: 在命令行上设置值,一般是替换模板的值。 +- `--values=[]`: 指定要应用到 `Clusterfile` 的values文件,一般是用于模板方式。 + +每个选项后面都可以跟随一个或多个参数。多个参数之间用逗号分隔。 + +例如,你可以使用 `--set` 选项在命令行上设置一些值: + +```shell +sealos apply -f Clusterfile --set key1=value1,key2=value2 +``` + +这条命令会将 `key1` 和 `key2` 的值设置为 `value1` 和 `value2`,然后应用 `Clusterfile`。 + +同样,你也可以使用 `--values` 选项指定一个值文件: + +```shell +sealos apply -f Clusterfile --values values.yaml +``` + +这条命令会根据 `values.yaml` 文件中的值应用 `Clusterfile`。 + +**更多示例请参考[启动镜像](/self-hosting/lifecycle-management/operations/run-cluster/)** + +以上就是 `sealos apply` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/build.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/build.md new file mode 100644 index 000000000000..4cf6b5ca5ae2 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/build.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 6 +--- + +# build 构建镜像 + +Sealos 的 `build` 命令用于使用 Sealfiles、Kubefiles、Dockerfiles 或 Containerfiles 中的指令构建 OCI 镜像。这是 Sealos 构建集群镜像的基础命令。 + +如果没有指定任何参数,Sealos 将使用当前工作目录作为构建上下文,并查找指令文件。如果不存在 Sealfile、Kubefile、Dockerfile 或 Containerfile,则构建失败。 + +下面是一些主要的 `build` 选项: + +1. `--all-platforms`:尝试为所有基础镜像平台构建镜像。 +2. `--authfile`:认证文件的路径。 +3. `--build-arg`:向构建器提供的 `argument=value`。 +4. `--build-context`:向构建器提供额外构建上下文的 `argument=value`。 +5. `--creds`:访问 registry 使用的 `[username[:password]]`。 +6. `-D, --disable-compression`:默认不压缩图层。 +7. `--env`:为镜像设置环境变量。 +8. `-f, --file`:Dockerfile 的 `pathname 或 URL`。 +9. `--force-rm`:即使构建不成功,也始终在构建后删除中间容器。 +10. `--format`:构建的镜像的清单和元数据的 `format`。 +11. `--from`:用于替换 Containerfile 中第一条 FROM 指令的值的镜像名称。 +12. `--http-proxy`:传递 HTTP Proxy 环境变量。 +13. `--isolation`:使用的进程隔离 `type`。可以是 'oci' 或 'chroot'。 +14. `--max-pull-procs`:拉取时使用的最大 goroutine 数量。 +15. `--platform`:设置镜像的 OS/ARCH/VARIANT 为提供的值,而不是主机的当前操作系统和架构。 +16. `--pull`:从 registry 拉取镜像,如果新的或存储中不存在,则拉取,如果 false,只有在不存在时才拉取镜像,如果 always,即使命名的镜像存在于存储中,也拉取镜像,如果 never,只使用存储中可用的镜像。 +17. `-q, --quiet`:克制不宣布构建指令和镜像读/写进度。 +18. `--retry`:在执行 push/pull 失败时重试的次数。 +19. `--retry-delay`:在 push/pull 失败时重试的延迟。 +20. `--rm`:在成功构建后删除中间容器。 +21. `--save-image`:保存从特定目录解析的镜像,以 registry 格式存储。 +22. `--sign-by`:使用指定 `FINGERPRINT` 的 GPG 密钥签名镜像。 +23. `-t, --tag`:应用到构建镜像的标签 `name`。 + +24. `--target`:设置要构建的目标构建阶段。 +25. `--timestamp`:将创建的时间戳设置为指定的 epoch 秒,以允许确定性构建,默认为当前时间。 + +这些选项可以灵活地应对多种构建需求,包括针对特定平台的构建、环境变量设置、构建上下文管理、镜像签名等。使用 `--save-image` 选项,Sealos 可以自动识别镜像列表(包括从镜像列表、Helm charts、manifests 中解析出的镜像)并保存为 registry 格式。 + +进程隔离模式 `--isolation` 支持 'oci' 和 'chroot' 两个参数。如果本地支持 OCI,可以选择 'oci' 模式;如果不支持 OCI,应该使用 'chroot' 模式。 + +`--save-image` 是 Sealos 构建命令的一个选项,这个选项的作用是在构建过程中自动查找并保存需要的镜像。在 Sealos 中,构建一个镜像可能涉及到其他依赖镜像。这些依赖镜像可能来自镜像列表、Helm charts 或 集群 manifests。当使用 `--save-image` 选项时,Sealos 将根据构建上下文,自动解析这些依赖并将其保存为 Docker Registry 格式。 + +例如,以下是一个使用 `--save-image` 选项的示例: + +```bash +sealos build -t myapp:v1.0.0 -f Dockerfile . +``` + +在这个示例中,Sealos 将使用当前目录作为构建上下文,从 Dockerfile 文件中读取构建指令,并尝试构建出一个标记为 `myapp:v1.0.0` 的镜像。同时,Sealos 将解析 Dockerfile 文件中所有 `FROM` 指令引用的基础镜像,并将这些镜像保存下来。这些镜像将以 Docker Registry 的格式保存,可以被直接推送到 Docker Registry 中。 + +如果你的构建上下文中还包含了 Helm charts 或 集群manifests,Sealos 也会解析这些文件中引用的镜像,并将这些镜像一并保存。 + +总的来说,`--save-image` 选项为 Sealos 的构建过程提供了一种自动处理依赖镜像的方式,大大提高了构建镜像的便捷性和效率。 + +下面有一些详细的示例: + +- [基于镜像清单构建](/self-hosting/lifecycle-management/operations/build-image/build-image-image_list.md) +- [基于部署清单构建](/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md) +- [基于helm-charts构建](/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [基于二进制构建](/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md) +- [基于go-template构建](/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md) +- [基于exec和scp构建](/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md) + +通过 Sealos `build` 命令,可以基于多种指令文件构建 OCI 镜像,为sealos提供所需的镜像。这个过程包括处理 Dockerfile 或其他指令文件中的各种指令,如 `FROM`、`RUN`、`ADD` 等,以及处理镜像层次、镜像标签等。构建过程也包括拉取基础镜像、运行命令、保存结果等步骤。每一个步骤都可以通过上述的选项进行详细的控制和定制,以满足不同的构建需求。 + +以上就是 `sealos build` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/cert.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/cert.md new file mode 100644 index 000000000000..c41a88581f69 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/cert.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 3 +--- + +# cert 更新集群证书 + +`sealos cert` 是 Sealos 命令行工具中的一个命令,主要用于在集群中更新 API 服务器的证书。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +要在证书中添加域名或 IP,可以使用 `--alt-names` 选项: + +```bash +sealos cert --alt-names sealos.io,10.103.97.2,127.0.0.1,localhost +``` + +在上述命令中,`sealos.io,10.103.97.2,127.0.0.1,localhost` 应替换为你想要添加的域名和 IP 地址。 + +**注意**:在执行此操作之前,你最好先备份旧的证书。 + +执行 `sealos cert` 命令后,会更新集群 API 服务器的证书,你无需手动重启 API 服务器,sealos会自动帮你重启服务。 + +## 选项 + +`sealos cert` 命令提供了以下选项: + +- `--alt-names=''`: 在证书中添加域名或 IP,例如 `sealos.io` 或 `10.103.97.2`。 + +- `-c, --cluster='default'`: 要执行 exec 操作的集群的名称。默认为 `default`。 + +每个选项后都可以跟随一个参数。 + +## 校验证书 + +更新证书后,你可以使用以下命令进行校验: + +```bash +kubectl -n kube-system get cm kubeadm-config -o yaml +openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text +``` + +上述命令将获取 kube-system 命名空间中的 kubeadm-config 配置映射,并显示 apiserver.crt 证书的详细信息。 + +以上就是 `sealos cert` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/commands.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/commands.md new file mode 100644 index 000000000000..cff45b9b3838 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/commands.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 0 +--- + +# Sealos 命令说明 + +## 集群管理命令 + +- `apply`:使用 Clusterfile 在 Kubernetes 集群内运行集群镜像。 +- `cert`:更新 Kubernetes API 服务器的证书。 +- `run`:轻松运行云原生应用。 +- `reset`:重置集群中的所有内容。 +- `status`:查看 Sealos集群 的状态。 + +## 节点管理命令 + +- `add`:将节点添加到集群中。 +- `delete`:从集群中删除节点。 + +## 远程操作命令 + +- `exec`:在指定节点上执行 shell 命令或脚本。 +- `scp`:将文件复制到指定节点的远程位置。 + +## 实验性命令 + +- `registry`:与镜像仓库相关的命令。 + +## 容器和镜像命令 + +- `build`:使用 Sealfile 或 Kubefile 中的指令构建镜像。 +- `create`:创建集群,但不运行 CMD,用于检查镜像。 +- `inspect`:检查容器或镜像的配置。 +- `images`:列出本地存储中的镜像。 +- `load`:从文件中加载镜像。 +- `login`:登录到容器仓库。 +- `logout`:登出容器仓库。 +- `manifest`:操作清单列表和镜像索引。 +- `merge`:合并多个镜像为一个。 +- `pull`:从指定位置拉取镜像。 +- `push`:将镜像推送到指定的目标。 +- `rmi`:从本地存储中删除一个或多个镜像。 +- `save`:将镜像保存到存档文件中。 +- `tag`:为本地镜像添加一个附加名称。 + +## 其他命令 + +- `completion`:为指定的 shell 生成自动补全脚本。 +- `docs`:生成 API 参考。 +- `env`:打印 Sealos 使用的所有环境信息。 +- `gen`:生成具有所有默认设置的 Clusterfile。 +- `version`:打印版本信息。 + +Sealos 的 `--debug` 参数是一个全局参数,用于开启调试模式,以便在出现问题时能更详细地了解系统的运行情况。 + +有关安装说明,请参见[下载 Sealos 命令行工具](/self-hosting/lifecycle-management/quick-start/install-cli.md); 如需安装 Kubernetes 集群,请参见[安装 Kubernetes 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md)。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/create.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/create.md new file mode 100644 index 000000000000..03cf8cf9d074 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/create.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 6 +--- + +# create 创建工作目录 + +`sealos create` 是 Sealos 命令行工具中的一个命令,主要用于在不执行 CMD 的情况下创建集群工作目录,以便审查镜像。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +`sealos create` 命令用于创建集群工作目录,但不实际运行,主要用于调试或测试,它可以输出集群镜像的地址,你可以校验集群镜像内容是否与预期一致。 + +```bash +sealos create docker.io/labring/kubernetes:v1.24.0 +``` + +在上述命令中,`clustername` 代表你要创建的集群的名称。 + +## 选项 + +`sealos create` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要创建但不实际运行的集群的名称。默认为 `default`。 + +- `--platform='linux/arm64/v8'`: 将镜像的操作系统/架构/版本设置为提供的值,而不是主机的当前操作系统和架构(例如 `linux/arm`)。 + +- `--short=false`: 如果为真,只打印挂载路径。 + +- `-e, --env=[]`: 指定渲染模板文件时使用的环境变量。 + +每个选项后都可以跟随一个参数。 + +## 示例 + +例如,你可以使用以下命令创建一个名为 `mycluster` 的集群,但不实际运行它: + +```bash +sealos create -e registryPort=8443 docker.io/labring/kubernetes:v1.24.0 +``` + +此命令将创建一个镜像名称为 `docker.io/labring/kubernetes:v1.24.0` 的集群工作目录,并输出集群镜像的地址。`-e registryPort=8443` 选项指定了在渲染模板文件时使用的环境变量,其中 `registryPort` 被设置为 `8443`。请注意,这个示例中集群并没有被实际运行。 + +以上就是 `sealos create` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/delete.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/delete.md new file mode 100644 index 000000000000..6072f79d58a5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/delete.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 4 +--- + +# delete 集群节点删除 + +`sealos delete` 是 Sealos 命令行工具中的一个命令,主要用于从集群中移除节点。本指南将详细介绍其使用方法和选项。 + +**注意要保证控制节点的个数为奇数个以保证etcd可以正常选举** + +## 基本用法 + +### 删除节点 + +要从集群中删除节点,可以使用 `--nodes` 选项: + +```bash +sealos delete --nodes x.x.x.x +``` + +在上述命令中,`x.x.x.x` 应替换为你想要删除的节点的 IP 地址。如果不小心删除了错误的节点,可以使用 `sealos add` 命令恢复它: + +```bash +sealos add --nodes x.x.x.x +``` + +### 删除控制节点 + +要从集群中删除控制节点,可以使用 `--masters` 选项: + +```bash +sealos delete --masters x.x.x.x +``` + +请注意,如果指定了 `--masters` 参数,sealos 将删除你的控制节点。 + +### 删除控制节点和节点 + +如果你想同时删除控制节点和节点,可以同时使用 `--masters` 和 `--nodes` 选项: + +```bash +sealos delete --masters x.x.x.x --nodes x.x.x.x +sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## 选项 + +`sealos delete` 命令提供了以下选项: + +- `--cluster='default'`: 执行删除操作应用的集群的名称。默认为 `default`。 + +- `--force=false`: 可以输入一个 `--force` 标志以强制删除节点。 + +- `--masters=''`: 要移除的控制节点。 + +- `--nodes=''`: 要移除的节点。 + +每个选项后都可以跟随一个参数。 + +## 使用示例 + +以下是一个使用示例,该示例删除了 IP 地址为 `192.168.0.2` 的节点: + +```bash +sealos delete --nodes 192.168.0.2 +``` + +以上就是 `sealos delete` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/env.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/env.md new file mode 100644 index 000000000000..89ea851ea940 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/env.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 7 +--- + +# env 环境变量 + +`sealos env` 是 Sealos 命令行工具中的一个命令,用于展示目前sealos支持的环境变量以及当前的环境变量值。 + +## 基本用法 + +### 查看环境变量 + +要查看环境变量,可以使用 `sealos env` 命令: + +```bash +sealos env +``` + +### 查看环境变量以及说明 + +要查看环境变量以及说明,可以使用 `sealos env -v` 命令: + +```bash +sealos env -v +``` + + +## 如何设置环境变量 + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t xxx . +``` + + +以上就是 `sealos env` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/exec.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/exec.md new file mode 100644 index 000000000000..7b0f955021f1 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/exec.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# exec 执行命令 + +`sealos exec` 是 Sealos 命令行工具中的一个命令,用于在指定的集群节点上执行 Shell 命令或脚本。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +基本的 `sealos exec` 命令格式如下: + +```bash +sealos exec "shell command or script" +``` + +在上述命令中,`shell command or script` 是你要在集群节点上执行的 Shell 命令或脚本。 + +## 选项 + +`sealos exec` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要在其上执行命令的集群的名称。默认为 `default`。 + +- `--ips=[]`: 在具有指定 IP 地址的节点上运行命令。 + +- `-r, --roles='':`: 在具有指定角色的节点上运行命令。目前支持 master,node,registry + +每个选项后都可以跟随一个或多个参数。 + +## 示例 + +例如,你可以使用以下命令在默认集群的所有节点上查看 `/etc/hosts` 文件的内容: + +```bash +sealos exec "cat /etc/hosts" +``` + +如果你想在名为 `my-cluster` 的集群的 `master` 和 `node` 角色的节点上查看 `/etc/hosts` 文件的内容,可以使用以下命令: + +```bash +sealos exec -c my-cluster -r master,node "cat /etc/hosts" +``` + +如果你只想在 IP 地址为 `172.16.1.38` 的节点上查看 `/etc/hosts` 文件的内容,可以使用以下命令: + +```bash +sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" +``` + +以上就是 `sealos exec` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/gen.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/gen.md new file mode 100644 index 000000000000..2df2301ef040 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/gen.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 3 +--- + +# gen 生成集群配置 + +Sealos 的 `gen` 命令是用于生成 Kubernetes 集群的配置文件(Clusterfile),这个配置文件可以在之后通过 `sealos apply` 命令来应用。`gen` 命令可以帮助用户快速生成一个基本的配置文件,用户可以在此基础上根据自己的需求进行修改和调整。 + +下面是 `sealos gen` 命令的基本使用方法和一些常见的示例: + +1. 生成一个默认配置的单节点集群: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 + ``` + +注意:labring/helm 应当在 labring/calico 之前。 + +2. 生成一个包含多个镜像、指定了主节点和工作节点的集群: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' + ``` + +注意:labring/helm 应当在 labring/calico 之前。 + +3. 指定 SSH 端口,对于所有服务器使用相同的 SSH 端口: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' + ``` + + 对于使用不同 SSH 端口的服务器: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ + --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd 'xxx' + ``` + +在生成了 Clusterfile 之后,用户可以根据自己的需求来修改这个文件。添加或修改环境变量;修改集群cidr配置。完成修改后,用户就可以通过 `sealos apply` 命令来根据这个配置文件来创建或更新集群了。 + +示例说明: + +- [自定义配置安装](/self-hosting/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) + +以上就是 `sealos gen` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/images.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/images.md new file mode 100644 index 000000000000..ad080890832f --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/images.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +--- + +# image 镜像列表 + +Sealos 的 `images` 命令主要用于查看本地存储的镜像。用户可以通过它来查看本地所有的镜像,或者筛选查看特定的镜像。该命令支持多种参数,可以帮助用户更方便的查看和管理镜像。 + +## 基本用法 + +基本的 `sealos images` 命令将显示所有非中间阶段的本地镜像,例如: + +```bash +sealos images +``` + +这将显示本地存储的所有最终阶段镜像。 + +## 示例 + +以下是 `sealos images` 命令的一些常见示例: + +1. 显示所有镜像,包括构建的中间镜像: + + ```bash + sealos images --all + ``` + +2. 显示特定镜像: + + ```bash + sealos images [imageName] + ``` + +3. 以指定的 Go 模板格式显示镜像: + + ```bash + sealos images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}' + ``` + +## 参数 + +以下是 `sealos images` 命令的一些常用参数: + +- `-a, --all`:显示所有镜像,包括构建过程中的中间镜像。 + +- `--digests`:显示镜像的摘要。 + +- `-f, --filter`:根据提供的条件过滤输出结果。 + +- `--format`:使用 Go 模板对镜像进行美化打印。 + +- `--history`:显示镜像的命名历史。 + +- `--json`:以 JSON 格式输出。 + +- `--no-trunc`:不截断输出。 + +- `-n, --noheading`:不打印列标题。 + +- `-q, --quiet`:只显示镜像 ID。 + +通过组合使用这些参数,用户可以轻松地获取和管理本地存储的镜像。例如,使用 `--all` 参数可以查看所有镜像,包括中间镜像;使用 `--filter` 参数可以根据特定条件过滤镜像;使用 `--json` 参数可以以 JSON 格式输出镜像信息,方便进行程序化处理等。 + +以上就是 `sealos images` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md new file mode 100644 index 000000000000..bed4b134332e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/inspect.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 6 +--- + +# inspect 详细信息 + +Sealos 的 `inspect` 命令主要用于查看构建容器或已构建镜像的配置信息。该命令支持查看镜像或容器的详细信息,包括其元数据、环境变量、启动命令等。 + +## 基本用法 + +使用 `sealos inspect` 命令查看指定容器或镜像的配置信息。例如,查看指定容器的配置: + +```bash +sealos inspect containerID +``` + +或者查看指定镜像的配置: + +```bash +sealos inspect --type image imageWithTag +``` + +## 示例 + +以下是 `sealos inspect` 命令的一些常见示例: + +1. 查看容器配置: + + ```bash + sealos inspect containerID + ``` + +2. 查看镜像配置: + + ```bash + sealos inspect --type image imageWithTag + ``` + +3. 查看镜像ID的配置信息: + + ```bash + sealos inspect --type image @imageID # 或直接输入imageID, '@' 是可选的 + ``` + +4. 查看远程镜像仓库的配置信息: + + ```bash + sealos inspect --type image docker://alpine:latest + ``` + +5. 查看本地OCI归档文件中镜像的配置信息: + + ```bash + sealos inspect --type image oci-archive:/abs/path/of/oci/tarfile.tar + ``` + +6. 查看本地Docker归档文件中镜像的配置信息: + + ```bash + sealos inspect --type image docker-archive:/abs/path/of/docker/tarfile.tar + ``` + +7. 使用 Go 模板格式显示镜像环境变量: + + ```bash + sealos inspect --format '{{.OCIv1.Config.Env}}' alpine + ``` + +## 参数 + +以下是 `sealos inspect` 命令的一些常用参数: + +- `-f, --format`:使用 Go 模板格式显示输出结果。**模板结构代码[InspectOutput](https://github.com/labring/sealos/blob/f8a17787822714c5fdf21f2a75cc86fadb88adfa/pkg/buildah/inspect.go#L189)** + +- `-t, --type`:指定查看的类型,可以是容器(`container`)或镜像(`image`)。 + +根据你的需要,你可以结合使用这些参数,以获取特定的配置信息。例如,使用 `-t` 参数可以指定你想要查看的是容器的配置信息还是镜像的配置信息;使用 `-f` 参数,可以定义特定的输出格式,方便对输出结果进行处理或解析。 + +以上就是 `sealos inspect` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/load.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/load.md new file mode 100644 index 000000000000..d85772714ac9 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/load.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 6 +--- + +# load 加载镜像 + +`sealos load` 是一个用来从存档文件中加载镜像的命令。这对于需要从已有的存档文件中导入镜像非常有用,尤其是在没有网络连接的环境中。 + +## 用法: + +`sealos load [flags] [options]` + +## 参数: + +以下是 `sealos load` 命令的参数: + +- `-i, --input=''`: 从 tar 存档文件中加载镜像。 + +## 示例: + +- 从一个存档文件中加载镜像:`sealos load -i myimage.tar` + +注意,在使用 `sealos load` 命令时,你需要确保指定的存档文件存在,并且格式正确。如果你在导入镜像时遇到问题,你可能需要检查你的存档文件,以确保它们没有被损坏或格式化错误。 + +以上就是 `sealos load` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/login.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/login.md new file mode 100644 index 000000000000..cec64c82b0d3 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/login.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 6 +--- + +# login 登录仓库 + +`sealos login` 命令用于在指定服务器上登录容器注册表。登录注册表后,你可以拉取、推送镜像。 + +## 用法: + +`sealos login [flags] [options] registryName` + +## 参数: + +以下是 `sealos login` 命令的参数: + +- `--authfile=''`: 身份验证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 来覆盖。 + +- `--cert-dir=''`: 使用指定路径的证书来访问镜像仓库。 + +- `--get-login=true`: 返回注册表的当前登录用户。 + +- `-k, --kubeconfig=''`: 使用 kubeconfig 登录到 sealos 镜像仓库 hub.sealos.io。 + +- `-p, --password=''`: 注册表的密码。 + +- `--password-stdin=false`: 从标准输入获取密码。 + +- `-u, --username=''`: 注册表的用户名。 + +- `-v, --verbose=false`: 将更详细的信息写入标准输出。 + +## 示例: + +- 登录到 quay.io 注册表:`sealos login -u myusername -p mypassword quay.io ` + +注意,在使用 `sealos login` 命令时,你需要确保提供了正确的用户名和密码,否则登录过程可能会失败。如果你在登录过程中遇到问题,你可能需要检查你的用户名和密码,以确保它们没有输入错误或被遗忘。 + +以上就是 `sealos login` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/logout.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/logout.md new file mode 100644 index 000000000000..843b157a1ba8 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/logout.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 6 +--- + +# logout 登出仓库 + +`sealos logout` 命令用于在指定服务器上移除本地缓存的镜像仓库的账号和密码。 + +## 用法: + +`sealos logout [flags] [options] registryName` + +## 参数: + +以下是 `sealos logout` 命令的参数: + +- `--authfile=''`: 身份验证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 来覆盖。 + +- `-a, --all=false`: 删除所有的认证信息。 + + +## 示例: + +- 登出到 quay.io 镜像仓库:`sealos logout quay.io ` + +以上就是 `sealos logout` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md new file mode 100644 index 000000000000..52d7809bc1be --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/manifest.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 6 +--- + +# manifest 镜像清单 + +Sealos 的 `manifest` 命令用于创建、修改和推送 manifest 列表和镜像索引。这些功能主要用于处理镜像的多架构支持。在 Docker 和 OCI 镜像规范中,manifest 列表(也被称为 "fat manifest")或镜像索引允许一个镜像标签(如 `myimage:latest`)在多种硬件架构(如 amd64, arm64, ppc64le 等)上都能使用。 + +以下是一些主要的 `manifest` 子命令: + +1. `create`:创建新的 manifest 列表或镜像索引。例如:`sealos manifest create localhost/list` +2. `add`:将镜像添加到 manifest 列表或镜像索引中。例如:`sealos manifest add localhost/list localhost/image` +3. `annotate`:在 manifest 列表或镜像索引的条目中添加或更新信息。例如:`sealos manifest annotate --annotation A=B localhost/list localhost/image` +4. `inspect`:显示 manifest 列表或镜像索引的内容。例如:`sealos manifest inspect localhost/list` +5. `push`:将 manifest 列表或镜像索引推送到 registry。例如:`sealos manifest push localhost/list transport:destination` +6. `remove` 和 `rm`:从 manifest 列表或镜像索引中移除条目,或者完全删除 manifest 列表或镜像索引。例如:`sealos manifest remove localhost/list sha256:entryManifestDigest` 或 `sealos manifest rm localhost/list` + +通过 `sealos manifest` 命令,可以灵活地管理 manifest 列表或镜像索引,为多架构的 Docker 或 OCI 镜像提供支持。用户可以根据自己的需求,创建自定义的 manifest 列表,方便在不同的硬件架构上部署和运行 Docker 镜像。 + +用户如果想通过manifest命令构建多架构镜像,可以参考文档[构建支持多架构的集群镜像](/self-hosting/lifecycle-management/operations/build-image/build-multi-arch-image.md) diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/merge.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/merge.md new file mode 100644 index 000000000000..e6f01f2dfc8a --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/merge.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 6 +--- + +# merge 合并镜像 + +Sealos 的 `merge` 命令的主要作用是将多个镜像合并为一个。它通过读取各个输入镜像的 Dockerfile,将其中的命令和层次结构合并到一个新的镜像中。这个命令的运行逻辑很像 `build` 命令,许多参数也是相同的。 + +这个功能在多个镜像有共享层的情况下非常有用,因为它可以减少镜像的大小,节省存储空间。同时,由于合并后的镜像包含了多个镜像的全部功能,所以它可以帮助简化应用部署。 + +以下是 `sealos merge` 的基本使用示例: + +```bash +sealos merge -t new:0.1.0 kubernetes:v1.19.9 mysql:5.7.0 redis:6.0.0 +``` + +在这个示例中,`kubernetes:v1.19.9`、`mysql:5.7.0` 和 `redis:6.0.0` 这三个镜像被合并为一个新的镜像 `new:0.1.0`。 + +`sealos merge` 命令提供了丰富的选项来定制合并过程,例如 `--all-platforms` 用于尝试为所有基础镜像平台构建镜像,`--build-arg` 用于向构建器提供参数,`--no-cache` 用于禁用现有的缓存镜像,等等。 + +请注意,`sealos merge` 命令会根据各个输入镜像的 Dockerfile 来构建新的镜像,所以如果输入镜像的 Dockerfile 不兼容,或者有任何构建错误,那么这个命令可能会失败。在使用 `sealos merge` 命令时,请确保你了解每个输入镜像的 Dockerfile,并根据需要进行调整。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/pull.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/pull.md new file mode 100644 index 000000000000..6f52bed1a007 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/pull.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 6 +--- + +# pull 拉取镜像 + +`sealos pull` 是一个非常有用的命令,它可以从容器镜像仓库下载镜像并将其存储在本地。用户可以通过镜像的标签(tag)或摘要(digest)来获取镜像。如果没有指定标签,那么会默认下载带有 'latest' 标签(如果存在)的镜像。 + +通过使用这个命令,用户可以方便地从远程仓库下载所需的镜像,极大地提高了工作效率。 + +## 用法: + +`sealos pull [flags] [options] imageName` + +## 参数: + +以下是 `sealos pull` 命令的参数: + +- `-a, --all-tags=false`: 下载仓库中所有带有标签的镜像。 + +- `--authfile=''`: 认证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 进行覆盖。 + +- `--cert-dir=''`: 用于访问镜像仓库的证书的指定路径。 + +- `--creds=''`: 使用 `[username[:password]]` 访问镜像仓库。 + +- `--decryption-key=[]`: 解密镜像所需要的密钥。 + +- `--platform=[linux/arm64/v8]`: 选择镜像时,优先使用指定的 OS/ARCH,而不是当前操作系统和架构。 + +- `--policy='missing'`: 设置策略,可选的值包括 'missing', 'always', 'never'。 + +- `-q, --quiet=false`: 在拉取镜像时,不输出进度信息。 + +- `--remove-signatures=false`: 在拉取镜像时,不复制签名。 + +- `--retry=3`: 在拉取失败时的重试次数。 + +- `--retry-delay=2s`: 拉取失败时,重试之间的延迟。 + +## 示例: + +- 拉取一个镜像:`sealos pull my-image:latest` + +- 从 Docker 守护进程拉取一个镜像:`sealos pull docker-daemon:my-image:tag` + +- 从特定的仓库拉取一个镜像:`sealos pull myregistry/myrepository/my-image:tag` + +- 拉取多个镜像:`sealos pull imageID1 imageID2 imageID3` + +以上就是 `sealos push` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/push.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/push.md new file mode 100644 index 000000000000..9f5241bfd1d5 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/push.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 6 +--- + +# push 上传镜像 + +`sealos push` 是 Sealos 命令行工具中的一个命令,用于将镜像推送到指定的位置。这个命令在你需要将本地 Docker 镜像推送到远程镜像仓库的时候特别有用。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos push` 命令格式如下: + +```bash +sealos push IMAGE_ID DESTINATION +``` + +在上述命令中,`IMAGE_ID` 是你想要推送的镜像的 ID,而 `DESTINATION` 是你想要推送到的位置。 `DESTINATION` 使用 "transport:details" 格式,如果未指定,将复用源 IMAGE 作为 DESTINATION。 + +在 Sealos 中,传输方式定义了源镜像和目标镜像在复制过程中的格式和位置。以下是 Sealos 支持的各种传输方式: + +1. `containers-storage`: 此传输方式用于存储和管理在本地运行的容器。例如,使用 Podman 或 CRI-O 创建的容器的镜像。 + +2. `dir`: 这种传输方式将镜像存储在本地文件系统的一个目录中,该目录结构符合 OCI 布局。 + +3. `docker`: 这种传输方式用于与 Docker 注册表进行交互,如 Docker Hub 或任何其他兼容的私有注册表。 + +4. `docker-archive`: 此传输方式将镜像存储为一个本地的 Docker tar 文件(`.tar`),这是 Docker 的原生格式。 + +5. `docker-daemon`: 这种传输方式用于与本地 Docker 守护程序交互,可以从 Docker 守护程序中提取镜像,或者将镜像推送到 Docker 守护程序。 + +6. `oci`: 该传输方式将镜像存储在一个符合 OCI 布局的目录中,它是一种开放的容器镜像格式。 + +7. `oci-archive`: 这种传输方式将镜像存储为一个本地的 OCI tar 文件(`.tar`)。 + +8. `ostree`: 这种传输方式将镜像存储在 OSTree 存储库中,这是一种支持原子升级和回滚的文件系统。 + +9. `sif`: 这是 Singularity SIF 格式,主要用于高性能计算和数据密集型应用。 + +示例: + +- 将一个镜像推送到 Docker 注册表:`sealos push my-image:latest docker://my-registry.example.com/my-image:latest` + +- 将一个镜像从 Docker 守护程序导出:`sealos push docker-daemon:my-image:latest dir:/path/to/save/` + +- 将一个镜像推送到本地的容器存储:`sealos push my-image:latest containers-storage:my-new-image:latest` + +## 示例 + +例如,你可以使用以下命令将一个镜像推送到 `registry.example.com` 的仓库: + +```bash +sealos push my_image_id docker://registry.example.com/my_repository:my_tag +``` + +## 可选参数 + +- `--all`: 该参数用于推送清单列表引用的所有镜像。 + +- `--authfile`: 该参数用于指定身份验证文件的路径。 可以使用 REGISTRY_AUTH_FILE 环境变量进行覆盖。 + +- `--cert-dir`: 该参数用于指定访问注册表所需的证书的路径。 + +- `--compression-format`: 该参数用于指定要使用的压缩格式。 + +- `--compression-level`: 该参数用于指定要使用的压缩级别。 + +- `--cr-option` 参数是用于控制是否将镜像的自定义资源(Custom Resource,简称 CR)推送到目标镜像仓库的。 + + 具体来说,这个参数的可选值包括: + + - "yes": 将会把镜像以及其关联的 CR 都推送到目标镜像仓库。 + + - "no": 仅推送镜像,而不推送任何 CR。 + + - "only": 仅推送 CR,不推送镜像本身。 + + - "auto": 根据镜像和 CR 的实际状态自动决定是否推送。例如,如果 CR 有更改或者不存在于目标仓库,就会被推送。 + + 请注意,这个参数主要在处理包含自定义资源(如 Kubernetes CRD 对象)的镜像时使用,它能够让你更加灵活地控制镜像和 CR 的推送过程。 + +- `--creds`: 该参数用于访问注册表,使用 `[username[:password]]` 形式。 + +- `--digestfile`: 该参数在复制图像后,将结果图像的摘要写入文件。 + +- `-D`, `--disable-compression`: 该参数用于不压缩层。 + +- `--encrypt-layer`: 该参数用于指定要加密的层,0 索引层索引支持负索引(例如,0 是第一层,-1 是最后一层)。 如果未定义,则在指定 encryption-key 标志时将加密所有层。 + +- `--encryption-key`: 该参数用于指定加密图像所需的密钥,与加密协议一起使用(例如,jwe:/path/to/key.pem)。 + +- `-f`, `--format`: 该参数用于指定目标中要使用的清单类型(oci, v2s1, 或 v2s2)(默认是源的清单类型,带回退)。 + +- `-q`, `--quiet`: 该参数用于在推送图像时不输出进度信息。 + +- `--remove-signatures`: 该参数用于在推送图像时不复制签名。 + +- `--retry + +`: 该参数用于指定在推送/拉取失败时的重试次数。 + +- `--retry-delay`: 该参数用于指定在推送/拉取失败时重试之间的延迟。 + +- `--rm`: 该参数用于在推送成功后删除清单列表。 + +- `--sign-by`: 该参数用于使用指定的 `FINGERPRINT` 的 GPG 密钥签名图像。 + +以上就是 `sealos push` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/registry.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/registry.md new file mode 100644 index 000000000000..746b32a8f879 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/registry.md @@ -0,0 +1,232 @@ +--- +sidebar_position: 8 +--- + +# registry 镜像仓库命令 + +## Sealos:sealos registry save 命令详解与使用指南 + +`registry save` 命令用于将远程的 Docker 镜像拉取到本地并保存在指定的目录中。这对于在离线或者内网环境中部署容器镜像特别有用。 + +在执行 `registry save` 命令时,将自动获取 `sealos login` 认证信息进行仓库认证。 + +**使用说明** + +1. 使用context自动获取镜像 + + 使用默认方式拉取并保存镜像。这种模式会自动解析 `charts` 目录、`manifests` 目录和 `images` 目录以获取镜像列表。 + + **使用示例** + + ```shell + sealos registry save --registry-dir=/tmp/registry1 my-context + ``` + + +2. 指定镜像列表方式 + + 使用参数传入镜像列表 + + **使用示例** + + ```shell + sealos registry save --registry-dir=/tmp/registry2 --images=docker.io/library/busybox:latest + ``` + +**选项** + +以下选项适用于 `save` 命令及其子命令: + +- `--max-procs`: 拉取镜像时使用的最大并行进程数。 +- `--registry-dir`: 保存镜像的本地目录。 +- `--arch`: 镜像的目标架构,例如:`amd64`、`arm64` 等。 +- `--images`: 需要拉取并保存的镜像列表,以逗号分隔。例如:"my-image1:latest,my-image2:v1.0"。 + +## Sealos:sealos registry serve 命令详解与使用指南 + +在管理 Docker 镜像仓库过程中,Sealos 提供了 `sealos registry serve` 命令以方便用户进行相关操作。本文将详细介绍 `sealos registry serve` 命令的使用方法和示例。 + +### 基本介绍 + +`sealos registry serve` 命令的主要作用是启动一个 Docker 分发镜像仓库服务器,支持两种模式:`filesystem` 和 `inmem`。 + +1. **Filesystem 模式**:在此模式下,sealctl 将运行一个针对指定目录的 Docker 分发镜像仓库服务器。该模式下,镜像数据将存储在硬盘上。 + +2. **In-memory 模式**:在此模式下,sealctl 将运行一个内存中的 Docker 分发镜像仓库服务器。该模式下,镜像数据仅保存在内存中,进程退出后数据将丢失。 + +### 命令参数 + +`sealos registry serve filesystem ` 命令支持以下参数: + +- `--disable-logging`: 禁用日志输出,默认为 false。 +- `--log-level`: 配置日志级别,默认为 'error'。 +- `-p, --port`: 服务器监听的端口,默认为随机未使用的端口。 + +### 使用示例 + +以下是一些 `sealos registry serve` 命令的使用示例: + +#### 在文件系统中启动镜像仓库服务器 + +```bash +sealos registry serve filesystem --port=5000 +``` + +以上命令将在端口5000上启动一个文件系统镜像仓库服务器。 + +#### 在内存中启动镜像仓库服务器 + +```bash +sealos registry serve inmem +``` + +以上命令将启动一个内存镜像仓库服务器。该服务器在进程退出后,存储的数据将丢失。 + +通过 `sealctl registry serve` 命令,用户可以轻松地管理和操作 Docker 镜像仓库。无论是在开发环境,还是在生产环境中,它都是一个强大且易用的工具。 + + +## Sealos:sealos registry passwd 命令详解与使用指南 + +在管理 Docker 镜像仓库过程中,Sealos 提供了 `sealos registry passwd` 命令以方便用户对集群registry进行密码修改。它提供了一种简便的方法,帮助用户修改 registry 的密码。 + +### 基本用法 + +使用 `sealos registry passwd` 命令来修改registry的密码。 + +```bash +sealos registry passwd +``` + +### 参数 + +以下是 `sealos registry passwd` 命令的参数: + +- `-c, --cluster-name`:集群名称,默认为'default'。 + +- `-f, --cri-shim-file-path`:镜像 cri shim 文件路径,如果为空将不会更新镜像 cri shim 文件。默认路径为'/etc/image-cri-shim.yaml'。 + +- `-p, --htpasswd-path`:registry 密码文件路径。默认路径为'/etc/registry/registry_htpasswd'。 + +### 使用步骤 + +1. 执行 `sealos registry passwd` 命令,可以根据需要指定参数来进行配置。 + +2. 根据命令提示,输入新的密码。 + +3. 命令执行成功后,registry 的密码将被修改为新的密码。 + +### 演示说明 + +[![asciicast](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ.svg)](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ) + +**在使用过程中,会让用户选择registry类型** + +- registry: 二进制启动,执行`systemctl restart registry`进行重启镜像仓库。 +- containerd: containerd启动,执行"nerdctl restart sealos-registry"进行重启镜像仓库。 +- docker: docker启动,执行"docker restart sealos-registry"进行重启镜像仓库。 + +### 注意事项 + +**修改 registry 密码后,修改Clusterfile中的registry密码** +修改 registry 密码后,所有使用该 registry 的节点和服务都需要更新配置,以使用新的密码进行身份验证。否则,它们将无法从该 registry 拉取或推送镜像。 + +如果你不确定如何更新节点和服务的配置,建议在修改 registry 密码之前,先查阅相关文档或者寻求专业的技术支持。 + +## Sealos:`sealos registry sync` 命令详解与使用指南 + +Sealos 的 `registry sync` 命令可帮助您在两个 registry 之间同步所有镜像。这不仅可以用于镜像的迁移,还可以备份您的镜像。 + +### 命令基本用法 + +执行 `sealos registry sync` 命令来进行镜像同步: + +```bash +sealos registry sync source dst +``` + +这里的 `source` 表示源 registry 的地址,而 `dst` 是目标 registry 的地址。 + +例如,您想将地址为 127.0.0.1:41669 的 registry 中的所有镜像同步到地址为 sealos.hub:5000 的 registry,您应执行以下命令: + +```bash +sealos registry sync 127.0.0.1:41669 sealos.hub:5000 +``` + +### 认证与权限 + +在执行 `sealos registry sync` 命令之前,请确保您具有访问源 registry 和目标 registry 的权限。可以使用`sealos login`对registry进行认证登录。 + +### 同步过程 + +请注意,镜像同步可能需要一些时间,这取决于镜像的数量和大小,以及网络的速度。在同步过程中,请保持网络的连通性,并确保在同步完成之前不要中断命令的执行。 + +重要的是,`sealos registry sync` 命令支持增量同步,已经存在于目标 registry 的镜像不会重新同步。 + +### 参数选项 + +`sealos registry sync` 命令还提供了一些参数选项,允许您更精细地控制同步过程: + +- `--override-arch ARCH`:使用指定的 `ARCH` 替代当前机器的架构来选择镜像。 + +- `--override-os OS`:使用指定的 `OS` 替代当前操作系统来选择镜像。 + +- `--override-variant VARIANT`:使用指定的 `VARIANT` 替代当前的架构变种来选择镜像。 + +- `-a` 或 `--all`:如果源镜像是一个列表,同步所有镜像。这对异构环境下特别有用,因为默认情况下,只会同步当前架构的镜像。 + +例如,如果您想同步所有架构的镜像,可以添加 `-a` 参数: + +```bash +sealos registry sync -a 127.0.0.1:41669 sealos.hub:5000 +``` + +以上就是 `sealos registry sync` 命令的详细说明与使用指南。希望这些信息能帮助您更好地理解和使用这个命令。如果您在使用过程中遇到任何问题,欢迎随时提问。 + +## Sealos:`sealos registry copy` 命令详解与使用指南 + +Sealos 的 `registry copy` 命令用于将指定镜像从一个 registry 复制到另一个 registry。这能帮助您在不同的 registry 之间进行镜像的迁移或备份。 + +### 命令基本用法 + +使用 `sealos registry copy` 命令来进行镜像的复制: + +```bash +sealos registry copy source-image dst +``` + +这里的 `source-image` 表示源镜像的全名(包括地址和镜像名),`dst` 是目标 registry 的地址。 + +例如,要将名为 `127.0.0.1:41669/my-image:tag` 的镜像复制到地址为 `sealos.hub:5000` 的 registry,您可以执行以下命令: + +```bash +sealos registry copy 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +### 认证与权限 + +在执行 `sealos registry copy` 命令之前,请确保您具有访问源镜像和目标 registry 的权限。可以使用`sealos login`对registry进行认证登录。 + +### 复制过程 + +请注意,镜像复制可能需要一些时间,这取决于镜像的大小,以及网络的速度。在复制过程中,请保持网络的连通性,并确保在复制完成之前不要中断命令的执行。 + +### 参数选项 + +`sealos registry copy` 命令提供了一些参数选项,允许您更精细地控制复制过程: + +- `--override-arch ARCH`:使用指定的 `ARCH` 替代当前机器的架构来选择镜像。 + +- `--override-os OS`:使用指定的 `OS` 替代当前操作系统来选择镜像。 + +- `--override-variant VARIANT`:使用指定的 `VARIANT` 替代当前的架构变种来选择镜像。 + +- `-a` 或 `--all`:如果源镜像是一个列表,复制所有镜像。这对异构环境下特别有用,因为默认情况下,只会复制当前架构的镜像。 + +例如,如果您想复制所有架构的镜像,可以添加 `-a` 参数: + +```bash +sealos registry copy -a 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + + +以上就是 `sealos registry` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/reset.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/reset.md new file mode 100644 index 000000000000..611df9bae442 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/reset.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 2 +--- + +# reset 重置集群 + +`sealos reset` 是 Sealos 命令行工具中的一个命令,用于重置整个集群。这个命令在你想要彻底清空集群数据或者重建集群的时候特别有用。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos reset` 命令格式如下: + +```bash +sealos reset --cluster cluster_name +``` + +在上述命令中,`cluster_name` 是你想要重置的集群的名称。 + +## 示例 + +例如,你可以使用以下命令重置名为 `mycluster` 的集群: + +```bash +sealos reset --cluster mycluster +``` + +## 可选参数 + +- `--force`: 该参数用于强制重置集群,即使集群重置操作未能成功完成。 + +```bash +sealos reset --cluster mycluster --force +``` + +- `--masters`: 该参数用于指定要重置的 master 节点。 + +```bash +sealos reset --cluster mycluster --masters master1 +``` + +- `--nodes`: 该参数用于指定要重置的工作节点。 + +```bash +sealos reset --cluster mycluster --nodes node1 node2 +``` + +- `-p`, `--passwd`: 该参数用于提供密码进行身份验证。 + +- `-i`, `--pk`: 该参数用于指定用于公钥认证的身份(私钥)读取的文件。 + +- `--pk-passwd`: 该参数用于解密 PEM 编码私钥的口令。 + +- `--port`: 该参数用于指定要连接的远程主机的端口。 + +- `-u`, `--user`: 该参数用于指定要作为身份验证的用户名。 + +```bash +sealos reset --cluster mycluster --user username --pk /root/.ssh/id_rsa --pk-passwd yourpassword +``` + +以上就是 `sealos reset` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md new file mode 100644 index 000000000000..7114566351dd --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/rmi.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +--- + +# rmi 删除本地镜像 + +`sealos rmi` 是 Sealos 命令行工具中的一个命令,用于删除本地存储的一个或多个镜像。这个命令可以帮助你清理无用或者过时的镜像,节省存储空间。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos rmi` 命令格式如下: + +```bash +sealos rmi imageID +``` + +在上述命令中,`imageID` 是你想要删除的镜像的 ID。 + +## 示例 + +例如,你可以使用以下命令删除 ID 为 `imageID` 的镜像: + +```bash +sealos rmi imageID +``` + +如果你想要删除多个镜像,只需要在命令行中列出所有的镜像 ID,例如: + +```bash +sealos rmi imageID1 imageID2 imageID3 +``` + +## 可选参数 + +- `-a`, `--all`: 该参数用于删除所有镜像。使用此选项时,命令将不接受任何镜像 ID。 + +```bash +sealos rmi --all +``` + +- `-f`, `--force`: 该参数用于强制删除镜像,以及使用该镜像的任何容器。 + +```bash +sealos rmi --force imageID +``` + +- `-p`, `--prune`: 该参数用于修剪悬挂的镜像(没有标签且没有被任何容器引用的镜像)。 + +```bash +sealos rmi --prune +``` + +以上就是 `sealos rmi` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/run.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/run.md new file mode 100644 index 000000000000..03117703b6d4 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/run.md @@ -0,0 +1,84 @@ +--- +sidebar_position: 1 +--- + +# run 运行集群镜像 + +Sealos 的 `run` 命令是一个强大且灵活的工具,它支持集群初始化、应用安装、多镜像执行,单机集群等操作。下面是对于 `sealos run` 命令及其参数的详细解释和一些使用示例。 + +## 命令概览 + +``` +sealos run --masters [arg] --nodes [arg] [Options] +``` + +`` 参数是您想要在集群中运行的 Docker 镜像名称和版本。`--masters` 和 `--nodes` 是您想要运行这个镜像的 master 节点和 node 节点的 IP 列表。 + +### 选项解释 + +- `--cluster='default'`: 要运行操作的集群名称。 + +- `--cmd=[]`: 覆盖镜像中的 CMD 指令。 + +- `--config-file=[]`: 自定义配置文件的路径,用于替换资源。 + +- `-e, --env=[]`: 在命令执行期间设置的环境变量。 + +- `-f, --force=false`: 强制覆盖此集群中的应用。 + +- `--masters=''`: 要运行的 master 节点。 + +- `--nodes=''`: 要运行的 node 节点。 + +- `-p, --passwd=''`: 使用提供的密码进行认证。 + +- `-i, --pk='/root/.ssh/id_rsa'`: 选择从其中读取公钥认证身份的私钥文件。 + +- `--pk-passwd=''`: 解密 PEM 编码的私钥的密码。 + +- `--port=22`: 远程主机的连接端口。 + +- `-t, --transport='oci-archive'`: 从 tar 归档文件加载镜像传输。(可选值: oci-archive, docker-archive) + +- `-u, --user=''`: 认证的用户名。 + +## 示例 + +1. 创建集群到您的裸机服务器,指定 IP 列表: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +2. 多镜像运行: +``` +sealos run labring/kubernetes:v1.24.0 labring/helm:v3.11.3 calico:v3.24.6 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 +``` + +3. 指定服务器的 InfraSSH 端口: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' +``` + +4. 自定义 VIP Kubernetes 集群: +``` +sealos run -e defaultVIP=10.103.97.2 labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +5. 创建单节点 Kubernetes 集群: +``` +sealos run labring/kubernetes:v1.24.0 +``` + +6. 使用自定义环境变量创建集群: +``` +sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +这些示例展示了 `sealos run` 命令的强大和灵活性,可以根据您的需求进行定制和调整。 + +更多示例请参考 [运行集群](/self-hosting/lifecycle-management/operations/run-cluster.md)。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/save.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/save.md new file mode 100644 index 000000000000..cea21ca73514 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/save.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 6 +--- + +# save 保存镜像 + +`sealos save` 是 Sealos 命令行工具中的一个命令,用于将镜像保存到归档文件中。这个命令可以帮助你方便地备份和迁移你的镜像。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos save` 命令格式如下: + +```bash +sealos save -o outputFilename imageName +``` + +在上述命令中,`outputFilename` 是你想要保存的归档文件的名称,`imageName` 是你想要保存的镜像的名称。 + +## 示例 + +例如,你可以使用以下命令将名为 `labring/kubernetes:latest` 的镜像保存到一个名为 `kubernetes.tar` 的归档文件中: + +```bash +sealos save -o kubernetes.tar labring/kubernetes:v1.24.0 +``` + +## 可选参数 + +- `--format`: 这个参数用于指定保存镜像的传输方式。目前可用的选项有 `oci-archive` 和 `docker-archive`、`oci-dir`,`docker-dir`。默认值是 `oci-archive`。 +- `-m`: 这个参数可以同时保存多个镜像,但是仅限于`docker-archive`格式。 + +例如,你可以使用以下命令将名为 `labring/kubernetes:latest` 的镜像以 `docker-archive` 的方式保存到一个名为 `kubernetes.tar` 的归档文件中: + +```bash +sealos save -o kubernetes.tar --format docker-archive labring/kubernetes:v1.24.0 +sealos save -o kubernetes.tar -m --format docker-archive labring/kubernetes:v1.24.0 labring/helm:v3.5.0 +``` + +以上就是 `sealos save` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/scp.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/scp.md new file mode 100644 index 000000000000..1508424fb4f8 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/scp.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 5 +--- + +# scp 拷贝文件 + +`sealos scp` 是 Sealos 命令行工具中的一个命令,用于将文件复制到指定的集群节点。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +基本的 `sealos scp` 命令格式如下: + +```bash +sealos scp "source file path" "destination file path" +``` + +在上述命令中,`source file path` 是你要复制的文件的本地路径,`destination file path` 是你要将文件复制到的远程节点路径。 + +## 选项 + +`sealos scp` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要将文件复制到其上的集群的名称。默认为 `default`。 + +- `--ips=[]`: 将文件复制到具有指定 IP 地址的节点。 + +- `-r, --roles='':`: 将文件复制到具有指定角色的节点。 + +每个选项后都可以跟随一个或多个参数。 + +## 示例 + +例如,你可以使用以下命令将本地的 `/root/aa.txt` 文件复制到默认集群的所有节点的 `/root/dd.txt`: + +```bash +sealos scp "/root/aa.txt" "/root/dd.txt" +``` + +如果你想在名为 `my-cluster` 的集群的 `master` 和 `node` 角色的节点上复制文件,可以使用以下命令: + +```bash +sealos scp -c my-cluster -r master,node "/root/aa.txt" "/root/dd.txt" +``` + +如果你只想在 IP 地址为 `172.16.1.38` 的节点上复制文件,可以使用以下命令: + +```bash +sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" +``` + +以上就是 `sealos scp` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/tag.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/tag.md new file mode 100644 index 000000000000..c3e250cc3447 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/commands/tag.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 6 +--- + +# tag 添加镜像名称 + +`sealos tag` 是 Sealos 命令行工具中的一个命令,用于给本地存储的镜像添加一个或多个附加名称。这个命令可以帮助你更好地管理你的镜像。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos tag` 命令格式如下: + +```bash +sealos tag imageName newName +``` + +在上述命令中,`imageName` 是你要操作的镜像的名称,`newName` 是你想要添加的新标签。 + +## 示例 + +例如,你可以使用以下命令给名为 `imageName` 的镜像添加一个新的名称 `firstNewName`: + +```bash +sealos tag imageName firstNewName +``` + +你也可以一次添加多个名称,例如,添加 `firstNewName` 和 `SecondNewName` 两个名称: + +```bash +sealos tag imageName firstNewName SecondNewName +``` + +以上就是 `sealos tag` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md new file mode 100644 index 000000000000..f09d34f7bf1e --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 10 +--- + +# Kubernetes 集群镜像说明文档 + +本文档主要介绍 Sealos 官方提供的 rootfs 类型集群镜像,包括镜像名称、镜像类型以及镜像版本等方面的说明。 + +## 镜像名称 + +Sealos 官方发布的集群镜像主要包括以下几种: + +1. kubernetes:使用 containerd 作为容器运行时(CRI)的 Kubernetes 镜像。 +2. kubernetes-docker:使用 docker 作为容器运行时(CRI)的 Kubernetes 镜像。 +3. kubernetes-crio:使用 crio 作为容器运行时(CRI)的 Kubernetes 镜像。 + +目前,Sealos 主要提供了 Kubernetes 相关的镜像,而其他类型的集群镜像,如 k3s、k0s 等,尚未提供。 + +## 镜像类型 + +根据容器运行时(CRI)的不同,Sealos 提供了不同类型的 Kubernetes 集群镜像: + +1. 使用 containerd 作为 CRI 的 Kubernetes 镜像。 +2. 使用 docker 作为 CRI 的 Kubernetes 镜像。 +3. 使用 crio 作为 CRI 的 Kubernetes 镜像。 + +用户可以根据自己的需求和偏好,选择合适的镜像类型。 + +## 镜像版本 + +Sealos 官方提供的集群镜像有多个版本可供选择,例如: + +### 1. 开发版(Development version) + +适用于想要尝试项目最新功能的用户。开发版可能包含尚未经过完整测试的新功能和改进,因此可能不够稳定。 + +示例:`v1.26(v1.26-amd64/v1.26-arm64)` + +### 2. 最新版(Latest version) + +通常比开发版更稳定,但可能不包含所有的新功能。这是推荐给大多数用户使用的版本。 + +示例:`v1.26.0(v1.26.0-amd64/v1.26.0-arm64)` + +### 3. 发布版(Release version,包含历史版本) + +包含了历史版本的发布版。发布版通常经过了严格的测试,被认为是稳定的。 + +示例:`v1.26.0-4.1.5(v1.26.0-4.1.5-amd64/v1.26.0-4.1.5-arm64)` `4.1.5`是sealos对应版本号 + +在选择镜像版本时,用户需要根据自己的需求和偏好来选择适合的版本。另外,Sealos 还提供了针对不同处理器架构的子版本,以满足用户在不同硬件平台上的需求。 + +## 总结 + +本文档对 Sealos 官方提供的 rootfs 类型集群镜像进行了说明,包括镜像名称、镜像类型以及镜像版本等方面。用户可以根据自己的需求和偏好,在不同的镜像类型和版本中进行选择,以便在 Kubernetes 集群中运行容器。 diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/sealos.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/sealos.md new file mode 100644 index 000000000000..69c2cc67ce96 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/lifecycle-management/reference/sealos/sealos.md @@ -0,0 +1,16 @@ +--- +sidebar_position: 0 +--- + +# Sealos 使用指南 + +Sealos 是一个统一的云操作系统,用于管理云原生应用。它提供了一系列命令行工具来帮助用户管理 Kubernetes 集群,管理节点,远程操作,管理容器和镜像,以及其他一些功能。下面是这些命令的详细介绍: + +本章节主要介绍 Sealos 的使用指南以及集群镜像相关说明。 + +- Sealos使用指南: 有关每个命令的详细信息,包括所有受支持的参数和子命令, 请参阅 [sealos](/self-hosting/lifecycle-management/reference/sealos/commands.md) 参考文档。 +- 集群镜像: + - [Rootfs集群镜像](/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application集群镜像](/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md) + + diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/QA.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/QA.md new file mode 100644 index 000000000000..47445d62f449 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/QA.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 3 +--- + +# 常见问题 + +在部署及使用 Sealos Cloud 过程中,您可能会遇到各种问题。为了更好地帮助您解决这些问题,我们对常见问题进行了总结,并提供了详细的答案和解决方法。 + +## 部署问题 + +下面总结了部署过程中可能遇到的问题及解决方法,假如您遇到了其他问题,请在 [Sealos 社区](https://forum.laf.run/)中联系我们。 + +### Q1:iptables / ip_forward 问题 + +**问题描述**:在部分操作系统中,iptables 或 IPv4 IP 转发默认未启用,例如旧版本的 Centos、RHEL 等。这可能导致部署过程中无法正常创建 +iptables 规则或转发数据包,从而导致集群无法正常启动。 + +**解决方法**:需要在每个节点上执行以下命令,以启用 iptables 和 IP 转发: + +```shell +$ modprobe br_netfilter +$ echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables +$ echo 1 > /proc/sys/net/ipv4/ip_forward +``` + +### Q2:系统内核问题 + +- **问题描述**:如果系统内核版本过低,可能导致集群无法正常启动。低版本内核也可能导致依赖 MongoDB 5.0 的应用无法正常运行。 +- **解决方法**:在部署前,请确保系统内核版本至少为 5.4 或更高。 + +### Q3:系统资源问题 + +- **问题描述**:系统资源紧张可能会导致部署过程中出现卡顿或停滞,当您等待过久时,请检查系统资源是否足够。 +- **解决方法**:使用命令 `kubectl describe nodes` 查看节点资源状态。一般情况下可以从 CPU、内存、存储等方向排查系统资源是否充足。 + +### Q4:网络问题 + +- **问题描述**: 在部署过程中,服务器的不当配置可能会引发多种网络问题,例如: + 1. http_proxy / https_proxy 环境变量配置; + 2. 服务器防火墙配置; + 3. 服务器路由配置; +- **解决方法**: 遇到网络问题时,请检查以上配置是否正确。 + +## 证书及域名相关问题 + +### 证书更新 + +在您使用 Sealos 过程中,证书是保障集群安全的重要组成部分。以下是详细的证书更新步骤,这些步骤可以帮助您在证书即将过期时顺利更新: + +1. **备份旧证书**: + + 在主节点 `master0` 上,您需要先备份当前使用的证书。这是一个防止更新过程中出现问题而导致证书丢失的重要步骤。使用以下命令进行备份: + + ```shell + $ kubectl get secret -n sealos-system wildcard-cert -o yaml > cert-backup.yaml + ``` + + 此命令会将名为 `wildcard-cert` 的证书以 YAML 格式保存到文件 `cert-backup.yaml` 中。 + +2. **保存新证书**: + + 将您已经准备好的新证书文件保存到 `master0` 节点上。确保新的证书文件(通常是 `.crt` 和 `.key` 文件)在节点上的某个位置。 + +3. **更新证书**: + + 使用以下脚本来更新证书。您需要替换脚本中的 `` 和 ``,以指向您的新证书文件和密钥文件的实际路径。 + + ```shell + #!/bin/bash + # 设置变量 + CRT_FILE= + KEY_FILE= + + # 将证书和密钥文件内容进行Base64编码 + CRT_BASE64=$(cat $CRT_FILE | base64 -w 0) + KEY_BASE64=$(cat $KEY_FILE | base64 -w 0) + + # 构建部分更新的JSON对象 + PATCH_JSON='{"data":{"tls.crt":"'$CRT_BASE64'","tls.key":"'$KEY_BASE64'"}}' + + # 使用kubectl patch命令更新Secret + kubectl patch secret wildcard-cert -n sealos-system -p $PATCH_JSON + ``` + + 这个脚本的主要作用是将新证书的内容编码为 Base64 格式,并使用 `kubectl patch` 命令更新 Kubernetes 集群中的相应 Secret + 对象。 + +### 域名更换 + +域名更换是一个更加复杂的过程,因为它通常涉及到集群内多个组件和服务的配置更改。目前,我们尚未在文档中提供域名更换的详细教程。不过,我们计划在未来推出 +Sealos 集群管理面板,该面板将提供更加简便的方法来替换集群域名和证书。 + +请注意,域名更换通常需要对集群的网络配置进行深入了解,并且可能涉及到 DNS +设置、服务发现等多个方面。因此,建议在执行此类操作时,确保您具备相应的技术知识或咨询专业人士的帮助。 + +### 用户注册开关 + +关闭用户注册: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: true/signUpEnabled: false/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + +开启用户注册: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: false/signUpEnabled: true/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + + +## 数据库相关问题 + +### Dify安装 + +由于 Sealos 离线包默认未包含向量数据库的启动配置(也未打包相关镜像),目前在私有化部署的 Sealos 集群中暂时无法安装 Dify 应用。 \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-1.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-1.jpg new file mode 100644 index 000000000000..680d572141b3 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-1.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-2.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-2.jpg new file mode 100644 index 000000000000..8b6a354702c0 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/chrome-certificate-2.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-1.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-1.png new file mode 100644 index 000000000000..6e2c46692cf6 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-2.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-2.jpg new file mode 100644 index 000000000000..a99aaeaf5a0a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-2.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-3.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-3.jpg new file mode 100644 index 000000000000..8a42e6c08c8e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/firefox-export-certificate-3.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-1.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-1.jpg new file mode 100644 index 000000000000..3a211ccd3e13 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-1.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-2.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-2.jpg new file mode 100644 index 000000000000..357d5880dbe4 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-2.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-3.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-3.jpg new file mode 100644 index 000000000000..9c33c03204f5 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/safari-export-certificate-3.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-cost-center.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-cost-center.jpg new file mode 100644 index 000000000000..e8e59ccb8709 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-cost-center.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-1.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-1.png new file mode 100644 index 000000000000..790d9cc7a3da Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-2.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-2.png new file mode 100644 index 000000000000..9f04a4668927 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-3.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-3.png new file mode 100644 index 000000000000..ea555c230e1d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-4.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-4.png new file mode 100644 index 000000000000..d409c55ba36d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-5.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-5.png new file mode 100644 index 000000000000..3cde5ae76850 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-5.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-6.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-6.png new file mode 100644 index 000000000000..4a6ef666fb1d Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-6.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate.jpg new file mode 100644 index 000000000000..97dbcc274b2e Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate1.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate1.png new file mode 100644 index 000000000000..3f1bfaebdc2a Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate1.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate2.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate2.png new file mode 100644 index 000000000000..7c33e2693e9c Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate2.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate3.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate3.png new file mode 100644 index 000000000000..609a653a4f15 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate3.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate4.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate4.png new file mode 100644 index 000000000000..6c6a20aadb78 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license-activate4.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license.png b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license.png new file mode 100644 index 000000000000..aa820a06c105 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/sealos-license.png differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/windows-trust-certificate.jpg b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/windows-trust-certificate.jpg new file mode 100644 index 000000000000..243d0d2d5583 Binary files /dev/null and b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/images/windows-trust-certificate.jpg differ diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/install-object-storage.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/install-object-storage.md new file mode 100644 index 000000000000..cade1f868519 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/install-object-storage.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 2 +--- + +# 部署对象存储 + +下载 helm 图表。 + +```bash +curl -O https://raw.githubusercontent.com/minio/operator/master/helm-releases/operator-5.0.6.tgz +``` + +安装 minio-operator。 + +```bash +helm install --namespace minio-system --create-namespace minio-operator operator-5.0.6.tgz +``` + +安装 Minio、Controller 等。 + +```bash +# DOMAIN 是 Sealos 集群的域名 +# 环境变量设置 Minio 管理员账户(默认的 Minio 管理员账户为 username/passw0rd) +# -e minioAdminUser={16位随机大小写字符串} -e minioAdminPassword={32位随机大小写字符串} +sealos run ghcr.io/labring/sealos-cloud-objectstorage:latest -e cloudDomain={DOMAIN} +``` + +# 卸载对象存储 + +安装「对象存储」失败,可以使用脚本清理残留资源,卸载「对象存储」也可以使用脚本清理。 + +```bash +#!/usr/bin/env bash +set +e + +kubectl delete app objectstorage -n app-system +kubectl delete ns objectstorage-system objectstorage-frontend +helm uninstall minio-operator -n minio-system +kubectl delete ns minio-system +kubectl delete crd objectstoragebuckets.objectstorage.sealos.io objectstorageusers.objectstorage.sealos.io +kubectl delete clusterrole objectstorage-manager-role objectstorage-metrics-reader objectstorage-proxy-role +kubectl delete clusterrolebinding objectstorage-manager-rolebinding objectstorage-proxy-rolebinding +``` \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md new file mode 100644 index 000000000000..de4c64dd2b0f --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md @@ -0,0 +1,434 @@ +--- +sidebar_position: 1 +toc_max_heading_level: 5 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Sealos 集群部署 + +export const Highlight = ({children, color}) => ( + + {children} + +); + +:::tip + +大规模集群以及企业生产环境强烈建议使用 [Sealos 私有云的企业版或者定制版](/self-hosting)。 + +::: + +## 准备工作 + +### 服务器 + +以下是一些基本的要求: + +- 每个集群节点应该有不同的主机名。 +- 所有节点的时间需要同步。 +- 建议使用干净的操作系统来创建集群。**不要自己装 Docker!** +- 支持大多数 Linux 发行版,例如:Ubuntu、Debian、CentOS、Rocky linux。 +- **系统内核版本在 5.4 及以上**。 +- **必须使用 root 用户安装!** + +推荐配置: + +推荐使用 Ubuntu 22.04 LTS 操作系统,内核版本在 5.4 及以上,配置如下: + +| 操作系统 | 内核版本 | CPU | 内存 | 存储 | Masters | Nodes | +|------------------|-------|-----|------|-------|---------|-------| +| Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | 奇数台 | 任意 | + +:::info注意 +Kubernetes 和 Sealos Cloud 的系统组件在每个 Master 节点上大约需要 2 核心(2c)和 2GB 内存(2g),在每个 Node 节点上则需要大约 +1 核心(1c)和 1GB 内存(1g),请确保集群中每个节点都有足够的计算资源以支持系统组件的运行。 +::: + +### 网络 + ++ 所有节点之间网络互通; ++ 需要在 Kubernetes 集群的**第一个 Master 节点**上执行脚本,目前**集群外的节点不支持集群安装**; ++ 所有节点之间可以互相通信。 + +### 域名 + ++ 需要一个域名用于访问 Sealos 及相关服务; ++ 如果您没有域名,可以使用 `nip.io` 提供的免费域名服务。 + +### 证书 + +Sealos 需要使用证书来保证通信安全,默认在您不提供证书的情况下我们会使用 [cert-manager](https://cert-manager.io/docs/) 来自动签发证书。 + +如果您能提供证书,证书需要解析下列域名 (假设您提供的域名为:cloud.example.io): + +- `*.cloud.example.io` +- `cloud.example.io` + +## 安装步骤 + +为了便于部署,我们提供了一个一键安装脚本。该脚本可以从零开始部署 Sealos 集群,也可以在已有的 Kubernetes 集群上部署 Sealos +集群(在已有集群上执行时请谨慎操作)。 + +:::info注意 + +该脚本只支持在 “使用 Sealos 安装的 Kubernetes 集群” 上部署 Sealos 集群,暂不支持其他方式部署的 Kubernetes。 + +关于如何使用 Sealos 部署 Kubernetes +集群,可以参考:[安装 Kubernetes 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md) +,支持 [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags) 中的几乎所有 Kubernetes 版本(**暂不支持 1.28 及以上版本 +**)。 + +::: + +如果您的机器还没有安装过 Kubernetes,**建议直接使用该脚本连同 Kubernetes 和 Sealos 集群一起安装。** + +根据您的域名情况,可以分为以下几种安装方式: + +### 1、无公网域名,也不想自定义域名 + +如果您没有公网域名,也不需要自定义域名,可以选择直接使用 `nip.io` 提供的免费域名服务。`nip.io` 是一个免费的通配符 DNS 服务,它可以将动态分配的 IP 地址映射到一个固定的子域名上,特别适合用于本地开发环境。具体的工作原理为: + +您可以使用任何 IP 地址作为 `nip.io` 子域名的一部分,而 `nip.io` 会将它解析回相应的 IP 地址。例如,如果你有一个内网 IP 地址 `192.168.1.10`,你可以使用 `192.168.1.10.nip.io` 作为域名,所有向这个域名发送的请求都会被解析到 `192.168.1.10` 这个 IP 地址上。这样就无需修改本地 hosts 文件,也不需要搭建内网 DNS 服务,直接通过这个域名就能访问内网服务了。 + +使用 nip.io 作为 Sealos 的域名非常简单,只需在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com +``` + +当需要你输入 Sealos Cloud 域名时,你需要输入这种格式的域名:`[ip].nip.io`,其中 [ip] 是你的 Master 节点 IP。 + +安装完成后,终端会输出 Sealos 访问域名以及用户名和密码,例如: + +```bash +Sealos cloud login info: +Cloud Version: latest +URL: https://10.214.210.102.nip.io +admin Username: admin +admin Password: sealos2023 +``` + +### 2、有公网域名,想公网访问 + +如果你有自己的公网域名,并且想通过公网访问 Sealos,那你就需要准备好公网受信任的 SSL/TLS 证书。你可以通过 acme.sh 等工具自动签发证书,也可以从域名提供商处下载免费证书或者购买商业证书。 + +:::info注意 +如果你的公网 IP 在国内,那么域名必须要备案! +::: + +准备好域名证书后,需要将证书放到第一个 Master 节点的某个目录中,例如 `/root/certs/`。 + +:::info注意 + +您还需要在域名服务商处添加一条该域名的 A 记录,地址解析到第一个 Master 节点的公网 IP 地址。同时还需要添加一条泛解析记录,将该域名的子域名也解析到第一个 Master 节点的公网 IP 地址。 + +例如 (假设你的域名是 `cloud.example.io`,假设你的 Master 节点公网地址是 `192.168.1.10`): + +```bash +cloud.example.io A 192.168.1.10 +*.cloud.example.io A 192.168.1.10 +``` + +::: + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= \ + --cert-path= \ + --key-path= +``` + ++ `` 需要替换成你自己的公网域名。 ++ `` 需要替换成你的证书位置,通常是 `.crt` 或 `.pem` 文件。例如:`/root/certs/example.crt`。 ++ `` 需要替换成你的私钥位置,通常是 `.key` 或 `.pem` 文件。例如:`/root/certs/example.key`。 + +### 3、有公网域名,想内网访问 + +如果您有自己的公网域名,但是只有内网 IP,或者只想在内网访问 Sealos,那您只需要在域名服务商处添加一条该域名的 A 记录,地址解析到第一个 Master 节点的内网 IP 地址。同时还需要添加一条泛解析记录,将该域名的子域名也解析到第一个 Master 节点的内网 IP 地址。 + +例如 (假设你的域名是 `cloud.example.io`,假设你的 Master 节点内网地址是 `192.168.1.10`): + +```bash +cloud.example.io A 192.168.1.10 +*.cloud.example.io A 192.168.1.10 +``` + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= +``` + +其中 `` 需要替换成你自己的公网域名。 + +安装过程中 Sealos 会使用 [cert-manager](https://cert-manager.io/docs/) 来自签名证书。 + +### 4、无公网域名,但想自定义域名 + +如果您没有公网域名,但是需要自定义域名,那么就需要在内网自建 DNS,然后将自定义域名解析到第一个 Master 节点的内网 IP 地址。 + +:::note + +假设您的第一个 Master 节点的内网 IP 地址为 `192.168.1.10`。假设您的域名为 `cloud.example.io`。 + +::: + +可以考虑使用 CoreDNS 来自建 DNS 服务,参考配置: + +```nginx +(global_cache) { + cache { + # [5, 60] + success 65536 3600 300 + # [1, 10] + denial 8192 600 60 + prefetch 1 60m 10% + } +} + +.:53 { + errors + health + ready + + import global_cache + + template IN A cloud.example.io { + answer "{{ .Name }} 60 IN A 192.168.1.10" + fallthrough + } + + forward . 223.5.5.5 + + log + loop + reload 6s +} +``` + +这样不管您访问 `cloud.example.io` 还是 `*.cloud.example.io` 都会解析到第一个 Master 节点的内网 IP 地址。 + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= +``` + +其中 `` 需要替换成你自己的自定义域名。 + +安装过程中 Sealos 会使用 [cert-manager](https://cert-manager.io/docs/) 来自签名证书。 + +## 信任自签名证书 + +如果您选择了上面提供的安装方式中的 1 或 3 或 4,那么您的证书默认是不受浏览器信任的,当你访问 Sealos Cloud 时,浏览器会提示下面的信息: + +![](images/chrome-certificate-1.jpg) + +即使点击继续访问,进入 Sealos Cloud 之后也无法正常显示 App 图标,无法打开 App。 + +![](images/chrome-certificate-2.jpg) + +我们需要导出自签名证书,并让系统信任自签名证书。步骤如下。 + +### 导出自签名证书 + +各个浏览器导出自签名证书的步骤略有不同。以下是在一些常用浏览器中导出自签名证书的步骤: + +#### Chrome (以及基于 Chromium 的浏览器如新版 Edge 和 Brave) + +1. 在浏览器地址栏左侧点击 “不安全” 字样。 +2. 点击 “证书无效”,这将打开一个证书信息窗口。 +3. 在打开的证书窗口中,切换到 “详细信息” 标签页。 +4. 在 “详细信息” 标签页中,找到并点击 “导出”。 +5. 选择一个文件名和保存位置,然后完成导出过程。 + +#### Firefox + +1. 点击页面中的 “高级”。 + + ![](images/firefox-export-certificate-1.png) + +2. 然后点击 “查看证书”。 + + ![](images/firefox-export-certificate-2.jpg) + +3. 在证书页面中点击 “PEM (证书)”。 + + ![](images/firefox-export-certificate-3.jpg) + +#### Safari + +1. 点击页面中的 “显示详细信息”。 + + ![](images/safari-export-certificate-1.jpg) + +2. 然后点击 “查看此证书”。 + + ![](images/safari-export-certificate-2.jpg) + +3. 在证书视图中,可以看到一个带有证书链的窗口。 + + ![](images/safari-export-certificate-3.jpg) + +4. 拖动红框圈出来的证书到桌面或文件夹中,证书就会被导出了。 + +### 信任自签名证书 + +#### macOS + +在 macOS 上信任自签名证书可以使用以下命令: + +```bash +# 假设证书保存在 ~/Downloads/10.214.210.102.nip.io.cer +$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/10.214.210.102.nip.io.cer +``` + +您也可以通过以下步骤来操作: + +##### 1、添加证书到钥匙串 + +1. 启动**钥匙串访问** (Keychain Access) 应用程序,点击左侧 “系统钥匙串” 下方的 “系统” 类别。 +2. 然后打开 “访达”,进入证书保存位置,**双击证书文件**:这通常是一个 `.cer`,`.crt`,或 `.pem` 文件。 +3. 输入您的用户名和密码以允许修改。 +4. 证书现在应该已经被添加到您的钥匙串中。 + +##### 2、信任证书 + +1. 在钥匙串访问中,从 “系统” 类别的列表中找到刚刚添加的自签名证书,并双击它。 +2. 在打开的窗口中,展开 “信任” 部分。 +3. 在 “当使用此证书时” 选项旁边,有一个下拉菜单,默认设置可能是 “使用系统默认值”。要信任证书,请改为 “始终信任”。 +4. 关闭证书信息窗口,系统可能会提示您验证您的用户名和密码,以确认更改。 +5. 现在,证书已经被标记为受信任。 + +:::info注意 + +自签名证书不由第三方证书颁发机构 (CA) 颁发,因此,其他设备也不会信任该证书。如果您是在公司或组织内部使用自签名证书,可能需要在每个需要信任此证书的设备上手动进行上述步骤。 + +::: + +#### Windows + +1. 在根证书文件点鼠标右键,选择 “安装证书”。 +2. 选择 “当前用户” 或者 “本地计算机”,下一步 +3. “将所有的证书都放入下列存储”,“浏览”,“受信任的根证书颁发机构”,“确定”,下一步。 +4. 完成,“是”,确定。 + +![](images/windows-trust-certificate.jpg) + +#### Linux + +Linux 不同发行版更新根证书存储的命令不一样,用来保存私有证书的路径也不一样。需要先复制自签名 CA 证书到特定路径,再运行命令更新根证书存储。 + + + + + ```bash + # Debian/Ubuntu/Gentoo + # - 安装 + $ sudo cp root_ca.crt /usr/local/share/ca-certificates/root_ca.crt + # update-ca-certificates 会添加 /etc/ca-certificates.conf 配置文件中指定的证书 + # 另外所有 /usr/local/share/ca-certificates/*.crt 会被列为隐式信任 + $ sudo update-ca-certificates + + # - 删除 + $ sudo rm /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates --fresh + ``` + + + + + ```bash + # CentOS/Fedora/RHEL + $ yum install ca-certificates + # 启用动态 CA 配置功能: + $ update-ca-trust force-enable + $ cp root_ca.crt /etc/pki/ca-trust/source/anchors/ + $ update-ca-trust + ``` + + + + + ```bash + # Alpine + $ apk update && apk add --no-cache ca-certificates + $ cp root_ca.crt /usr/local/share/ca-certificates/ + $ update-ca-certificates + ``` + + + + + ```bash + # OpenSUSE/SLES + $ cp root_ca.crt /etc/pki/trust/anchors/ + $ update-ca-certificates + ``` + + + + +## 激活集群 + +集群安装完成后,需要激活集群,步骤如下: + +1. 首先点击桌面的「许可证」打开许可证应用: + + ![](images/sealos-license-1.png) + +2. 然后点击出现的页面左侧的「激活/购买」: + + ![](images/sealos-license-2.png) + + 然后浏览器会跳转到 License 页面: + + ![](images/sealos-license-3.png) + +3. 如果你还没有在该页面创建过集群,就点击左上角-价格-获取,立即开始创建一个集群;如果你已经创建过集群了,只需要点击「我的集群」便会跳转到已有集群。 + +4. 新购买一个集群后,点进进入会看到新购买的集群显示-未激活: + + ![](images/sealos-license-4.png) + + 点击集群管理,在集群激活处输入一个集群id(自取,不能和他人重复),输入完成后点击激活即可成功激活集群。 + + ![](images/sealos-license-5.png) + + 5.成功激活集群后,集群管理的右侧会出现License管理选项,点击进入,在License列表下面导出License: + + ![](images/sealos-license-6.png) + + + + 6.导出License后,回到 Sealos 集群的「许可证」应用界面,点击「上传 License 文件」: + + ![](images/sealos-license-activate4.png) + + 选择刚刚下载的 License 文件进行上传,然后点击右下角的「激活 License」,便可激活 License。 + + ![](images/sealos-cost-center.jpg) \ No newline at end of file diff --git a/docs/archived/4.0/i18n/zh-Hans/sidebar.json b/docs/archived/4.0/i18n/zh-Hans/sidebar.json new file mode 100644 index 000000000000..0b3bab535d25 --- /dev/null +++ b/docs/archived/4.0/i18n/zh-Hans/sidebar.json @@ -0,0 +1,106 @@ +{ + "version.label": { + "message": "4.0.0", + "description": "The label for version current" + }, + "sidebar.docSidebar.category.Examples": { + "message": "案例", + "description": "The label for category Examples in sidebar docSidebar" + }, + "sidebar.docSidebar.category.ai-applications": { + "message": "AI 应用", + "description": "The label for category ai-applications in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Quick Start": { + "message": "快速开始", + "description": "The label for category Quick Start in sidebar docSidebar" + }, + "sidebar.docSidebar.category.App Deployments": { + "message": "部署应用", + "description": "The label for category App Deployments in sidebar docSidebar" + }, + "sidebar.docSidebar.category.How-to Guides": { + "message": "使用指南", + "description": "The label for category How-to Guides in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Master Services Agreement": { + "message": "服务政策", + "description": "The label for category Master Services Agreement in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Self Hosting": { + "message": "私有化部署", + "description": "The label for the doc item Self Hosting in sidebar docSidebar, linking to the doc Community" + }, + "sidebar.docSidebar.category.Sealos": { + "message": "Sealos 私有云", + "description": "The label for category Sealos Self Hosting in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Kubernetes Lifecycle Management": { + "message": "K8s 集群生命周期管理", + "description": "The label for category Kubernetes Lifecycle Management in sidebar docSidebar" + }, + "sidebar.docSidebar.doc.Community": { + "message": "社区", + "description": "The label for the doc item Community in sidebar docSidebar, linking to the doc Community" + }, + "sidebar.docSidebar.category.Operations": { + "message": "操作说明", + "description": "The label for category Operations in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Advanced Guide": { + "message": "进阶指南", + "description": "The label for category Advanced Guide in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Reference": { + "message": "参考", + "description": "The label for category Reference in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealctl 使用指南": { + "message": "Sealctl 使用指南", + "description": "The label for category Sealctl 使用指南 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 使用指南": { + "message": "Sealos 使用指南", + "description": "The label for category Sealos 使用指南 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 集群镜像构建": { + "message": "Sealos 集群镜像构建", + "description": "The label for category Sealos 集群镜像构建 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 镜像仓库": { + "message": "Sealos 镜像仓库", + "description": "The label for category Sealos 镜像仓库 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 集群镜像运行": { + "message": "Sealos 集群镜像运行", + "description": "The label for category Sealos 集群镜像运行 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 命令说明": { + "message": "Sealos 命令说明", + "description": "The label for category Sealos 命令说明 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.baas": { + "message": "后端即服务", + "description": "The label for category baas in sidebar docSidebar" + }, + "sidebar.docSidebar.category.low-code-platform": { + "message": "低代码平台", + "description": "The label for category low-code-platform in sidebar docSidebar" + }, + "sidebar.docSidebar.category.dial-testing-system": { + "message": "拨测系统", + "description": "The label for category dial-testing-system in sidebar docSidebar" + }, + "sidebar.docSidebar.category.blog-platform": { + "message": "博客平台", + "description": "The label for category blog-platform in sidebar docSidebar" + }, + "sidebar.docSidebar.category.social-communication": { + "message": " IM 应用", + "description": "The label for category social-communication in sidebar docSidebar" + }, + "sidebar.docSidebar.category.programming-languages": { + "message": "常用语言案例", + "description": "The label for category programming-languages in sidebar docSidebar" + } +} \ No newline at end of file diff --git a/docs/archived/4.0/sidebar.json b/docs/archived/4.0/sidebar.json new file mode 100644 index 000000000000..fb0b1df11989 --- /dev/null +++ b/docs/archived/4.0/sidebar.json @@ -0,0 +1,265 @@ +{ + "docSidebar": [ + "Intro", + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "quick-start" + } + ] + }, + { + "type": "category", + "label": "Examples", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "programming-languages", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/programming-languages" + } + ] + }, + { + "type": "category", + "label": "dial-testing-system", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/dial-testing-system" + } + ] + }, + { + "type": "category", + "label": "BaaS", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/baas" + } + ] + }, + { + "type": "category", + "label": "low-code-platform", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/low-code-platform" + } + ] + }, + { + "type": "category", + "label": "blog-platform", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/blog-platform" + } + ] + }, + { + "type": "category", + "label": "ai-applications", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/ai-applications" + } + ] + }, + { + "type": "category", + "label": "social-communication", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "examples/social-communication" + } + ] + } + ] + }, + { + "type": "category", + "label": "How-to Guides", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "guides" + } + ] + }, + { + "type": "category", + "label": "Advanced Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "advanced-guide" + } + ] + }, + { + "type": "category", + "label": "Self Hosting", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "category", + "label": "Sealos", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/sealos" + } + ] + }, + { + "type": "category", + "label": "Kubernetes Lifecycle Management", + "link": { + "type": "doc", + "id": "self-hosting/lifecycle-management/lifecycle-management" + }, + "items": [ + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/quick-start" + } + ] + }, + { + "type": "category", + "label": "Operations", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/operations" + } + ] + }, + { + "type": "category", + "label": "Advanced Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/advanced-guide" + } + ] + }, + { + "type": "category", + "label": "Reference", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/reference" + } + ] + }, + "self-hosting/lifecycle-management/QA" + ] + } + ] + }, + { + "type": "doc", + "id": "Community", + "label": "Community" + }, + { + "type": "category", + "label": "Master Services Agreement", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "msa" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/archived/5.0/auto-sidebar.json b/docs/archived/5.0/auto-sidebar.json new file mode 100644 index 000000000000..c701e92f1a15 --- /dev/null +++ b/docs/archived/5.0/auto-sidebar.json @@ -0,0 +1,8 @@ +{ + "defaultSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} \ No newline at end of file diff --git a/docs/archived/5.0/code.json b/docs/archived/5.0/code.json new file mode 100644 index 000000000000..7f236043c689 --- /dev/null +++ b/docs/archived/5.0/code.json @@ -0,0 +1,646 @@ +{ + "theme.ErrorPageContent.title": { + "message": "页面已崩溃。", + "description": "The title of the fallback page when the page crashed" + }, + "theme.ErrorPageContent.tryAgain": { + "message": "重试", + "description": "The label of the button to try again rendering when the React error boundary captures an error" + }, + "theme.NotFound.title": { + "message": "找不到页面", + "description": "The title of the 404 page" + }, + "theme.NotFound.p1": { + "message": "我们找不到您要找的页面。", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "关闭", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.BackToTopButton.buttonAriaLabel": { + "message": "回到顶部", + "description": "The ARIA label for the back to top button" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "博文列表分页导航", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "较新的博文", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "较旧的博文", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.archive.title": { + "message": "历史博文", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "历史博文", + "description": "The page & hero description of the blog archive page" + }, + "theme.blog.post.readingTime.plurals": { + "message": "{readingTime} 分钟阅读", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.post.readMoreLabel": { + "message": "阅读 {title} 的全文", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, + "theme.blog.post.readMore": { + "message": "阅读更多", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "博文分页导航", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "较新一篇", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "较旧一篇", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.blog.post.plurals": { + "message": "{count} 篇博文", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.tagTitle": { + "message": "{nPosts} 含有标签「{tagName}」", + "description": "The title of the page for a blog tag" + }, + "theme.tags.tagsPageLink": { + "message": "查看所有标签", + "description": "The label of the link targeting the tag list page" + }, + "theme.colorToggle.ariaLabel": { + "message": "切换浅色/暗黑模式(当前为{mode})", + "description": "The ARIA label for the navbar color mode toggle" + }, + "theme.colorToggle.ariaLabel.mode.dark": { + "message": "暗黑模式", + "description": "The name for the dark color mode" + }, + "theme.colorToggle.ariaLabel.mode.light": { + "message": "浅色模式", + "description": "The name for the light color mode" + }, + "theme.docs.breadcrumbs.home": { + "message": "主页面", + "description": "The ARIA label for the home page in the breadcrumbs" + }, + "theme.docs.breadcrumbs.navAriaLabel": { + "message": "页面路径", + "description": "The ARIA label for the breadcrumbs" + }, + "theme.docs.DocCard.categoryDescription": { + "message": "{count} 个项目", + "description": "The default description for a category card in the generated index about how many items this category includes" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "文档分页导航", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "上一页", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "下一页", + "description": "The label used to navigate to the next doc" + }, + "theme.docs.tagDocListPageTitle.nDocsTagged": { + "message": "{count} 篇文档带有标签", + "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.tagDocListPageTitle": { + "message": "{nDocsTagged}「{tagName}」", + "description": "The title of the page for a docs tag" + }, + "theme.docs.versionBadge.label": { + "message": "版本:{versionLabel}" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版尚未发行的文档。", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。", + "description": "The label used to tell the user to check the latest version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "最新版本", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.editThisPage": { + "message": "编辑此页", + "description": "The link label to edit the current page" + }, + "theme.common.headingLinkTitle": { + "message": "标题的直接链接", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": "于 {date} ", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": "由 {user} ", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "最后{byUser}{atDate}更新", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.navbar.mobileVersionsDropdown.label": { + "message": "选择版本", + "description": "The label for the navbar versions dropdown on mobile view" + }, + "theme.common.skipToMainContent": { + "message": "跳到主要内容", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + }, + "theme.tags.tagsListLabel": { + "message": "标签:", + "description": "The label alongside a tag list" + }, + "theme.blog.sidebar.navAriaLabel": { + "message": "最近博文导航", + "description": "The ARIA label for recent posts in the blog sidebar" + }, + "theme.CodeBlock.copied": { + "message": "复制成功", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "复制代码到剪贴板", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copy": { + "message": "复制", + "description": "The copy button label on code blocks" + }, + "theme.CodeBlock.wordWrapToggle": { + "message": "切换自动换行", + "description": "The title attribute for toggle word wrapping button of code block lines" + }, + "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": { + "message": "打开/收起侧边栏菜单「{label}」", + "description": "The ARIA label to toggle the collapsible sidebar category" + }, + "theme.navbar.mobileLanguageDropdown.label": { + "message": "选择语言", + "description": "The label for the mobile language switcher dropdown" + }, + "theme.TOCCollapsible.toggleButtonLabel": { + "message": "本页总览", + "description": "The label used by the button on the collapsible TOC component" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "收起侧边栏", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { + "message": "← 回到主菜单", + "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "展开侧边栏", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "Simple": { + "message": "简单", + "description": "homepage simple" + }, + "Any highly available distributed application on Kubernetes can be easily installed with just one click.": { + "message": "像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用程序,几乎不需要任何专业的交付和运维成本", + "description": "homepage simple" + }, + "Flexible": { + "message": "灵活", + "description": "homepage flexible" + }, + "Easily customize the cloud you need by freely combining various distributed applications.": { + "message": "利用独特的集群镜像能力将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用程序,轻松订制所需的云", + "description": "homepage flexible intro" + }, + "Powerful": { + "message": "强大", + "description": "homepage powerful" + }, + "The cloud services can be easily found and acquired in the application marketplace, offering simplicity and power.": { + "message": "通过应用商店来灵活满足各类用户的需求,形成强大的应用生态", + "description": "homepage flexible intro" + }, + "Kernel Arch": { + "message": "以 kubernetes 为云内核架构" + }, + "Cloud Driver": { + "message": "云驱动" + }, + "Cloud Kernel": { + "message": "云内核" + }, + "Distributed Applications": { + "message": "分布式应用程序" + }, + "Used By": { + "message": "客户列表" + }, + "theme.SearchBar.seeAll": { + "message": "查看全部 {count} 个结果" + }, + "theme.SearchBar.label": { + "message": "搜索", + "description": "The ARIA label and placeholder for search button" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "找到 {count} 份文件", + "description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "「{query}」的搜索结果", + "description": "The search page title for non-empty query" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "在文档中搜索", + "description": "The search page title for empty query" + }, + "theme.SearchPage.inputPlaceholder": { + "message": "在此输入搜索字词", + "description": "The placeholder for search page input" + }, + "theme.SearchPage.inputLabel": { + "message": "搜索", + "description": "The ARIA label for search page input" + }, + "theme.SearchPage.algoliaLabel": { + "message": "通过 Algolia 搜索", + "description": "The ARIA label for Algolia mention" + }, + "theme.SearchPage.noResultsText": { + "message": "未找到任何结果", + "description": "The paragraph for empty search result" + }, + "theme.SearchPage.fetchingNewResults": { + "message": "正在获取新的搜索结果...", + "description": "The paragraph for fetching new search results" + }, + "Contact": { + "message": "联系我们" + }, + "Contact US": { + "message": "联系我们" + }, + "theme.admonition.note": { + "message": "备注", + "description": "The default label used for the Note admonition (:::note)" + }, + "theme.admonition.tip": { + "message": "提示", + "description": "The default label used for the Tip admonition (:::tip)" + }, + "theme.admonition.danger": { + "message": "危险", + "description": "The default label used for the Danger admonition (:::danger)" + }, + "theme.admonition.info": { + "message": "信息", + "description": "The default label used for the Info admonition (:::info)" + }, + "theme.admonition.caution": { + "message": "警告", + "description": "The default label used for the Caution admonition (:::caution)" + }, + "theme.docs.sidebar.closeSidebarButtonAriaLabel": { + "message": "Close navigation bar", + "description": "The ARIA label for close button of mobile sidebar" + }, + "theme.docs.sidebar.toggleSidebarButtonAriaLabel": { + "message": "Toggle navigation bar", + "description": "The ARIA label for hamburger menu button of mobile navigation" + }, + "theme.SearchModal.searchBox.resetButtonTitle": { + "message": "清除查询", + "description": "The label and ARIA label for search box reset button" + }, + "theme.SearchModal.searchBox.cancelButtonText": { + "message": "取消", + "description": "The label and ARIA label for search box cancel button" + }, + "theme.SearchModal.startScreen.recentSearchesTitle": { + "message": "最近搜索", + "description": "The title for recent searches" + }, + "theme.SearchModal.startScreen.noRecentSearchesText": { + "message": "没有最近搜索", + "description": "The text when no recent searches" + }, + "theme.SearchModal.startScreen.saveRecentSearchButtonTitle": { + "message": "保存这个搜索", + "description": "The label for save recent search button" + }, + "theme.SearchModal.startScreen.removeRecentSearchButtonTitle": { + "message": "从历史记录中删除这个搜索", + "description": "The label for remove recent search button" + }, + "theme.SearchModal.startScreen.favoriteSearchesTitle": { + "message": "收藏", + "description": "The title for favorite searches" + }, + "theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": { + "message": "从收藏列表中删除这个搜索", + "description": "The label for remove favorite search button" + }, + "theme.SearchModal.errorScreen.titleText": { + "message": "无法获取结果", + "description": "The title for error screen of search modal" + }, + "theme.SearchModal.errorScreen.helpText": { + "message": "你可能需要检查网络连接。", + "description": "The help text for error screen of search modal" + }, + "theme.SearchModal.footer.selectText": { + "message": "选中", + "description": "The explanatory text of the action for the enter key" + }, + "theme.SearchModal.footer.selectKeyAriaLabel": { + "message": "Enter 键", + "description": "The ARIA label for the Enter key button that makes the selection" + }, + "theme.SearchModal.footer.navigateText": { + "message": "导航", + "description": "The explanatory text of the action for the Arrow up and Arrow down key" + }, + "theme.SearchModal.footer.navigateUpKeyAriaLabel": { + "message": "向上键", + "description": "The ARIA label for the Arrow up key button that makes the navigation" + }, + "theme.SearchModal.footer.navigateDownKeyAriaLabel": { + "message": "向下键", + "description": "The ARIA label for the Arrow down key button that makes the navigation" + }, + "theme.SearchModal.footer.closeText": { + "message": "关闭", + "description": "The explanatory text of the action for Escape key" + }, + "theme.SearchModal.footer.closeKeyAriaLabel": { + "message": "Esc 键", + "description": "The ARIA label for the Escape key button that close the modal" + }, + "theme.SearchModal.footer.searchByText": { + "message": "搜索提供", + "description": "The text explain that the search is making by Algolia" + }, + "theme.SearchModal.noResultsScreen.noResultsText": { + "message": "没有结果:", + "description": "The text explains that there are no results for the following search" + }, + "theme.SearchModal.noResultsScreen.suggestedQueryText": { + "message": "试试搜索", + "description": "The text for the suggested query when no results are found for the following search" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsText": { + "message": "认为这个查询应该有结果?", + "description": "The text for the question where the user thinks there are missing results" + }, + "theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": { + "message": "请告知我们。", + "description": "The text for the link to report missing results" + }, + "theme.SearchModal.placeholder": { + "message": "搜索文档", + "description": "The placeholder of the input of the DocSearch pop-up modal" + }, + "theme.tags.tagsPageTitle": { + "message": "标签", + "description": "The title of the tag list page" + }, + "Run Your Business on Sealos Cloud": { + "message": "Sealos - 以 kubernetes 为内核的云操作系统发行版" + }, + "Features": { + "message": "特性" + }, + "Sealos is a Kubernetes distribution, a general-purpose cloud operating system for managing cloud-native applications.": { + "message": "Sealos 以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,实现一个开源的、可随处运行的 AWS 替代方案" + }, + "Copy": { + "message": "复制" + }, + "START NOW": { + "message": "在线使用" + }, + "CONTACT US NOW": { + "message": "联系我们" + }, + "Sealos cloud-native App Store": { + "message": "Sealos 应用商店" + }, + "Fully compatible with Docker registry, enjoy seamless one-click installation of various cloud-native distributed applications": { + "message": "Sealos 应用商店中的每一个应用都是一个集群镜像,集群镜像中包含了分布式应用的所有依赖(例如 Helm Chart、容器镜像和二进制文件),可一键安装。" + }, + "Sealos PostgreSQL Database": { + "message": "Sealos PostgreSQL 数据库" + }, + "One-click creation of a highly available PostgreSQL database, with multi-database cluster management and backup and recovery capabilities": { + "message": "Sealos 注重为数据库用户提供沉浸式的使用体验,可一键创建高可用 PostgreSQL 数据库,并提供多数据库集群的管理、备份和恢复功能。" + }, + "Sealos Cloud Provider": { + "message": "Sealos Cloud Provider" + }, + "Effortlessly establish an independent Kubernetes cluster within minutes on AWS or other public clouds, and effortlessly manage multiple clusters": { + "message": "可在 AWS 等公有云上数分钟内创建一个独立的 Kubernetes 集群并管理多个集群,启动性能高出绝大多数公有云的三倍以上,而价格仅为 AWS 的 80%。" + }, + "Examples": { + "message": "使用样例" + }, + "Anything else as can be freely combined as Cloud OS Applications, allowing for unlimited combinations to fulfill a wide range of requirements and scenarios.": { + "message": "任意分布式应用都可以作为云操作系统的应用进行自由组合,以满足各种需求和场景。" + }, + "By utilizing Kubernetes as the core of the Cloud OS, we can abstract underlying resources, define resource interfaces, and standardize application management.": { + "message": "使用 Kubernetes 作为云操作系统内核,抽象底层资源 / 定义资源接口 / 标准化应用程序管理。" + }, + "The Cloud OS leverages CRI, CNI, and CSI as drivers and utilizes technologies such as Containerd, Calico, and OpenEBS to pool resources and ensure seamless access across the compute, storage, and network domains.": { + "message": "使用 CRI、CNI、CSI 作为云操作系统驱动程序,并利用 Containerd、Calico、OpenEBS 等插件将所有的计算、存储和网络等资源池化。" + }, + "Comprehensive Kubernetes Cluster management throughout its entire lifecycle, including Installation, Scaling, Backup, Recovery, and Upgrades": { + "message": "管理 Kubernetes 集群的整个生命周期,包括集群的包括安装、扩容、备份、恢复和升级。" + }, + "Storage": { + "message": "存储" + }, + "Supports block storage, object storage, and file storage with one-click operation": { + "message": "一键安装各种分布式存储。" + }, + "Network": { + "message": "网络" + }, + "Supports network plugins such as Calico, Flannel, and Cilium with the freedom of customization and choice": { + "message": "支持 Calico、Flannel 和 Cilium 等网络插件,可自由选择和定制。" + }, + "Supports both relational and non-relational databases with high availability, automatic backups, and multi-database instance management capabilities": { + "message": "支持关系型数据库和非关系型数据库,并提供高可用、自动备份和多数据库实例管理等功能。" + }, + "Monitoring": { + "message": "可观测性" + }, + "Easily deploy a monitoring and alarm system with just one click, without the need for additional configuration, and enjoy a simple and intuitive visual experience": { + "message": "一键轻松部署监控和告警系统,无需额外配置,享受简单直观的可视化体验。" + }, + "One-click construction of GPU drivers, device discovery, GPU resource monitoring, and the easy creation of a deep learning platform with AI capabilities": { + "message": "一键安装 GPU 驱动,监控 GPU 资源,轻松打造具备 AI 能力的深度学习平台。" + }, + "MQ": { + "message": "消息队列" + }, + "Supports various mainstream message queues with high availability and automatic monitoring": { + "message": "支持各种主流消息队列,并提供高可用和自动监控功能。" + }, + "Dashboard": { + "message": "可视化" + }, + "Supports various dashboards, enabling users to manage Kubernetes clusters with ease": { + "message": "一键安装各种 Kubernetes 可视化管理工具,轻松管理 Kubernetes 集群。" + }, + "Platform": { + "message": "容器管理平台" + }, + "Support various Container Platforms, enabling users to manage Kubernetes clusters with ease": { + "message": "一键安装各种 企业级容器管理平台,轻松管理 Kubernetes 集群。" + }, + "Support one-click operation for mainstream CI/CD systems such as Argo CD and Drone": { + "message": "一键安装 Argo CD 和 Drone 等主流 DevOps 系统。" + }, + "More than 4,000 Companies and 100,000+ Individuals!": { + "message": "已被 4000 多家公司和 10w+ 个人用户采用!" + }, + "Start": { + "message": "Home" + }, + "GitOps": { + "message": "GitOps" + }, + "Kubernetes": { + "message": "Kubernetes" + }, + "GPU": { + "message": "GPU" + }, + "theme.NavBar.navAriaLabel": { + "message": "主导航", + "description": "The ARIA label for the main navigation" + }, + "theme.docs.sidebar.navAriaLabel": { + "message": "文档侧边栏", + "description": "The ARIA label for the sidebar navigation" + }, + "Docs": { + "message": "文档", + "description": "The Header Documentation" + }, + "Community": { + "message": "社区", + "description": "The Header Community" + }, + "Join Us": { + "message": "加入我们" + }, + "The Capabilities of Sealos": { + "message": "Sealos 的能力" + }, + "Application Management": { + "message": "应用管理" + }, + "Easy management and quick release of publicly accessible distributed applications in the app store.": { + "message": "在应用商店中轻松管理并快速发布可公网访问的分布式应用。" + }, + "Database": { + "message": "数据库管理" + }, + "Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, MongoDB, and Redis.": { + "message": "秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。" + }, + "Cloud Universality": { + "message": "公私一致" + }, + "Equally effective in both public and private cloud, enabling a seamless transition of traditional applications to the cloud.": { + "message": "即是公有云也是私有云,支持传统应用无缝迁移到云环境。" + }, + "Why Choose Sealos?": { + "message": "为什么选择 Sealos" + }, + "Efficient & Cost-Effective": { + "message": "高效 & 经济" + }, + "Universal & User-Friendly": { + "message": "通用性强,无心智负担" + }, + "Flexible & Secure": { + "message": "灵活 & 安全" + }, + "Pay only for the containers you use. Auto-scaling prevents wasted resources, saving you a lot of money.": { + "message": "仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。" + }, + "Focus on your business without worrying about complexity. There is almost no learning curve.": { + "message": "专注于业务本身,无需担心复杂性,几乎没有学习成本。" + }, + "The multi-tenant sharing mechanism ensures security while providing resource isolation and efficient collaboration.": { + "message": "多租户共享机制在确保安全的同时,实现资源隔离与高效协作。" + }, + "Get early access to the latest Sealos versions and stay connected with developers and users on Discord.": { + "message": "加入 Sealos 社区,在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通" + }, + "Who are Using Sealos": { + "message": "谁在使用 Sealos" + }, + "Join Sealos Community": { + "message": "加入 Sealos 社区" + }, + "Experience the latest version of Sealos for the first time and communicate with developers and users in Discord!": { + "message": "在这里您可以在第一时间体验 Sealos 的最新版本,以及在 Discord 或微信群与开发者和用户保持沟通" + }, + "Start Now": { + "message": "立即使用" + }, + "for all applications": { + "message": "托管所有分布式应用" + }, + "Hosting": { + "message": "私有云" + }, + "A cloud operating system based on the Kubernetes kernel": { + "message": "以 Kubernetes 为内核的云操作系统" + }, + "Explore": { + "message": "了解更多" + }, + "Blog": { + "message": "博客" + }, + "App Store": { + "message": "应用商店" + } +} \ No newline at end of file diff --git a/docs/archived/5.0/docs/Intro.md b/docs/archived/5.0/docs/Intro.md new file mode 100644 index 000000000000..a29697e34e95 --- /dev/null +++ b/docs/archived/5.0/docs/Intro.md @@ -0,0 +1,92 @@ +--- +keywords: [cloud operating system, Kubernetes, Sealos, cloud-native, distributed applications, private cloud, public cloud, high-availability, cluster imaging, app store] +description: Sealos is a cloud operating system based on Kubernetes, enabling effortless cloud usage with high-availability and flexible app store capabilities for both public and private clouds. +--- + +import Highlight from '@site/src/components/Highlight' + +# What is Sealos? + + + +The vastness and complexity of the cloud-native ecosystem undoubtedly leaves many businesses in disarray, yearning for +an efficient, ready-to-use distribution, which this ecosystem sorely lacks. The cloud-native market is in dire need of a +cloud operating system to further reduce barriers and costs. + +The advent of Sealos opens a new window for enterprises. It provides a novel choice for businesses and developers alike, +necessitating only the installation of a cloud operating system, regardless of public or private cloud use. It allows +various applications to operate stably and securely, tackling all sorts of dependency issues they might require. + +Sealos adheres to the philosophy that **cloud usage can be as simple as Linux**. You can commence usage right after +installation, as you would with Linux, without entanglement in excessive complexities. The sole difference is that while +Linux is installed on a single server, Sealos is installed across an entire data center. + +In Sealos's architecture, **there's no essential difference between public and private clouds**. They should be separate +instances of the same codebase, the only distinction lying in their configuration and installed applications. Installing +it on an intranet renders it a private cloud, while offering services on the public internet converts it into a public +cloud. + +We firmly believe that with the continuous development and advancement of Sealos, the future of cloud computing will be +more open, more straightforward. The benefits of cloud computing should belong to all providers of computational power, +and the value of the cloud should be shared by all participants of cloud computing. **They will be able to enjoy the +various conveniences of cloud computing in a more economical fashion in a diversified market environment.** + +## Overview + +Sealos is a **cloud operating system distribution** based on the Kubernetes kernel. It adopts a cloud-native approach, +discarding traditional cloud computing architecture, and shifting towards a new architecture with Kubernetes as the +cloud kernel. This allows enterprises to use the cloud **as effortlessly as they would use a personal computer**. + +Users will be able to install any high-availability distributed application on Kubernetes with the click of a button, +similar to using a personal computer. It practically requires no professional delivery or operational costs. +Simultaneously, utilizing unique cluster imaging capabilities, users can package any distributed application into an OCI +image, freely combine a variety of distributed applications, and easily customize their desired cloud. The powerful and +flexible app store function can meet the diverse needs of various users. + +![](./images/overview-1.png) + +## Scenarios & Advantages + +Sealos is a powerful business operating platform, providing flawless support for various applications such as Java, Go, +Python, PHP, and more, unrestricted by the programming language. The platform offers a stable operating environment for +applications and resolves backend dependency issues like databases, object storage, and messaging queues. Further, it +can flexibly handle application configuration management, service discovery, public network exposure, and automatic +scaling issues. + +### Public Cloud + +If your business requires operation within a public cloud environment, you can directly use +the [public cloud services provided by Sealos](https://cloud.sealos.io). + +#### Capabilities of Sealos + +- 🚀 **Application Management**: Easy management and quick release of publicly accessible distributed applications in the + app store. +- 🗄️ **Database Management**: Create high-availability databases in seconds, offering support for MySQL, PostgreSQL, + MongoDB, and Redis. +- 🌥️ **Cloud Universality**: Equally effective in both public and private cloud, enabling a seamless transition of + traditional applications to the cloud. + +#### Advantages of Sealos + +- 💰 **Efficient & Economical**: Pay solely for the containers you utilize; automatic scaling prevents resource + squandering and substantially reduces costs. +- 🌐 **High Universality & Ease of Use**: Concentrate on your core business activities without worrying about system + complexities; negligible learning costs involved. +- 🛡️ **Agility & Security**: The distinctive multi-tenancy sharing model ensures both effective resource segmentation + and collaboration, all under a secure framework. + +### Private Cloud + +The Sealos cloud operating system is **100% open source**. You can find all source code +on [GitHub](https://github.com/labring/sealos), which means all features of the **Sealos Public Cloud** can also be +deployed in a private cloud environment to meet more diversified business needs. + +### Strong Application Delivery Capability + +- Sealos has excellent Kubernetes life cycle management capabilities, and you can freely customize the Kubernetes + environment. +- Sealos can package an entire cluster and deliver it to the customer environment with a single click. All services and + businesses can be delivered as a whole. +- Unlike Docker, which mainly focuses on single-machine imaging, Sealos goes further, packaging the **entire cluster** + or a distributed application. \ No newline at end of file diff --git a/docs/archived/5.0/docs/community.md b/docs/archived/5.0/docs/community.md new file mode 100644 index 000000000000..eb8c107e584c --- /dev/null +++ b/docs/archived/5.0/docs/community.md @@ -0,0 +1,15 @@ +--- +keywords: [Sealos, open source project, Kubernetes, Discord server, GitHub Issues] +description: Sealos is an open source project driven by user participation. Join our Discord server, tweet at @Sailos_io, or create GitHub Issues for support. +--- + +# Community + +Sealos is an open source project that is driven by the participation of users and contributors. If you have questions or +suggestions about using the product while reading this documentation, please try the following ways to seek support. Our +team and community will do their best to help you. + ++ 💬 Join our [Discord server](https://discord.gg/qzBmGGZGk7) is to chat with Sealos developers and other Sealos users. + This is a good place to learn about Sealos and Kubernetes, ask questions, and share your experiences. ++ 🐦 Tweet at @Sailos_io on [Twitter](https://twitter.com/Sailos_io) and follow us. ++ 🐞 Create [GitHub Issues](https://github.com/labring/sealos/issues/new/choose) for bug reports and feature requests. \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/QA.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/QA.md new file mode 100644 index 000000000000..bf69f3699b1f --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/QA.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 1 +keywords: [Sealos, Kubernetes, image building, runtime selection, version compatibility, file storage, FAQ] +description: Comprehensive FAQ for Sealos users covering image building, runtime selection, version compatibility, and file storage issues. Includes solutions and best practices. +--- + +# Frequently Asked Questions + +When using Sealos, you may encounter some common questions and issues. Here are answers and solutions to some of the +common problems. + +## Image Building Issues + +### Q1: How to set up a proxy service during the build phase? + +During the execution of the build command, you can configure a proxy service by setting the HTTP_PROXY environment +variable. + +```shell +HTTP_PROXY=socket5://127.0.0.1:7890 sealos build xxxxx +``` + +### Q2: How to enable debug logs for buildah? + +To view debug logs for buildah, you can set the `BUILDAH_LOG_LEVEL` environment variable. + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +### Q3: How to execute Sealos build within a Pod? + +If you want to execute Sealos build within a Pod, follow these steps: + +1. Build the image within the Pod. You can create a Deployment with the following YAML configuration: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sealoscli + name: sealoscli +spec: + replicas: 1 + selector: + matchLabels: + app: sealoscli + strategy: {} + template: + metadata: + labels: + app: sealoscli + spec: + containers: + - image: # Replace with your sealos image + name: sealoscli + stdin: true + stdinOnce: true + securityContext: + privileged: true +``` + +2. Create a Dockerfile. Here's an example that you can modify as per your needs: + +```dockerfile +FROM bitnami/minideb:buster + +ARG TARGETOS +ARG TARGETARCH + +LABEL from=bitnami/minideb:buster platform=rootcloud team=oam tag=buster name=base + +RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && sed -i "s@http://security.debian.org@http://mirrors.aliyun.com/debian-security@g" /etc/apt/sources.list +RUN install_packages curl iputils-ping net-tools telnet procps vim wget jq + +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV TZ=Asia/Shanghai +``` + +3. Execute the build command within the Pod. + +```shell +sealos build --arch arm64 --build-arg TARGETOS=linux --build-arg TARGETARCH=arm64 -t test -f Dockerfile . +``` + +### Q4: How to Build Cluster Images Using Other Build Tools? + +If you want to use other container tools like Docker or Podman to build cluster images, you can +utilize [sreg](https://github.com/labring/sreg) to cache the images. + +Follow these steps: + +1. Install sreg: + ```shell + wget https://github.com/labring/sreg/releases/download/v0.1.1/sreg_0.1.1_linux_amd64.tar.gz + tar -xzf sreg_0.1.1_linux_amd64.tar.gz sreg + mv sreg /usr/bin/ + ``` +2. Cache the images: + ```shell + sreg save --registry-dir=registry . + ``` +3. Build the cluster image: + ```shell + docker build -t xxxx -f Sealfile . + ``` + +### Q5: Encounter the error "lgetxattr /var/lib/containers/storage/overlay/0c2afe770ec7870ad4639f18a1b50b3a84718f95c8907f3d54e14dbf0a01d50d/merged/dev/ptmx: no such device" during Sealos build. How to fix it? + +This issue might be related to the version of `fuse-overlayfs`. We recommend downloading the latest version +from [here](https://github.com/containers/fuse-overlayfs/releases) and replacing `/bin/fuse-overlayfs`. + +## Runtime Selection Issues + +### Q1: How to select the Kubernetes runtime? + +Sealos determines the runtime based on the image you choose. If you select the `kubernetes-docker` image, Sealos will +use Docker as the runtime. If you choose the `kubernetes-crio` image, Sealos will use CRI-O as the runtime. + +## Version Compatibility Issues + +### Q1: Error "Applied to cluster error: failed to + +init exec auth.sh failed exit status 127"? + +This error is often caused by a mismatch between the version of Sealos and the version of the image being used. Make +sure that the image version and the Sealos version are compatible. For example, if you are using a Kubernetes version +like `v1.xx.x`, you may need to upgrade Sealos, especially if you are using an older version of Sealos while the Sealos +cluster image is using the latest version. Another solution is to choose the corresponding version of the Sealos image. +For example, if your Sealos version is 4.1.3, then the cluster image should be something like +`kubernetes:v1.24.0-4.1.3`. Ensuring that the image version and Sealos version are compatible can help avoid such +issues. + +### Q2: Error when adding additional domains or modifying the service CIDR in the cluster during the addition of a master node + +To address this issue, the Sealos team made the necessary fixes in version 4.2.0. You can refer to the specific fix and +discussion in this pull +request: [https://github.com/labring/sealos/pull/2943](https://github.com/labring/sealos/pull/2943). + +Therefore, if you encounter this problem, we recommend upgrading to Sealos version 4.2.0. The updated version should +handle these changes correctly and not produce errors when adding a master node. + +## File and Directory Location Issues + +### Q1: How to modify the default storage location for `/root/.sealos`? + +If you need to change the default storage location, you can set the `SEALOS_RUNTIME_ROOT` environment variable and then +run the Sealos command. It is recommended to set this environment variable globally so that it can be conveniently used +in other commands or scenarios. + +```shell +export SEALOS_RUNTIME_ROOT=/data/.sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q2: How to modify the default storage location for `/var/lib/sealos`? + +If you need to change the default storage location, you can set the `SEALOS_DATA_ROOT` environment variable and then run +the Sealos command. Similarly, it is recommended to set this environment variable globally. + +```shell +export SEALOS_DATA_ROOT=/data/sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q3: How to modify the storage paths for Sealos image data and status? + +> When using the Sealos cluster, you may need to change the default storage paths for image data and status data. By +> default, these data are stored at the locations defined in the `/etc/containers/storage.conf` file. + +1. **View the current storage configuration** + First, you can use the following command to view the current image storage configuration: + ``` + sealos images --debug + ``` + This command will print the file that contains the current storage configuration, for example: + ``` + 2023-06-07T16:27:02 debug using file /etc/containers/storage.conf as container storage config + REPOSITORY TAG IMAGE ID CREATED SIZE + ``` +2. **Modify the storage path for image data** + If you want to change the storage path for image data, you can edit the `/etc/containers/storage.conf` file. In this + file, find and modify the `graphroot` field to set it to the new path. For example: + ``` + vim /etc/containers/storage.conf + ``` + In the editor, modify the value of the `graphroot` field to the desired new path. +3. **Modify the storage + +path for status data** +Similar to the design of Buildah, Sealos also provides the ability to set the storage path for status data. In the same +configuration file `/etc/containers/storage.conf`, find and modify the `runroot` field to the new path. + +By following these steps, you can save the image data and status data of the Sealos cluster to the new paths you set. +Each time you run a Sealos command, it will use the new paths you set in `graphroot` and `runroot` to store the image +data and status data, respectively. + +### Q4: How to disable file md5 check during SSH file transfer? + +When the network environment is good, disabling the md5 check can greatly improve transfer speed. If you don't want to +check the md5 of files during SSH file transfer, you can add the `-o "HashKnownHosts no"` option to the SSH command. + +```shell +scp -o "HashKnownHosts no" local_file remote_user@remote_ip:/path/to/destination +``` + +This option tells SSH not to hash the hostnames in the known_hosts file, which avoids the md5 check during file +transfer. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md new file mode 100644 index 000000000000..3fd51a4e1d1b --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 2 +keywords: [Sealos image building, registry sync, image caching, skopeo copy, lightweight registry] +description: Learn how to enhance Sealos image building with registry sync and skopeo copy for efficient and lightweight image caching. +--- + +# Guide to Image Building Improvement + +## Deep Understanding of Sealos Image Building + +To understand the work behind Sealos image building, we will first reveal what operations it actually performs at the +underlying level. Here is an intuitive architectural diagram: + +![](images/build.png) + +Sealos covers the following core steps in the image building process: + +- **Cache images**: Parse the working directory during the build execution (here we call it the "context" directory), + save the cache image to the registry structure, and store it in the ./registry directory. +- **Build images**: Build images in the context directory and generate new images. (Please note, you need to copy the + ./registry directory when building images.) + +## Enhancing Image Building Efficiency + +In the current project, we have borrowed the source code of `github.com/distribution/distribution`. During the process +of caching images, we directly call the registry's sdk and start the registry-proxy function. With the caching ability +of the image repository, we cache the image and store it in the context/registry directory. + +The key to this process is to call the method of the distribution repository to save the image: + +- Start the registry-proxy function. +- Save image digest and related index data (by calling the saveManifestAndGetDigest method). +- Save image file data (by calling the saveBlobs method). + +This method does have some significant advantages: + +- Lightweight: Images can be saved without relying on other components. +- Free control: You can freely control the save logic without relying on third-party components. + +However, we have also noticed some potential problems: + +- For beginners, the code is difficult to understand and the logic here is not easy to grasp. +- Unable to cache using the token authentication method. +- Need to rely on some temporary storage space, which requires space. + +Considering these issues, we decided to try a new mode: start a lightweight registry locally and use the sdk of +`skopeo copy` for code reuse. This change directly solves all the previous problems. + +![](images/registry-build.png) + +**Therefore, the new construction method ✨Image Repository Sync✨ gracefully debuts 🎉🎉** + +The [#3154](https://github.com/labring/sealos/pull/3154) PR in the official repository has completed the implementation +of this feature. Currently, Sealos supports these two ways of image construction. Next, I will introduce how to start +the new feature (if the new feature performs stably, we may abandon the old construction method). + +## How to Start the New Feature + +> Sealos v4.3.0 and later versions support this function by default. + +Starting the new feature is very simple, just add an environment variable before you build the image. This feature +supports both build and merge commands. + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +``` + +Here is the expected output after executing the above command: + +```tex +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +Getting image source signatures +Copying blob fee740108510 done +Copying config f92f3ea6e4 done +Writing manifest to image destination +Storing signatures +Getting image source signatures +Copying blob 08409d417260 done +Copying config 44dd6f2230 done +Writing manifest to image destination +Storing signatures +2023-06-01T13:16:07 info saving images busybox, alpine +STEP 1/2: FROM scratch +STEP 2/2: COPY registry ./registry +COMMIT test +Getting image source signatures +Copying blob 13ab73c881c8 done +Copying config 4e22d16b36 done +Writing manifest to image destination +Storing signatures +--> 4e22d16 + +b366 +Successfully tagged localhost/test:latest +4e22d16b366e9fec25641522a74cbd73a7db67dc0516b8f8e00200c4d0551592 +``` + +I hope the above content can help you better understand and use Sealos's new image building method. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md new file mode 100644 index 000000000000..096fc72c0f5a --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md @@ -0,0 +1,794 @@ +--- +sidebar_position: 5 +keywords: [dual-stack cluster, Calico, Kubernetes, IPv6, Sealos, cluster configuration, Kubernetes networking, VXLAN, kubeadm, Kubernetes setup] +description: Learn how to install a dual-stack cluster with Calico on Kubernetes using Sealos. Follow our guide for configuring IPv6 and IPv4 networking. +--- + +# install Dual-stack cluster with Calico + +1. Prerequisites: + - Sealos version >=4.3.0 + - The hosts can communicate using both IPv6 and IPv4 addresses. + - Calico adopts the VXLAN mode,the kernel version must be >= 3.12 。 refer + to [official docs](https://github.com/cyclinder/kubespray/blob/042c960c6617f8a360a8281464ff63f99ee2471c/docs/calico.md) +2. run`sealos gen` to generate a Clusterfile, for example: + +```shell +$ sealos gen labring/kubernetes:v1.26.1 labring/helm:v3.10.3 labring/calico:v3.25.0 --masters 192.168.0.10 --nodes 192.168.0.11 --passwd "xxx" >Clusterfile +``` + +Notice: labring/helm should be set before labring/calico. + +The generated Clusterfile is as follows: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + ExtraArgs: + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10 + ServiceSubnet: 10.96.0.0/22 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +``` + +
+ +3. After generating the Clusterfile, modify the cluster configuration.to add IPv6 pod and svc CIDR 。Here's use the fd85: + ee78:d8a6:8607::1:0000/112、fd85:ee78:d8a6:8607::1000/116 as a example。The main modifications are as follows: + +
+Clusterfile + +```yaml +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a #The IPv6 address of the control node,If you need to access the APIserver using this IP,add it. + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #add svc IPv6 subnet +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 #Default to 64 + node-cidr-mask-size-ipv4: 24 #Default to 24 +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" #add pod IPv6 subnet +--- +# add configure dual stack for calico +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 #add pod IPv6 subnet + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +The final Clusterfile would look like this: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 + node-cidr-mask-size-ipv4: 24 + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +4. run`sealos apply -f Clusterfile` to install the cluster。 + +5. More refer to [Calico official docs](https://docs.tigera.io/calico/latest/networking/ipam/ipv6) + and [k8s offical docs](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/dual-stack-support/) + diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md new file mode 100644 index 000000000000..1b33030a374e --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 1 +keywords: [Sealos image building, directory structure, Kubernetes yaml files, Helm chart, Kubefile] +description: Learn how to build Sealos images with a standardized directory structure, including Kubernetes yaml files and Helm charts, for efficient and error-free management. +--- + +# Image Building and Standard Directory Configuration + +Before embarking on Sealos image building tasks, we suggest first constructing a directory structure that conforms to +standards. This makes the building process more standardized, easier to manage, and reduces the likelihood of errors. +This article will guide you in detail on how to create such a directory structure and explain the purpose of each +directory. + +## Directory Structure Example + +A complete, standardized directory structure example is as follows: + +```shell +. +├── charts +│   └── nginx +│   ├── Chart.lock +│   ├── charts +│   ├── Chart.yaml +│   ├── README.md +│   ├── templates +│   ├── values.schema.json +│   └── values.yaml +├── images +│   └── shim +│   └── nginxImages +├── init.sh +├── Kubefile +├── manifests +│   └── nginx +│   ├── deployment.yaml +│   ├── ingress.yaml +│   └── service.yaml +├── opt +│   └── helm +└── registry +``` + +## Directory Descriptions + +Each directory plays a specific role during the build process, and their detailed descriptions are as follows: + +- `Kubefile` (required): This file is similar to Dockerfile and is the core file for image building. It defines various + steps in the build process, such as the selection of the base image, setting of environment variables, file copying, + etc. +- `manifests`: This directory is used to store Kubernetes yaml files, which describe the configuration information of + your applications, such as Pod, Service, Deployment configurations. +- `charts`: This directory is used to store Helm chart files. Helm chart is a package management tool for Kubernetes + that simplifies the deployment and management of Kubernetes applications. +- `images/shim`: This directory is used to store images that cannot be automatically extracted from yaml files or Helm + charts. During the build process, sealos will automatically pull these images. +- `opt`: Binary files are stored here. +- `registry`: This directory is used to store images pulled locally during the build process. During the build process, + this directory will be automatically generated, and there is no need to manually create it. +- `init.sh`: This script is automatically run by GitHub Action during the build process. You can write some automated + tasks in this script, such as initializing the environment, preprocessing data, etc. (Following the rules + of [cluster-image](https://github.com/labring-actions/cluster-image)) + +## Kubefile Parameters + +The `Kubefile` file is at the core of image building and supports various parameters. Below is a detailed analysis of +these parameters: + +```shell +FROM labring/kubernetes:v1.24.0 +ENV version v1.1.0 +COPY manifests ./manifests +COPY registry ./registry +ENTRYPOINT ["kubectl apply -f manifests/tigera-operator.yaml"] +CMD ["kubectl apply -f manifests/custom-resources.yaml"] +``` + +Descriptions of each parameter: + +- `FROM`: This directive is used to set the base image for building. All build steps are based on this image. +- `LABEL`: `LABEL` defines some internal configurations of the sealos cluster image. + - `check`: Some check scripts operation before the cluster image runs. + - `clean`: Cleanup scripts for cluster reset or node deletion. + - `clean-registry`: The script to clean the image repository when the cluster is reset. + - `image`: The lvscare image address of the cluster (Sealos's IPVS image). + - `init`: Cluster initialization script. + + +- `init-registry`: The script to start the container image repository when initializing the cluster. + - `sealos.io.type`: Cluster image type, currently mainly rootfs, application, and patch. + - Rootfs is the basic image for running the cluster, such as Kubernetes, Kubernetes-docker, which includes + images, binaries, etc. required by the cluster (**required for each node**). + - Application is the application image, such as calico, helm, istio, etc. application service images. (**only + stored on the master0 node**) + - Patch is needed to adjust after the rootfs image. It is another way to modify the rootfs image (**another + method is the Config method**), it will overwrite the first image of the default cluster running. + - `sealos.io.version`: The version number of the image, currently the opened version is v1beta1. + - `version`: The version number of the cluster, currently it's the version number of Kubernetes. + - `vip`: It's the VIP address for modifying the IPVS virtual IP. +- `ENV`: The `ENV` directive sets the environment variable `` to the value ``. (There will be some default + environment variables in rootfs, which can modify some default parameters in rootfs, such as the username and password + of the image repository, the storage directory of docker, containerd, etc.) + + For specific cluster images, you need to inspect it specifically, check the corresponding environment variables with + `sealos inspect` image, different versions of the image have slight differences. + - SEALOS_SYS_CRI_ENDPOINT: The criSocket of the current cluster image (different types of cluster images may be + different). + - criData: Data directory of cri. + - defaultVIP: Default VIP address. + - disableApparmor: Whether to disable apparmor (containerd has this issue). + - registryConfig: Configuration directory of the container image registry. + - registryData: Data directory of the container image registry (The configuration itself has no practical meaning. + because the directory is mounted and it's actually stored in `/var/lib/sealos`). + - registryDomain: Default domain of the container image registry. + - registryPassword: Default Password of the container image registry. + - registryPort: Default port number of the container image registry. + - registryUsername: Default username of the container image registry. + - sandboxImage: Default sandbox_image for cri to start. (No need to write repo, just need to write image name, eg: + pasue:3.7). +- `COPY`: The `COPY` directive copies new files or directories from `` and adds them to the file system path + `` on the container. (**Note that the registry directory needs to be copied, otherwise the cluster has no + container images**) +- `ENTRYPOINT`: This directive is used to set the startup command for the image. When the image starts, this command + will be executed. +- `CMD`: This directive is also used to set the startup command for the image. However, the difference between it and + the ENTRYPOINT directive is that if users provide a startup command when running the image (`sealos run --cmd`), the + command in the CMD directive will be overridden. + +During the build process, Sealos will also automatically set some built-in environment variables, including (environment +variables with the prefix 'SEALOS_SYS' cannot be modified): + +- SEALOS_SYS_KUBE_VERSION: The version number of Kubernetes, for example v1.26.0 +- SEALOS_SYS_SEALOS_VERSION: The version number of Sealos, for example 4.1.3. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/build.png b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/build.png new file mode 100644 index 000000000000..d1ae7d6aeadd Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/build.png differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png new file mode 100644 index 000000000000..485b531d28f8 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png new file mode 100644 index 000000000000..70ef65ffa13f Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/sealos-run.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/sealos-run.md new file mode 100644 index 000000000000..d32958c50dfb --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/sealos-run.md @@ -0,0 +1,151 @@ +--- +sidebar_position: 2 +keywords: [Sealos run, Kubernetes deployment, cluster management, Kubernetes images, node synchronization] +description: Learn how the Sealos run command simplifies Kubernetes deployment with detailed steps on node checks, image pulling, configuration rendering, and more. +--- + +# How Sealos Run Works + +Below is a detailed explanation of how the `sealos run` command works. To aid understanding, we break it down into nine +main steps. + +## 1. Execution of Command + +Firstly, users launch Sealos by executing the following command: + +```bash +sealos run kubernetes:v1.23.8 +``` + +## 2. Execution of Pipeline + +After the command is executed, Sealos starts to execute a series of related operational steps, which we call the " +Pipeline". This process includes the following substeps: + +### 2.1 Node Check + +Sealos will check all nodes to ensure they are available. For example, checking for duplicate hostnames, and whether the +time of all nodes has been synchronized. + +### 2.2 Pulling Images + +Next, Sealos pulls the Kubernetes images from the remote repository and loads them into local storage. + +### 2.3 Configuration Rendering + +Then, Sealos renders the configuration of the pulled image's rootfs (file system) to prepare for the subsequent node +distribution. + +### 2.4 Distribution of Image Files + +Once the configuration rendering is complete, Sealos distributes the rendered rootfs files to each node. + +### 2.5 Distribution of Registry Directory + +In addition, Sealos also distributes the registry directory in the image to the corresponding registry role nodes. +Sealos supports two modes: + +- **scp mode**: Directly copy the directory to each node via scp, which consumes a lot of network bandwidth. +- **Image synchronization mode**: Using the skopeo sdk's image synchronization mechanism, incremental image + synchronization can be achieved to save network bandwidth. This feature can be enabled with the environment variable + `SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true`. + +### 2.6 Execution of Bootstrap + +Bootstrap is a crucial step, including the following operations: + +#### 2.6.1 Addition of Host Resolution + +Sealos adds host resolution for the registry on each node. + +#### 2.6.2 Execution of Registry-Init Script + +After adding host resolution, Sealos executes the registry-init script. + +#### 2.6.3 Execution of Init Script + +Finally, Sealos executes the init script to start the Kubernetes service. + +## 3. Execution of InitMaster0 + +After the Pipeline execution, Sealos enters the InitMaster0 phase. In this phase, Sealos performs the following +operations: + +### 3.1 Generation of Kubeadm Configuration + +Firstly, Sealos generates the initialization configuration file for Kubeadm. + +### 3.2 Generation and Distribution of Certificate Files + +Then, Sealos generates all certificate files for the cluster and distributes them to each node. + +### 3.3 Initialization of Master0 + +Finally, Sealos executes `kubeadm init` to initialize the Master0 node. + +## 4. Execution of JoinMaster + +After the initialization of the Master0 node, Sealos enters the JoinMaster phase. In this phase, Sealos performs the +following operations: + +### 4.1 Synchronization of Kubeconfig + +Firstly, Sealos synchronizes the Kubeconfig file to each node. + +### 4.2 Synchronization of Certificate Files + +Next, Sealos synchronizes certificate files to each node. + +### 4.3 Generation of Join Configuration + +Then, Sealos generates the JoinMaster configuration file. + +### 4.4 Generation of Join Token + +Next, Sealos generates the Join token. + +### 4.5 Execution of Join Master Process + +Finally, Sealos executes the `kubeadm join` process to add other Master nodes to the cluster. + +## 5. Execution of JoinNode + +After all Master nodes have joined the cluster, Sealos enters + +the JoinNode phase and performs the following operations: + +### 5.1 Generation of Join Token + +Firstly, Sealos generates the Join token again. + +### 5.2 Generation of IPVS Rules + +Then, Sealos generates IPVS rules. + +### 5.3 Generation of Join Configuration + +Next, Sealos generates the JoinNode configuration again. + +### 5.4 Execution of Join Node Process + +Finally, Sealos executes the Join Node process to add Worker nodes to the cluster. + +## 6. Synchronization of IPVS + +After all nodes have joined the cluster, Sealos synchronizes the IPVS rules of the Master to all nodes. This is mainly +done through the lvscare static pod, which synchronizes the static pod configuration based on the number of Master +nodes. + +## 7. Execution of Guest Phase + +After the IPVS rules are synchronized, Sealos executes the Guest phase. This is the stage to execute the cluster image +command. + +## 8. Writing to the Clusterfile + +Finally, Sealos writes the final execution result to the Clusterfile. + +This is how the `sealos run` command works. Through this article, we hope that you have a deeper understanding of the +`sealos run` command. Below is the panorama we mentioned to help you better understand the entire process. + +![](images/sealos-run.png) diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/template-function.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/template-function.md new file mode 100644 index 000000000000..1c0a11c2ea58 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/advanced-guide/template-function.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 9 +keywords: [Sealos, template rendering, Kubernetes, image building, template functions, configuration files, semantic versioning, Golang templates, cluster image, Kubernetes manifests] +description: Learn how Sealos enhances image building with template rendering and functions for Kubernetes configurations, scripts, and manifests. +--- + +# Template Inclusion and Function Enhancement + +In the image building process of Sealos, files with the "tmpl" extension under the "etc", "scripts", and "manifests" +directories are specially treated. + +1. "etc" directory: This directory is usually used to store configuration files. During the build process, Sealos will + render the files with the "tmpl" extension in this directory. The files, once rendered, are copied to the + corresponding directory of the cluster image. + +2. "scripts" directory: This directory is usually used to store execution scripts. Sealos will render the files with + the "tmpl" extension in this directory. The generated scripts will be executed during the cluster image build + process. + +3. "manifests" directory: This directory is usually used to store Kubernetes resource manifest files. Sealos will render + the files with the "tmpl" extension in this directory. The generated manifest files will be applied to the Kubernetes + cluster during the cluster image build process. + +In summary, files with the "tmpl" extension in these three directories are treated as template files during the image +build process of Sealos. This approach provides more flexibility, allowing us to dynamically generate configurations, +scripts, or Kubernetes resource manifests during the build process. + +When building an image, we support `template` to allow maintainers to fully control the generated configuration files ( +module rendering). For example: + +```yaml +service: + name: {{ .serviceName }} + port: + {{- if typeIs "string" .servicePort }} + name: {{ .servicePort }} + {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} + number: {{ .servicePort | int }} + {{- end }} +``` + +Please refer to Golang's [text/template](https://pkg.go.dev/text/template) for a basic introduction and more details. + +## Template Function Enhancement + +In addition, we support `templateFunc` to enhance template functions. For example: + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://{{ .registryDomain }}:{{ .registryPort }} +force: true +debug: false +image: /var/lib/image-cri-shim +{{ if and (ne .SEALOS_SYS_KUBE_VERSION "") (semverCompare "^1.26.0" .SEALOS_SYS_KUBE_VERSION) }}version: v1{{ else }}version: v1alpha2{{ end }} +timeout: 15m +auth: {{ .registryUsername }}:{{ .registryPassword }} +``` + +Here we use `semverCompare` to check whether the user is running on k8s version v1.26.0 or above. If so, generate +`version: v1`, otherwise generate `version: v1alpha2`. With this support, we can easily manage multiple versions of +Kubernetes support with a single cluster image file. + +### Some Most Commonly Used Template Functions + +* [semverCompare](http://masterminds.github.io/sprig/semver.html) compares semantic versions, not string comparisons. +* [default](http://masterminds.github.io/sprig/defaults.html) default can provide a default value when a value is empty + or undefined. +* [toYaml](https://github.com/labring/sealos/blob/main/lifecycle/pkg/template/funcmap.go#L66) displays the current value (object, + map, array) as a yaml formatted string. + +For a complete list of supported functions, [click here](http://masterminds.github.io/sprig/). diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/images/sealos.webp b/docs/archived/5.0/docs/developer-guide/lifecycle-management/images/sealos.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/images/sealos.webp differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md new file mode 100644 index 000000000000..630413ae83e1 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 0 +keywords: [Sealos, cloud operating system, Kubernetes clusters, cloud-native applications, cluster management, container management, image management, cluster images, Sealos user guide, Rootfs cluster images] +description: Explore Sealos, a unified cloud OS for managing Kubernetes clusters and cloud-native apps. Learn about cluster management, containers, and images with our comprehensive user guide. +--- + +# Kubernetes Lifecycle Management + +Sealos provides a powerful set of tools that allow users to easily manage the entire lifecycle of a cluster. + +## Features + +With Sealos, you can install a bare Kubernetes cluster without any components. Additionally, Sealos can assemble various +upper-layer distributed applications on top of Kubernetes using cluster image capabilities, such as databases, message +queues, and more. + +Sealos not only allows you to install a single-node Kubernetes development environment but also enables you to build +production-grade highly available clusters with thousands of nodes. + +Sealos offers features like cluster scaling, backup and recovery, and cluster release. It provides an excellent +Kubernetes runtime experience even in offline environments. + +## Key Features + +- ARM support. Offline packages v1.20 and above support integration with both containerd and Docker. +- Provides 99-year certificates and supports cluster backup and upgrade. +- Does not rely on Ansible, HAProxy, or Keepalived. It is a standalone binary tool with zero dependencies. +- Provides offline installation. Different versions of Kubernetes only require different cluster images. +- High availability is achieved through localLB based on IPVS, which consumes fewer resources and provides stability and + reliability, similar to kube-proxy implementation. +- Automatically recognizes image names using image-cri-shim, making offline delivery more convenient. +- Almost compatible with all x86_64 architectures that support systemd. +- Easy addition/deletion of cluster nodes. +- Trusted by tens of thousands of users in production environments, stable and reliable. +- Supports cluster images, allowing you to customize and combine the cluster components you need, such as OpenEBS + storage + database + MinIO object storage. +- Uses the SDK of Buildah to standardize the image format, fully compatible with OCI standards. + +## Running a Kubernetes Cluster with Sealos + +Running a Kubernetes cluster with Sealos is straightforward. Just follow these steps: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.3.0/scripts/install.sh \ + | sh -s v4.3.0 labring/sealos +# Create a cluster +$ sealos run labring/kubernetes:v1.25.0-4.2.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +[![asciicast](https://asciinema.org/a/519263.svg)](https://asciinema.org/a/519263?speed=3) + +## Running Distributed Applications on the Cluster + +With the `sealos run` command, you can run various distributed applications on the cluster, such as databases, message +queues, AI capabilities, and even enterprise-level SaaS software. For example: + +```shell +# MySQL cluster +$ sealos run labring/mysql-operator:8.0.23-14.1 + +# Clickhouse cluster +$ sealos run labring/clickhouse:0.18.4 + +# Redis cluster +$ sealos run labring/redis-operator:3.1.4 +``` + +## Customizing the Cluster + +For cluster images not available in the Sealos ecosystem, users can easily build and customize their own cluster images. +For example: + +[Building an Ingress Cluster Image](/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md) + +You can also customize your own Kubernetes cluster: + +Sealfile: + +```shell +FROM kubernetes:v1.25.0 +COPY flannel-chart . +COPY mysql-chart . +CMD ["helm install flannel flannel-chart", "helm install mysql mysql-chart"] +``` + +```shell +sealos build -t my-kubernetes:v1.25.0 . +sealos run my-kubernetes:v1.25.0 ... +``` + +## Frequently Asked Questions + +**Is Sealos a Kubernetes installation tool?** + +Installation and deployment are basic functions of Sealos, similar to the boot module in a single-node operating system. +Sealos' boot module effectively manages the lifecycle of Kubernetes in any scenario. + +**What are the differences between Sealos, Rancher, and KubeSphere?** + +Sealos is designed with the philosophy of "simplifying complexity, freely assembling, and simplicity as the ultimate +goal." Sealos leverages the capabilities of Kubernetes to provide users with exactly what they need in a simple way. +Users may not necessarily need Kubernetes; what they need is specific functionality. + +Sealos is highly flexible and does not impose additional burdens on users. Its form depends on user requirements and the +applications being installed. The core of Sealos is distributed applications, and all applications are treated equally. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md new file mode 100644 index 000000000000..52ef7466d61e --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 4 +keywords: [build cluster image, sealos tool, helm binary, create Sealfile, push image DockerHub] +description: Learn how to use the sealos tool to package a binary file like helm into a cluster image, build it, and push it to DockerHub. +--- + +# Building Cluster Images Based on Binary Files + +This document primarily details how to use the `sealos` tool to package a single binary file (like `helm` or +`kustomize`) into a cluster image and install them by deploying the cluster image on the master node. Using `helm` as an +example, we will thoroughly discuss how to package a binary file into a cluster image. + +## Create a Build Workspace + +Firstly, create a base directory to serve as a build workspace: + +```shell +$ mkdir ~/cluster-images +``` + +In the workspace, create an `opt` directory for storing the binary files: + +```shell +$ cd cluster-images +$ mkdir opt/ +``` + +## Prepare the Binary File + +Next, we prepare the `helm` binary file. Here, we download from [github release](https://github.com/helm/helm/releases): + +```shell +wget https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz +tar -zxvf helm-v3.10.1-linux-amd64.tar.gz +chmod a+x linux-amd64/helm +mv linux-amd64/helm opt/ +``` + +## Create the `Sealfile` Required for Building the Image + +Create a file named `Sealfile`, with the following content: + +```shell +FROM scratch +COPY opt ./opt +CMD ["cp opt/helm /usr/bin/"] +``` + +The current directory structure is as follows: + +``` +. +├── Sealfile +└── opt + └── helm +``` + +## Build the Cluster Image + +Now, everything is ready, and you can begin building the cluster image: + +```shell +sealos build -t labring/helm:v3.10.1 . +``` + +**Note:** Firstly, you need to install the `sealos` command on the local host. + +You can view the build log to understand the building process. + +```shell +root@ubuntu:~/cluster-images# sealos build -t labring/helm:v3.10.1 . +... +``` + +View the built image, and now all the dependent binary files have been built into the cluster image: + +```shell +root@ubuntu:~/cluster-images# sealos images +labring/helm v3.10.1 19ed4a24f0fe 3 minutes ago 45.1 MB +``` + +## Push the Image + +You can push the image to any Docker image repository, the following command pushes the image to DockerHub: + +```shell +sealos push labring/helm:v3.10.1 +``` + +**Note:** Please use the `sealos` command to operate the cluster image; Docker commands are not supported. + +If you are using a private image repository, you can use the `sealos login` command to log into your image repository, +then push or pull the image. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md new file mode 100644 index 000000000000..e2cc83204727 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 5 +keywords: [build cluster images, go-template, sealos, environment variables, Kubefile, yaml template, helm command, Kubernetes service] +description: Learn how to build cluster images using go-template with sealos, customize environment variables in Kubefile, and render yaml templates for Kubernetes services. +--- + +# Building Cluster Images Based on go-template + +During the process of building cluster images, we can use the `--env` option to pass some variables through the sealos +command line. These environment variables can be used by the `CMD` command of the Kubefile or the yaml file template. + +## Using Environment Variables in Kubefile + +This example defines a `SERVICE_TYPE` variable that allows the user to customize the service exposure type when +installing the application and pass parameters to the helm command in CMD. + +Kubefile example: + +```shell +FROM scratch +ENV SERVICE_TYPE "NodePort" +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=$(SERVICE_TYPE)"] +``` + +Run the cluster application and set a custom `SERVICE_TYPE=LoadBalancer`, if not set, it will default to NodePort. + +```shell +sealos run labring/nginx:v1.23.1 --env SERVICE_TYPE=LoadBalancer +``` + +## Using Environment Variables in Yaml Files + +Prepare a simple nginx service yaml file, this file must be a `*.tmpl` extension to be rendered when running +`sealos run --env` command. + +```shell +$ cat manifests/service.yaml.tmpl +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: {{ .serviceType }} + ports: + - port: 80 + nodePort: {{ .http_NodePort }} + name: http + - port: 443 + nodePort: {{ .https_NodePort }} + name: https + selector: + name: nginx +``` + +Here is a Kubefile example where you can set the default environment variables. + +```shell +FROM scratch +ENV serviceType NodePort +ENV http_NodePort 30080 +ENV https_NodePort 30443 + +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/service.yaml"] +``` + +When you build the image, nothing will happen, it only renders when running the application. If `--env` is not set, it +will use the default ENV in Kubefile. + +```shell +sealos run labring/nginx:1.23.1 --env serviceType=LoadBalancer --env http_NodePort=30080 --env https_NodePort=30443 +``` + +You will find that sealos renders a new yaml file `service.yaml` based on `service.yaml.tmpl` on the local path of the +master node. + +**Note** The new version of the application's rootfs is placed in the `/var/lib/sealos/data/default/applications` +directory, each application has its independent directory. + +```shell +root@node1:~# ls /var/lib/sealos/data/default/rootfs/manifests |grep service +service.yaml +service.yaml.tmpl +``` + +Check the yaml content: + +```shell +root@node1:~# cat /var/lib/sealos/data/default/rootfs/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30080 + name: http + - port: 443 + nodePort: 30443 + name: https + selector: + name: nginx +``` + +**Note:** All types of files support this feature (the file name suffix is .tmpl and the build directory is in etc, +scripts, and manifests), you can try it yourself. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md new file mode 100644 index 000000000000..957a26cdab84 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md @@ -0,0 +1,209 @@ +--- +sidebar_position: 3 +keywords: [Helm Charts, build cluster image, Kubernetes, sealos, nginx Helm Charts] +description: Learn how to build a cluster image using Helm Charts with sealos, featuring a step-by-step guide for Kubernetes and nginx Helm Charts. +--- + +# Building Cluster Images Based on Helm Charts + +Let's use the simplest nginx application as an example to introduce how to build a cluster image based on nginx using +Helm Charts. + +## 1. Preparation + +Create a base directory for the build work. + +```shell +$ mkdir ~/cloud-images +``` + +Create a `charts` directory to store the Kubernetes nginx Helm Charts files. + +```shell +$ cd cloud-images +$ mkdir charts +``` + +## 2. Prepare Helm Charts + +Prepare the nginx Helm Charts. Here we use [the official nginx Helm Charts by bitnami](https://bitnami.com/stack/nginx). +Let's pull the Helm Chart files locally and unzip them to the `charts` directory. + +```shell +helm repo add bitnami https://charts.bitnami.com/bitnami +helm search repo bitnami/nginx +helm pull bitnami/nginx --version=13.2.13 -d charts/ --untar +``` + +**Note:** First, you should install the Helm command tool to your local host. + +Now, the structure of the charts directory is as follows: + +``` +charts/ +└── nginx + ├── Chart.lock + ├── charts + ├── Chart.yaml + ├── README.md + ├── templates + ├── values.schema.json + └── values.yaml +``` + +## 3. Create Kubefile + +Create a file named `Kubefile` for image construction: + +```shell +$ cat Kubefile +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm install nginx charts/nginx --namespace=nginx --create-namespace"] +``` + +It is recommended to use `helm upgrade --install` instead of `helm install` so that you can rerun the same command when +updating the application in the future. + +You can add other options as needed, such as exposing the service through NodePort. + +```shell +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +``` + +## 4. Build the Cluster Image + +Now everything is ready, and you can start building the cluster image. + +```shell +sealos build -t labring/nginx:v1.23.2 . +``` + +**Note:** You should first install the sealos command to your local host. + +You can view the build log. + +```shell +root@ubuntu:~/cloud-images# sealos build -t labring/nginx:v1.23.2 . +... +``` + +sealos will automatically extract the images from the charts directory, pull them locally, and store them in the +registry directory. + +The current directory structure is as follows: + +```shell +. +├── charts +│ └── nginx +│ ├── Chart.lock +│ ├── charts +│ ├── Chart.yaml +│ ├── README.md +│ ├── templates +│ ├── values.schema.json +│ └── values.yaml +├── Kubefile +└── registry + └── docker + └── registry +``` + +Check the built image locally. Now all dependent deployment manifests and image caches are built into the cluster image. + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.2 521c85942ee4 4 minutes ago 56.8 MB +``` + +You can push the image to any Docker image repository. The following command pushes it to Docker Hub. + +```shell +sealos push labring/nginx:v1.23.2 +``` + +**Note:** Please use the sealos command to operate the cluster + +image, Docker commands are not supported. + +If you use a private image repository, just use the `sealos login` command to log in to the registry before pulling or +pushing the image. + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +## 5. Install the Cluster Image + +Then, you can run the cluster image in your cluster. + +```shell +sealos run labring/nginx:v1.23.2 +``` + +The helm binary command will be installed on the master node of your Kubernetes cluster. + +```shell +root@ubuntu:~# helm -n nginx ls +``` + +## 6. Explanation + +By default, when building images, sealos only parses the default values.yml file. However, you can also provide a custom +values.yaml file for sealos. + +**The custom values file must be placed in the same directory as your Chart, and must be named in the form +of `.values.yaml`, for example `loki-stack.values.yaml`.** + +```shell +. +├── charts +│ ├── loki-stack +│ │ ├── charts +│ │ ├── Chart.yaml +│ │ ├── README.md +│ │ ├── requirements.lock +│ │ ├── requirements.yaml +│ │ ├── templates +│ │ └── values.yaml +│ └── loki-stack.values.yaml +├── init.sh +├── Kubefile +``` + +The content of `loki-stack.values.yaml` file is as follows: + +```shell +$ cat charts/loki-stack.values.yaml +promtail: + enabled: false +fluent-bit: + enabled: true +grafana: + enabled: true +``` + +Different values files may output different image lists, allowing sealos to automatically parse the images during the +`sealos build` process. + +```shell +$ helm template charts/loki-stack/ -f charts/loki-stack/values.yaml|grep image: + image: "grafana/promtail:2.0.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + +$ helm template charts/loki-stack/ -f charts/loki-stack.values.yaml|grep image: + image: "grafana/fluent-bit-plugin-loki:1.6.0-amd64" + image: "kiwigrid/k8s-sidecar:0.1.209" + image: "grafana/grafana:6.7.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + image: bats/bats:v1.1.0 +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md new file mode 100644 index 000000000000..cf799c740f58 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md @@ -0,0 +1,84 @@ +--- +sidebar_position: 1 +keywords: [Cluster Images, Image Manifests, Docker Tarballs, Kubernetes, Calico CNI, Image Manifest Support, OCI Container Images, Docker Archive Images] +description: Learn how to build cluster images using image manifests or Docker tarballs with Kubernetes and Calico CNI. Follow our step-by-step guide for seamless image management. +--- + +# Building Cluster Images Based on Image Manifests + +This guide will walk you through the process of building cluster images using image manifests or using existing tarballs +stored in Docker. + +## Building from Image Manifests + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── shim +│   └── CalicoImageList +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM labring/kubernetes:v1.24.0 +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +Explanation: + +The images listed in CalicoImageList will be pulled locally and then applied to the cluster using the `kubectl apply -f` +command. + +The image manifest currently supports the following: + +- Remote images like docker.io/calico/cni:v3.20.0 +- Local OCI container images like containers-storage:docker.io/labring/coredns:v0.0.1 +- Local Docker container images like docker-daemon:docker.io/library/nginx:latest + +## Building from Image Tarballs + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── skopeo +│   ├── calico.tar +│   └── tar.txt +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM scratch +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +Explanation: + +The configurations in tar.txt will be pulled locally and redirected to the image list. Then, they will be applied to the +cluster using the `kubectl apply -f` command. The configuration file format is as follows: + +``` +docker-archive:calico.tar@calico/cni:v3.20.0 +``` + +The image manifest currently supports the following: + +- Docker archive images, supporting a single image, like docker-archive +- OCI archive images, supporting a single image, like oci-archive diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md new file mode 100644 index 000000000000..6d4352761bab --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 2 +keywords: [build cluster images, deployment manifest, nginx deployment, Kubernetes yaml, sealos build] +description: Learn how to build cluster images based on deployment manifests using a simple nginx application example with Kubernetes yaml and sealos build. +--- + +# Building Cluster Images Based on Deployment Manifest + +This document will detail how to build cluster images based on Deployment Manifests. We will use a simple nginx +application as an example. + +## I. Preparations + +1. First, create a base directory as the build workspace. + +```shell +$ mkdir ~/cloud-images +``` + +2. Create a directory named `manifests` to store the kubernetes nginx deployment yaml file. + +```shell +$ cd cloud-images +$ mkdir manifests +``` + +## II. Prepare the Manifest File + +At this stage, we will prepare a simple nginx kubernetes yaml file. + +```shell +$ cat manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.23.1 + ports: + - containerPort: 80 +``` + +## III. Create Kubefile + +At this stage, we need to create a Kubefile that will be used to build the image. + +```shell +FROM scratch +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/deployment.yaml"] +``` + +## IV. Build the Cluster Image + +After preparing all the necessary files and directories, we can start building the cluster image. + +```shell +sealos build -t labring/nginx:v1.23.1 . +``` + +**Note:** Before starting the build, you need to install the sealos command on your local host. + +During the build, you can view the build log. + +## V. Verify the Image + +After the build is complete, you can view the built image with the following command: + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.1 521c85942ee4 4 minutes ago 56.8 MB +``` + +## VI. Push the Image + +Finally, we can push the built image to any Docker image repository. The following command pushes it to DockerHub. + +```shell +sealos push labring/nginx:v1.23.1 +``` + +**Note:** Please use the sealos command to operate the cluster image, the Docker command is not supported. + +If you are using a private image repository, just use `sealos login` to log into the repository before pulling or +pushing the image. + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +At this point, the cluster image based on the deployment manifest is successfully built. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md new file mode 100644 index 000000000000..f3084241176f --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md @@ -0,0 +1,137 @@ +--- +sidebar_position: 6 +keywords: [sealos, build cluster image, exec command, scp command, Kubernetes, openebs mayastor, flannel installation, cluster management, hugepages, kernel modules] +description: Learn how to build cluster images using sealos exec and scp commands. Follow step-by-step examples for openebs mayastor and flannel installation. +--- + +# Building Cluster Images Using exec and scp Commands + +By default, `sealos run xx` only runs the command and copies the file on the first master node. When you want to run the +command or copy files on specific nodes or all nodes, you can use the `sealos exec` or `sealos scp` commands when +building the cluster image. + +- sealos exec: Connects to one or more nodes and runs any shell command; +- sealos scp: Connects to one or more nodes and copies local files to remote nodes. + +Although you can directly use these commands on the host, this article mainly describes how to use these two commands +when building the cluster image using sealos build. + +## sealos exec Example + +Below is an example of building an openebs cluster image. Before installing openebs maystor, some initialization +operations need to be performed on the node, you can use sealos exec to achieve this. + +First, create a base directory for building. + +```shell +$ mkdir ~/cloud-images +``` + +Create a `charts` directory to store the kubernetes nginx helm charts file. + +```shell +$ cd cloud-images +``` + +Create a file named `Kubefile` for image building: + +```shell +$ cat Kubefile +FROM scratch +COPY manifests manifests +COPY registry registry +COPY opt opt +COPY mayastor.sh mayastor.sh +CMD ["bash mayastor.sh"] +``` + +Create a script file named `mayastor.sh`, the shell command after sealos exec will be executed on all nodes (create +hugepage, load kernel modules on all nodes), but other commands will only run on the master node. + +```shell +$ cat mayastor.sh +#!/usr/bin/env bash +set -e + +sealos exec " +echo vm.nr_hugepages = 1024 | sudo tee -a /etc/sysctl.d/mayastor.conf +sysctl -p +sudo modprobe -- nbd +sudo modprobe -- nvmet +sudo modprobe -- nvmet_rdma +sudo modprobe -- nvme_fabrics +sudo modprobe -- nvme_tcp +sudo modprobe -- nvme_rdma +sudo modprobe -- nvme_loop +cat < /tmp/buildimage/kubeadm.yml < /tmp/buildimage/Kubefile < Clusterfile +``` + +Notice: labring/helm should be set before labring/calico. + +The generated Clusterfile is as follows: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.24.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +``` + +
+ +2. After generating the Clusterfile, update the cluster configuration. For example, to modify the CIDR range of pods, + you can modify the `networking.podSubnet` and `spec.data.spec.calicoNetwork.ipPools.cidr` fields. The final + Clusterfile would look like this: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +--- +apiVersion: kubeadm.k8s.io/v1beta2 +kind: ClusterConfiguration +networking: + podSubnet: 10.160.0.0/12 +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + ipPools: + - blockSize: 26 + cidr: 10.160.0.0/12 + encapsulation: IPIP + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*" +``` + +
+ +3. Run `sealos apply -f Clusterfile` to start the cluster. After the cluster is successfully running, the Clusterfile + will be saved in the `.sealos/default/Clusterfile` file. You can modify the fields in it to reapply changes to the + cluster. + +**Note:** + +- You can refer to + the [official documentation](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/) or run the + `kubeadm config print init-defaults` command to print the kubeadm configuration. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md new file mode 100644 index 000000000000..7fd997559684 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 0 +keywords: [Sealos cluster, Docker images, cluster initialization, node management, service management] +description: Learn how to effectively run and manage a Sealos cluster with Docker images. Get detailed tutorials on cluster initialization, node management, and service management. +--- + +# Running a Sealos Cluster with Docker Images + +In this directory, we provide a series of detailed tutorials and resources to help you effectively run and manage a +Sealos cluster. Here is a brief overview of each file or subdirectory: + +1. **Cluster Initialization**: This section provides all the necessary steps to start and initialize a new Sealos + cluster. It includes information on how to configure Sealos and how to run initialization commands. +2. **Node Management**: This section covers how to add and remove cluster nodes, as well as how to maintain and manage + detailed information about the cluster nodes. +3. **Service Management**: This section includes guides on how to deploy and manage various services on the Sealos + cluster. It covers how to use Sealos' command-line tools to manage services and how to configure services to meet + your specific requirements. +4. **Practical Examples**: This section contains various practical examples, including how to run and manage various + applications and how to handle specific operational issues. + +We hope this directory provides comprehensive assistance for running and managing a Sealos cluster. If you have any +questions or suggestions, please feel free to contact us at any time. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md new file mode 100644 index 000000000000..5d81ff82b735 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 2 +keywords: [Go template, Clusterfile, Kubernetes cluster, sealos, cluster deployment] +description: Learn how to configure and deploy a Kubernetes cluster using Go template syntax with sealos. Follow our guide for a seamless setup. +--- + +# Template Configuration Installation + +We can also use Go template syntax to write the Clusterfile (similar to [Helm](https://helm.sh/)), but partial template +functions such as `include`/`tpl`/`require`/`lookup` are not supported yet. For example, create a `Clusterfile.yaml` as +follows: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: {{ .Values.clusterName }} +spec: + hosts: + - ips: {{ .Values.masters | toYaml | nindent 8 }} + roles: ["master", "amd64"] + {{- with .Values.nodes }} + - ips: {{ . | toYaml | nindent 8 }} + roles: ["node", "amd64"] + {{- end }} + image: {{ .Values.images | toYaml | nindent 4 }} + ssh: + passwd: {{ env "SSH_PASSWORD" .Values.ssh.passwd }} + pk: {{ default "~/.ssh/id_rsa" .Values.ssh.pk }} + port: {{ default 22 .Values.ssh.port | int }} + user: {{ default "root" .Values.ssh.user }} +--- +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +networking: + dnsDomain: {{ default "cluster.local" .Values.networking.dnsDomain }} + serviceSubnet: {{ default "10.96.0.0/18" .Values.networking.serviceSubnet }} + podSubnet: {{ default "100.64.0.0/17" .Values.networking.podSubnet }} +``` + +Then, create a custom values file `example.values.yaml`: + +```yaml +clusterName: default +images: + - dockerhub.tencentcloudcr.com/labring/kubernetes:v1.23.8 + - dockerhub.tencentcloudcr.com/labring/calico:v3.24.1 +masters: + - 10.74.16.27:22 + - 10.74.16.140:22 + - 10.74.16.101:22 +nodes: [] +ssh: + # passwd: notSetYet + pk: /path/to/private/key/file + port: 22 + user: root +networking: + dnsDomain: cluster.local + serviceSubnet: 10.96.0.0/18 + podSubnet: 100.64.0.0/17 +``` + +You can then deploy the cluster like this: + +```shell +$ sealos apply -f Clusterfile.yaml --values example.values.yaml --set clusterName=testlocal --env SSH_PASSWORD=s3cret +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md new file mode 100644 index 000000000000..b09b4113d32d --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 3 +keywords: [Kubernetes upgrade, sealos run, upgrade cluster, Kubernetes v1.25.0, Kubernetes lifecycle management] +description: Learn how to upgrade your Kubernetes cluster using sealos run. Follow our step-by-step guide to ensure a smooth upgrade to Kubernetes v1.25.0. +--- + +# How to Upgrade the Cluster + +If you want to upgrade your Kubernetes cluster, you just need to run the following command: + +```sh +sealos run labring/kubernetes: +``` + +Make sure you have already set up the cluster. + +## Example Scenario + +1. Let's say you have previously run the following command: + +```sh +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.8 --nodes 192.168.64.7 +``` + +2. Now, if you want to upgrade the cluster to v1.25.0, you can do the following: + +```sh +sealos run labring/kubernetes:v1.25.0 +``` + +During the execution of 'kubeadm upgrade v1.25.0', you will see: + +```txt +[upgrade/version] You have chosen to change the cluster version to "v1.25.0" +[upgrade/versions] Cluster version: v1.24.0 +[upgrade/versions] kubeadm version: v1.25.0 +[upgrade] Are you sure you want to proceed? [y/N]: +``` + +Type 'y' to proceed with the upgrade. + +If there are any **errors**, you can rerun the command 'sealos run labring/kubernetes:v1.25.0'. Even if it fails, it +will ensure the same result. + +## Important Notes + +1. **Upgrades cannot skip minor version numbers**. For example, upgrading from 'v1.23.0' to 'v1.25.0' is not allowed. If + you do need to upgrade from 'v1.23.0' to 'v1.25.0', you can do it in two steps, such as upgrading from 'v1.23.0' to ' + v1.24.0' first, and then from 'v1.24.0' to 'v1.25.0'. + +2. Once the upgrade is successful, the old version images mounted by the cluster will be replaced. Adding master or + worker nodes will apply the new version. + +This is the entire process of upgrading a Kubernetes cluster. If you encounter any issues during the upgrade process, +don't hesitate to refer to relevant documentation or seek assistance. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md new file mode 100644 index 000000000000..269165eb9dfb --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 3 +keywords: [nginx ingress, Helm chart, Kubernetes, Docker image, Sealos] +description: Learn how to build an nginx-ingress cluster image using Helm and Sealos, including steps for downloading charts, writing Dockerfiles, and pushing to image registries. +--- + +# Building an Ingress Cluster Image + +Here we demonstrate how to build an nginx-ingress cluster image using Helm. + +## Download the Helm Chart + +```shell +$ mkdir ingress-nginx && cd ingress-nginx +$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +$ helm pull ingress-nginx/ingress-nginx +``` + +You will find the downloaded chart: + +```shell +$ ls +ingress-nginx-4.1.0.tgz +``` + +## Add Image List + +Sealos will download the images in the image list and cache them in the registry directory. + +The directory must be in the format `images/shim/[your image list filename]`: + +```shell +$ cat images/shim/nginxImages +k8s.gcr.io/ingress-nginx/controller:v1.2.0 +k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1 +``` + +## Write the Dockerfile + +```Dockerfile +FROM scratch +COPY ../examples . +CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"] +``` + +## Build the Cluster Image + +```shell +$ sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 . +``` + +Sealos will automatically add the image dependencies from the image list to the cluster image, magically saving the +Docker images it depends on inside. When running in another environment, it will magically check if the Docker images +exist in the cluster. If they do, it will automatically download them; otherwise, it will download them from k8s.gcr.io. +Users do not need to modify the Docker image addresses in the Helm chart. This utilizes the black technology of image +caching proxy. + +## Push to the Image Registry + +```shell +$ sealos login docker.io +$ sealos push docker.io/fanux/ingress-nginx:v1.2.0 +``` + +## Run the Cluster Image + +```shell +$ sealos run docker.io/fanux/ingress-nginx:v1.2.0 +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md new file mode 100644 index 000000000000..dc9edf21ffed --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 2 +keywords: [Kubernetes installation, Sealos CLI, multi-node Kubernetes, Kubernetes cluster setup, offline Kubernetes installation] +description: Learn how to install Kubernetes clusters using Sealos CLI on both amd64 and arm64 architectures, including single-node and multi-node setups, with offline installation support. +--- + +# Install Kubernetes + +Sealos supports installing Kubernetes clusters on `amd64` and `arm64` architecture machines. + +## Prerequisites + +You'll first need to [download the Sealos CLI tool](/developer-guide/lifecycle-management/quick-start/install-cli.md). +Sealos is a simple Golang binary that can be installed on most Linux operating systems. + +Here are some basic installation requirements: + +- Each cluster node should have a unique hostname without underscores. +- System times must be synchronized across all nodes. +- The `sealos run` command must be run on the **first master node** of the Kubernetes cluster. **Installation on nodes + outside the cluster is not currently supported**. +- It is recommended to use a clean OS to create the cluster. **Do not install Docker manually!** +- Supports most Linux distributions like Ubuntu, CentOS, Rocky Linux. +- Compatible with all Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags). +- Supports Containerd as the container runtime. +- When installing on public clouds, use **private IP** addresses. + +## View Available Cluster Images + +All Sealos cluster images are hosted in the [cluster-image-docs](https://github.com/labring-actions/cluster-image-docs) +repository. + +You can browse all Sealos images on Docker Hub +here: [https://hub.docker.com/u/labring](https://hub.docker.com/u/labring). + +Use [Registry Explorer](https://explore.ggcr.dev/) to view all versions of the Kubernetes cluster images by entering +`labring/kubernetes` and clicking "Submit Query": + +![](images/registry-explorer.png) + +This will display all available tags for that image. + +:::info Note + +Kubernetes cluster stability improves with higher minor version numbers. For example in v1.28.x, x is the minor version. +It is recommended to use versions with relatively high minor numbers. Currently, the highest v1.27 version is v1.27.7, +while the highest v1.28 version is v1.28.3, so **v1.27.7 is recommended**. Choose the optimal Kubernetes version based +on your needs. + +::: + +## Install Single-Node Kubernetes + +```shell +# sealos version must >= v4.1.0 +$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 --single +``` + +## Install Kubernetes Cluster + +```shell +$ sealos run labring/kubernetes:v1.27.7 labring/helm:v3.9.4 labring/cilium:v1.13.4 \ +--masters 192.168.64.2,192.168.64.22,192.168.64.20 \ +--nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +Note: labring/helm must come before labring/cilium. + +Parameter descriptions: + +| Parameter | Example | Description | +|--------------|----------------------------|----------------------------------| +| --masters | 192.168.0.2 | List of master node IP addresses | +| --nodes | 192.168.0.3 | List of worker node IP addresses | +| --ssh-passwd | [your-ssh-passwd] | SSH password | +| kubernetes | labring/kubernetes:v1.25.0 | Kubernetes cluster image | + +Running the above commands directly on clean servers will install a highly available Kubernetes cluster without any +extra steps. + +## Install Various Distributed Apps + +```shell +sealos run labring/helm:v3.9.4 # Install Helm + +sealos run labring/openebs:v3.9.0 # Install OpenEBS + +sealos run labring/minio-operator:v4.5.5 labring/ingress-nginx:4.1.0 +``` + +This provides highly available apps like Minio without worrying about dependencies. + +## Add Kubernetes Nodes + +Add worker nodes: + +```shell +$ sealos add --nodes 192.168.64.21,192.168.64.19 +``` + +Add master nodes: + +```shell +$ sealos add --masters 192.168.64.21,192.168.64.19 +``` + +## Delete Kubernetes Nodes + +Delete worker nodes: + +```shell +$ sealos delete --nodes 192.168.64.21,192.168.64.19 +``` + +Delete master nodes: + +```shell +$ sealos delete --masters 192.168.64.21,192.168.64.19 +``` + +## Clean Up Kubernetes Cluster + +```shell +$ sealos reset +``` + +## Offline Installation + +For offline environments, first import the images - subsequent steps are identical to online installation. + +First, export images from an online environment: + +```shell +$ sealos pull registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 + +$ sealos save -o kubernetes.tar registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +``` + +### Import and Install + +Copy kubernetes.tar to the offline environment and import it with: + +```shell +$ sealos load -i kubernetes.tar +``` + +The rest of the installation process is identical to online installation: + +```shell +$ sealos images # Verify import succeeded + +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 # Single node +``` + +### Quick Cluster Install + +You can also install Kubernetes by directly running: + +```shell +$ sealos run kubernetes.tar +``` + +## Cluster Image Version Compatibility + +### Kubernetes Compatibility with Containerd + +It's advised to use Containerd as the container runtime interface (CRI) in Kubernetes clusters. Containerd stands out +for its lightweight structure and high efficiency, and it's fully compatible with Docker. Kubernetes images that utilize +Containerd benefit from enhanced performance and optimized resource usage. Here are the specific supported versions: + +| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version | +|--------------------|-------------------------|-------------|----------------------------| +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes:v1.28.0 | +| `>=1.30` | `>=v5.1.0` | v1 | labring/kubernetes:v1.28.0 | + +The choice of Sealos and CRI versions is dependent on the Kubernetes version in question. For instance, Kubernetes +v1.26.0 would require Sealos v4.1.4-rc3 or newer, along with the v1 CRI version. + +### Kubernetes Compatibility with Docker + +Alternatively, Docker can also be used as the container runtime. The following table provides a breakdown of compatible +Kubernetes versions with their corresponding Sealos and CRI versions for Docker-based setups: + +| Kubernetes Version | Required Sealos Version | CRI Version | Cluster Image Version | +|--------------------|-------------------------|-------------|-----------------------------------| +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes-docker:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes-docker:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes-docker:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes-docker:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes-docker:v1.28.0 | +| `>=1.30` | `>=v5.1.0` | v1 | labring/kubernetes-docker:v1.28.0 | + +As with the Containerd setup, the appropriate Sealos and CRI versions must be matched with the specific version of +Kubernetes being used. For a Kubernetes v1.26.0 setup, this means selecting Sealos v4.1.4-rc3 or later, and a v1 CRI +version. + +### k3s Compatibility with Containerd + +| k3s Version | Required Sealos Version | Cluster Image Version | +|-------------|-------------------------|-----------------------| +| `>=1.24` | `>=v5.0.0` | labring/k3s:v1.24.0 | + +## Summary + +You can choose from a variety of image types and versions to suit your individual needs and preferences. Don't forget to +consult the [CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) to stay informed about the +latest updates and bug fixes for each version. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png new file mode 100644 index 000000000000..a9e28004026c Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/install-cli.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/install-cli.md new file mode 100644 index 000000000000..8157c97f2e31 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/quick-start/install-cli.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 1 +keywords: [Sealos CLI, Sealos installation, Sealos download, Sealos binary, Sealos package management] +description: Learn how to download and install Sealos CLI with our comprehensive guide, including binary and package management tool installation methods. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Download Sealos CLI + +You can get the list of versions by running: + +```bash +curl --silent "https://api.github.com/repos/labring/sealos/releases" | jq -r '.[].tag_name' +``` + +Note: While choosing the version, it's recommended to use a stable version. The versions like `v4.3.0-rcx`, +`v4.3.0-alpha1` are pre-releases, use them with caution. + +Set the 'VERSION' environment variable to the latest VERSION number, or replace 'version' with the Sealos version you +want to install: + +```shell +VERSION=`curl -s https://api.github.com/repos/labring/sealos/releases/latest | grep -oE '"tag_name": "[^"]+"' | head -n1 | cut -d'"' -f4` +``` + +## Binary Auto Download + +```bash +curl -sfL https://raw.githubusercontent.com/labring/sealos/${VERSION}/scripts/install.sh | + sh -s ${VERSION} labring/sealos + +``` + +## Binary Manual Download + + + + +```bash +$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_amd64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +```bash +$ wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_arm64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +## Package Management Tool Installation + +### DEB Repository + +```bash +echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | sudo tee /etc/apt/sources.list.d/labring.list +sudo apt update +sudo apt install sealos +``` + +### RPM Repository + +```bash +sudo cat > /etc/yum.repos.d/labring.repo << EOF +[fury] +name=labring Yum Repo +baseurl=https://yum.fury.io/labring/ +enabled=1 +gpgcheck=0 +EOF +sudo yum clean all +sudo yum install sealos +``` + +## Source Code Installation + +### Prerequisites +1. `linux` +2. `git` +3. `golang` 1.20+ +4. `libgpgme-dev libbtrfs-dev libdevmapper-dev` + +If you are in an `arm64` environment, add the `:arm64` suffix. + +### Build + +```bash +# git clone the repo +git clone https://github.com/labring/sealos.git +# just make it +make build BINS=sealos +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json new file mode 100644 index 000000000000..950b839be0aa --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 3 +} diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md new file mode 100644 index 000000000000..2f227581975e --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 3 +keywords: [image-cri-shim, Kubernetes, container runtime, CRI, kubelet, container deployment, image registry, middleware] +description: Learn how image-cri-shim simplifies Kubernetes container deployment by automatically recognizing image names, streamlining the process. +--- + +# image-cri-shim User Guide + +## Overview + +image-cri-shim is a gRPC (Google Remote Procedure Call) shim based on CRI (Container Runtime Interface) and kubelet. CRI +is the interface used in Kubernetes to interact with container runtimes, while kubelet is the Kubernetes component +responsible for maintaining container runtime status and node-level resource management. + +The main functionality of image-cri-shim is automatic image name recognition, which eliminates the need for users to +manually specify the image name when deploying containers with Kubernetes. This simplifies the container image +deployment process and improves the convenience for users. + +In practical usage, image-cri-shim serves as middleware that receives requests from kubelet and forwards them to the +container runtime. By automatically recognizing the image name, image-cri-shim streamlines the deployment process of +container images and reduces user burden. + +``` ++------------+ +----------------+ +-------------------+ +| User | | Kubelet | | image-cri-shim | +| (Kubernetes| | (Node agent) | | (Middleware) | +| Manifest) | | | | | ++-----+------+ +-------+--------+ +-------+-----------+ + | | | + | YAML Manifest | | + |---------------> | | + | | | + | | | + | | CRI Request | + | |------------------------> | + | | | + | | Image Name | + | | Auto-Recognition | + | | | + | | | + | | CRI Response | + | | <------------------------+ + | | | + | | | + | Container | | + | Deployment | | + | <----------------------| | + | | | + | | | ++------------+ +-------+--------+ +-------+-----------+ + +``` + +From the above flowchart, it can be seen that a user creates a Kubernetes YAML manifest containing container information +and submits the manifest to kubelet. kubelet, which acts as an agent on Kubernetes nodes, is responsible for managing +containers. +Next, kubelet sends a CRI request to the image-cri-shim middleware. The main task of image-cri-shim is to automatically +recognize the image name. It processes the CRI request and retrieves relevant image information. Once image-cri-shim +identifies the image name, it returns the CRI response to kubelet. + +Finally, kubelet deploys the container using the image name obtained from image-cri-shim. This process is transparent to +the user, as they do not need to manually specify the image name, simplifying the container deployment process and +improving convenience. + +## Architecture + +The architecture of image-cri-shim is illustrated in the following diagram: + +![](images/image-cri-shim.png) + +## Usage + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://sealos.hub:5000 +force: true +debug: true +timeout: 15m +auth: admin:passw0rd + +registries: +- address: http://172.18.1.38:5000 + auth: admin:passw0rd +``` + +The above configuration is a YAML file used to set the parameters of image-cri-shim. It includes several key parameters, +and the explanation for each parameter is as follows: + +1. shim: Specifies the UNIX socket file path + +for image-cri-shim. This path is used for communication with kubelet. + +2. cri: Specifies the UNIX socket file path for the container runtime (e.g., containerd). image-cri-shim uses this path + to communicate with the container runtime. +3. address: Defines the address of the image registry. In this example, the registry address is http://sealos.hub:5000. +4. force: When set to true, image-cri-shim forcefully starts the shim without waiting for the cri to start. +5. debug: When set to true, enables debug mode and outputs more logging information. +6. timeout: Defines the timeout for image operations. In this example, the timeout is set to 15 minutes (15m). +7. auth: Defines the authentication credentials for accessing the image registry. In this example, the username is admin + and the password is passw0rd. + +Additionally, the configuration file includes a list of registries, which defines other image registries and their +authentication credentials. In this example, there is only one additional registry: + +- address: The address of the registry is http://172.18.1.38:5000. +- auth: The authentication credentials for accessing the registry. In this example, the username is admin and the + password is passw0rd. + +This configuration file provides image-cri-shim with the necessary information to communicate with kubelet, the +container runtime (such as containerd), and access and manage the image registry. + +Note: image-cri-shim is compatible with both CRI API v1alpha2 and v1. + +### Service Management + +image-cri-shim is typically run as a system service. To manage image-cri-shim, you can use system service management +tools (such as systemctl) to start, stop, restart, or view the status of the service. First, make sure you have +correctly installed image-cri-shim and configured it as a system service. + +1. Start the service: `systemctl start image-cri-shim` +2. Stop the service: `systemctl stop image-cri-shim` +3. Restart the service: `systemctl restart image-cri-shim` +4. View the service status: `systemctl status image-cri-shim` + +### Log Management + +To view the logs of the image-cri-shim service, you can use the journalctl command. journalctl is a tool used to query +and display system logs, and it is used in conjunction with the systemd service manager. + +Here are the commands to view the logs of the image-cri-shim service using journalctl: + +```shell +journalctl -u image-cri-shim +``` + +This will display all logs of the image-cri-shim service. If you want to view the logs in real-time, you can add the -f +option: + +```shell +journalctl -u image-cri-shim -f +``` + +Additionally, you can filter the logs by time. For example, if you only want to view the logs from the past hour, you +can use the following command: + +```shell +journalctl -u image-cri-shim --since "1 hour ago" +``` + +These commands should help you view and analyze the logs of the image-cri-shim service, allowing you to better +understand the service's operation status and potential issues. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png new file mode 100644 index 000000000000..5c8d5f81b23a Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/_category_.json b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/_category_.json new file mode 100644 index 000000000000..e47d9e07a6d5 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 99 +} \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/images/01.webp b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/images/01.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/images/01.webp differ diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/lvscare.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/lvscare.md new file mode 100644 index 000000000000..d14d51d5f7ca --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/lvscare/lvscare.md @@ -0,0 +1,101 @@ +--- +keywords: [LVScare, Sealos, Kubernetes high availability, IPVS load balancing, Kubernetes health check] +description: Learn how LVScare and Sealos ensure high availability and reliability of Kubernetes clusters with IPVS-based load balancing and health checks. +--- + +# LVScare User Guide + +## Introduction + +LVScare is a lightweight load balancing and health checking tool based on IPVS technology. It provides real-time +monitoring and management of backend services. Sealos, a cloud operating system, is a Kubernetes HA installation tool +based on kubeadm that can be used to install and upgrade high availability Kubernetes clusters on any Linux system. When +combined, Sealos and LVScare enable high availability of Kubernetes master nodes. Leveraging the lifecycle management +capabilities of Sealos and the lightweight, zero-dependency, and high availability features of LVScare, it ensures the +stability and reliability of Kubernetes clusters effectively. + +### How LVScare Works and Its Features + +LVScare monitors the health status of backend services (real servers) in real-time using IPVS. If a service becomes +unavailable, LVScare immediately sets its weight to 0 (for graceful TCP termination) and removes it from the service +list during the next check. When the service recovers, LVScare automatically adds it back to the service list. This +design of LVScare makes it lightweight, zero-dependency, and highly available. It occupies fewer resources, is stable +and reliable, and similar to the implementation of kube-proxy, it can ensure the continuous availability of services +through IPVS-based local load balancing. + +## Integration of Sealos and LVScare + +In Sealos, we utilize the recommended approach of static pods to automatically configure and manage LVScare for +achieving high availability of Kubernetes clusters. During the installation process of a Kubernetes cluster, Sealos +automatically uses LVScare for health checking and load balancing of the master nodes. This means that even if one of +the master nodes fails, it does not affect the overall functionality of the Kubernetes cluster. + +![](images/01.webp) + +### Running LVScare as a Static Pod + +First, use the `sealctl static-pod` command to generate the configuration for LVScare and place it in the +`/etc/kubernetes/manifests` directory. This allows all nodes in the Kubernetes cluster to access this configuration. + +```bash +lvscare care --vs 10.103.97.12:6443 --rs 192.168.0.2:6443 --rs 192.168.0.3:6443 --rs 192.168.0.4:6443 --interval 5 --mode route +``` + +### Generating and Adjusting Static Pod Configuration + +To start LVScare on each node, we can use the following command to generate the configuration for the static pod: + +```bash +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +When there are changes in the master nodes, Sealos only needs to rerun the `sealctl static-pod` command to adjust the +master nodes, simplifying the logic of maintaining static pods. Before joining the cluster nodes, we need to manually +start the IPVS rules by calling `sealctl ipvs` (which directly calls the LVScare SDK) to maintain the IPVS cluster. +After the node joins successfully, Kubernetes static pods can take over the IPVS rules. + +## Advantages of Using LVScare + +### High Availability + +By combining LVScare with Sealos, high availability of Kubernetes cluster master nodes can be achieved. + +### Health Checking Mechanism + +The health checking mechanism of LVScare can promptly detect and handle issues, preventing them from causing larger +problems due to the failure of a single node. + +### Seamless Integration + +As a static pod in Seal + +os, LVScare can seamlessly integrate with other parts of the Kubernetes cluster. + +### Simplified Operations + +Sealos automatically configures and manages LVScare, greatly simplifying the operational tasks of the Kubernetes +cluster. + +## LVScare Usage Example + +Please note that all real servers need to listen on the same host and be set in "route" mode. Then, you can run LVScare +in the foreground. For example: + +```bash +docker run -p 8081:80 --name echoserver1 -d cilium/echoserver +docker run -p 8082:80 --name echoserver2 -d cilium/echoserver +docker run -p 8083:80 --name echoserver3 -d cilium/echoserver +lvscare care --vs 169.254.0.1:80 --rs 127.0.0.1:8081 --rs 127.0.0.1:8082 --rs 127.0.0.1:8083 --logger DEBG --health-schem http --health-path / +``` + +## Cleanup + +Finally, you can use the following command to clean up: + +```bash +lvscare care --vs 169.254.0.1:80 --logger DEBG -C +``` + +Conclusion: LVScare is a lightweight load balancing and health checking tool based on IPVS. When seamlessly integrated +with Sealos, it greatly improves the availability and performance of Kubernetes clusters. Give it a try and see how +LVScare can help you better manage your Kubernetes cluster! diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/_category_.json b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/_category_.json new file mode 100644 index 000000000000..a788e786e6c5 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 2 +} diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cert.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cert.md new file mode 100644 index 000000000000..81b7fd871b32 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cert.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +keywords: [Kubernetes certificates, cert command, secure communication, TLS encryption, Kubernetes cluster] +description: Generate secure Kubernetes certificates with the `cert` command to ensure encrypted communication within your cluster using TLS. +--- + +# Certificate Management with `cert` + +The `cert` command is used to generate the necessary certificate files for a Kubernetes cluster. In a Kubernetes +cluster, certificates are used to ensure secure communication between components such as the API server, kubelet, and +etcd. Certificates provide encryption using the Transport Layer Security (TLS) protocol to ensure the confidentiality +and integrity of data during transit. + +The `sealctl cert` command generates certificates automatically based on the provided parameters. These parameters +include node IP, node name, service CIDR, DNS domain, and optional additional alternate names. By generating and +configuring these certificates, you can ensure secure communication within your Kubernetes cluster. + + +``` +The `cert` command is used to generate Kubernetes certificates. + +Options: + --alt-names Alternate names, such as sealos.io or 10.103.97.2. Can specify multiple alternate names. + --node-name Node name, such as master0. + --service-cidr Service CIDR, such as 10.103.97.2/24. + --node-ip IP address of the node, such as 10.103.97.2. + --dns-domain DNS domain for the cluster. Default value is cluster.local. + --cert-path Path to Kubernetes certificate files. Default value is /etc/kubernetes/pki. + --cert-etcd-path Path to Kubernetes etcd certificate files. Default value is /etc/kubernetes/pki/etcd. + +Examples: + sealctl cert --alt-names sealos.io --alt-names 10.103.97.2 \ + --node-name master0 --service-cidr 10.103.97.2/24 \ + --node-ip 10.103.97.2 --dns-domain cluster.local + +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cri.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cri.md new file mode 100644 index 000000000000..d9fae8c2fa1d --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/cri.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 2 +keywords: [CRI management, Kubernetes containers, container runtime, sealctl cri, Docker management, containerd, CRI-O, Kubernetes cluster, container operations, cgroup driver] +description: Manage and inspect the Container Runtime Interface (CRI) in Kubernetes with sealctl cri. Check runtime status, list containers, and more. +--- + +# CRI Container Management + +The `cri` command is used to manage and inspect the Container Runtime Interface (CRI) environment in a Kubernetes +cluster. The container runtime is the underlying technology responsible for running containers, such as Docker, +containerd, or CRI-O. In Kubernetes, the container runtime is used to start, stop, and manage containers to support +workloads in the cluster. + +The `sealctl cri` command provides a set of subcommands that allow you to perform various operations related to the +container runtime, such as checking if the runtime is Docker, if it is running, listing Kubernetes containers, deleting +containers, pulling images, checking image existence, and retrieving CGroup driver information. + +By using the `sealctl cri` command, you can easily manage and inspect the container runtime environment in your +Kubernetes cluster to ensure proper configuration and smooth operation. + + +```shell +sealctl cri [flags] +``` + +Subcommands: + +1. `socket`: Check the CRI socket. + +```shell +sealctl cri socket +``` + +2. `cgroup-driver`: Get the cgroup driver of the container runtime. + +```shell +sealctl cri cgroup-driver [--short] +``` + +- `--short`: Print only the result. + +Global flags: + +- `--socket-path`: Path to the CRI socket. +- `--config`: Path to the CRI configuration file. + +Examples: + +```shell +sealctl cri socket +sealctl cri cgroup-driver --short +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hostname.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hostname.md new file mode 100644 index 000000000000..e273b42c7cc9 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hostname.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 3 +keywords: [hostname command, sealctl, get hostname, operating system hostname, sealctl hostname] +description: Learn how to use the sealctl command to get the hostname of your operating system effortlessly. No additional parameters required. +--- + +# Get Hostname + +Get the hostname of the operating system: + +```shell +sealctl hostname +``` + +Example: + +```shell +sealctl hostname +``` + +Executing this command will return the hostname of the operating system. No additional parameters need to be passed. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hosts.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hosts.md new file mode 100644 index 000000000000..a4404df13505 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/hosts.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 4 +keywords: [manage hosts, domain name resolution, sealctl command, IP mapping, DNS override] +description: Learn how to manage the hosts file using sealctl commands to list, add, and delete domain-to-IP mappings for better control over DNS resolution. +--- + +# Manage Hosts + +The `hosts` command is used to manage the hosts file of the operating system. The hosts file is a file used for domain +name resolution to IP addresses and is typically used locally to override DNS resolution. By modifying the hosts file, +you can assign a custom IP address to a specific domain name without relying on a DNS server. + +`sealctl hosts` provides the following three subcommands to manage the hosts file: + +1. `list`: List all entries in the current hosts file. +2. `add`: Add a new domain-to-IP mapping to the hosts file. +3. `delete`: Delete a specified domain-to-IP mapping from the hosts file. + +With these subcommands, you can conveniently view, add, and delete mappings in the hosts file, allowing you better +control over domain name resolution to IP addresses. + +1. `sealctl hosts list`: List all entries in the current hosts file. + + Example: + + ```shell + sealctl hosts list + ``` + +2. `sealctl hosts add`: Add a new entry to the hosts file. + + Parameters: + + - `--ip`: IP address (required) + - `--domain`: Domain name (required) + + Example: + + ```shell + sealctl hosts add --ip 192.168.1.100 --domain example.com + ``` + +3. `sealctl hosts delete`: Delete an entry from the hosts file. + + Parameters: + + - `--domain`: Domain name to delete (required) + + Example: + + ```shell + sealctl hosts delete --domain example.com + ``` + +Note: You can specify the path of the hosts file by adding the `--path` parameter after any `hosts` subcommand. The +default path is `/etc/hosts` (Linux systems). + +Example: + +```shell +sealctl hosts list --path /custom/path/hosts +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/ipvs.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/ipvs.md new file mode 100644 index 000000000000..51956714df66 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/ipvs.md @@ -0,0 +1,78 @@ +--- +sidebar_position: 6 +keywords: [IPVS, load balancing, sealctl ipvs, virtual servers, real servers, Linux kernel, proxy modes, health check, scheduling algorithms] +description: Learn how to use sealctl ipvs command for high-performance load balancing in Linux. Manage virtual and real servers, configure health checks, and optimize your network. +--- + +# Manage IPVS + +The `ipvs` command is used to create and manage local IPVS (IP Virtual Server) load balancing. IPVS is a module in the +Linux kernel that allows high-performance load balancing to be implemented in the kernel space. The `ipvs` command +achieves load balancing of services by managing the mapping between virtual servers and real servers. + +`sealctl ipvs` supports the following features: + +1. Creation and management of mappings between virtual servers and real servers. +2. Health-check functionality to periodically check the health status of real servers and perform necessary + online/offline operations. +3. Support for two proxy modes: `route` and `link`. +4. Support for configuring proxy scheduling algorithms (e.g., round-robin, weighted round-robin, etc.). +5. Support for one-time creation of proxy rules (`--run-once` flag) or continuous operation and management of proxy + rules. +6. Support for cleanup: Existing IPVS rules can be cleared and the command will exit using the `-C` or `--clean` flag. + +With the `sealctl ipvs` command, users can easily create and manage high-performance load balancing services locally. + +**Usage** + +```shell +sealctl ipvs [flags] +``` + +**Options** + +- `-C`, `--clean`: Clear existing rules and then exit. +- `--health-insecure-skip-verify`: Skip verification of insecure requests (default is true). +- `--health-path string`: URL path for probing (default is "/healthz"). +- `--health-req-body string`: Request body sent by the health checker. +- `--health-req-headers stringToString`: HTTP request headers (default is []). +- `--health-req-method string`: HTTP request method (default is "GET"). +- `--health-schem string`: HTTP scheme for the probe (default is "https"). +- `--health-status ints`: Valid status codes. +- `-h`, `--help`: Help for ipvs. +- `-i`, `--iface string`: Name of the virtual interface to create, behaving the same as kube-proxy (default is " + lvscare"). Enabled only in mode=link. +- `--interval durationOrSecond`: Health check interval (default is 0s). +- `--ip ip`: Target IP as the routing gateway, used together with mode=route. +- `--logger string`: Log level: DEBG/INFO (default is "INFO"). +- `--masqueradebit int`: IPTables masquerade bit. Enabled only in mode=link. +- `--mode string`: Proxy mode: route/link (default is "route"). +- `--rs strings`: Real server addresses, e.g., 192.168.0.2:6443. +- `--run-once`: Create proxy rules and then exit. +- `--scheduler string`: Proxy scheduler (default is "rr"). +- `--vs string`: Virtual server address, e.g., 169.254.0.1:6443. + +**Global Options** + +- `--debug`: Enable debug logging. +- `--show-path`: Enable displaying code path. + +**Documentation** + +To use the `sealctl ipvs` command, follow these steps: + +1. Provide the necessary options and parameters for the command. +2. Execute the command, which will create or manage local IPVS load balancing. + +**Examples** + +Create proxy rules and then exit: + +```shell +sealctl ipvs --vs 169.254.0.1:6443 --rs 192.168.0.2:6443 --run-once +``` + +Clear existing IPVS rules: + +```shell +sealctl ipvs --clean diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/registry.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/registry.md new file mode 100644 index 000000000000..4b0fed564ffd --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/registry.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 5 +keywords: [sealctl registry serve, Docker image repository, Sealos, filesystem mode, in-memory mode, image synchronization, container registry, Kubernetes] +description: Learn how to use the sealctl registry serve command in Sealos to manage Docker image repositories in filesystem and in-memory modes for Kubernetes environments. +--- + +# Registry Image Repository + +## Sealos: Detailed Guide and Usage of `sealctl registry serve` Command + +Sealos provides the `sealctl registry serve` command to facilitate the construction and management of Docker image +repositories. This document provides a detailed guide and usage examples for the `sealctl registry serve` command. + +### Introduction + +The `sealctl registry serve` command is primarily used to start a Docker distribution image repository server. It +supports two modes: `filesystem` and `inmem`. + +1. **Filesystem Mode**: In this mode, `sealctl` runs a Docker distribution image repository server for a specified + directory. The image data is stored on disk in this mode. **This command is also used by Sealos for incremental image + synchronization**. + +2. **In-memory Mode**: In this mode, `sealctl` runs an in-memory Docker distribution image repository server. The image + data is only stored in memory, and the data will be lost when the process exits. + +### Command Options + +The `sealctl registry serve filesystem` command supports the following options: + +- `--disable-logging`: Disable logging output (default is false). +- `--log-level`: Configure the log level (default is 'error'). +- `-p, --port`: The port the server listens on (default is a randomly unused port). + +### Usage Examples + +Here are some usage examples of the `sealctl registry serve` command: + +#### Start a Filesystem Image Repository Server + +```bash +sealctl registry serve filesystem --port=5000 +``` + +The above command starts a filesystem image repository server on port 5000. + +#### Start an In-memory Image Repository Server + +```bash +sealctl registry serve inmem +``` + +The above command starts an in-memory image repository server. The server will lose stored data when the process exits. + +With the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. It is a +powerful and user-friendly tool for both development and production environments. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/sealctl.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/sealctl.md new file mode 100644 index 000000000000..45d0058e3bb4 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/sealctl.md @@ -0,0 +1,78 @@ +--- +sidebar_position: 0 +keywords: [sealctl, Sealos, cluster management, container runtime, IPVS, static pods, certificate management, registry, Kubernetes] +description: Explore Sealctl, the powerful command-line tool for Sealos cluster management. Learn about its subcommands for container runtime, IPVS, static pods, and more. +--- + +# Sealctl User Guide + +Sealos provides `sealctl`, a command-line tool for operating with Sealos and cluster nodes. It includes the following +subcommands: + +1. `cert`: Manages certificates for generating, viewing, and updating TLS certificates. +2. `cri`: Manages Container Runtime Interface (CRI) configuration, such as Docker or containerd. +3. `hostname`: Views or sets the system hostname. +4. `hosts`: Manages the system's hosts file, which defines static hostname-to-IP address mappings. +5. `ipvs`: Manages IP Virtual Server (IPVS) rules for load balancing and proxying. +6. `registry`: Manages image repositories for storing container images in container repository format and repository + management. +7. `static_pod`: Manages static Pods and creates static Pod configurations. +8. `token`: Generates and manages access tokens for authorizing access to Kubernetes clusters. + +With these subcommands, you can conveniently manage and configure your Sealos system, enabling control over containers, +image repositories, networks, and other aspects. + +# Sealos Dependent Commands + +1. **Add Hosts** + + Adds a new hosts record on the node with the specified IP address. The parameters include the IP address, hostname, + and domain name. Use the `sealctl hosts add` command. + +2. **Delete Hosts** + + Deletes a hosts record on the node with the specified IP address. The parameters include the IP address and domain + name. Use the `sealctl hosts delete` command. + +3. **Hostname** + + Retrieves the hostname of the node with the specified IP address. Use the `sealctl hostname` command. + +4. **IPVS Load Balancing** + + Configures IPVS for load balancing on the node with the specified IP address. The parameters include the node IP + address, virtual IP address, and a list of master node IP addresses. Use the `sealctl ipvs` command. + +5. **Clear IPVS Rules** + + Clears the IPVS configuration on the node with the specified IP address. The parameters include the node IP address + and virtual IP address. Use the `sealctl ipvs` command. + +6. **Generate Static Pods** + + Deploys a static Pod (lvscare) on the node with the specified IP address. The parameters include the node IP address, + virtual IP address, Pod name, image name, and a list of master node IP addresses. Use the + `sealctl static-pod lvscare` command. + +7. **Manage Cluster Interact Authentication Token** + + Generates a token for the node with the specified IP address. The parameters include the node IP address, + configuration file, and certificate key. Use the `sealctl token` command. + +8. **Get Node's CGroup Information** + + Retrieves the CRI CGroup information of the node with the specified IP address. Use the `sealctl cri cgroup` command. + +9. **Get Node's CRI Socket Information** + + Retrieves the CRI socket information of the node with the specified IP address. Use the `sealctl cri socket` command. + +10. **Generate Self-signed HTTPS Certificates on Node** + + Generates certificates for the node with the specified IP address. The parameters include the node IP address, a + list of alternate names, host IP address, hostname, service CIDR, and DNS domain. Use the `sealctl cert` command. + +11. **Start Registry on Node** + + Starts the registry on the specified node for incremental image synchronization. Use the `sealctl registry serve` + command. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/static-pod.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/static-pod.md new file mode 100644 index 000000000000..09275070d85b --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/static-pod.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 7 +keywords: [static pod, sealctl, lvscare, Kubernetes, kubelet, IPVS, VIP, cluster management, control plane, configuration] +description: Learn how to use sealctl static-pod command to generate and manage static Pods in Kubernetes, focusing on lvscare for IPVS rules and control plane setup. +--- + +# Static Pod Configuration + +The `static-pod` command is used to generate static Pods that are managed directly by kubelet instead of the API server. +Static Pods are useful in certain scenarios, such as setting up and managing control plane components in a Kubernetes +cluster. + +The `sealctl static-pod` command provides a convenient way to generate static Pod configuration files for specific +purposes. Currently, it mainly supports generating the `lvscare` static Pod, which is a tool for managing IPVS rules. + +Using `sealctl static-pod lvscare`, you can generate the `lvscare` static Pod YAML file based on specified parameters +such as VIP, master node addresses, and image name. This file can then be stored in the static Pod path of kubelet, and +kubelet will automatically create and manage the corresponding Pod. + +**Usage** + +```shell +sealctl static-pod lvscare [flags] +``` + +**Options** + +- `--vip`: Default VIP IP (default is "10.103.97.2:6443"). +- `--name`: Name of the generated lvscare static Pod. +- `--image`: Image for the generated lvscare static Pod (default is `sealos.hub:5000/sealos/lvscare:latest`). +- `--masters`: List of master addresses for the generated static Pod. +- `--print`: Whether to print the YAML. + +**Examples** + +Generate the lvscare static Pod file and print the YAML: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +If the `--print` option is not used, the configuration file will be directly generated in `/etc/kubernetes/manifests` +and the static Pod will be enabled: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md new file mode 100644 index 000000000000..3e2232597812 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 9 +keywords: [sealctl tar, sealctl untar, file compression, file decompression, Sealos commands, archive management, data backup, file migration, command-line tools, Linux utilities] +description: Learn how to use Sealos' sealctl tar and untar commands for efficient file compression and decompression. Ideal for data backup and file migration tasks. +--- + +# Tar and Untar in Sealos + +Sealos provides the `sealctl tar` and `sealctl untar` commands for compressing and decompressing files or directories. +This guide explains how to use these two commands in detail. + +## The `sealctl tar` Command + +The `sealctl tar` command is used to compress a specified directory path into an archive file. Note that it will strip +the parent directory. + +**Command Options:** + +- `--clear`: Whether to delete the source files after compression, default is false. +- `--compression`: Compression algorithm, available options are tar/gzip/zstd/disable, default is disable. +- `-o, --output`: Path of the archive file. + +**Basic Usage:** + +```bash +sealctl tar [flags] [options] +``` + +## The `sealctl untar` Command + +The `sealctl untar` command is used to search for archive files that match a glob pattern in the specified source path ( +`src`) and extract them to the destination path (`dst`). + +**Command Options:** + +- `--clear`: Whether to delete the source files after extraction, default is false. +- `-o, --output`: Path to extract the archive file. + +**Basic Usage:** + +```bash +sealctl untar [flags] [options] +``` + +## Usage Examples + +Here are some examples of using the `sealctl tar` and `sealctl untar` commands: + +**Create a compressed file:** + +```bash +sealctl tar --output=/path/to/archive.tar /path/to/source +``` + +The above command compresses the `source` directory into the `archive.tar` file. + +**Extract a compressed file:** + +```bash +sealctl untar --output=/path/to/destination /path/to/archive.tar +``` + +The above command extracts the `archive.tar` file to the `destination` directory. + +With the `sealctl tar` and `sealctl untar` commands, users can easily compress and decompress files or directories. +These commands are useful tools for file management, particularly in backup and file migration scenarios. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/token.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/token.md new file mode 100644 index 000000000000..1f72b36023fe --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealctl/token.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 8 +keywords: [sealctl token, Kubernetes cluster, worker node authentication, token generation, cluster security, node joining, sealos, cluster management, authentication token, Kubernetes security] +description: Learn how to use the sealctl token command to generate authentication tokens for securely adding worker nodes to your Kubernetes cluster with Sealos. +--- + +# Token Management + +The `sealctl token` command is primarily used to generate a token for connecting master and worker nodes in a Kubernetes +cluster. In a Kubernetes cluster, when you want to add a new worker node, you typically need to provide a token for +authentication. This token ensures that only worker nodes with the correct token can join the cluster. + +The `sealctl token` command generates a token for authentication by accepting a configuration file (optional) and a +certificate key (optional) as parameters. By default, if no configuration file and certificate key are provided, the +command uses built-in default settings to generate the token. + +In summary, the `sealctl token` command is used to generate a token for authentication, allowing worker nodes to +securely join a Kubernetes cluster. Using this command simplifies the process of adding nodes to the cluster and ensures +the security of the cluster. + +**Usage** + +```shell +sealctl token [config] [certificateKey] +``` + +**Parameters** + +- `config`: Configuration file (optional). +- `certificateKey`: Certificate key (optional). + +**Examples** + +Generate a token with default parameters: + +```shell +sealctl token +``` + +Generate a token with a custom configuration file and certificate key: + +```shell +sealctl token my-config my-certificate-key +``` diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/_category_.json b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/_category_.json new file mode 100644 index 000000000000..c9cb98244ebc --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 1 +} diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md new file mode 100644 index 000000000000..6edb120c9e61 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 11 +keywords: [Sealos, Kubernetes cluster images, application images, cluster-image repository, GitHub Actions, Docker images, configuration images, image build workflow, Kubefile, Helm charts] +description: Learn how to use Sealos' cluster-image repository to build and publish application images for Kubernetes clusters, including Docker and configuration images, using GitHub Actions. +--- + +# Application Cluster Image Usage Guide + +Sealos provides a repository called [cluster-image](https://github.com/labring-actions/cluster-image) on GitHub for +building and publishing application images for Kubernetes clusters. These images can be created by submitting code to +this repository and can be published to `docker.io/labring/` as official application images. It supports building Docker +container images as well as cluster images for applications. + +## Types of Images + +The repository supports three types of image builds: + +- **Application Cluster Images**: These are primarily for building application images using GitHub Actions and support + both amd64 and arm64 architectures. +- **Configuration Cluster Images**: These are mainly for building configuration images using GitHub Actions. They are + not container images and are not architecture-specific. They typically contain configuration scripts or customizations + to default configurations. +- **Docker Images**: These are mainly for building container images using GitHub Actions and support both amd64 and + arm64 architectures. + +## Workflow for Image Builds + +You can trigger image builds directly in the GitHub repository by creating an issue. Here are a few examples: + +- `/imagebuild_dockerimages helm v3.8.2 Key1=Value1,Key2=Value2` +- `/imagebuild_configs coredns v0.0.1` +- `/imagebuild_apps helm v3.8.2` + +The format of the image build commands for each type is `/imagebuild_ [Key=Value,...]`, where +`` can be `dockerimages`, `configs`, or `apps`, `` and `` represent the application name and +version respectively, and `[Key=Value,...]` is optional buildArg parameters used only for the `dockerimages` type. + +## Location of Image Configurations + +You can place your configuration files in the `applications///` directory, including Dockerfiles, +Kubefiles, and init.sh scripts, among others. The init.sh script is typically used for downloading dependencies such as +Helm and kubectl-minio. You can choose to use either a Dockerfile or Kubefile to define your image build logic. + +## Image Build Rules + +The build rules vary slightly for each type of image. Generally, you need to create different subdirectories under the +application directory and place different types of files in them, which Sealos will use to build the images. The +specific rules are as follows: + +1. `charts` directory: Place the Helm charts required for the cluster images. Kubernetes will scan the charts and fetch + the images for building, and the registry directory will be placed at the same level as the Kubefile. +2. `manifests` directory: Place the Kubernetes yaml configurations directly. Kubernetes will scan all the images in the + manifests directory and build the registry directory, which will be placed at the same level as the Kubefile. +3. `images/shim` directory: Store additional image lists and build the registry directory, which will be placed at the + same level as the Kubefile. +4. If templates are required, place files with the `.tmpl` extension in `etc`, `charts`, or `manifests`. These files can + be rendered by the `sealos run` command with environment variables and the `.tmpl` extension will be removed. For + example, a file named `aa.yaml.tmpl` will be rendered as `aa.yaml`. Please ensure that the file names do not conflict + with existing files. +5. The `registry` directory must be placed at the same level as the Kubefile. Otherwise, it will not be copied to the + private repository of master0. Also, ensure that the registry is not stored in a chart, as it may cause slow scanning + by Helm and potentially lead to OOM (out-of-memory) issues during image builds. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/add.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/add.md new file mode 100644 index 000000000000..8c9847d73ec3 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/add.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 4 +keywords: [sealos add, cluster nodes, Kubernetes cluster, add control nodes, add regular nodes, cluster management, sealos command-line tool, node addition, cluster expansion, Kubernetes administration] +description: Learn how to use the 'sealos add' command to efficiently add control and regular nodes to your Kubernetes cluster. Explore options and best practices for cluster expansion. +--- + +# Adding Cluster Nodes with `sealos add` + +`sealos add` is a command in the Sealos command-line tool used to add nodes to a cluster. This guide provides detailed +instructions on how to use the command and its options. + +**Note: Make sure the number of control nodes is odd to ensure proper etcd leader election.** + +## Basic Usage + +### Adding Nodes + +To add nodes to the cluster, you can use the `--nodes` option: + +```bash +sealos add --nodes x.x.x.x +``` + +In the above command, replace `x.x.x.x` with the IP address of the node you want to add. + +### Adding Control Nodes + +To add control nodes to the cluster, you can use the `--masters` option: + +```bash +sealos add --masters x.x.x.x +``` + +### Adding Control and Regular Nodes Together + +If you want to add both control nodes and regular nodes to the cluster, you can use the `--masters` and `--nodes` +options together: + +```bash +sealos add --masters x.x.x.x --nodes x.x.x.x +sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## Options + +The `sealos add` command provides the following options: + +- `--cluster='default'`: The name of the cluster to perform the add operation. Defaults to `default`. + +- `--masters=''`: The control nodes to be added. + +- `--nodes=''`: The nodes to be added. + +Each option can be followed by an argument. + +## Usage Example + +Here's an example usage that adds a node with the IP address `192.168.0.2` to the cluster: + +```bash +sealos add --nodes 192.168.0.2 +``` + +That's it for the usage guide of the `sealos add` command. We hope this helps you. If you have any questions or +encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md new file mode 100644 index 000000000000..4c79c068f7bb --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 1 +keywords: [sealos apply, Kubernetes cluster, Clusterfile, cluster images, command-line tool, cluster management, sealos command options, Kubernetes deployment] +description: Learn how to use the 'sealos apply' command to start and manage Kubernetes clusters. Explore options, Clusterfile usage, and best practices for efficient cluster deployment. +--- + +# Starting a Cluster with `sealos apply` + +`sealos apply` is an important command in the Sealos command-line tool used to run cluster images in a Kubernetes +cluster. This guide provides detailed instructions on how to use the command and its options. + +## Basic Usage + +The basic usage of the `sealos apply` command is as follows: + +```shell +$ sealos apply -f Clusterfile +``` + +Clusterfile content: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: default +spec: + # Server IP addresses and roles + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +``` + +This command will run cluster images in the Kubernetes cluster based on the specified `Clusterfile`. + +## Options + +The `sealos apply` command provides several options to customize its behavior: + +- `-f, --Clusterfile='Clusterfile'`: Specifies the Clusterfile to apply. Defaults to `Clusterfile`. +- `--config-file=[]`: Specifies the path to a custom config file to replace or modify resources. +- `--env=[]`: Sets environment variables to be used during command execution. +- `--set=[]`: Sets values on the command line, usually for replacing template values. +- `--values=[]`: Specifies values files to be applied to the `Clusterfile`, usually used for templating. + +Each option can be followed by one or more parameters. Multiple parameters are separated by commas. + +For example, you can use the `--set` option to set values on the command line: + +```shell +sealos apply -f Clusterfile --set key1=value1,key2=value2 +``` + +This command will set the values of `key1` and `key2` to `value1` and `value2`, and then apply the `Clusterfile`. + +Similarly, you can use the `--values` option to specify a values file: + +```shell +sealos apply -f Clusterfile --values values.yaml +``` + +This command will apply the `Clusterfile` based on the values in the `values.yaml` file. + +**For more examples, please refer to the [Run Cluster](/developer-guide/lifecycle-management/operations/run-cluster/.md) +section.** + +That's it for the usage guide of the `sealos apply` command. We hope this helps you. If you have any questions or +encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md new file mode 100644 index 000000000000..665e2281fd46 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 6 +keywords: [sealos build, OCI images, Dockerfile, image building, containerization, Sealfile, Kubefile, build options, image dependencies, Docker Registry] +description: Learn how to use 'sealos build' command to create OCI images with various instruction files, manage dependencies, and optimize your containerization workflow. +--- + +# Building Images with `sealos build` + +The `build` command in Sealos is used to build OCI images using instructions from Sealfiles, Kubefiles, Dockerfiles, or +Containerfiles. This is the fundamental command in Sealos for building cluster images. + +If no parameters are specified, Sealos will use the current working directory as the build context and look for +instruction files. If no Sealfile, Kubefile, Dockerfile, or Containerfile is found, the build will fail. + +Here are some key options for the `build` command: + +1. `--all-platforms`: Attempts to build the image for all supported base image platforms. +2. `--authfile`: Path to the authentication file. +3. `--build-arg`: Provides an `argument=value` to the builder. +4. `--build-context`: Provides additional build context to the builder as `argument=value`. +5. `--creds`: Credentials to access the registry as `[username[:password]]`. +6. `-D, --disable-compression`: Disables layer compression by default. +7. `--env`: Sets environment variables for the image. +8. `-f, --file`: Pathname or URL of the Dockerfile. +9. `--force-rm`: Always removes intermediate containers after a build, even if the build fails. +10. `--format`: Format for the manifest and metadata of the built image. +11. `--from`: Replaces the value of the first FROM instruction in the Containerfile with the specified image name. +12. `--http-proxy`: Passes the HTTP Proxy environment variable. +13. `--isolation`: Process isolation `type` to use, can be 'oci' or 'chroot'. +14. `--max-pull-procs`: Maximum number of goroutines to use for pulling images. +15. `--platform`: Sets the OS/ARCH/VARIANT for the image to the provided value instead of the host's current operating + system and architecture. +16. `--pull`: Pulls the image from the registry, if new or not present in the store. Can be set to false, always, or + never. +17. `-q, --quiet`: Suppresses the build output and image read/write progress. +18. `--retry`: Number of times to retry on push/pull failure. +19. `--retry-delay`: Delay in seconds between retries on push/pull failure. +20. `--rm`: Removes intermediate containers after a successful build. +21. `--save-image`: Saves resolved images from a specific directory in the registry format. +22. `--sign-by`: Signs the image with the GPG key of the specified `FINGERPRINT`. +23. `-t, --tag`: Name and optionally a tag in the 'name:tag' format to apply to the built image. +24. `--target`: Sets the target build stage to build. +25. `--timestamp`: Sets the created timestamp to the specified epoch seconds for reproducible builds. Default is the + current time. + +These options provide flexibility for various build requirements, including platform-specific builds, environment +variable settings, build context management, image signing, and more. With the `--save-image` option, Sealos can +automatically recognize and save the required images (including those resolved from image lists, Helm charts, and +manifests) in the Docker Registry format. + +The process isolation mode `--isolation` supports two parameters: 'oci' and 'chroot'. Choose 'oci' mode if OCI is +supported locally and 'chroot' mode if OCI is not supported. + +The `--save-image` option is used in Sealos build commands to automatically find and save the required images during the +build process. In Sealos, building an image may involve other dependent images. These dependent + +images can come from image lists, Helm charts, or cluster manifests. When using the `--save-image` option, Sealos will +automatically resolve these dependencies based on the build context and save them in the Docker Registry format. + +For example, here is an example using the `--save-image` option: + +```bash +sealos build -t myapp:v1.0.0 -f Dockerfile . +``` + +In this example, Sealos will use the current directory as the build context, read the build instructions from the +Dockerfile, and attempt to build an image tagged as `myapp:v1.0.0`. Additionally, Sealos will resolve all base images +referenced in the Dockerfile's `FROM` instructions and save those images as well. These images will be saved in the +Docker Registry format and can be pushed directly to a Docker Registry. + +If your build context also includes Helm charts or cluster manifests, Sealos will also resolve the images referenced in +those files and save them accordingly. + +Overall, the `--save-image` option provides a convenient way for Sealos to handle image dependencies during the build +process, greatly improving the convenience and efficiency of building images. + +Here are some detailed examples: + +- [Build with Image Manifests](/developer-guide/lifecycle-management/operations/build-image/build-image-image_list.md) +- [Build with Deploy Manifests](/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md) +- [Build with Helm Charts](/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [Build with Binary](/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md) +- [Build with go-template](/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md) +- [Build with exec and scp](/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md) + +With the `build` command in Sealos, you can build OCI images based on various instruction files to provide the required +images for Sealos. This process includes handling various instructions in Dockerfiles or other instruction files, such +as `FROM`, `RUN`, `ADD`, etc., as well as managing image layers, labels, and more. The build process also involves +pulling base images, running commands, saving the results, and more. Each step can be finely controlled and customized +using the options mentioned above to accommodate different build requirements. + +That's the usage guide for the `sealos build` command. We hope this helps you. If you have any questions or encounter +any issues during the process, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/cert.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/cert.md new file mode 100644 index 000000000000..a5d7fabb56e4 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/cert.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 3 +keywords: [sealos cert, cluster certificates, API server certificates, Kubernetes certificates, certificate management, alt-names, certificate verification, kubeadm-config, apiserver.crt] +description: Learn how to update Kubernetes cluster certificates using the sealos cert command. Discover options for adding domain names and IP addresses, and verify your updates easily. +--- + +# Updating Cluster Certificates with `sealos cert` + +The `cert` command in Sealos is used to update the API server certificates in a cluster. This guide provides detailed +instructions on how to use this command and its options. + +## Basic Usage + +To add domain names or IP addresses to the certificate, you can use the `--alt-names` option: + +```bash +sealos cert --alt-names sealos.io,10.103.97.2,127.0.0.1,localhost +``` + +In the above command, replace `sealos.io,10.103.97.2,127.0.0.1,localhost` with the domain names and IP addresses you +want to add. + +**Note**: It is recommended to back up the old certificates before performing this operation. + +After executing the `sealos cert` command, the API server certificates in the cluster will be updated. You don't need to +manually restart the API server as Sealos will automatically handle the restart. + +## Options + +The `cert` command provides the following options: + +- `--alt-names='`': Adds domain names or IP addresses to the certificate, e.g., `sealos.io` or `10.103.97.2`. + +- `-c, --cluster='default'`: Specifies the name of the cluster on which to perform the exec operation. Default is + `default`. + +Each option can be followed by an argument. + +## Certificate Verification + +After updating the certificates, you can use the following commands for verification: + +```bash +kubectl -n kube-system get cm kubeadm-config -o yaml +openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text +``` + +The above commands retrieve the kubeadm-config ConfigMap in the kube-system namespace and display detailed information +about the apiserver.crt certificate. + +That concludes the usage guide for the `sealos cert` command. We hope this helps you. If you have any questions or +encounter any issues during the process, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md new file mode 100644 index 000000000000..f387f4fb910f --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 0 +keywords: [Sealos commands, cluster management, node management, container operations, Kubernetes cluster, cloud-native applications, image registry, Clusterfile, Sealos installation] +description: Comprehensive guide to Sealos commands for managing Kubernetes clusters, nodes, and containers. Learn about cluster operations, image handling, and cloud-native app deployment. +--- + +# Sealos Command Reference + +## Cluster Management Commands + +- `apply`: Runs cluster images within a Kubernetes cluster using Clusterfile. +- `cert`: Updates the certificates of the Kubernetes API server. +- `run`: Easily runs cloud-native applications. +- `reset`: Resets all content in the cluster. +- `status`: Views the status of the Sealos cluster. + +## Node Management Commands + +- `add`: Adds nodes to the cluster. +- `delete`: Removes nodes from the cluster. + +## Remote Operation Commands + +- `exec`: Executes shell commands or scripts on the specified node. +- `scp`: Copies files to the remote location of the specified node. + +## Experimental Commands + +- `registry`: Commands related to the image registry. + +## Container and Image Commands + +- `build`: Builds images using instructions from Sealfile or Kubefile. +- `create`: Creates a cluster but does not run CMD, used for image inspection. +- `inspect`: Inspects the configuration of containers or images. +- `images`: Lists images in local storage. +- `load`: Loads images from a file. +- `login`: Logs into a container registry. +- `logout`: Logs out of a container registry. +- `manifest`: Operates on manifest lists and image indexes. +- `merge`: Merges multiple images into one. +- `pull`: Pulls images from a specified location. +- `push`: Pushes images to the specified destination. +- `rmi`: Removes one or more images from local storage. +- `save`: Saves images to an archive file. +- `tag`: Adds an additional name to a local image. + +## Other Commands + +- `completion`: Generates autocompletion scripts for the specified shell. +- `docs`: Generates API reference documentation. +- `env`: Prints all environment information used by Sealos. +- `gen`: Generates a Clusterfile with all default settings. +- `version`: Prints version information. + +The `--debug` flag in Sealos is a global flag used to enable debug mode for more detailed information about the system's +operation when issues occur. + +For installation instructions, please refer to +the [Sealos Installation Guide](/developer-guide/lifecycle-management/quick-start/installation); for a quick start guide, +please refer to the [Quick Start Guide](/developer-guide/lifecycle-management/quick-start/.md). diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/create.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/create.md new file mode 100644 index 000000000000..6f7e800bafbf --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/create.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +keywords: [sealos create, cluster working directory, Sealos command line tool, debugging, image review] +description: Learn how to use the 'sealos create' command to generate a cluster working directory for debugging and image review without executing the actual cluster. +--- + +# Create Working Directory + +`sealos create` is a command in the Sealos command line tool, mainly used for creating the cluster working directory +without executing the CMD, so as to review the image. This guide will detail its usage and options. + +## Basic Usage + +The `sealos create` command is used to create a cluster working directory but does not actually run, mainly used for +debugging or testing. It can output the address of the cluster image, you can check whether the content of the cluster +image is consistent with the expectation. + +```bash +sealos create docker.io/labring/kubernetes:v1.24.0 +``` + +In the above command, `clustername` represents the name of the cluster you want to create. + +## Options + +The `sealos create` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster to be created but not actually run. The default is `default`. + +- `--platform='linux/arm64/v8'`: Set the operating system/architecture/version of the image to the provided value, + rather than the current operating system and architecture of the host (for example, `linux/arm`). + +- `--short=false`: If true, only print the mount path. + +- `-e, --env=[]`: Specify environment variables used during the rendering of template files. + +Each option can be followed by an argument. + +## Example + +For example, you can use the following command to create a cluster named `mycluster`, but do not actually run it: + +```bash +sealos create -e registryPort=8443 docker.io/labring/kubernetes:v1.24.0 +``` + +This command will create a cluster working directory with an image name of `docker.io/labring/kubernetes:v1.24.0` and +output the address of the cluster image. The `-e registryPort=8443` option specifies the environment variable used +during the rendering of template files, where `registryPort` is set to `8443`. Please note that in this example, the +cluster is not actually run. + +The above is the usage guide for the `sealos create` command, hope it helps. If you encounter any problems during use, +feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/delete.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/delete.md new file mode 100644 index 000000000000..6ff2867edb11 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/delete.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 4 +keywords: [sealos delete, cluster node removal, Kubernetes cluster management, delete control node, sealos command line tool] +description: Learn how to use the 'sealos delete' command to remove nodes from your Kubernetes cluster. Explore options for deleting control nodes and regular nodes efficiently. +--- + +# Delete Cluster Node Removal + +`sealos delete` is a command in the Sealos command line tool, mainly used to remove nodes from the cluster. This guide +will detail its usage and options. + +**Note to ensure the number of control nodes is odd to ensure normal election of etcd** + +## Basic Usage + +### Delete Node + +To remove a node from the cluster, you can use the `--nodes` option: + +```bash +sealos delete --nodes x.x.x.x +``` + +In the above command, `x.x.x.x` should be replaced with the IP address of the node you want to delete. If you +accidentally delete the wrong node, you can use the `sealos add` command to recover it: + +```bash +sealos add --nodes x.x.x.x +``` + +### Delete Control Node + +To remove a control node from the cluster, you can use the `--masters` option: + +```bash +sealos delete --masters x.x.x.x +``` + +Please note, if the `--masters` parameter is specified, sealos will delete your control node. + +### Delete Control Node and Node + +If you want to delete both control nodes and nodes at the same time, you can use the `--masters` and `--nodes` options +at the same time: + +```bash +sealos delete --masters x.x.x.x --nodes x.x.x.x +sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## Options + +The `sealos delete` command provides the following options: + +- `--cluster='default'`: The name of the cluster to which the deletion operation applies. The default is `default`. + +- `--force=false`: You can enter a `--force` flag to force delete nodes. + +- `--masters=''`: The control nodes to be removed. + +- `--nodes=''`: The nodes to be removed. + +Each option can be followed by an argument. + +## Usage Example + +Here is a usage example that deletes a node with the IP address of `192.168.0.2`: + +```bash +sealos delete --nodes 192.168.0.2 +``` + +The above is the usage guide for the `sealos delete` command, hope it helps. If you encounter any problems during use, +feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/env.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/env.md new file mode 100644 index 000000000000..2c0a93af56c9 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/env.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 7 +keywords: [sealos env, environment variables, Sealos CLI, Docker environment, container configuration, Buildah log level, registry sync, Sealos command-line tool, container development, DevOps] +description: Learn how to use the 'sealos env' command to view and set environment variables for Sealos, enhancing your container development and DevOps workflows. +--- + +# Environment Variables + +`sealos env` is a command in the Sealos command-line tool, used to display the current environment variables supported +by sealos and their current values. + +## Basic Usage + +### Viewing Environment Variables + +To view the environment variables, you can use the `sealos env` command: + +```bash +sealos env +``` + +### Viewing Environment Variables and Descriptions + +To view the environment variables and their descriptions, you can use the `sealos env -v` command: + +```bash +sealos env -v +``` + +## How to Set Environment Variables + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t xxx . +``` + +That's the usage guide for the `sealos env` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/exec.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/exec.md new file mode 100644 index 000000000000..1e126ecdabe5 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/exec.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 5 +keywords: [sealos exec, cluster management, shell commands, remote execution, Kubernetes administration, node selection, cluster operations, DevOps tools, container orchestration, infrastructure management] +description: Learn how to use the sealos exec command for executing shell commands on cluster nodes. Explore options for node selection and see practical examples for efficient cluster management. +--- + +# Execute Command + +`sealos exec` is a command in the Sealos command-line tool, used to execute Shell commands or scripts on specified +cluster nodes. This guide will detail its usage and options. + +## Basic Usage + +The basic `sealos exec` command format is as follows: + +```bash +sealos exec "shell command or script" +``` + +In the above command, `shell command or script` is the Shell command or script you want to execute on the cluster nodes. + +## Options + +The `sealos exec` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster on which the command will be executed. The default is `default`. + +- `--ips=[]`: Run commands on nodes with specified IP addresses. + +- `-r, --roles='':` Run commands on nodes with specified roles. Currently supports master,node,registry. + +Each option can be followed by one or more parameters. + +## Examples + +For example, you can use the following command to view the contents of the `/etc/hosts` file on all nodes of the default +cluster: + +```bash +sealos exec "cat /etc/hosts" +``` + +If you want to view the contents of the `/etc/hosts` file on nodes with `master` and `node` roles in a cluster named +`my-cluster`, you can use the following command: + +```bash +sealos exec -c my-cluster -r master,node "cat /etc/hosts" +``` + +If you only want to view the contents of the `/etc/hosts` file on a node with the IP address `172.16.1.38`, you can use +the following command: + +```bash +sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" +``` + +That's the usage guide for the `sealos exec` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md new file mode 100644 index 000000000000..510cf2291036 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 3 +keywords: [Sealos gen command, Kubernetes cluster configuration, Clusterfile generation, sealos apply, cluster setup, multi-node cluster, SSH configuration, custom cluster settings] +description: Learn how to use the Sealos gen command to generate Kubernetes cluster configurations. Create single-node or multi-node clusters with custom settings and apply them easily. +--- + +# Generate Cluster Configuration + +Sealos' `gen` command is used to generate a Kubernetes cluster configuration file (Clusterfile), which can then be +applied using the `sealos apply` command. The `gen` command can help users quickly generate a basic configuration file, +which can then be modified and adjusted according to their needs. + +Here are the basic usage of `sealos gen` command and some common examples: + +1. Generate a single-node cluster with default configuration: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 + ``` + +Notice: labring/helm should be set before labring/calico. + +2. Generate a cluster that includes multiple images and specifies the master and worker nodes: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' + ``` + +Notice: labring/helm should be set before labring/calico. + +3. Specify SSH port, for servers using the same SSH port: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' + ``` + + For servers using different SSH ports: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ + --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd 'xxx' + ``` + +After the Clusterfile is generated, users can modify this file according to their needs. Add or modify environment +variables; modify the cluster cidr configuration. Once the modifications are done, users can use the `sealos apply` +command to create or update the cluster based on this configuration file. + +Example explanations: + +- [Custom Configuration Installation](/developer-guide/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) + +That's the usage guide for the `sealos gen` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/images.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/images.md new file mode 100644 index 000000000000..ed81879d0db3 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/images.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 6 +keywords: [sealos images, Docker images, container images, image management, Sealos CLI, image filtering, image listing, Go template, JSON output, container orchestration] +description: Learn how to use the 'sealos images' command to view, filter, and manage Docker images in Sealos. Discover various parameters for efficient image handling and output customization. +--- + +# Image List + +Sealos' `images` command is primarily used to view locally stored images. Users can use it to view all local images or +to filter and view specific images. The command supports various parameters to help users view and manage images more +conveniently. + +## Basic Usage + +The basic `sealos images` command will display all non-intermediate stage local images, for example: + +```bash +sealos images +``` + +This will display all final stage images stored locally. + +## Examples + +Here are some common examples of the `sealos images` command: + +1. Display all images, including intermediate images built: + + ```bash + sealos images --all + ``` + +2. Display a specific image: + + ```bash + sealos images [imageName] + ``` + +3. Display images in a specified Go template format: + + ```bash + sealos images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}' + ``` + +## Parameters + +Here are some common parameters for the `sealos images` command: + +- `-a, --all`: Display all images, including intermediate images built. + +- `--digests`: Display the digests of images. + +- `-f, --filter`: Filter output based on provided conditions. + +- `--format`: Beautify the images printout using a Go template. + +- `--history`: Display the naming history of images. + +- `--json`: Output in JSON format. + +- `--no-trunc`: Do not truncate the output. + +- `-n, --noheading`: Do not print column headings. + +- `-q, --quiet`: Only display image IDs. + +By combining these parameters, users can easily retrieve and manage locally stored images. For example, using the +`--all` parameter displays all images, including intermediate ones; using the `--filter` parameter filters images based +on specific conditions; using the `--json` parameter outputs image information in JSON format, facilitating programmatic +processing, etc. + +That's the usage guide for the `sealos images` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md new file mode 100644 index 000000000000..6ac75af5d6fe --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 6 +keywords: [sealos inspect, container configuration, image configuration, Docker inspection, OCI archive inspection, Go template format, Buildah inspect, container metadata, image metadata, Sealos commands] +description: Learn how to use Sealos' inspect command to view detailed configuration information of containers and images, including metadata, environment variables, and startup commands. +--- + +# Inspect Details + +Sealos' `inspect` command is primarily used to view the configuration information of build containers or built images. +The command supports viewing detailed information about an image or container, including its metadata, environment +variables, startup commands, etc. + +## Basic Usage + +Use the `sealos inspect` command to view the configuration information of a specified container or image. For example, +to view the configuration of a specified container: + +```bash +sealos inspect containerID +``` + +Or to view the configuration of a specified image: + +```bash +sealos inspect --type image imageWithTag +``` + +## Examples + +Here are some common examples of the `sealos inspect` command: + +1. View container configuration: + + ```bash + sealos inspect containerID + ``` + +2. View image configuration: + + ```bash + sealos inspect --type image imageWithTag + ``` + +3. View configuration information of an image ID: + + ```bash + sealos inspect --type image @imageID # Or just input the imageID, '@' is optional + ``` + +4. View configuration information of a remote image repository: + + ```bash + sealos inspect --type image docker://alpine:latest + ``` + +5. View configuration information of an image in a local OCI archive file: + + ```bash + sealos inspect --type image oci-archive:/abs/path/of/oci/tarfile.tar + ``` + +6. View configuration information of an image in a local Docker archive file: + + ```bash + sealos inspect --type image docker-archive:/abs/path/of/docker/tarfile.tar + ``` + +7. Display image environment variables in Go template format: + + ```bash + sealos inspect --format '{{.OCIv1.Config.Env}}' alpine + ``` + +## Parameters + +Here are some common parameters for the `sealos inspect` command: + +- `-f, --format`: Display output results in Go template format. **Template structure + code [InspectOutput](https://github.com/labring/sealos/blob/f8a17787822714c5fdf21f2a75cc86fadb88adfa/pkg/buildah/inspect.go#L189) + ** + +- `-t, --type`: Specify the type to view, which can be a container (`container`) or an image (`image`). + +Depending on your needs, you can combine these parameters to get specific configuration information. For example, using +the `-t` parameter can specify whether you want to view the configuration information of the container or the image; +using the `-f` parameter, you can define a specific output format, which is convenient for processing or parsing the +output results. + +That's the usage guide for the `sealos inspect` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/load.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/load.md new file mode 100644 index 000000000000..0aa247478f52 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/load.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 6 +keywords: [sealos load, image loading, container images, tar archive, offline image import, Kubernetes images, container management, image archiving, sealos command, Docker alternative] +description: Learn how to use the 'sealos load' command to import container images from tar archives, perfect for offline environments or when network access is limited. +--- + +# Load Image + +`sealos load` is a command used to load images from archive files. This is very useful when you need to import images +from existing archive files, especially in environments without a network connection. + +## Usage: + +`sealos load [flags] [options]` + +## Parameters: + +Here are the parameters for the `sealos load` command: + +- `-i, --input=''`: Load image from a tar archive file. + +## Examples: + +- Load an image from an archive file: `sealos load -i myimage.tar` + +Note that when using the `sealos load` command, you need to ensure that the specified archive file exists and is +correctly formatted. If you encounter problems when importing images, you may need to check your archive files to ensure +they have not been corrupted or incorrectly formatted. + +That's the usage guide for the `sealos load` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/login.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/login.md new file mode 100644 index 000000000000..c44b1510b811 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/login.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 6 +keywords: [sealos login, container registry, image repository, authentication, kubeconfig] +description: Learn how to use the sealos login command to access container registries. Discover parameters, usage examples, and troubleshooting tips for seamless image pulling and pushing. +--- + +# Login to Repository + +The `sealos login` command is used to log into the container registry on the specified server. After logging into the +registry, you can pull and push images. + +## Usage: + +`sealos login [flags] [options] registryName` + +## Parameters: + +Here are the parameters for the `sealos login` command: + +- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment + variable. + +- `--cert-dir=''`: Use certificates at the specified path to access the image repository. + +- `--get-login=true`: Return the current login user for the registry. + +- `-k, --kubeconfig=''`: Log into the sealos image repository hub.sealos.io using kubeconfig. + +- `-p, --password=''`: Password for the registry. + +- `--password-stdin=false`: Take the password from standard input. + +- `-u, --username=''`: Username for the registry. + +- `-v, --verbose=false`: Write more detailed information to standard output. + +## Examples: + +- Log into the quay.io registry: `sealos login -u myusername -p mypassword quay.io` + +Please note that when using the `sealos login` command, you need to make sure that you provide the correct username and +password, otherwise the login process might fail. If you encounter problems during the login process, you might need to +check your username and password to ensure they have not been entered incorrectly or forgotten. + +That's the usage guide for the `sealos login` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/logout.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/logout.md new file mode 100644 index 000000000000..bdbbd10f0b19 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/logout.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 6 +keywords: [sealos logout, image repository, authentication, registry, command line, Docker, container, credentials, security, DevOps] +description: Learn how to use the 'sealos logout' command to securely remove cached credentials for image repositories. Enhance your container management and security practices. +--- + +# Logout from Repository + +The `sealos logout` command is used to remove the locally cached account and password of the image repository on the +specified server. + +## Usage: + +`sealos logout [flags] [options] registryName` + +## Parameters: + +Here are the parameters for the `sealos logout` command: + +- `--authfile=''`: Path to the authentication file. It can be overridden with the REGISTRY_AUTH_FILE environment + variable. + +- `-a, --all=false`: Delete all authentication information. + +## Examples: + +- Logout from the quay.io image repository: `sealos logout quay.io` + +That's the usage guide for the `sealos logout` command, and we hope it has been helpful. If you encounter any problems +during usage, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md new file mode 100644 index 000000000000..1326a68652dc --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 6 +keywords: [sealos manifest, image manifest, multi-architecture support, Docker images, OCI images, manifest list, image index, container registry, image management, cross-platform deployment] +description: Learn how to use Sealos manifest commands to create, modify, and push multi-architecture image manifests for flexible Docker and OCI image management across platforms. +--- + +# Image Manifest + +The `manifest` command of Sealos is used to create, modify, and push manifest lists and image indexes. These functions +are mainly used to handle multi-architecture support of images. In Docker and OCI image specifications, manifest lists ( +also known as "fat manifests") or image indexes allow an image tag (like `myimage:latest`) to be used on various +hardware architectures (like amd64, arm64, ppc64le, etc.). + +Here are some of the major `manifest` subcommands: + +1. `create`: Creates a new manifest list or image index. Example: `sealos manifest create localhost/list` +2. `add`: Adds an image to the manifest list or image index. Example: + `sealos manifest add localhost/list localhost/image` +3. `annotate`: Adds or updates information in entries of the manifest list or image index. Example: + `sealos manifest annotate --annotation A=B localhost/list localhost/image` +4. `inspect`: Displays the content of the manifest list or image index. Example: + `sealos manifest inspect localhost/list` +5. `push`: Pushes the manifest list or image index to the registry. Example: + `sealos manifest push localhost/list transport:destination` +6. `remove` and `rm`: Removes entries from the manifest list or image index, or completely deletes the manifest list or + image index. Example: `sealos manifest remove localhost/list sha256:entryManifestDigest` or + `sealos manifest rm localhost/list` + +With the `sealos manifest` command, you can flexibly manage manifest lists or image indexes, providing support for +multi-architecture Docker or OCI images. Users can create custom manifest lists according to their needs, making it +convenient to deploy and run Docker images on different hardware architectures. + +Users who want to build multi-architecture images through the manifest command can refer to the +document [Building Cluster Images that Support Multiple Architectures](/developer-guide/lifecycle-management/operations/build-image/build-multi-arch-image.md). diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/merge.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/merge.md new file mode 100644 index 000000000000..7dbd3080e3d5 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/merge.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 6 +keywords: [sealos merge, image merging, Docker images, Kubernetes, container optimization, image layers, application deployment, Dockerfile, build process, image storage] +description: Learn how to use Sealos merge command to combine multiple Docker images, optimize storage, and simplify application deployment in Kubernetes environments. +--- + +# Merge Image Merging + +The primary function of the `merge` command in Sealos is to merge multiple images into one. It does this by reading the +Dockerfiles of each input image, merging the commands and layer structures into a new image. The running logic of this +command is very similar to the `build` command, and many of the parameters are the same. + +This function is very useful when multiple images share layers, as it can reduce the size of the image and save storage +space. Moreover, since the merged image contains all the functionalities of multiple images, it can help simplify +application deployment. + +Here is a basic usage example of `sealos merge`: + +```bash +sealos merge -t new:0.1.0 kubernetes:v1.19.9 mysql:5.7.0 redis:6.0.0 +``` + +In this example, the three images `kubernetes:v1.19.9`, `mysql:5.7.0`, and `redis:6.0.0` are merged into a new image +`new:0.1.0`. + +The `sealos merge` command provides rich options to customize the merging process, such as `--all-platforms` to attempt +to build images for all base image platforms, `--build-arg` to provide parameters to the builder, `--no-cache` to +disable existing cached images, and so on. + +Please note that the `sealos merge` command builds a new image based on the Dockerfiles of each input image. Therefore, +if the Dockerfiles of the input images are incompatible, or there are any build errors, this command may fail. When +using the `sealos merge` command, make sure you understand the Dockerfile of each input image and adjust as needed. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/pull.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/pull.md new file mode 100644 index 000000000000..778842f11128 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/pull.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 6 +keywords: [sealos pull, container image, image repository, pull command, Docker daemon, image download, remote repositories, authentication, decryption key, pull policy] +description: Learn how to use the sealos pull command to efficiently download container images from repositories, with options for authentication, decryption, and custom policies. +--- + +# Pull Image + +The `sealos pull` command is a very useful one that allows you to download images from the container image repository +and store them locally. Users can obtain images by their tags or digests. If no tag is specified, the image with the ' +latest' tag (if it exists) will be downloaded by default. + +By using this command, users can easily download the required images from remote repositories, greatly improving work +efficiency. + +## Usage: + +`sealos pull [flags] [options] imageName` + +## Parameters: + +The following are the parameters of the `sealos pull` command: + +- `-a, --all-tags=false`: Download all tagged images in the repository. + +- `--authfile=''`: The path to the authentication file. The REGISTRY_AUTH_FILE environment variable can be used to + override it. + +- `--cert-dir=''`: The specified path to the certificate for accessing the image repository. + +- `--creds=''`: Use `[username[:password]]` to access the image repository. + +- `--decryption-key=[]`: The key needed to decrypt the image. + +- `--platform=[linux/arm64/v8]`: When choosing an image, prioritize the specified OS/ARCH over the current operating + system and architecture. + +- `--policy='missing'`: Set the policy, the optional values include 'missing', 'always', 'never'. + +- `-q, --quiet=false`: Do not output progress information when pulling images. + +- `--remove-signatures=false`: Do not copy signatures when pulling images. + +- `--retry=3`: The number of retries when the pull fails. + +- `--retry-delay=2s`: The delay between retries when the pull fails. + +## Examples: + +- Pull an image: `sealos pull my-image:latest` + +- Pull an image from the Docker daemon: `sealos pull docker-daemon:my-image:tag` + +- Pull an image from a specific repository: `sealos pull myregistry/myrepository/my-image:tag` + +- Pull multiple images: `sealos pull imageID1 imageID2 imageID3` + +The above is a usage guide for the `sealos push` command, hoping to help you. If you encounter any problems during use, +feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/push.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/push.md new file mode 100644 index 000000000000..702fd3330347 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/push.md @@ -0,0 +1,131 @@ +--- +sidebar_position: 6 +keywords: [sealos push, image push, Docker registry, container image, OCI format, image transport, image encryption, custom resources, authentication, compression] +description: Learn how to use the 'sealos push' command to push container images to various destinations, including Docker registries and local storage, with advanced options for encryption and custom resources. +--- + +# Push Image + +`sealos push` is a command in the Sealos command-line tool, used to push images to a specified location. This command is +particularly useful when you need to push local Docker images to a remote image repository. This guide will provide a +detailed introduction to its usage. + +## Basic Usage + +The basic format of the `sealos push` command is as follows: + +```bash +sealos push IMAGE_ID DESTINATION +``` + +In the above command, `IMAGE_ID` is the ID of the image you want to push, and `DESTINATION` is the location where you +want to push it. The `DESTINATION` uses the "transport:details" format. If not specified, the source IMAGE is reused as +the DESTINATION. + +In Sealos, the transport defines the format and location of the source image and the target image during the copying +process. Here are the various transports supported by Sealos: + +1. `containers-storage`: This transport is used for storing and managing containers running locally, such as images of + containers created using Podman or CRI-O. + +2. `dir`: This transport stores the image in a directory in the local file system, the structure of which conforms to + the OCI layout. + +3. `docker`: This transport is used for interacting with Docker registries, such as Docker Hub or any other compatible + private registry. + +4. `docker-archive`: This transport stores the image as a local Docker tar file (`.tar`), which is Docker's native + format. + +5. `docker-daemon`: This transport is used for interacting with the local Docker daemon. It can extract images from the + Docker daemon or push images to the Docker daemon. + +6. `oci`: This transport stores the image in a directory that conforms to the OCI layout, which is an open container + image format. + +7. `oci-archive`: This transport stores the image as a local OCI tar file (`.tar`). + +8. `ostree`: This transport stores the image in an OSTree repository, which is a file system that supports atomic + upgrades and rollbacks. + +9. `sif`: This is the Singularity SIF format, mainly used for high-performance computing and data-intensive + applications. + +Examples: + +- Push an image to a Docker registry: `sealos push my-image:latest docker://my-registry.example.com/my-image:latest` + +- Export an image from the Docker daemon: `sealos push docker-daemon:my-image:latest dir:/path/to/save/` + +- Push an image to local container storage: `sealos push my-image:latest containers-storage:my-new-image:latest` + +## Example + +For example, you can use the following command to push an image to the `registry.example.com` repository: + +```bash +sealos push my_image_id docker://registry.example.com/my_repository:my_tag +``` + +## Optional Parameters + +- `--all`: This parameter is used to push all images referred by the manifest list. + +- `--authfile`: This parameter is used to specify the path to the authentication file. The REGISTRY_AUTH_FILE + environment variable can be overridden. + +- `--cert-dir`: This parameter is used to specify the path to the certificate required to access the registry. + +- `--compression-format`: This parameter is used to specify the compression format to be used. + +- `--compression-level`: This parameter is used to specify the compression level to be used. + +- `--cr-option`: This parameter is used to control whether the image's Custom Resources (CR) are pushed to the target + image repository. + + Specifically, the optional values for this parameter include: + + - "yes": The image and its associated CR will be pushed to the target image repository. + + - "no": Only push the image, do not push any CR. + + - "only": Only push the CR, do not push the image itself. + + - "auto": Automatically decide whether to push based on the actual status of the image and CR. For example, if the + CR has changed or does not exist in the target repository, it will be pushed. + + Please note, this parameter is mainly used when dealing with images containing custom resources (such as Kubernetes + CRD objects), allowing you to more flexibly control the push process of images and CRs. + +- `--creds`: This parameter is used to access the registry, in the form of `[username[:password]]`. + +- `--digestfile`: This parameter, after copying the image, writes the digest of the resulting image to a file. + +- `-D`, `--disable-compression`: This parameter is used to not compress layers. + +- `--encrypt-layer`: This parameter is used to specify the layer to be encrypted. 0-index layer index supports negative + index (for example, 0 is the first layer, -1 is the last layer). If not defined, all layers will be encrypted when the + encryption-key flag is specified. + +- `--encryption-key`: This parameter is used to specify the key needed to encrypt the image, used together with the + encryption protocol (for example, jwe:/path/to/key.pem). + +- `-f`, `--format`: This parameter is used to specify the type of manifest to use in the target (oci, v2s1, or v2s2) ( + default is the source's + +manifest type, with fallback). + +- `-q`, `--quiet`: This parameter is used to not output progress information when pushing the image. + +- `--remove-signatures`: This parameter is used to not copy signatures when pushing the image. + +- `--retry`: This parameter is used to specify the number of retries when the push/pull fails. + +- `--retry-delay`: This parameter is used to specify the delay between retries when the push/pull fails. + +- `--rm`: This parameter is used to delete the manifest list after the push is successful. + +- `--sign-by`: This parameter is used to sign the image using a GPG key with the specified `FINGERPRINT`. + +That's the guide to using the `sealos push` command, and I hope it's helpful to you. If you encounter any problems +during use, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/registry.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/registry.md new file mode 100644 index 000000000000..502b2f38ee93 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/registry.md @@ -0,0 +1,269 @@ +--- +sidebar_position: 8 +keywords: [sealos registry, Docker image repository, registry save, registry serve, registry passwd, registry sync, registry copy, image synchronization, container management, Kubernetes] +description: Explore Sealos registry commands for efficient Docker image management. Learn to save, serve, sync, and copy images across registries, enhancing your container deployment workflow. +--- + +# Registry Image Repository Commands + +## Sealos: Detailed Explanation and User Guide for the `sealos registry save` Command + +The `registry save` command is used to pull remote Docker images to the local and save them in a specified directory. +This is particularly useful for deploying container images in offline or intranet environments. + +When executing the `registry save` command, it will automatically obtain the `sealos login` authentication information +for repository authentication. + +**Usage Guide** + +1. Use context to automatically retrieve images + + Pull and save images in the default manner. This mode will automatically parse the `charts` directory, `manifests` + directory, and `images` directory to get the image list. + + **Usage Example** + + ```shell + sealos registry save --registry-dir=/tmp/registry1 my-context + ``` + +2. Specified image list mode + + Pass the image list using arguments + + **Usage Example** + + ```shell + sealos registry save --registry-dir=/tmp/registry2 --images=docker.io/library/busybox:latest + ``` + +**Options** + +The following options apply to the `save` command and its subcommands: + +- `--max-procs`: The maximum number of parallel processes used to pull images. +- `--registry-dir`: The local directory to save images. +- `--arch`: The target architecture of the image, such as: `amd64`, `arm64`, etc. +- `--images`: The image list to be pulled and saved, separated by commas. For example: "my-image1:latest,my-image2: + v1.0". + +## Sealos: Detailed Explanation and User Guide for the `sealos registry serve` Command + +In the process of managing Docker image repositories, Sealos provides the `sealos registry serve` command to facilitate +user operations. This article will detail the usage methods and examples of the `sealos registry serve` command. + +### Basic Introduction + +The main function of the `sealos registry serve` command is to start a Docker distribution image repository server, +supporting two modes: `filesystem` and `inmem`. + +1. **Filesystem mode**: In this mode, sealctl will run a Docker distribution image repository server for the specified + directory. In this mode, the image data will be stored on the hard drive. + +2. **In-memory mode**: In this mode, sealctl will run a Docker distribution image repository server in memory. In this + mode, the image data is only stored in memory, and the data will be lost after the process exits. + +### Command Parameters + +The `sealos registry serve filesystem ` command supports the following parameters: + +- `--disable-logging`: Disable log output, default is false. +- `--log-level`: Configure log level, default is 'error'. +- `-p, --port`: The port the server listens to, default is a random unused port. + +### Usage Examples + +Here are some usage examples of the `sealos registry serve` command: + +#### Start an image repository server in the filesystem + +```bash +sealos registry serve filesystem --port=5000 +``` + +The above command will start a filesystem image repository server on port 5000. + +#### Start an image repository server in memory + +```bash +sealos registry serve inmem +``` + +The above command will start an in-memory image repository server. The stored data of this server will be lost when the +process exits. + +Through the `sealctl registry serve` command, users can easily manage and operate Docker image repositories. Whether in +the development environment or in the production environment, it is a powerful and easy-to-use tool. + +## Sealos: Detailed Explanation and User Guide of the `sealos registry passwd` Command + +In the process of managing Docker image repositories, Sealos provides the `sealos registry passwd` command to facilitate +users in modifying the password for the cluster registry. It offers a convenient method to help users change the +password of the registry. + +### Basic Usage + +Use the `sealos registry passwd` command to modify the password of the registry. + +```bash +sealos registry passwd +``` + +### Parameters + +Below are the parameters of the `sealos registry passwd` command: + +- `-c, --cluster-name`: Cluster name, the default is 'default'. + +- `-f, --cri-shim-file-path`: Image cri shim file path, if null it will not update the image cri shim file. The default + path is '/etc/image-cri-shim.yaml'. + +- `-p, --htpasswd-path`: Registry password file path. The default path is '/etc/registry/registry_htpasswd'. + +### Usage Steps + +1. Execute the `sealos registry passwd` command, you can specify parameters according to the needs. + +2. According to the command prompt, input the new password. + +3. After the command is successfully executed, the registry's password will be changed to the new password. + +### Demo Explanation + +[![asciicast](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ.svg)](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ) + +**In the usage process, it will let the user choose the registry type** + +- registry: Binary startup, execute `systemctl restart registry` to restart the image repository. + +- containerd: Containerd startup, execute "nerdctl restart sealos-registry" to restart the image repository. + +- docker: Docker startup, execute "docker restart sealos-registry" to restart the image repository. + +### Notice + +**After changing the registry password, modify the registry password in the Clusterfile** +After changing the registry password, all nodes and services using this registry need to update their configurations to +use the new password for authentication. Otherwise, they will not be able to pull or push images from this registry. + +If you are unsure about how to update the configuration of nodes and services, it is recommended to consult related +documentation or seek professional technical support before changing the registry password. + +## Sealos: Detailed Explanation and User Guide of the `sealos registry sync` Command + +Sealos' `registry sync` command can help you synchronize all images between two registries. This can be used not only +for image migration but also for backing up your images. + +### Basic Command Usage + +Execute the `sealos registry sync` command for image synchronization: + +```bash +sealos registry sync source dst +``` + +Here `source` represents the address of the source registry, and `dst` is the address of the target registry. + +For example, if you want to synchronize all images in the registry with the address of 127.0.0.1:41669 to the registry +with the address of sealos.hub:5000, you should execute the following command: + +```bash +sealos registry sync 127.0.0.1:41669 sealos.hub:5000 +``` + +### Authentication and Permissions + +Before executing the `sealos registry sync` command, please ensure that you have permissions to access the source +registry and the target registry. You can use `sealos login` to authenticate the registry. + +### Synchronization Process + +Please note that image synchronization may take some time, depending on the number and size of images, as well as the +speed of the network. During the synchronization process, please keep the network connected and ensure not to interrupt +the execution of the command before synchronization is complete. + +Importantly, the `sealos registry sync` command supports incremental synchronization. Images that already exist in the +target registry will not be re-synchronized. + +### Parameter Options + +The `sealos registry sync` command also provides some parameter options, allowing you to control the synchronization +process more finely: + +- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images. + +- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images. + +- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select + images. + +- `-a` or `--all`: If the source image is a list, synchronize all images. This is particularly useful in heterogeneous + environments because by default, only images of the current architecture will be synchronized. + +For example, if you want to synchronize all architecture images, you can add the `-a` parameter: + +```bash +sealos registry sync -a 127.0.0.1:41669 sealos.hub:5000 +``` + +The above is a detailed explanation and usage guide for the `sealos registry sync` command. We hope this information +helps you better understand and use this command. If you encounter any problems during use, feel free to ask at any +time. + +## Sealos: Detailed Explanation and User Guide of the `sealos registry copy` Command + +The `registry copy` command in Sealos is used to copy a specified image from one registry to another registry. This can +assist you in migrating or backing up images between different registries. + +### Basic Command Usage + +Use the `sealos registry copy` command for image copying: + +```bash +sealos registry copy source-image dst +``` + +Here `source-image` represents the full name of the source image (including the address and image name), and `dst` is +the address of the target registry. + +For example, to copy an image named `127.0.0.1:41669/my-image:tag` to a registry with the address `sealos.hub:5000`, you +can execute the following command: + +```bash +sealos registry copy 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +### Authentication and Permissions + +Before executing the `sealos registry copy` command, please ensure that you have permissions to access the source image +and the target registry. You can use `sealos login` to authenticate the registry. + +### Copying Process + +Please note that image copying may take some time, depending on the size of the image and the speed of the network. +During the copying process, please keep the network connected and ensure not to interrupt the execution of the command +before the copying is complete. + +### Parameter Options + +The `sealos registry copy` command provides some parameter options, allowing you to control the copying process more +finely: + +- `--override-arch ARCH`: Use the specified `ARCH` to replace the current machine architecture to select images. + +- `--override-os OS`: Use the specified `OS` to replace the current operating system to select images. + +- `--override-variant VARIANT`: Use the specified `VARIANT` to replace the current architecture variant to select + images. + +- `-a` or `--all`: If the source image is a list, copy all images. This is particularly useful in heterogeneous + environments because, by default, only images of the current architecture will be copied. + +For example, if you want to copy all architecture images, you can add the `-a` parameter: + +```bash +sealos registry copy -a 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +The above is the usage guide for the `sealos registry copy` command. We hope it is helpful to you. If you encounter any +problems during use, feel free to ask us any questions. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/reset.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/reset.md new file mode 100644 index 000000000000..1d4734931026 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/reset.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 2 +keywords: [sealos reset, cluster reset, Kubernetes cluster management, sealos command-line tool, cluster data clearing] +description: Learn how to use the 'sealos reset' command to reset and rebuild Kubernetes clusters. Explore basic usage, optional parameters, and best practices for efficient cluster management. +--- + +# Reset Cluster + +The `sealos reset` is a command in the Sealos command-line tool for resetting the entire cluster. This command is +particularly useful when you want to completely clear cluster data or rebuild the cluster. This guide will detail how to +use it. + +## Basic Usage + +The basic format of the `sealos reset` command is as follows: + +```bash +sealos reset --cluster cluster_name +``` + +In the above command, `cluster_name` is the name of the cluster you want to reset. + +## Example + +For instance, you can use the following command to reset a cluster named `mycluster`: + +```bash +sealos reset --cluster mycluster +``` + +## Optional Parameters + +- `--force`: This parameter is used to force the reset of the cluster, even if the cluster reset operation has not been + successfully completed. + +```bash +sealos reset --cluster mycluster --force +``` + +- `--masters`: This parameter is used to specify the master nodes to be reset. + +```bash +sealos reset --cluster mycluster --masters master1 +``` + +- `--nodes`: This parameter is used to specify the worker nodes to be reset. + +```bash +sealos reset --cluster mycluster --nodes node1 node2 +``` + +- `-p`, `--passwd`: This parameter is used to provide a password for authentication. + +- `-i`, `--pk`: This parameter specifies the file to read the identity (private key) used for public key authentication. + +- `--pk-passwd`: This parameter is used for the passphrase to decrypt the PEM-encoded private key. + +- `--port`: This parameter is used to specify the port of the remote host to connect to. + +- `-u`, `--user`: This parameter is used to specify the username for authentication. + +```bash +sealos reset --cluster mycluster --user username --pk /root/.ssh/id_rsa --pk-passwd yourpassword +``` + +The above is the usage guide for the `sealos reset` command. We hope it is helpful to you. If you encounter any problems +during use, feel free to ask us any questions. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md new file mode 100644 index 000000000000..fddfc7612b06 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 6 +keywords: [sealos rmi, delete local images, image management, container cleanup, storage optimization, Docker alternative, Kubernetes tools, image pruning, force delete images, multiple image deletion] +description: Learn how to use the 'sealos rmi' command to efficiently delete local images, manage storage, and optimize your container environment with optional parameters for advanced cleanup. +--- + +# Rmi: Delete Local Images + +`sealos rmi` is a command in the Sealos command-line tool that is used to delete one or more images stored locally. This +command can help you clean up useless or outdated images and save storage space. This guide will detail how to use it. + +## Basic Usage + +The basic format of the `sealos rmi` command is as follows: + +```bash +sealos rmi imageID +``` + +In the above command, `imageID` is the ID of the image you want to delete. + +## Example + +For instance, you can use the following command to delete an image with ID `imageID`: + +```bash +sealos rmi imageID +``` + +If you want to delete multiple images, just list all the image IDs in the command line, for example: + +```bash +sealos rmi imageID1 imageID2 imageID3 +``` + +## Optional Parameters + +- `-a`, `--all`: This parameter is used to delete all images. When using this option, the command will not accept any + image IDs. + +```bash +sealos rmi --all +``` + +- `-f`, `--force`: This parameter is used to forcefully delete an image and any containers using that image. + +```bash +sealos rmi --force imageID +``` + +- `-p`, `--prune`: This parameter is used to prune dangling images (images without a tag and not referenced by any + containers). + +```bash +sealos rmi --prune +``` + +The above is the usage guide for the `sealos rmi` command. We hope it is helpful to you. If you encounter any problems +during use, feel free to ask us any questions. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md new file mode 100644 index 000000000000..c0d3089052cb --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 1 +keywords: [sealos run, cluster initialization, Kubernetes cluster, Docker image, multi-image execution, single-node cluster, custom configuration, remote host authentication] +description: Learn how to use the powerful 'sealos run' command for Kubernetes cluster initialization, application installation, and multi-image execution with various configuration options. +--- + +# Run: Execute Cluster Images + +The `run` command of Sealos is a powerful and flexible tool that supports cluster initialization, application +installation, multi-image execution, single-node clusters, and more. Below is a detailed explanation and some usage +examples of the `sealos run` command and its parameters. + +## Command Overview + +``` +sealos run --masters [arg] --nodes [arg] [Options] +``` + +The `` parameter is the name and version of the Docker image you want to run in the cluster. `--masters` and +`--nodes` are the IP lists of the master and node nodes where you want to run this image. + +### Option Explanation + +- `--cluster='default'`: The name of the cluster where the operation is to be run. + +- `--cmd=[]`: Overwrite the CMD instruction in the image. + +- `--config-file=[]`: The path to the custom configuration file, used to replace resources. + +- `-e, --env=[]`: The environment variables set during command execution. + +- `-f, --force=false`: Forcefully overwrite the application in this cluster. + +- `--masters=''`: The master nodes to be run. + +- `--nodes=''`: The node nodes to be run. + +- `-p, --passwd=''`: Authenticate using the provided password. + +- `-i, --pk='/root/.ssh/id_rsa'`: Choose the private key file from which to read the public key authentication identity. + +- `--pk-passwd=''`: The password to decrypt the PEM-encoded private key. + +- `--port=22`: The connection port of the remote host. + +- `-t, --transport='oci-archive'`: Load image transport from a tar archive file. (Optional values: oci-archive, + docker-archive) + +- `-u, --user=''`: The username for authentication. + +## Examples + +1. Create a cluster on your bare-metal servers, specifying the IP list: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +2. Run multiple images: +``` +sealos run labring/kubernetes:v1.24.0 labring/helm:v3.11.3 calico:v3.24.6 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 +``` + +3. Specify the InfraSSH port of the server: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' +``` + +4. Customize a VIP Kubernetes cluster: +``` +sealos run -e defaultVIP=10.103.97.2 labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +5. Create a single-node Kubernetes cluster: +``` +sealos run labring/kubernetes:v1.24.0 +``` + +6. Create a cluster using custom environment variables: +``` +sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +These examples demonstrate the power and flexibility of the `sealos run` command, which can be customized and adjusted +according to your needs. + +For more examples, please refer to [Run Cluster](/developer-guide/lifecycle-management/operations/run-cluster.md). diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/save.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/save.md new file mode 100644 index 000000000000..9e07391f889a --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/save.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 6 +keywords: [sealos save, image backup, container migration, oci-archive, docker-archive, image storage, Kubernetes images, CLI tool, image archiving, container management] +description: Learn how to use the 'sealos save' command to store and backup container images efficiently. Explore options for different archive formats and multi-image saving. +--- + +# Save: Store Images + +`sealos save` is a command in the Sealos command-line tool used to save images to archive files. This command can help +you conveniently back up and migrate your images. This guide will detail its usage. + +## Basic Usage + +The basic `sealos save` command format is as follows: + +```bash +sealos save -o outputFilename imageName +``` + +In the above command, `outputFilename` is the name of the archive file you want to save, and `imageName` is the name of +the image you want to save. + +## Example + +For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file +named `kubernetes.tar`: + +```bash +sealos save -o kubernetes.tar labring/kubernetes:v1.24.0 +``` + +## Optional Parameters + +- `--format`: This parameter is used to specify the transport format for saving the image. The currently available + options are `oci-archive`, `docker-archive`, `oci-dir`, and `docker-dir`. The default value is `oci-archive`. +- `-m`: This parameter can be used to save multiple images at the same time, but it is only applicable to the + `docker-archive` format. + +For example, you can use the following command to save an image named `labring/kubernetes:latest` to an archive file +named `kubernetes.tar` in the `docker-archive` method: + +```bash +sealos save -o kubernetes.tar --format docker-archive labring/kubernetes:v1.24.0 +sealos save -o kubernetes.tar -m --format docker-archive labring/kubernetes:v1.24.0 labring/helm:v3.5.0 +``` + +The above is the usage guide of the `sealos save` command, and we hope it is helpful to you. If you encounter any +problems during use, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/scp.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/scp.md new file mode 100644 index 000000000000..02ab624e9c86 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/scp.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 5 +keywords: [sealos scp, file transfer, cluster management, remote file copy, sealos command, cluster nodes, IP-based copy, role-based copy, sealos CLI, DevOps tool] +description: Learn how to use the sealos scp command for efficient file transfer to cluster nodes. Explore options for IP and role-based copying in Sealos clusters. +--- + +# Scp: Copy Files + +`sealos scp` is a command in the Sealos command-line tool, used for copying files to specified cluster nodes. This guide +will detail its usage and options. + +## Basic Usage + +The basic `sealos scp` command format is as follows: + +```bash +sealos scp "source file path" "destination file path" +``` + +In the above command, `source file path` is the local path of the file you want to copy, and `destination file path` is +the remote node path you want to copy the file to. + +## Options + +The `sealos scp` command provides the following options: + +- `-c, --cluster='default'`: The name of the cluster to which the files should be copied. The default is `default`. + +- `--ips=[]`: Copies the files to nodes with the specified IP addresses. + +- `-r, --roles='':`: Copies the files to nodes with specified roles. + +Each option can be followed by one or more arguments. + +## Examples + +For example, you can use the following command to copy the local file `/root/aa.txt` to `/root/dd.txt` on all nodes in +the default cluster: + +```bash +sealos scp "/root/aa.txt" "/root/dd.txt" +``` + +If you want to copy files on the nodes with the `master` and `node` roles in the cluster named `my-cluster`, you can use +the following command: + +```bash +sealos scp -c my-cluster -r master,node "/root/aa.txt" "/root/dd.txt" +``` + +If you only want to copy files on the node with the IP address `172.16.1.38`, you can use the following command: + +```bash +sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" +``` + +The above is the usage guide for the `sealos scp` command, and we hope it is helpful to you. If you encounter any +problems during use, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/tag.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/tag.md new file mode 100644 index 000000000000..a94abeffabae --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/tag.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 6 +keywords: [sealos tag, image management, Docker tagging, container images, Sealos CLI, image naming, Kubernetes images, container orchestration, image tagging, DevOps tools] +description: Learn how to use the 'sealos tag' command to efficiently manage and rename container images in Sealos. Enhance your Kubernetes workflow with this powerful image tagging tool. +--- + +# Tag: Add Image Names + +`sealos tag` is a command in the Sealos command-line tool, used to add one or more additional names to the images stored +locally. This command can help you better manage your images. This guide will detail its usage. + +## Basic Usage + +The basic `sealos tag` command format is as follows: + +```bash +sealos tag imageName newName +``` + +In the above command, `imageName` is the name of the image you want to operate on, and `newName` is the new tag you want +to add. + +## Examples + +For example, you can use the following command to add a new name `firstNewName` to an image named `imageName`: + +```bash +sealos tag imageName firstNewName +``` + +You can also add multiple names at once, for example, add two names `firstNewName` and `SecondNewName`: + +```bash +sealos tag imageName firstNewName SecondNewName +``` + +The above is the usage guide for the `sealos tag` command, and we hope it is helpful to you. If you encounter any +problems during use, feel free to ask us. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md new file mode 100644 index 000000000000..1fb9d1b51f19 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 10 +keywords: [Kubernetes cluster image, Sealos, container runtime interface, image versions, rootfs-type images, Kubernetes deployment, cluster management, container orchestration, CRI options, Sealos documentation] +description: "Explore Sealos' Kubernetes cluster images: types, versions, and CRI options. Learn about rootfs-type images for efficient Kubernetes deployment and management." +--- + +# Kubernetes Cluster Image Documentation + +This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, +types, and versions. + +## Image Names + +The official cluster images provided by Sealos include the following: + +1. kubernetes: Kubernetes image with containerd as the container runtime interface (CRI). +2. kubernetes-docker: Kubernetes image with Docker as the CRI. +3. kubernetes-crio: Kubernetes image with Crio as the CRI. + +Currently, Sealos primarily provides Kubernetes-related images and has not yet provided other types of cluster images +such as k3s or k0s. + +## Image Types + +Sealos offers different types of Kubernetes cluster images based on the container runtime interface (CRI): + +1. Kubernetes image with containerd as the CRI. +2. Kubernetes image with Docker as the CRI. +3. Kubernetes image with Crio as the CRI. + +Users can choose the appropriate image type based on their requirements and preferences. + +## Image Versions + +Sealos offers multiple versions of cluster images. Examples include: + +### 1. Development Version + +This version is suitable for users who want to try out the latest features of the project. The development version may +contain new features and improvements that have not been thoroughly tested and may not be as stable. + +Example: `v1.26(v1.26-amd64/v1.26-arm64)` + +### 2. Latest Version + +The latest version is typically more stable than the development version but may not include all the new features. This +version is recommended for most users. + +Example: `v1.26.0(v1.26.0-amd64/v1.26.0-arm64)` + +### 3. Release Version (Including Historical Versions) + +The release version includes historical versions. Release versions have usually undergone rigorous testing and are +considered stable. + +Example: `v1.26.0-4.1.5(v1.26.0-4.1.5-amd64/v1.26.0-4.1.5-arm64)` `4.1.5` is the corresponding version number for +Sealos. + +When choosing an image version, users should consider their requirements and preferences. Additionally, Sealos provides +sub-versions for different processor architectures to meet the needs of users on different hardware platforms. + +## Summary + +This document provides an overview of the rootfs-type cluster images offered by Sealos, including the image names, +types, and versions. Users can select the appropriate image type and version based on their requirements and preferences +to run containers in a Kubernetes cluster. diff --git a/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md new file mode 100644 index 000000000000..e62e147d7221 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 0 +keywords: [Sealos, cloud operating system, Kubernetes clusters, cloud-native applications, cluster management, container management, image management, cluster images, Sealos user guide, Rootfs cluster images] +description: Explore Sealos, a unified cloud OS for managing Kubernetes clusters and cloud-native apps. Learn about cluster management, containers, and images with our comprehensive user guide. +--- + +# Sealos User Guide + +Sealos is a unified cloud operating system designed to manage cloud-native applications. It provides a set of +command-line tools to help users manage Kubernetes clusters, nodes, perform remote operations, manage containers and +images, and perform other functions. Below are detailed introductions to these commands: + +This chapter provides a user guide for using Sealos, as well as information related to cluster images. + +- Sealos User Guide: For detailed information about each command, including all supported parameters and subcommands, + please refer to the [sealos](/developer-guide/lifecycle-management/reference/sealos/commands.md) reference documentation. +- Cluster Images: + - [Rootfs Cluster Images](/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application Cluster Images](/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md) diff --git a/docs/archived/5.0/docs/developer-guide/sealos/QA.md b/docs/archived/5.0/docs/developer-guide/sealos/QA.md new file mode 100644 index 000000000000..d8475c3065fc --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/sealos/QA.md @@ -0,0 +1,126 @@ +--- +sidebar_position: 3 +keywords: [Sealos Cloud, deployment issues, certificate renewal, domain name change, user registration, kubernetes cluster, iptables, system kernel, networking, resource constraints] +description: Comprehensive guide for Sealos Cloud deployment issues, certificate management, and user registration. Includes solutions for common problems and step-by-step instructions for system administrators. +--- + +# Q&A + +Encountering issues during the deployment and use of Sealos Cloud is not uncommon. To assist you effectively, we have +compiled a list of frequently encountered problems along with comprehensive solutions. + +## Deployment Related Issues + +This section details the problems you may face during the deployment phase and their respective solutions. For issues +not covered here, please consult with us at the [Sealos Community](https://github.com/labring/sealos/discussions). + +### Q1: iptables / ip_forward Concerns + +**Problem Overview**: In some operating systems, such as older versions of Centos and RHEL, iptables or IPv4 IP +forwarding is not enabled by default. This can hinder the creation of iptables rules or the forwarding of packets, +potentially preventing the cluster from starting correctly. + +**Resolution Strategy**: To address this, execute the following commands on each node to activate iptables and IP +forwarding: + +```shell +$ modprobe br_netfilter +$ echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables +$ echo 1 > /proc/sys/net/ipv4/ip_forward +``` + +### Q2: Issues with System Kernel + +- **Problem Overview**: An outdated system kernel can impede the proper startup of the cluster. Also, certain + applications, especially those dependent on MongoDB 5.0, might not function with an older kernel. +- **Resolution Strategy**: Ensure your system's kernel version is at least 5.4 or higher before commencing the + deployment. + +### Q3: System Resource Constraints + +- **Problem Overview**: Limited system resources can lead to deployment delays or even halts. If you encounter prolonged + wait times, it's likely due to insufficient system resources. +- **Resolution Strategy**: Check the resource status of your nodes using `kubectl describe nodes`, focusing on CPU, + memory, and storage availability. + +### Q4: Networking Issues + +- **Problem Overview**: Incorrect server configuration can lead to various network issues during deployment. Common + areas of concern include: + 1. Misconfiguration of http_proxy / https_proxy environment variables; + 2. Inadequate server firewall settings; + 3. Improper server routing configurations; +- **Resolution Strategy**: Troubleshoot network issues by verifying the correctness of these configurations. + +## Certificate and Domain Name Issues + +### Certificate Renewal Process + +Certificates are crucial for the security of your Sealos cluster. Follow these steps to update your certificates, +especially as they approach their expiration date: + +1. **Backup Existing Certificate**: + + On the `master0` node, backup your current certificate. This step is crucial to prevent loss of the certificate + during the update process. Use this command: + + ```shell + $ kubectl get secret -n sealos-system wildcard-cert -o yaml > cert-backup.yaml + ``` + + This will save the `wildcard-cert` certificate in YAML format to `cert-backup.yaml`. + +2. **Storing the New Certificate**: + + Place your new certificate files (.crt and .key) on the `master0` node. + +3. **Updating the Certificate**: + + To update, use the script below, replacing `` and `` with the actual paths of your + new certificate and key files. + + ```shell + #!/bin/bash + # Set Variables + CRT_FILE= + KEY_FILE= + + # Base64 encode the certificate and key files + CRT_BASE64=$(cat $CRT_FILE | base64 -w 0) + KEY_BASE64=$(cat $KEY_FILE | base64 -w 0) + + # Create JSON for update + PATCH_JSON='{"data":{"tls.crt":"'$CRT_BASE64'","tls.key":"'$KEY_BASE64'"}}' + + # Update the Secret using kubectl patch + kubectl patch secret wildcard-cert -n sealos-system -p $PATCH_JSON + ``` + + This script encodes the new certificate in Base64 and updates the Kubernetes cluster's Secret object using + `kubectl patch`. + +### Changing the Domain Name + +Changing a domain name in a Sealos cluster is complex, often requiring adjustments in multiple components and services. +We do not currently provide a comprehensive guide for this process in our documentation. However, future plans include +the release of a Sealos Cluster Management Panel for easier domain name and certificate replacement. + +It's important to note that domain name changes demand deep knowledge of the cluster's network setup and may involve +intricate DNS settings and service discovery. We recommend undertaking such changes only if you have the requisite +expertise or with guidance from a professional. + +### user registration switch + +disabled user register: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: true/signUpEnabled: false/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + +enabled user register: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: false/signUpEnabled: true/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-1.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-1.jpg new file mode 100644 index 000000000000..680d572141b3 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-1.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-2.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-2.jpg new file mode 100644 index 000000000000..8b6a354702c0 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/chrome-certificate-2.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-1.png b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-1.png new file mode 100644 index 000000000000..6e2c46692cf6 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-1.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-2.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-2.jpg new file mode 100644 index 000000000000..a99aaeaf5a0a Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-2.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-3.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-3.jpg new file mode 100644 index 000000000000..8a42e6c08c8e Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/firefox-export-certificate-3.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-1.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-1.jpg new file mode 100644 index 000000000000..3a211ccd3e13 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-1.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-2.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-2.jpg new file mode 100644 index 000000000000..357d5880dbe4 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-2.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-3.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-3.jpg new file mode 100644 index 000000000000..9c33c03204f5 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/safari-export-certificate-3.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-cost-center.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-cost-center.jpg new file mode 100644 index 000000000000..e8e59ccb8709 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-cost-center.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-1.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-1.png new file mode 100644 index 000000000000..790d9cc7a3da Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-1.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-2.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-2.png new file mode 100644 index 000000000000..9f04a4668927 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-2.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-3.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-3.png new file mode 100644 index 000000000000..ea555c230e1d Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-3.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-4.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-4.png new file mode 100644 index 000000000000..d409c55ba36d Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-4.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-5.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-5.png new file mode 100644 index 000000000000..3cde5ae76850 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-5.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-6.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-6.png new file mode 100644 index 000000000000..4a6ef666fb1d Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-6.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate.jpg new file mode 100644 index 000000000000..97dbcc274b2e Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate1.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate1.png new file mode 100644 index 000000000000..3f1bfaebdc2a Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate1.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate2.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate2.png new file mode 100644 index 000000000000..7c33e2693e9c Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate2.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate3.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate3.png new file mode 100644 index 000000000000..609a653a4f15 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate3.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate4.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate4.png new file mode 100644 index 000000000000..6c6a20aadb78 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license-activate4.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license.png b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license.png new file mode 100644 index 000000000000..aa820a06c105 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/sealos-license.png differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/images/windows-trust-certificate.jpg b/docs/archived/5.0/docs/developer-guide/sealos/images/windows-trust-certificate.jpg new file mode 100644 index 000000000000..243d0d2d5583 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/sealos/images/windows-trust-certificate.jpg differ diff --git a/docs/archived/5.0/docs/developer-guide/sealos/installation.md b/docs/archived/5.0/docs/developer-guide/sealos/installation.md new file mode 100644 index 000000000000..65948142f032 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/sealos/installation.md @@ -0,0 +1,390 @@ +--- +sidebar_position: 1 +keywords: [sealos cluster, kubernetes installation, self-signed certificates, domain configuration, cloud deployment, nip.io, cert-manager, cluster setup, network considerations, hardware requirements] +description: Learn how to install and configure a Sealos cluster with Kubernetes, including domain setup, certificate management, and network considerations for optimal deployment. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Install Sealos Cluster + +export const Highlight = ({children, color}) => ( + + {children} + +); + +:::tip + +In the context of large-scale clusters and corporate production +environments, [the enterprise or bespoke editions of the Sealos private cloud](/self-hosting) +come highly recommended. + +::: + +## Before You Begin + +### Hardware + +- Unique hostnames for each cluster node. +- Time synchronization across all nodes. +- Preferably use a fresh operating system for setting up the cluster. **Refrain from installing Docker yourself!** +- Compatible with numerous Linux versions like Ubuntu, Debian, CentOS, Rocky Linux. +- **Ensure a system kernel version of 5.4 or higher**. + +Recommended System Configuration: + +We advise using Ubuntu 22.04 LTS with a kernel version of 5.4 or higher. The specifications are as follows: + +| Operating System | Kernel Version | CPU | Memory | Storage | Masters | Nodes | +|------------------|----------------|-----|--------|---------|------------|-------| +| Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | Odd Number | Any | + +:::info +Kubernetes and Sealos Cloud Operating system require roughly 2 cores (2c) and 2GB of memory (2g) per Master node, and +about 1 core (1c) and 1GB of memory (1g) per Node node. Ensure each node in your cluster is well-equipped for these +system components. +::: + +### Network Considerations + +- All nodes must be interconnected; +- Installation scripts should be run on the **first Master node** of the Kubernetes cluster; **nodes outside the cluster + aren't set up for installations yet**; +- Make sure all nodes are capable of mutual communication. + +### Domain Name Usage + +- A domain name is necessary for accessing Sealos and its services; +- If you lack a domain name, `nip.io` offers a free domain name service. + +### Certificate Requirements + +Sealos uses certificates for secure communication. Without your +certificates, [cert-manager](https://cert-manager.io/docs/) will be employed for auto-certification. + +If you are providing your certificate, it should support the following domains (assuming your domain is +cloud.example.io): + +- `*.cloud.example.io` +- `cloud.example.io` + +## Installation Steps + +We offer a one-click installation script to streamline the deployment process. This script is capable of setting up a +Sealos cluster from scratch or integrating it into an existing Kubernetes cluster (use with caution on pre-existing +clusters). + +:::info +This script is specifically designed for deploying Sealos clusters on "Kubernetes clusters installed via Sealos," and is +not compatible with Kubernetes clusters set up in other ways. + +For instructions on deploying Kubernetes clusters using Sealos, refer +to: [Installing Kubernetes Clusters](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md). This method +is compatible with most Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags), +except for version 1.28 and higher. +::: + +If your system is new to Kubernetes, **we advise using this script to install both Kubernetes and the Sealos cluster +simultaneously.** + +There are different installation options based on your domain name needs: + +### 1. No public domain, don't want custom domains + +If you don't have a public domain or need custom domains, you can use the free `nip.io` wildcard DNS service. `nip.io` +maps dynamic IP addresses to fixed subdomains, useful for local dev environments. + +It works by taking any IP address as part of a `nip.io` subdomain, and resolving requests back to that IP. For example, +if your intranet IP is `192.168.1.10`, you can use the domain `192.168.1.10.nip.io`. Requests to this domain will +resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up intranet DNS. + +To use nip.io for Sealos, run the below on the first master node and enter prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh +``` + +When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP. + +After installation, the terminal will display the Sealos access domain and login credentials, as shown below: + +```shell +Sealos cloud login info: +Cloud Version: latest +URL: https://10.214.210.102.nip.io +admin Username: admin +admin Password: sealos2023 +``` + +### 2. Have public domain, want public access + +If you have a public domain and want public Sealos access, you'll need a trusted public SSL/TLS certificate. You can use +acme.sh to automatically issue certs, or get free certs from your domain provider or purchase commercial certificates. + +Place the certificate files in a directory on the first master, like `/root/certs/`. + +:::info + +You'll also need to configure DNS records at your domain provider: + +``` +cloud.example.io A +*.cloud.example.io A +``` + +This maps your domain and subdomains to the first master's public IP. + +::: + +Then run below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= \ +--cert-path= \ +--key-path= +``` + ++ `` is your public domain ++ `` is the certificate file path (`.crt` or `.pem`) e.g. `/root/certs/example.crt` ++ `` is the private key file path (`.key` or `.pem`) e.g. `/root/certs/example.key` + +### 3. Have public domain, want internal access + +If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS +records resolving to the first master's internal IP: + +``` +cloud.example.io A +*.cloud.example.io A +``` + +Then run the below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` + +Where `` is your public domain. + +The installer will use [cert-manager](https://cert-manager.io/docs/) to automatically sign certificates. + +### 4. No public domain, want custom domain + +If you don't have a public domain but need a custom domain, set up internal DNS resolving a custom domain to the first +master's internal IP. + +:::note + +Assuming the first master internal IP is `192.168.1.10`, and your custom domain is `cloud.example.io`. + +::: + +You can use CoreDNS, Reference configuration: + +``` +(global_cache) { + cache { + # [5, 60] + success 65536 3600 300 + # [1, 10] + denial 8192 600 60 + prefetch 1 60m 10% + } +} + +.:53 { + errors + health + ready + + import global_cache + + template IN A cloud.example.io { + answer "{{ .Name }} 60 IN A 192.168.1.10" + fallthrough + } + + forward . 223.5.5.5 + + log + loop + + reload 6s +} +``` + +This resolves `cloud.example.io` and subdomains to the first master internal IP. + +Then run below on the first master, entering prompts: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` + +Where `` is your custom domain. + +The installer uses [cert-manager](https://cert-manager.io/docs/) for certificates. + +## Ensuring Browsers Trust Your Self-Signed Certificate + +When using installation options 1, 3, or 4, as highlighted earlier, your browser won't automatically recognize and trust +your certificate. This leads to a warning message when you try to enter Sealos Cloud: + +![](images/chrome-certificate-1.jpg) + +Continuing to Sealos Cloud despite the warning results in issues like the App icons not appearing correctly, and Apps +failing to launch. + +![](images/chrome-certificate-2.jpg) + +To address this, you'll need to export the self-signed certificate and set up your system to trust it. Follow these +steps to do so. + +### Exporting Self-Signed Certificates + +The process of exporting self-signed certificates differs slightly in various browsers. Here's a guide for exporting +these certificates in some of the most commonly used browsers: + +#### Chrome (includes browsers based on Chromium, like the updated Edge and Brave) + +1. In the address bar, click the "Not Secure" warning on the left side. +2. Select "Certificate is invalid" to bring up a window with certificate information. +3. In this window, go to the "Details" tab. +4. Within "Details", find and click on the "Export" option. +5. Choose a file name and location for saving, then complete the exporting procedure. + +#### Firefox + +1. On the webpage, click on "Advanced". + + ![Firefox Certificate Export Step 1](images/firefox-export-certificate-1.png) + +2. Next, click "View Certificate". + + ![Firefox Certificate Export Step 2](images/firefox-export-certificate-2.jpg) + +3. On the certificate page, select "PEM (certificate)". + + ![Firefox Certificate Export Step 3](images/firefox-export-certificate-3.jpg) + +#### Safari + +1. Select "Show Details" on the page. + + ![Safari Certificate Export Step 1](images/safari-export-certificate-1.jpg) + +2. Click on "View this Certificate". + + ![Safari Certificate Export Step 2](images/safari-export-certificate-2.jpg) + +3. You'll see a window displaying the certificate chain. + + ![Safari Certificate Export Step 3](images/safari-export-certificate-3.jpg) + +4. To export the certificate, drag the one highlighted in red to either your desktop or a specific folder. + +### Trusting Self-Signed Certificates + +#### macOS + +To trust a self-signed certificate on macOS, use the following command: + +```bash +# Assuming the certificate is stored at ~/Downloads/10.214.210.102.nip.io.cer +$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/10.214.210.102.nip.io.cer +``` + +You can also trust the certificate through these steps: + +##### 1. Adding the Certificate to Keychain + +1. Open **Keychain Access**, click on the "System" category under "System Keychains." +2. Navigate to the certificate's location in "Finder" and **double-click the certificate file** (usually `.cer`, `.crt`, + or `.pem`). +3. Authenticate with your username and password to allow changes. +4. The certificate is now added to your keychain. + +##### 2. Trusting the Certificate + +1. In Keychain Access, locate and double-click the newly added self-signed certificate in the "System" category. +2. In the certificate window, expand the "Trust" section. +3. Change the setting next to "When using this certificate" from "Use System Defaults" to "Always Trust." +4. Close the window and authenticate if prompted to confirm the changes. +5. The certificate is now trusted. + +:::info Note + +Self-signed certificates, not issued by a third-party Certificate Authority (CA), won't be trusted by other devices. +Manually perform these steps on each device within your company or organization that needs to trust the certificate. + +::: + +#### Windows + +1. Right-click the root certificate file and select "Install Certificate." +2. Choose "Current User" or "Local Machine," then "Next." +3. Select "Place all certificates in the following store," click "Browse," choose "Trusted Root Certification + Authorities," "OK," then "Next." +4. Click "Finish," "Yes," then "OK." + +![](images/windows-trust-certificate.jpg) + +#### Linux + +Linux distributions vary in updating the root certificate store and paths for private certificates. First, copy the +self-signed CA certificate to the designated path, then run the command to update the root certificate store. + + + + + ```bash + # Debian/Ubuntu/Gentoo + $ sudo cp root_ca.crt /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates + $ sudo rm /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates --fresh + ``` + + + + + ```bash + # CentOS/Fedora/RHEL + $ yum install ca-certificates + $ update-ca-trust force-enable + $ cp root_ca.crt /etc/pki/ca-trust/source/anchors/ + $ update-ca-trust + ``` + + + + + ```bash + # Alpine + $ apk update && apk add --no-cache ca-certificates + $ cp root_ca.crt /usr/local/share/ca-certificates/ + $ update-ca-certificates + ``` + + + + + ```bash + # OpenSUSE/SLES + $ cp root_ca.crt /etc/pki/trust/anchors/ + $ update-ca-certificates + ``` + + + diff --git a/docs/archived/5.0/docs/developer-guide/system-design/billing-system.md b/docs/archived/5.0/docs/developer-guide/system-design/billing-system.md new file mode 100644 index 000000000000..4eeb1d57574b --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/system-design/billing-system.md @@ -0,0 +1,6 @@ +--- +keywords: [billing system, Sealos, cloud platform, usage tracking, payment processing, subscription management, cost optimization, cloud billing, enterprise billing, scalable billing] +description: Explore Sealos' robust billing system for cloud platforms. Learn about usage tracking, payment processing, and subscription management for optimized cloud costs. +--- + +# Billing System \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/system-design/images/application_dark.png b/docs/archived/5.0/docs/developer-guide/system-design/images/application_dark.png new file mode 100644 index 000000000000..403e9223d16c Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/system-design/images/application_dark.png differ diff --git a/docs/archived/5.0/docs/developer-guide/system-design/images/application_light.png b/docs/archived/5.0/docs/developer-guide/system-design/images/application_light.png new file mode 100644 index 000000000000..500f67bbe589 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/system-design/images/application_light.png differ diff --git a/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_dark.png b/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_dark.png new file mode 100644 index 000000000000..6d14555ef4d6 Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_dark.png differ diff --git a/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_light.png b/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_light.png new file mode 100644 index 000000000000..17bf8cf5d6ae Binary files /dev/null and b/docs/archived/5.0/docs/developer-guide/system-design/images/architecture_light.png differ diff --git "a/docs/archived/5.0/docs/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" "b/docs/archived/5.0/docs/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" new file mode 100644 index 000000000000..44b16d80d808 Binary files /dev/null and "b/docs/archived/5.0/docs/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" differ diff --git "a/docs/archived/5.0/docs/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" "b/docs/archived/5.0/docs/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" new file mode 100644 index 000000000000..08124bf3e466 Binary files /dev/null and "b/docs/archived/5.0/docs/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" differ diff --git a/docs/archived/5.0/docs/developer-guide/system-design/monitor-system.md b/docs/archived/5.0/docs/developer-guide/system-design/monitor-system.md new file mode 100644 index 000000000000..6c947d3a54b1 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/system-design/monitor-system.md @@ -0,0 +1,6 @@ +--- +keywords: [monitor system, Sealos, Kubernetes monitoring, system performance, observability, metrics collection, log analysis, alerting, dashboard visualization, cloud-native monitoring] +description: Explore Sealos' robust monitor system for Kubernetes clusters. Learn about performance tracking, log analysis, and alerting features for optimal cloud-native observability. +--- + +# Monitor System \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/system-design/system-application.md b/docs/archived/5.0/docs/developer-guide/system-design/system-application.md new file mode 100644 index 000000000000..eb2f7faa9760 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/system-design/system-application.md @@ -0,0 +1,6 @@ +--- +keywords: [system application, Sealos, containerization, cloud-native, Kubernetes, application management, scalability, microservices, DevOps, cloud computing] +description: Explore Sealos system applications for efficient cloud-native containerization. Learn how to optimize application management and scalability in Kubernetes environments. +--- + +# System Application \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/system-design/system-architecture.md b/docs/archived/5.0/docs/developer-guide/system-design/system-architecture.md new file mode 100644 index 000000000000..6eb88755de28 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/system-design/system-architecture.md @@ -0,0 +1,6 @@ +--- +keywords: [system architecture, Sealos, cloud native, distributed systems, microservices, scalability, containerization, Kubernetes, infrastructure design, DevOps] +description: Explore Sealos' system architecture for cloud-native environments. Learn about distributed systems, microservices, and scalable infrastructure design for modern applications. +--- + +# System Architecture \ No newline at end of file diff --git a/docs/archived/5.0/docs/developer-guide/system-design/user-system.md b/docs/archived/5.0/docs/developer-guide/system-design/user-system.md new file mode 100644 index 000000000000..3bcb6451b729 --- /dev/null +++ b/docs/archived/5.0/docs/developer-guide/system-design/user-system.md @@ -0,0 +1,6 @@ +--- +keywords: [user system, Sealos, authentication, authorization, identity management, access control, user roles, permissions, security, cloud native] +description: Explore Sealos' user system for robust authentication, authorization, and identity management in cloud-native environments. Learn about access control and security features. +--- + +# User System \ No newline at end of file diff --git a/docs/archived/5.0/docs/images/overview-1.png b/docs/archived/5.0/docs/images/overview-1.png new file mode 100644 index 000000000000..af1a5bfadae7 Binary files /dev/null and b/docs/archived/5.0/docs/images/overview-1.png differ diff --git a/docs/archived/5.0/docs/images/overview-2.jpeg b/docs/archived/5.0/docs/images/overview-2.jpeg new file mode 100644 index 000000000000..dc2a48c6c653 Binary files /dev/null and b/docs/archived/5.0/docs/images/overview-2.jpeg differ diff --git a/docs/archived/5.0/docs/images/sealos-desktop.webp b/docs/archived/5.0/docs/images/sealos-desktop.webp new file mode 100644 index 000000000000..8a1e98ad2d87 Binary files /dev/null and b/docs/archived/5.0/docs/images/sealos-desktop.webp differ diff --git a/docs/archived/5.0/docs/msa/privacy-policy.md b/docs/archived/5.0/docs/msa/privacy-policy.md new file mode 100644 index 000000000000..6c8a67e47662 --- /dev/null +++ b/docs/archived/5.0/docs/msa/privacy-policy.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 2 +--- + +# Sealos Cloud Privacy Policy + +We prioritize your privacy immensely. This policy details the collection, use, disclosure, and protection of your +personal information when you utilize our services. Please thoroughly understand its contents. + +**Information Collection** + +1. We may gather your personal details like name, phone number, email, and address during registration or service usage. +2. We also collect data produced during service use, such as operational logs, IP addresses, and device types. +3. For enhanced user experience, technologies like cookies may be employed to store information related to your service + interaction. + +**Information Usage** + +1. The handling of your personal data adheres to legal regulations and user agreements. +2. We utilize the information for improving service quality, innovating new products, and other similar purposes. +3. Collected data might be used for delivering service-related notifications and advertisements. + +**Disclosure of Information** + +1. Your personal data will not be shared with third parties, except under these circumstances: + + 1. With your prior agreement; + 2. When legally or regulatorily required; + 3. To uphold our or other users' legal rights. + +2. We may share your information with affiliated entities or partners, ensuring data security through strict + confidentiality. + +**Protection of Information** + +1. We apply various security measures, like encryption and access control, to prevent unauthorized data access, use, or + disclosure. +2. Regular safety assessments are conducted on stored personal data to guarantee its security. +3. Should a data leak or similar security incident occur, we'll immediately implement emergency measures and inform you + as required by law. + +**User Rights** + +1. You have complete authority to access, correct, or delete your personal data at any time. +2. Opting out of data collection can limit your access to certain service features. +3. You may request a halt in processing your personal data, which might affect service continuation. + +**Privacy Policy Updates** + +1. We may periodically update this policy, and any changes will be published on our service page. Continuing service use + implies acceptance of any updated terms. +2. Regularly reviewing this policy is encouraged to stay informed about your data protection. + +**Minor Protection** + +Minors' data protection is crucial to us. If you're a minor, please engage with our services under guardian supervision, +ensuring proper data management. + +**Cross-border Data Transfer** + +Considering our servers' diverse locations, your data may be transferred and processed internationally. We assure that +cross-border data remains securely protected. + +**Contact Us** + +1. For any questions, suggestions, or complaints about this policy, please reach us via email fanux@sealos.io +2. We are committed to quickly resolving any issues you raise. \ No newline at end of file diff --git a/docs/archived/5.0/docs/msa/terms-of-service.md b/docs/archived/5.0/docs/msa/terms-of-service.md new file mode 100644 index 000000000000..0120c1a78778 --- /dev/null +++ b/docs/archived/5.0/docs/msa/terms-of-service.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 1 +--- + +# Sealos Cloud Terms of Service + +These Sealos Cloud Terms of Service (this “Agreement”) are entered into by and between you and Zhuhai Huanjie Cloud +Computing Co., Ltd. (referred to as “we/us” or "the Company") regarding the use of our cloud services (hereafter +referred to as "the Service"). We urge you to read and understand every clause, particularly those concerning +limitations of our liability, restrictions on your rights, and the terms about dispute resolution and legal +jurisdiction. If any aspect of this agreement is not acceptable to you, refrain from registering for or using the +Service. + +**Article 1: Scope of Services** + +1. We offer a range of internet-based IT services, including but not limited to storage, computation, and network + transmission. +2. Technical support and customer assistance will be provided to enhance your experience with the Service. +3. We reserve the right to modify, suspend, or discontinue any or all aspects of the Service without any obligation to + you. + +**Article 2: Account Registration and Management** + +1. Prior to using the Service, registering an account is mandatory. You must ensure that the information provided at + registration is accurate and updated regularly. +2. The security of your account credentials is your responsibility. Any misuse of your account should be reported to us + immediately. +3. We hold the right to inspect your account and, if irregularities are detected, may halt or terminate the services + provided to you. + +**Article 3: Usage Guidelines** + +1. The Service must not be used for illegal activities or in ways that infringe upon the rights of others, including + intellectual property rights and confidentiality breaches. +2. The Service must not be used for malicious registration of accounts, whether for profit, speculation, or other + purposes. +3. Dissemination of illegal, harmful, or malevolent content via the Service is strictly prohibited. +4. You are liable for adhering to all relevant laws and the terms of this agreement, including the responsibility for + the content shared and outcomes arising from your use of the Service. + +**Article 4: Fees and Payment** + +1. You agree to the fee structure associated with the Service, as outlined in our published rates. +2. We may revise the fee structure based on market conditions, notifying you in advance. Continued use of the Service + implies agreement to the revised fees. +3. Non-payment of fees may lead to the suspension or termination of the Service, and we retain the right to seek + compensation. + +**Article 5: Disclaimer and Limitation of Liability** + +1. The Service is provided based on existing technology and conditions, and we do not guarantee uninterrupted, + completely secure, or error-free service. +2. We are not liable for Service disruptions or unavailability due to force majeure. +3. We bear no responsibility for loss or damage of data caused by your actions. + +**Article 6: Privacy Policy** + +Your agreement to and compliance with our privacy policy, an integral part of this agreement, is required. Please refer +to the detailed "Privacy Policy" section below. + +**Article 7: Intellectual Property Rights** + +1. The intellectual property of the Service and all related materials, including software and documentation, is owned by + us. Unauthorized reproduction, distribution, or reverse engineering is prohibited. +2. While you retain intellectual property rights over your data and content generated within the Service, you grant us + permission for necessary operations such as storage and backup to facilitate the Service. + +**Article 8: Agreement Termination** + +1. Violation of any terms of this agreement permits us to cease providing the Service immediately and to pursue legal + remedies. +2. You can terminate this agreement by requesting the cancellation of your account. + +**Article 9: Dispute Resolution and Governing Law** + +1. This agreement is governed by the laws of the People's Republic of China. Disputes arising from this agreement should + be resolved through negotiation; failing that, legal action may be pursued in a competent court. +2. The agreement includes provisions for resolving disputes and the applicable legal framework. + +**Article 10: Network Security** + +1. You bear the consequences for any services provided through the platform that violate national laws. +2. You are responsible for overseeing the services you offer and bear consequences for any illegal activities conducted + by your users. +3. Should we discover your involvement in illegal activities, legal action will be taken and evidence provided to the + authorities. + +**Article 11: Additional Provisions** + +1. Should parts of this agreement become invalid due to legal conflicts, it does not affect the validity of the + remaining terms. +2. The Company retains the ultimate interpretive authority over this agreement and the privacy policy. For inquiries, + please contact us. \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos1.png b/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos1.png new file mode 100644 index 000000000000..7313d36f5487 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos2.png b/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos2.png new file mode 100644 index 000000000000..32f0fb67f319 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/ai-applications/imgs/sealos2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/5.0/docs/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md new file mode 100644 index 000000000000..1b4b1f6ebdb0 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 1 +keywords: [FastGPT, AI Knowledge Base, Sealos, FastGPT installation, deploy FastGPT] +description: Quick and easy installation of FastGPT AI Knowledge Base on Sealos. No server needed, deploy in minutes and start using immediately. +--- + +# Quick Installation of FastGPT AI Knowledge Base + +No server, no magic, no domain name, click to deploy 👇 + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3DFastGPT) + +The database needs to be deployed. After the deployment, you need to wait 2 to 4 minutes to access the database. The +minimum configuration is used by default, and the first access is a little slow. + +![](imgs/sealos1.png) + +## Run + +Click on the [external address] provided by sealos to use it. The login user name is root, and the password is the newly +set environment variable (1234 in the figure above) + +![](imgs/sealos2.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/baas/install-fireboom.md b/docs/archived/5.0/docs/quick-start/examples/baas/install-fireboom.md new file mode 100644 index 000000000000..6b49a051cea7 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/baas/install-fireboom.md @@ -0,0 +1,109 @@ +--- +keywords: [Fireboom, visual API development, Sealos, application deployment, full-stack development, declarative programming, API endpoint, persistent storage, Swagger] +description: Quick installation guide for Fireboom on Sealos. Learn how to deploy and configure Fireboom, a visual API development platform, with step-by-step instructions. +--- + +# Quick Installation of Fireboom + +[Fireboom](https://fireboom.io) is a visual API development platform designed for developers, providing a declarative +development framework, a visual editing suite, and a hook mechanism. + +Official website: https://fireboom.io + +**Features** + +- Minute-level delivery: Reduce the interface development time from 2 days to 2 minutes in traditional mode. + +- Front-end to full-stack: Visual development significantly reduces the threshold for interface development, allowing + front-end developers to easily become full-stack developers. + +- Back-end without bricklaying: Declarative programming avoids copy and paste, reduces the probability of bugs, and + allows back-end developers to focus on business development. + +## Step 1: Create a New Application + +- Open the "Application Management" application in the [Sealos](https://cloud.sealos.io) desktop environment. + +![](../images/fireboom/1-1.png) + +- In "Application Management", click "Create New Application" to create a new application. + +## Step 2: Application Deployment + +- Basic Configuration: + + - Application Name (custom): fireboom + + - Image Name (default latest version): fireboomapi/fireboom_server:latest + + - CPU (recommended): 2 Core (minimum 1 Core) + + - Memory (recommended): 4 G (minimum 512 M) + +- Deployment Mode: + + - Number of Instances (custom): 1 + +![](../images/fireboom/2-1.png) + +- Network Configuration: + + - Control Panel: 9123 + - API Endpoint: 9991, enable external access + +![](../images/fireboom/2-2.png) + +- Advanced Configuration: + + - Command Settings: + - Parameter Settings: dev (dev for development mode, start for production mode) + + - Persistent Storage: + + - Metadata: /fbserver/store + - Uploaded Files: /fbserver/upload + +![](../images/fireboom/2-3.png) + +## Step 3: Deploy the Application + +1. Click "Deploy Application" to start deploying the application. + +![](../images/fireboom/3-1.png) + +2. Click "Details" to check. When the STATUS of the application changes from Pending to Running, it means that the + application has been successfully launched. + +![](../images/fireboom/3-2.png) + +## Step 4: Access the Application + +- Access the Control Panel: Domain corresponding to 9123 + +![](../images/fireboom/4-1.png) + +- Access the API Endpoint: Domain corresponding to 9991 + +``` +status ok +``` + +## Step 5: Configure Fireboom (optional) + +After setting, you can easily use the Swagger document~ + +1. View the public address of the API: Settings -> System + +![](../images/fireboom/5-1.png) + +2. Modify it to the public domain of the API endpoint, corresponding to 9991 + + - Static Value: Select static value and set it to the public domain of the **API Endpoint** + - Environment Variable: Go to Environment Variables, find FB_API_PUBLIC_URL and set it to the public domain of the * + *API Endpoint** + +![](../images/fireboom/5-2.png) + +3. Use the Swagger document to test the API + +![](../images/fireboom/5-3.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-halo.md b/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-halo.md new file mode 100644 index 000000000000..c85753930c61 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-halo.md @@ -0,0 +1,165 @@ +--- +sidebar_position: 2 +keywords: [Halo Blogging Platform, Sealos, PostgreSQL deployment, open-source website tool, Halo installation] +description: Quickly deploy the Halo Blogging Platform on Sealos with PostgreSQL. Follow our step-by-step guide for seamless setup and management. +--- + +# Quick Installation of Halo Blogging Platform + +Deploy Halo Blogging Platform swiftly on Sealos + +![](../images/halo_img-1.png) + +[Halo](https://github.com/halo-dev/halo) is a powerful and easy-to-use open-source website building tool. +This article will introduce how to deploy Halo on Sealos, while deploying PostgreSQL on Sealos can help you easily +manage and maintain the database to meet different business needs. + +## Step 1: Deploy PostgreSQL on Sealos + +### First, open [Sealos](https://cloud.sealos.io) and click on "More Apps" to enter the PostgreSQL deployment interface: + +![](../images/halo_img-2.png) + +### Fill in the configuration + +In the PostgreSQL deployment interface, click on "Create Cluster" and fill in the relevant configuration information as +prompted. + +![](../images/halo_img-3.png) + +After entering the corresponding configuration, click on "Create Cluster": + +![](../images/halo_img-4.png) + +### Check the database configuration: + +![](../images/halo_img-5.png) + +Save the generated configuration containing the database username, password, and connection method for use in the +following Halo configuration: + +![](../images/halo_img-6.png) + +> The PostgreSQL configuration can be seen in the image as follows: +> +> Username: root +> +> Password: jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3 +> +> pg DNS name: acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432 + +## Step 2: Deploy Halo on Sealos + +### Open App Launchpad + +![](../images/halo_img-7.png) + +![](../images/halo_img-8.png) + +### Fill in the configuration + +- Customize the application name + +- Set the image name to `halohub/halo:2.5` + +- CPU and storage should be configured according to actual conditions, memory requires 500Mi to start Halo, you can + refer to the official [prepare](https://docs.halo.run/getting-started/prepare) + +- Expose port should be 8090. At the same time, setting it to external network access will automatically configure an + exit domain for the application for external network access, and you also need to configure this address in Halo + +- Environment variables include database configuration and other related configurations. It is important to note that + you need to configure the database connection information based on the username, password, and DNS you set in Step 1. + Additionally, you need to configure the username and password for the super administrator of Halo. + +Configure Halo environment variables as follows: + +```Bash +spring.sql.init.platform=postgresql +spring.r2dbc.url=r2dbc:pool:postgresql://acid-halo-pg.ns-sy32q9p9.svc.cluster.local:5432/halo +spring.r2dbc.username=root +spring.r2dbc.password=jxidRwmY82eeuFa01tHN28msb86woounM0QMbyl1jhwzKxT9IDqlNkFfyy4R34G3 +halo.external-url=tmtdvfjiyyfh.cloud.sealos.io +halo.security.initializer.superadminusername=root +halo.security.initializer.superadminpassword=sealos +``` + +Detailed explanation of environment variable configuration: + +| Parameter Name | Description | +|----------------------------------------------|----------------------------------------------------------------------------------------------------------| +| spring.r2dbc.url | Database connection address, see Database Link Format below for details | +| spring.r2dbc.username | Database username | +| spring.r2dbc.password | Database password | +| spring.sql.init.platform | Database platform name, supports postgresql, mysql, h2 | +| halo.external-url | External access link, if you need public network access, you need to configure the actual access address | +| halo.security.initializer.superadminusername | Initial super administrator username | +| halo.security.initializer.superadminpassword | Initial super administrator password | + +Database link format (here we use the postgresql format): + +| Connection Method | Connection Address Format | spring.sql.init.platform | +|-------------------|------------------------------------------------------------------------------------|--------------------------| +| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql | +| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 | + +### Network configuration + +![](../images/halo_img-9.png) + +### Environment variable configuration + +![](../images/halo_img-10.png) + +### Configure Persistent Storage Volume + +Mount `/root/.halo2` directory to persist Halo data: + +![](../images/halo_img-11.png) + +## Step 3: Access Halo via the Public Network + +After successfully launching the application, you can access Halo through the public network address for configuration: + +![](../images/halo_img-12.png) + +![](../images/halo_img-13.png) + +### **Initialize Halo** + +![](../images/halo_img-14.png) + +![](../images/halo_img-15.png) + +### **First Halo:** + +![](../images/halo_img-16.png) + +![](../images/halo_img-17.png) + +![](../images/halo_img-18.png) + +## FAQ + +### Forgot Password + +#### Enter the App Launchpad and click on "Logs": + +![](../images/halo_img-19.png) + +![](../images/halo_img-20.png) + +You can see the following information in the logs: Username: `admin` Password: `QTu2J2xgUQ3ngqUo` + +#### Alternatively, you can view the logs by executing the following command in the terminal: + +![](../images/halo_img-21.png) + +``` +root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1 +2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin === +``` + +![](../images/halo_img-22.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-wordpress.md b/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-wordpress.md new file mode 100644 index 000000000000..d40e234680c7 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/blog-platform/install-wordpress.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 1 +keywords: [WordPress installation, Sealos, MySQL database, WordPress setup, CMS deployment] +description: Quick and easy WordPress installation with Sealos. Learn how to set up WordPress and MySQL database for a seamless CMS deployment. +--- + +# Quick Installation of WordPress + +[WordPress](https://github.com/WordPress/WordPress) is a powerful, user-friendly and open source content management +system (CMS) that can be used to publish websites, news, blogs and other content online. It provides convenient methods +for content editing, publishing and updating, and can also be highly customized through themes and plugins. WordPress +has become a very successful and popular CMS system, with over 30 million websites built on WordPress worldwide. + +> By deploying **WordPress** with **Sealos**, you can greatly reduce deployment time and start users' content publishing +> journey at lightning speed. + +WordPress requires a MySQL database to be installed and used together. As a best practice for using Sealos, it is +recommended to deploy the MySQL database independently using a database application, and quickly install WordPress based +on the independent database. + +## Create MySQL instance + +Open the **Database** application in the [Sealos](https://cloud.sealos.io) desktop environment, quickly set up the +required database instance and wait for the database deployment to be completed. + +![](../images/wordpress_15.png) + +![](../images/wordpress_16.png) + +![](../images/wordpress_17.png) + +## Create a New Database + +When installing a MySQL instance through the **Database** application, user-defined databases are not automatically +created. When connecting an application to the MySQL database, you need to manually create the corresponding database in +the MySQL instance. + +You can access the database terminal command line in the following ways: + +![](../images/wordpress_12.png) +![](../images/wordpress_13.png) + +Alternatively, you can access the database terminal command line using the following methods: + +`mysql -u username -p -h yourhost` + +![](../images/wordpress_14.png) + +After successfully connecting to the database, you can create a new database using the following command: + +`CREATE DATABASE yourdb;` + +At this point, you have manually created a user-defined database in the MySQL instance. + +## App Launchpad + +Click on App Launchpad to enter the installation interface. + +![](../images/wordpress_1.png) + +## Basic Configuration + +Begin by setting a custom application name and selecting the image to install. Sealos supports both public and private +image sources. In this example, the official image source is used, and if no image version is specified, the latest +version will be pulled by default. + +Next, configure CPU and memory resources for WordPress. WordPress is very lightweight, and a small amount of resources +is sufficient to ensure the normal operation of the program. + +![](../images/wordpress_8.png) + +## NetWork Configuration + +For network configuration, enable external access, and Sealos will automatically assign an exit domain name for users to +access from the internet. If the user has already registered their custom domain, they can create a CNAME record +pointing to the assigned exit domain name, allowing the custom domain to be used to access the installed application. + +Note: In subsequent demonstrations, a randomly generated exit domain name by Sealos will be used. + +![](../images/wordpress_9.png) + +## Advanced Configuration + +In the advanced configuration, you need to configure the environment variables for WordPress based on the actual +database situation. + +In the advanced configuration, you need to set environment variables and local storage for WordPress. + +Common WordPress environment variables are shown below: + +```Plain +WORDPRESS_DB_HOST IP address of the database instance +WORDPRESS_DB_USER Database user for connection +WORDPRESS_DB_PASSWORD User password required for connecting to the database +WORDPRESS_DB_NAME Name of the connected database +``` + +Based on the previously installed MySQL example, you can configure the environment variables as follows: + +```Plain +WORDPRESS_DB_HOST Database IP Address +WORDPRESS_DB_USER Database username +WORDPRESS_DB_PASSWORD Database password +``` + +The storage volume mount point for the official WordPress image is: /var/www/html. Users can allocate storage space for +the application according to their needs. + +With this, WordPress is also successfully installed. Click on the details, and you can successfully access it through +the external network address. + +![](../images/wordpress_10.png) + +Click on the link, and the following page shows that the installation was successful: + +![](../images/wordpress_11.png) + +**Note: If the deployment fails, first check whether the user-defined database that WordPress is trying to access exists +in the MySQL instance.** + + diff --git a/docs/archived/5.0/docs/quick-start/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/5.0/docs/quick-start/examples/dial-testing-system/install-uptime-kuma.md new file mode 100644 index 000000000000..8f21bddc3cf3 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/dial-testing-system/install-uptime-kuma.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 1 +keywords: [Uptime Kuma, server monitoring, open-source monitoring, Sealos, application deployment] +description: Quickly install Uptime Kuma for real-time server monitoring with Sealos. Follow these steps to deploy and configure your application efficiently. +--- + +# Quick Installation of Uptime Kuma + +![](../images/uptimekuma_img-10.png) + +[Uptime Kuma](https://github.com/louislam/uptime-kuma) is an open-source and easy-to-use server monitoring tool. It +helps you monitor the real-time status, response time, and other key metrics of your server to ensure that it always +remains in optimal condition. If you want to quickly install Uptime Kuma, follow these steps: + +### Step 1: First, enter Sealos and open the App Launchpad + +![](../images/uptimekuma_img-1.png) + +### Step 2: Create a New Application + +Open the App Launchpad in the [Sealos](https://cloud.sealos.io) desktop environment, click "New Application" to create a +new application. + +![](../images/uptimekuma_img-2.png) + +### Step 3: Set Startup Parameters + +Configure the startup parameters as follows: + +- Set the container exposed port to 3001 +- Enable public network access to access the application via the provided domain + +![](../images/uptimekuma_img-3.png) + +![](../images/uptimekuma_img-4.png) + +### Step 4: Set up Storage Volumes + +In the advanced settings, add a storage volume mount to persist the Uptime Kuma data directory `/app/data`. This ensures +that the application data is not lost when the container restarts. + +![](../images/uptimekuma_img-5.png) + +### Step 5: Deploy the Application + +Click 「Deploy Application」 to start the application: + +![](../images/uptimekuma_img-6.png) + +### Step 6: Access the Application + +Once the application is successfully launched, you can access it using its public network address. Enter the application +domain in your browser to access the main interface. + +![](../images/uptimekuma_img-7.png) + +### Step 7: Create an Administrator Account + +To ensure the security of Uptime Kuma, you will need to create an administrator account when you first access the +application. Fill in the basic information for the administrator account, including the username and password. After +completing the form, click the "Create" button to register the account. + +![](../images/uptimekuma_img-8.png) + +### Step 8: Add Custom Monitoring Items + +![](../images/uptimekuma_img-9.png) + +![](../images/uptimekuma_img-0.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/gitea/install-gitea.md b/docs/archived/5.0/docs/quick-start/examples/gitea/install-gitea.md new file mode 100644 index 000000000000..90c7691ced52 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/gitea/install-gitea.md @@ -0,0 +1,118 @@ +--- +keywords: [Gitea installation, self-hosted Git, Sealos, CI/CD, MySQL database setup] +description: Quick guide to install Gitea on Sealos, covering self-hosted Git, CI/CD, and MySQL database setup. Follow these steps for a seamless deployment. +--- + +# Quick Installation of Gitea + +Git with a cup of tea! Painless self-hosted all-in-one software development service, includes Git hosting, code review, +team collaboration, package registry and CI/CD. + +## Step 1: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](../images/gitea-0.png) + +## Step 2: Open the App Launchpad + +![](../images/gitea-1.png) + +## Step 3: Create a new application + +- In App Launchpad, click "Create New Application" to create a new application. + +![](../images/gitea-2.png) + +## Step 4: Application deployment + +- Basic configuration: + + - Application name (custom): gitea + - Image name (default latest version): gitea/gitea:latest-rootless + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + +- Deployment mode: + + - Number of instances (custom): 1 + +![](../images/gitea-3.png) + +- Network configuration: + + - Container port: 3000 + - Accessible to the Public: enabled + +![](../images/gitea-4.png) + +- Advanced configuration: + + - Custom local storage, persist Gitea data (recommended 1 G). + +![](../images/gitea-5.png) + +## Step 5: Deploy the application + +- Click「Deploy Application」 to start deploying the application. + +![](../images/gitea-6.png) + +## Step 6: Configure the database + +- Configure MySQL database for Gitea via Sealos +- Click Database + +![](../images/gitea-9.png) + +- Create a new database + +![](../images/gitea-10.png) + +- Basic configuration: + + - Cluster type: mysql + - Database version: ac-mysql-8.0.30 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + +![](../images/gitea-11.png) + +- After deployment is successful, enter the details page to view the MySQL connection information + +![](../images/gitea-12.png) + +- Click "One-click Connection" to enter the MySQL terminal connection + +![](../images/gitea-13.png) + +- Execute + +```sql +CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +``` + +Create the database needed for Gitea + +![](../images/gitea-14.png) + +## Step 7: Access the application + +- Click 「App Launchpad」 to view.When the application's STATUS changes from Pending to Running, it means the application + has started successfully. + +- When the STATUS is Running, you can directly access the external address. + +![](../images/gitea-7.png) + +- After accessing the external address, enter the configuration page and configure according to the details of the MySQL + just created + +![](../images/gitea-15.png) + +- After configuring, click Install, wait for a while and you can enter the login page, deployment successful! + +![](../images/gitea-16.png) + +![](../images/gitea-17.png) + diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png new file mode 100644 index 000000000000..025a0c4e45a9 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png new file mode 100644 index 000000000000..1ca1cd0ed53a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-1iudM0.png new file mode 100644 index 000000000000..5ba9b3ad8784 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-1iudM0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png new file mode 100644 index 000000000000..ac8de099a1ae Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg new file mode 100644 index 000000000000..1b1fe554ea55 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg new file mode 100644 index 000000000000..a773666c4ff2 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-59-FxJE12.png new file mode 100644 index 000000000000..fdc343a80edc Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-11-59-FxJE12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-01-DKect7.png new file mode 100644 index 000000000000..b4d2db4792a7 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-01-DKect7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-05-CWczxm.png new file mode 100644 index 000000000000..d512ad14195a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-05-CWczxm.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png new file mode 100644 index 000000000000..28a241ade20c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png new file mode 100644 index 000000000000..aa8e415fb5b7 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-09-RslDGj.png new file mode 100644 index 000000000000..0935a645d999 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-09-RslDGj.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png new file mode 100644 index 000000000000..a446015a5c59 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png new file mode 100644 index 000000000000..6996e718eb53 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png new file mode 100644 index 000000000000..c76f2e0535ae Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png new file mode 100644 index 000000000000..ba4d166331c6 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg new file mode 100644 index 000000000000..7ca839b52b2f Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png new file mode 100644 index 000000000000..ac7134504f86 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png new file mode 100644 index 000000000000..59460d087f22 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png new file mode 100644 index 000000000000..0fc1e2d152b9 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png new file mode 100644 index 000000000000..00e7aa10391a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png new file mode 100644 index 000000000000..08ff96c0c126 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-1.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-2.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-3.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-3.png new file mode 100644 index 000000000000..01876eeff7a0 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-4.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-4.png new file mode 100644 index 000000000000..fad0d90a71df Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-5.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-5.png new file mode 100644 index 000000000000..d6567da245ae Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-6.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-6.png new file mode 100644 index 000000000000..78b505e75298 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-7.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-7.png new file mode 100644 index 000000000000..9771645376a2 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/appsmith-8.png b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-8.png new file mode 100644 index 000000000000..ca7f7e62e60c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/appsmith-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/1-1.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/1-1.png new file mode 100644 index 000000000000..2e8a8d45325b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/1-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-1.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-1.png new file mode 100644 index 000000000000..5fb4c0ec51f5 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-2.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-2.png new file mode 100644 index 000000000000..714ad3891845 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-3.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-3.png new file mode 100644 index 000000000000..e0d5a1a605d4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/2-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-1.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-1.png new file mode 100644 index 000000000000..10d9ef1b793a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-2.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-2.png new file mode 100644 index 000000000000..eef62cdfc5f9 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/3-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/4-1.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/4-1.png new file mode 100644 index 000000000000..f717abd1abf4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/4-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-1.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-1.png new file mode 100644 index 000000000000..477f9ebd26ef Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-2.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-2.png new file mode 100644 index 000000000000..e353d414d31b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-3.png b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-3.png new file mode 100644 index 000000000000..d1bddbf12c65 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/5-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-architecture.jpeg new file mode 100644 index 000000000000..7f9660c847fa Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-architecture.jpeg differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-intro.jpeg new file mode 100644 index 000000000000..848bd2b5d97d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/fireboom/fireboom-intro.jpeg differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-0.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-0.png new file mode 100644 index 000000000000..946d921d4ce3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-1.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-1.png new file mode 100644 index 000000000000..cae3923a16fb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-10.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-10.png new file mode 100644 index 000000000000..0087724d0b14 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-11.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-11.png new file mode 100644 index 000000000000..b820dc0b7817 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-12.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-12.png new file mode 100644 index 000000000000..31c2451f5e33 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-13.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-13.png new file mode 100644 index 000000000000..5557c68da9c4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-13.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-14.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-14.png new file mode 100644 index 000000000000..566c44f970bd Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-14.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-15.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-15.png new file mode 100644 index 000000000000..90e280b8053a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-15.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-16.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-16.png new file mode 100644 index 000000000000..901bcb8ac4ae Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-16.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-17.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-17.png new file mode 100644 index 000000000000..d0f6300f2494 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-17.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-2.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-2.png new file mode 100644 index 000000000000..295b148d1798 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-3.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-3.png new file mode 100644 index 000000000000..2d9a9a6554b5 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-4.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-4.png new file mode 100644 index 000000000000..9a7d30cba2e6 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-5.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-5.png new file mode 100644 index 000000000000..9fe9a5f7ac9d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-6.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-6.png new file mode 100644 index 000000000000..e7db5c844514 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-7.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-7.png new file mode 100644 index 000000000000..6ced9251709d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-8.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-8.png new file mode 100644 index 000000000000..9395b5fdd4de Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/gitea-9.png b/docs/archived/5.0/docs/quick-start/examples/images/gitea-9.png new file mode 100644 index 000000000000..9da2f0456b52 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/gitea-9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-1.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-1.png new file mode 100644 index 000000000000..ccc68e469f5e Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-10.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-10.png new file mode 100644 index 000000000000..e1137ca41541 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-11.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-11.png new file mode 100644 index 000000000000..707d5e9c313f Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-12.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-12.png new file mode 100644 index 000000000000..a62bad32ef89 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-13.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-13.png new file mode 100644 index 000000000000..d270bab0a421 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-13.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-14.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-14.png new file mode 100644 index 000000000000..2865276b96df Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-14.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-15.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-15.png new file mode 100644 index 000000000000..c9afff34a93e Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-15.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-16.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-16.png new file mode 100644 index 000000000000..8d843a1f0e7f Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-16.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-17.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-17.png new file mode 100644 index 000000000000..6e7d3f8e8585 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-17.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-18.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-18.png new file mode 100644 index 000000000000..a3d0fcb8dd93 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-18.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-19.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-19.png new file mode 100644 index 000000000000..5254c15d9678 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-19.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-2.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-2.png new file mode 100644 index 000000000000..5a5cfc3f2684 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-20.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-20.png new file mode 100644 index 000000000000..d20e5101e4b4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-20.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-21.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-21.png new file mode 100644 index 000000000000..42dcb2303a18 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-21.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-22.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-22.png new file mode 100644 index 000000000000..e560a5aec5b7 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-22.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-3.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-3.png new file mode 100644 index 000000000000..3a23c04cef78 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-4.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-4.png new file mode 100644 index 000000000000..13665f4535d1 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-5.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-5.png new file mode 100644 index 000000000000..200db6367fde Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-6.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-6.png new file mode 100644 index 000000000000..19ce13c9b30b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-7.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-7.png new file mode 100644 index 000000000000..d450ab20f873 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-8.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-8.png new file mode 100644 index 000000000000..5d857a1b40e7 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/halo_img-9.png b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-9.png new file mode 100644 index 000000000000..4e32d5475cf3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/halo_img-9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-1.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-10.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-10.png new file mode 100644 index 000000000000..0aa0f2546ce3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-11.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-11.png new file mode 100644 index 000000000000..ef702aadb7e3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-2.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-3.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-3.png new file mode 100644 index 000000000000..00edfb9382b2 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-4.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-4.png new file mode 100644 index 000000000000..c947ad852ca0 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-5.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-5.png new file mode 100644 index 000000000000..289789fd639f Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-6.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-6.png new file mode 100644 index 000000000000..a58fbc16537b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-7.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-7.png new file mode 100644 index 000000000000..414ff44e067b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-8.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-8.png new file mode 100644 index 000000000000..dc7bb21bd4eb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/pageplug-9.png b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-9.png new file mode 100644 index 000000000000..0011841a7d14 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/pageplug-9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/0.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/0.png new file mode 100644 index 000000000000..197223de4a8c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/1.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/1.png new file mode 100644 index 000000000000..3eced36954fd Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/10.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/10.png new file mode 100644 index 000000000000..599ead17397d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/11.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/11.png new file mode 100644 index 000000000000..2841c01c2057 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/12.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/12.png new file mode 100644 index 000000000000..43310c129d89 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/13.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/13.png new file mode 100644 index 000000000000..c89cc68375b3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/13.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/14.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/14.png new file mode 100644 index 000000000000..3c637c422d93 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/14.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/15.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/15.png new file mode 100644 index 000000000000..3203e025d9fb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/15.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/16.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/16.png new file mode 100644 index 000000000000..5f9b13ef61eb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/16.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/17.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/17.png new file mode 100644 index 000000000000..21e6091eb4bd Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/17.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/2.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/2.png new file mode 100644 index 000000000000..234358eae33c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/3.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/3.png new file mode 100644 index 000000000000..e8341c9130cb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/4.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/4.png new file mode 100644 index 000000000000..8d8db3ca8bb8 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/5.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/5.png new file mode 100644 index 000000000000..4efdc860247d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/6.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/6.png new file mode 100644 index 000000000000..f0dc20aee71a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/7.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/7.png new file mode 100644 index 000000000000..563c28b48c19 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/8.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/8.png new file mode 100644 index 000000000000..602a961b7035 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/tailchat/9.png b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/9.png new file mode 100644 index 000000000000..06ad0d6ea766 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/tailchat/9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-0.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-0.png new file mode 100644 index 000000000000..e2940389dae3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-1.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-1.png new file mode 100644 index 000000000000..b250072db39a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-10.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-10.png new file mode 100644 index 000000000000..6411f1d99ccf Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-2.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-2.png new file mode 100644 index 000000000000..083044758d59 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-3.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-3.png new file mode 100644 index 000000000000..566495613ee1 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-4.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-4.png new file mode 100644 index 000000000000..ec61518674cf Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-5.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-5.png new file mode 100644 index 000000000000..4e5a2e13b63a Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-6.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-6.png new file mode 100644 index 000000000000..cdeb8f3db368 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-7.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-7.png new file mode 100644 index 000000000000..a33d5580c9dd Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-8.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-8.png new file mode 100644 index 000000000000..850d72fbb991 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-9.png b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-9.png new file mode 100644 index 000000000000..2003e96b9002 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/uptimekuma_img-9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_1.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_1.png new file mode 100644 index 000000000000..bbfd6001594b Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_10.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_10.png new file mode 100644 index 000000000000..d1cd7993028d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_11.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_11.png new file mode 100644 index 000000000000..103bf474654c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_12.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_12.png new file mode 100644 index 000000000000..80c69d185806 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_13.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_13.png new file mode 100644 index 000000000000..250114b79ef9 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_13.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_14.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_14.png new file mode 100644 index 000000000000..d180432b02c4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_14.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_15.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_15.png new file mode 100644 index 000000000000..877112038094 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_15.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_16.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_16.png new file mode 100644 index 000000000000..b490fade878c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_16.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_17.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_17.png new file mode 100644 index 000000000000..9d0bc0391604 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_17.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_2.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_2.png new file mode 100644 index 000000000000..83254bdb6cd1 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_3.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_3.png new file mode 100644 index 000000000000..1b0a1f926bb8 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_4.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_4.png new file mode 100644 index 000000000000..86ad0a605c7d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_5.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_5.png new file mode 100644 index 000000000000..1a9a1566ae1c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_6.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_6.png new file mode 100644 index 000000000000..52b0dc908d42 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_7.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_7.png new file mode 100644 index 000000000000..a18bfa6d1703 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_8.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_8.png new file mode 100644 index 000000000000..484684f79ae4 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/images/wordpress_9.png b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_9.png new file mode 100644 index 000000000000..24db5cadb900 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/images/wordpress_9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-appsmith.md b/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-appsmith.md new file mode 100644 index 000000000000..d5c450cd423c --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-appsmith.md @@ -0,0 +1,73 @@ +--- +keywords: [Appsmith, open-source platform, internal apps, Sealos, application deployment, App Launchpad, custom business apps, multi-step workflows] +description: Quick installation guide for Appsmith on Sealos. Learn how to deploy and access internal apps using this open-source platform. +--- + +# Quick installation of Appsmith + +[Appsmith](https://github.com/appsmithorg/appsmith) is an open-source platform to build, deploy, and maintain internal +apps. You can build anything from simple CRUD apps, admin panels, dashboards to custom business apps and complicated +multi-step workflows. + +## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment + +![](../images/appsmith-1.png) + +## Step 2: create a new application + +- In App Launchpad, click "New Application" to create a new application. + +![](../images/appsmith-2.png) + +## Step 3: application deployment + +- Basic configuration: + + - Application name (Custom): appsmith + + - Image name (default latest version): appsmith/appsmith-ce + + - CPU (recommended): 2 Core + + - Memory (recommended): 4 GB + +- deployment mode: + + - number of instances (custom): 1 + +![](../images/appsmith-3.png) + +- Network configuration: + + - Container exposure port: 80 + + - Public network access: enabled + +![](../images/appsmith-4.png) + +- Advanced configuration: + + - Customize local storage and persist Appsmith data (15 GB is recommended). + +![](../images/appsmith-5.png) + +## Step 4: deploy the application + +- Click "deploy Application" to start deploying the application. + +![](../images/appsmith-6.png) + +## Step 5: access the application + +- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the + application has been launched successfully. + +- When STATUS is Running, you can directly access the public network address. + +![](../images/appsmith-7.png) + +- The visit was successful! + +![](../images/appsmith-8.png) + + diff --git a/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-pageplug.md b/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-pageplug.md new file mode 100644 index 000000000000..281a7aa0ece4 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/low-code-platform/install-pageplug.md @@ -0,0 +1,81 @@ +--- +keywords: [PagePlug, Appsmith, low-code platform, Sealos, application deployment] +description: Quick installation guide for PagePlug, a Chinese low-code platform based on Appsmith, optimized for performance and integrated with Formily and Echarts. +--- + +# Quick installation of PagePlug + +[PagePlug](https://github.com/cloudtogo/pageplug) is a Chinese project +of [Appsmith](https://github.com/appsmithorg/appsmith), which optimizes the overall performance and Sinicizes based on +Appsmith, and also integrates the characteristic form solution Formily component, chart solution Echarts component, low +code Mini Program development, etc. +Is an open source, declarative, visual, intuitive front-end low-code framework for research and development. + +## Step 1: Step 1: open the App Launchpad application in the [Sealos](https://cloud.sealos.io) desktop environment + +![](../images/pageplug-1.png) + +## Step 2: create a new application + +- In App Launchpad, click "New Application" to create a new application. + +![](../images/pageplug-2.png) + +## Step 3: application deployment + +- Basic configuration: + + - Application name (Custom): pageplug + + - Image name (default latest version): cloudtogouser/pageplug-ce + + - CPU (recommended): 2 Core + + - Memory (recommended): 4 GB + +- Deployment model: + + - Number of instances (custom): 1 + +![](../images/pageplug-3.png) + +- Network configuration: + + - Container exposure port: 80 + + - Public network access: enabled + +![](../images/pageplug-4.png) + +- Advanced configuration: + + - Customize local storage and persist PagePlug data. + +![](../images/pageplug-5.png) + +## Step 4: deploy the application + +- Click "deploy Application" to start deploying the application. + +![](../images/pageplug-6.png) + +## Step 5: access the application + +- Click "details" to view, when the STATUS of the application has changed from Pending to Running, it indicates that the + application has been launched successfully. + +![](../images/pageplug-7.png) + +![](../images/pageplug-8.png) + +- When STATUS is Running, you can directly access the public network address. + +![](../images/pageplug-9.png) + +- If a 503 exception occurs in the access, wait for a while and try again. + +![](../images/pageplug-10.png) + +- The visit was successful! + +![](../images/pageplug-11.png) diff --git a/docs/archived/5.0/docs/quick-start/examples/others/install-anki.md b/docs/archived/5.0/docs/quick-start/examples/others/install-anki.md new file mode 100644 index 000000000000..2766ca2c62b7 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/others/install-anki.md @@ -0,0 +1,165 @@ +--- +keywords: [Anki sync server, custom Anki server, Anki installation, Anki self-hosted, Anki sync setup] +description: Learn how to quickly install and configure a custom Anki sync server using Sealos for seamless syncing across devices. +--- + +# Quick Installation of a Custom Anki Sync Server + +Anki is a spaced repetition flashcard program. Essentially, it is a card sorting tool that **actively tests users on +custom card content, allows users to self-grade their responses, and then uses an algorithm to reschedule cards based on +the judgments to optimize long-term retention**. + +The so-called "cards" are technically called flash cards. They are small cards with a question or prompt on one side and +the answer on the reverse side. You first read the question/prompt, try to recall the answer mentally, then flip the +card to verify against the answer provided. + +The core principle of flashcard creation is:**one knowledge point per card**. As such, Anki is well-suited for learning +languages, memorizing historical dates, formulas, etc. + +Anki's official sync server is hosted overseas and is a personal project with limited bandwidth. Syncing is very slow. +To sync learning progress and new cards across multiple clients would be extremely painful. + +To address this, we need to deploy a custom sync server and have the clients connect to it instead. + +## Deploying an Anki Sync Server + +Since the February 2023 release of Anki 2.1.57 for desktop, Anki on desktop, Android, and iOS now support custom sync +servers without needing to install plugins. Anki users no longer need to worry about sync issues. The longstanding sync +problem has finally been completely solved. + +Currently, the only project that supports the latest Anki versions +is [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs). Other sync server projects are now +mostly obsolete. This Rust project tracks progress on Anki's official sync server and also uses sqlite as the backend +data store. + +Below we will deploy anki-sync-server-rs on Sealos and configure it. + +First, enter the URL https://cloud.sealos.io/ in your browser to access the Sealos desktop. Then open "App Launchpad": + +![](../images/2023-06-26-11-54-EIVahX.jpg) + +Click "Create Application": + +![](../images/2023-06-26-11-55-NDkuEg.jpg) + +Enter the application name and image name. The exposed container port is `27701`. Enable external network access: + +![](../images/2023-06-26-11-59-FxJE12.png) + +Scroll down and expand "Advanced Configuration". Click "Edit Environment Variables": + +![](../images/2023-06-26-12-01-DKect7.png) + +Paste the following into the environment variable input box: + +``` +bash +Copy code + +ANKISYNCD_USERNAME= +ANKISYNCD_PASSWORD= +``` + +Replace `` with your username and `` with your password. + +![](../images/2023-06-26-12-05-CWczxm.png) + +Click "Add Storage Volume": + +![](../images/2023-06-26-12-06-lvv6ms.png) + +Set the mount path to `/app` and confirm: + +![](../images/2023-06-26-12-07-s8W7iu.png) + +Finally, click "Deploy Application" in the top right corner. + +After deployment, click "Details" to enter the application details screen. + +![](../images/2023-06-26-12-09-RslDGj.png) + +Here you can see the instance status. Wait until the status shows as "running" before proceeding. If it stays pending +for a while, click "Details" to check the failure reason: + +![](../images/2023-06-26-13-09-Vs9ccy.png) + +Once deployed successfully, you can monitor metrics like CPU and memory usage. Click the external URL to directly access +the sync server's web interface. + +![](../images/2023-06-26-13-09-YFHPYc.png) + +If you see the following screen, the deployment succeeded: + +![](../images/2023-06-26-13-09-FwsbfW.png) + +Viewing logs is also straightforward - click the "three dots" on the instance panel and select "Logs": + +![](../images/2023-06-26-13-09-hdHfxP.png) + +![](../images/2023-06-26-13-09-nwrxrv.png) + +## Client Configuration + +### Desktop + +To configure the desktop client (macOS/Windows/Linux): + +1. Open "Preferences" + + ![](../images/2023-06-26-12-24-QHYKZt.png) + +2. Click "Network" and look for the "self-hosted sync server" section. Enter your server's address: + + ![](../images/2023-06-26-12-26-HYOaBJ.png) + +3. Restart Anki and click "Sync": + + ![](../images/2023-06-26-12-28-ccnUOj.png) + +4. A prompt will appear asking for username and password. Enter what you configured earlier: + + ![](../images/2023-06-26-12-29-z5E9gi.png) + +5. Click OK and syncing will begin. + +### Android + +On Android, directly configure via "Settings -> Advanced -> Custom sync server": + +![](../images/2022-04-10-14-31-vrNHJU.png) + +Also enter username and password: + +> Settings -> Basic -> AnkiWeb Account + +This completes the configuration. All card decks should sync over: + +![](../images/2022-04-10-14-32-ADfk8T.png) +![](../images/2022-04-10-14-32-1iudM0.png) + +The official app version is quite old. For the latest community version, download the Beta here: + +- https://github.com/ankidroid/Anki-Android/releases + +The **arm64-v8a** version is recommended. + +After installing, the sync server can be configured under "Settings -> Sync -> Custom sync server": + +![](../images/2023-06-26-12-39-1jsF0t.jpeg) + +Also enter the username and password: + +> Settings -> Sync -> AnkiWeb Account + +### iOS + +AnkiMobile also supports syncing with a custom server. At least version Ankimobile 2.0.90(20090.2) seems to work based +on [reports in the Anki forums](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862). + +If you encounter sync issues after configuring, try toggling "Allow Anki to access local network" off and on in iOS +settings per Anki's docs: + +> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS +> settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and on again. + +The tip above is excerpted from the [Anki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup). \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Go Apps.md new file mode 100644 index 000000000000..960e40895e00 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Go Apps.md @@ -0,0 +1,154 @@ +--- +keywords: [Go application deployment, Docker Go app, Sealos Go app, Go Docker image, Go app tutorial] +description: Learn how to quickly install and deploy Go applications using Docker and Sealos with this step-by-step guide. Perfect for beginners. +--- + +# Quick installation of Go Apps + +First, make sure you have installed the following tools: + +- Docker + +## Step 1: Write Your Go Program + +```go +package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") + }) + + http.ListenAndServe(":8080", nil) +} +``` + +## Step 2: Create Docker Image + +- In the same directory as `main.go`, create a file named `Dockerfile` with the following content: + +``` +FROM golang:1.17 as builder + +WORKDIR /app +COPY . . + +RUN go mod init myapp +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /app/main /app/ + +EXPOSE 8080 +ENTRYPOINT ["/app/main"] +``` + +This `Dockerfile` defines a multi-stage build process. In the first stage, we use the official `golang` image as the +base image and then compile the Go application. In the second stage, we use the lightweight `alpine` image, copy the +compiled binary file to the `/app` directory, and expose port 8080. + +- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Go + application: + +``` +docker build -t your_image_name . +``` + +Replace `your_image_name` with your image name and tag. + +## Step 3: Push Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are + already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to + push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set for the image (e.g., latest). + + For example: + + ``` + docker tag demo damager6666/demo:latest + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/demo:latest + ``` + +## Step 4: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 5: Open the "App Launchpad" App + +![](images/java-example-3.png) + +## Step 6: Create a New Application + +- In "App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 7: Application Deployment + +- Basic Configuration: + - Application Name (custom): go-demo + - Image Name: damager6666/demo:latest + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment Mode: + - Number of Instances (custom): 1 + +![](images/java-example-5.png) + +- Network Configuration: + - Container Exposed Port: 8080 + - Internet Access: Enable + +![](images/java-example-6.png) + +## Step 8: Deploy Application + +- Click "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 9: Access Application + +- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the + application has started successfully. +- When STATUS is Running, you can directly access the external network address. + +![](images/java-example-8.png) + +- In the browser, enter + +``` +https://tmgkflgdlstl.cloud.sealos.io/hello +``` + +![](images/java-example-9.png) + +- The page displays "Hello, World!", indicating that your Go application is running on Sealos. \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Java Apps.md new file mode 100644 index 000000000000..8aaa1b0b2436 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Java Apps.md @@ -0,0 +1,275 @@ +--- +keywords: [Java Spring Boot, Docker Java app, Java MySQL integration, Spring Boot Docker, deploy Java app] +description: Learn how to quickly install and deploy Java apps using Spring Boot, Docker, and MySQL. Follow our step-by-step guide for seamless integration and deployment. +--- + +# Quick installation of Java Apps + +First, make sure you have installed the following tools: + +- Docker +- Maven + +## Step 1: Write Your Java Program + +- Here, directly generate a basic Spring Boot project through [Spring Initializr](https://start.spring.io/). + +![](images/java-example-1.png) + +- Open the project's `pom.xml` file and add the following content to the `` section to include the MySQL + JDBC driver: + + ``` + + mysql + mysql-connector-java + + ``` + +- Next, add the following content to the `application.properties` file in the `src/main/resources` directory to + configure the database connection: + + ``` + spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 + spring.datasource.username=root + spring.datasource.password=your_password + spring.jpa.hibernate.ddl-auto=update + ``` + +## Step 2: Create Entity and Repository Classes + +Create a new Java class in the project, such as `Person.java`, to represent the entity in the database. Add the +following content: + +```java +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + // Getters and setters + // ... +} +``` + +- Then create a repository interface, such as `PersonRepository.java`: + + ```java + import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.stereotype.Repository; + + @Repository + public interface PersonRepository extends JpaRepository { + } + ``` + +## Step 3: Create a Web Controller + +- In the `src/main/java` directory of the project, create a simple web controller. For example, create a file named + `PersonController.java` and add the following content: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class PersonController { + @Autowired + private PersonRepository personRepository; + + @GetMapping("/getPersons") + public String getAllPersons() { + List persons = personRepository.findAll(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < persons.size(); i++) { + sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n"); + } + System.out.println(sb.toString()); + return sb.toString(); + } + + @PostMapping(value = "/addPerson") + public Person addPerson(@RequestBody Person person) { + return personRepository.save(person); + } + + +} +``` + +## Step 4: Build the Application + +- Build the project using Maven by running the following command: + + ``` + mvn clean install + ``` + +![](images/java-example-2.png) + +## Step 5: Create a Docker Image + +- In the root directory of the Java project, create a file named `Dockerfile`. This file will contain the instructions + required to build the Docker image. Here's a simple example Dockerfile: + + ```` + # Use the official OpenJDK image as the base image + FROM openjdk:8-jre-slim + + # Set the working directory + WORKDIR /app + + # Copy the built JAR file into the image + COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar + + # Expose the application's port + EXPOSE 8080 + + # Set the start command + CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"] + ``` + ```` + +- In the project root directory, run the following command to build the Docker image: + +``` +docker build -t java-demo . +``` + +## Step 6: Push the Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are + already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to + push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, + `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set + for the image (e.g., `latest`). + + - Then, push the tagged image to the Docker repository: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + +## Step 7: Log in to Sealos + +- Visit the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 8: Open the "Database" application + +![](images/java-example-10.png) + +## Step 9: Create a database + +- Click on "Create New Database" + +![](images/java-example-11.png) + +- Basic configuration: + - Cluster type: mysql + - Cluster name: demo-db + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + - Number of instances: 1 + - Storage capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 10: Configure the database + +![](images/java-example-12.png) + +- Click on "One-click Connect to Database" and execute the following statements: + + - Create the `test_db` database + + ```sql + create database test_db; + ``` + + - Create the `test` table + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 11: Open the "App Launchpad" application + +![](images/java-example-3.png) + +## Step 12: Create a new application + +- In "App Launchpad", click on "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 13: Deploy the application + +- Basic configuration: + - Application name (custom): java-demo + - Image name: damager6666/java-demo:v2 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment mode: + - Number of instances (custom): 1 + +![](images/java-example-5.png) + +- Network configuration: + - Exposed container port: 8080 + - Public network access: enabled + +![](images/java-example-6.png) + +## Step 14: Deploy the application + +- Click on "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 15: Access the application + +- Click on "Application Management" to view the application. When the application's STATUS changes from Pending to + Running, it indicates that the application has started successfully. +- When the STATUS is Running, you can directly access the public network address. + +![](images/java-example-8.png) + +- Enter the following in your browser: + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/java-example-13.png) + +- The page displays the data inserted into the database earlier, indicating that your Java application is now running on + Sealos. + diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md new file mode 100644 index 000000000000..be1525eec313 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md @@ -0,0 +1,230 @@ +--- +keywords: [Node.js installation, Docker image, MySQL database, Sealos deployment, Node.js app] +description: Learn how to quickly install and deploy Node.js apps with Docker and MySQL on Sealos. Follow our step-by-step guide for seamless setup. +--- + +# Quick installation of Node.js Apps + +First, make sure you have installed the following tools: + +- Docker + +## Step 1: Write your Node.js program + +- Create a file named `app.js` in your directory and add the following code: + + ```js + const express = require('express'); + const mysql = require('mysql'); + const app = express(); + + const db = mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + + db.connect((err) => { + if (err) throw err; + console.log('Connected to the database.'); + }); + + app.get('/', (req, res) => { + db.query('SELECT id, name FROM users', (err, results) => { + if (err) throw err; + res.send(JSON.stringify(results)); + }); + }); + + app.listen(8080, () => { + console.log('Server is running on port 8080'); + }); + ``` + +- Create a file named `package.json` in the root directory of your project, and include the following content: + + ~~~json + { + "name": "nodejs-web-app-mysql", + "version": "1.0.0", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "^4.17.1", + "mysql": "^2.18.1" + } + } + ``` + ~~~ + +## Step 2: Create a Docker image + +- In the root directory of your project, create a file named `Dockerfile`. This file will contain the instructions + needed to build the Docker image. Here is a simple example Dockerfile: + +```dockerfile +FROM node:16 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "start"] +``` + +## Step 3: Build the Docker image + +- Run the following command in the directory containing the `Dockerfile` to build the Docker image: + + ``` + docker build -t nodejs-demo . + ``` + +## Step 4: Push the Docker image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you have + logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to + push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with the name of your local image, `your-dockerhub-username` with your Docker Hub + username, `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with + the tag you set for the image (e.g. latest). + + For example: + + ``` + docker tag nodejs-demo damager6666/nodejs-demo:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/nodejs-demo:v1 + ``` + +## Step 5: Login to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) website. + +![](images/java-example-0.png) + +## Step 6: Open the "Database" application + +![](images/java-example-10.png) + +## Step 7: Create a database + +- Click on "Create a database" + +![](images/java-example-11.png) + +- Basic configuration: + - Cluster type: mysql + - Cluster name: demo-db + - CPU (recommended): 1 Core + - Memory (recommended): 1 G + - Number of instances: 1 + - Storage capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 8: Configure the database + +![](images/java-example-12.png) + +- Click "Connect to the database" to execute the following statements: + + - Create a database named test_db + + ```sql + create database test_db; + ``` + + - Create a table named test + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert some data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 9: Open the "App Launchpad" application + +![](images/java-example-3.png) + +## Step 10: Create a new application + +- In App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 11: Deploy the application + +- Basic configuration: + - Application name (customizable): nodejs-demo + - Image name : damager6666/nodejs-demo:v1 + - CPU (recommended): 1 core + - Memory (recommended): 1 G +- Deployment mode: + - Number of instances (customizable): 1 + +![](images/nodejs-example-0.png) + +- Network configuration: + - Exposed container port: 8080 + - External network access: enabled + +![](images/java-example-6.png) + +## Step 12: Deploy the application + +- Click "Deploy Application" to start deploying the application. + +![](images/java-example-7.png) + +## Step 13: Access the application + +- Click "App Launchpad" to view the application status. When the STATUS changes from Pending to Running, it means that + the application has been successfully launched. +- When the STATUS is Running, you can directly access the external network address. + +![](images/java-example-8.png) + +- Enter the following URL in your browser: + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/nodejs-example-1.png) + +- The page will display the data inserted into the database, indicating that your Node.js application is running on + Sealos. \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Python Apps.md new file mode 100644 index 000000000000..48a1812748f4 --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Python Apps.md @@ -0,0 +1,248 @@ +--- +keywords: [Python app deployment, Docker image, Sealos, MySQL database, Flask application, Docker Hub, Python Dockerfile, app management, database configuration, external network access] +description: Learn how to quickly install and deploy Python apps using Docker, Sealos, and MySQL. Follow our step-by-step guide to create, configure, and access your Flask application. +--- + +# Quick installation of Python Apps + +First, make sure you have the following tools installed: + +- Docker + +## Step 1: Write your Python program + +- Create a file named `app.py` in your project directory and add the following code: + + ```python + from flask import Flask, render_template + import mysql.connector + + app = Flask(__name__) + + def get_data_from_database(): + cnx = mysql.connector.connect( + host="java-demo-db-mysql.ns-7otl3mb2.svc", + user="root", + password="l9h8f24b", + database="test_db" + ) + + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + data = cursor.fetchall() + cursor.close() + cnx.close() + + return data + + @app.route('/') + def index(): + data = get_data_from_database() + return render_template('index.html', data=data) + + if __name__ == '__main__': + app.run(host='0.0.0.0', debug=True) + + ``` + +- Create a file named `requirements.txt` in the directory to list the dependencies required by the application. + +``` +Flask +mysql-connector-python +``` + +- Create a folder named `templates` in your project directory and create a file named `index.html` inside it. Add the + following content to `index.html`: + + ```html + + + + + Data from MySQL + + +

Data from MySQL

+ + + + + + + {% for row in data %} + + + + + + {% endfor %} +
Column1Column2Column3
{{ row[0] }}{{ row[1] }}{{ row[2] }}
+ + + + Replace `Column1`, `Column2`, and `Column3` with the actual column names you want to display. + ``` + +## Step 2: Create a Docker image + +- Create a file named `Dockerfile` in the root directory of the project. This file will contain the instructions needed + to build the Docker image. Here is a simple example Dockerfile: + + ```dockerfile + FROM python:3.8 + + WORKDIR /app + + COPY requirements.txt . + + RUN pip install --no-cache-dir -r requirements.txt + + COPY . . + + EXPOSE 5000 + + CMD ["python", "app.py"] + ``` + +## Step 3: Build the Docker image + +- Run the following command in the directory containing the `Dockerfile` to build the Docker image: + + ``` + docker build -t python-demo . + ``` + +## Step 4: Push the Docker image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming that you + have logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing the image, you need to add a tag to it so that Docker knows where to + push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, + `your-repo-name` with the name of the repository you want to create on Docker Hub, and `your-tag` with the tag you + set for the image (e.g. latest). + + For example: + + ``` + docker tag python-demo damager6666/python-demo:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/python-demo:v1 + ``` + +## Step 5: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) website. + +![](images/java-example-0.png) + +## Step 6: Open the "Database" application + +![](images/java-example-10.png) + +## Step 7: Create a database + +- Click "Create Database" + +![](images/java-example-11.png) + +- Basic Configuration: + - Cluster Type: mysql + - Cluster Name: demo-db + - CPU (Recommended): 1 Core + - Memory (Recommended): 1 G + - Number of Instances: 1 + - Storage Capacity: 1 Gi +- Click "Deploy Cluster" + +## Step 8: Configure the database + +![](images/java-example-12.png) + +- Click "Connect to the Database" and execute the following SQL statements: + + - Create the test_db database + + ```sql + create database test_db; + ``` + + - Create the test table + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - Insert data + + ```sql + insert into test values (1,'Sealos'); + ``` + +## Step 9: Open the "App Management" application + +![](images/java-example-3.png) + +## Step 10: Create a new app + +- In "App Management", click "New App" to create a new app. + +![](images/java-example-4.png) + +## Step 11: App deployment + +- Basic Configuration: + - Application Name (Customized): python-demo + - Image Name: damager6666/python-demo:v1 + - CPU (Recommended): 1 Core + - Memory (Recommended): 1 G +- Deployment Mode: + - Number of Instances (Customized): 1 + +![](images/python-example-0.png) + +- Network Configuration: + - Container Exposed Port: 5000 + - External Access: Enabled + +## Step 12: Deploy the app + +![](images/python-example-2.png) + +## Step 13: Access the app + +- Click "App Management" to check when the STATUS of the app changes from "Pending" to "Running", indicating that the + app has been successfully launched. +- When the STATUS is Running, you can directly access the external network address. + +![](images/python-example-3.png) + +- Enter the external network address in the browser to access the data that was just stored in MySQL, indicating that + the program has been successfully deployed on Sealos. + + ```http + https://mkqreaqfgwac.cloud.sealos.io/ + ``` + +![](images/python-example-4.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Vue Apps.md new file mode 100644 index 000000000000..8da28e5369aa --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/programming-languages/Quick installation of Vue Apps.md @@ -0,0 +1,148 @@ +--- +keywords: [Vue.js, Docker, Sealos, Vue CLI, Docker image, application deployment, cloud hosting, web development] +description: Learn how to quickly install and deploy Vue.js applications using Docker and Sealos. Follow our step-by-step guide for seamless web development and cloud hosting. +--- + +# Quick installation of Vue Apps + +First, make sure you have installed the following tools: + +- Docker +- Node.js +- npm + +## Step 1: Install Vue CLI + +```bash +npm install -g @vue/cli +``` + +## Step 2: Create a new project with Vue CLI + +```bash +vue create my-vue-app +``` + +## Step 3: Build the production version of the project with Vue CLI + +```bash +cd my-vue-app +npm run build +``` + +## Step 4: Create a Docker image + +- In the `my-vue-app` directory, create a file named `Dockerfile` with the following content: + +```dockerfile +# 使用 nginx 镜像作为基础镜像 +FROM nginx:1.21-alpine + +# 复制构建的前端文件到 nginx 容器中 +COPY dist/ /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] +``` + +- Run the following command in the directory where the `Dockerfile` is located to build a Docker image for the Vue + application: + +```bash +docker build -t my-vue-app . +``` + +## Step 5: Push Docker Image + +- Push the created Docker image to a Docker repository, such as Docker Hub or a private repository. **Assuming you are + already logged in to the Docker repository**, use the following command to push the image: + + - First, tag the Docker image. Before pushing an image, you need to add a tag to it so that Docker knows where to + push it. Run the following command to add a tag to the image: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-image-name` with your local image name, `your-dockerhub-username` with your Docker Hub username, + `your-repo-name` with the repository name you want to create on Docker Hub, and `your-tag` with the tag you set + for the image (e.g., latest). + + For example: + + ``` + docker tag my-vue-app damager6666/my-vue-app:v1 + ``` + + - Next, push the Docker image. Use the following command to push the image to Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + Replace `your-dockerhub-username`, `your-repo-name`, and `your-tag` with the actual values. For example: + + ``` + docker push damager6666/my-vue-app:v1 + ``` + +## Step 6: Log in to Sealos + +- Go to the [Sealos](https://cloud.sealos.io/) official website + +![](images/java-example-0.png) + +## Step 7: Open the "App Launchpad" App + +![](images/java-example-3.png) + +## Step 8: Create a New Application + +- In "App Launchpad", click "Create New Application" to create a new application. + +![](images/java-example-4.png) + +## Step 9: Application Deployment + +- Basic Configuration: + - Application Name (custom): my-vue-app + - Image Name: damager6666/my-vue-app:v1 + - CPU (recommended): 1 Core + - Memory (recommended): 1 G +- Deployment Mode: + - Number of Instances (custom): 1 + +![](images/vue-example-0.png) + +- Network Configuration: + - Container Exposed Port: 80 + - Internet Access: Enable + +![](images/vue-example-1.png) + +## Step 10: Deploy Application + +- Click "Deploy Application" to start deploying the application. + +![](images/vue-example-3.png) + +## Step 11: Access Application + +- Click "Application Management" to view. When the application's STATUS changes from Pending to Running, it means the + application has started successfully. +- When STATUS is Running, you can directly access the external network address. + +![](images/vue-example-2.png) + +- In the browser, enter + +``` +https://mvpztqzczudy.cloud.sealos.io +``` + +![](images/vue-example-4.png)) + +- The page displays "Hello, Vue.js!", indicating that your Vue application is running on Sealos. \ No newline at end of file diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-0.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-0.png new file mode 100644 index 000000000000..6ef487195a8c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-1.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-1.png new file mode 100644 index 000000000000..b149426e0e64 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-10.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-10.png new file mode 100644 index 000000000000..3b10465747db Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-10.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-11.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-11.png new file mode 100644 index 000000000000..b1cf562ca8c0 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-11.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-12.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-12.png new file mode 100644 index 000000000000..6eedee90f87c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-12.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-13.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-13.png new file mode 100644 index 000000000000..3548b505e15f Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-13.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-2.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-2.png new file mode 100644 index 000000000000..179f4b85a5fb Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-3.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-3.png new file mode 100644 index 000000000000..3aa6b5e891aa Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-4.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-4.png new file mode 100644 index 000000000000..bcb67e312203 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-5.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-5.png new file mode 100644 index 000000000000..21df65190ff2 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-6.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-6.png new file mode 100644 index 000000000000..ae30ba940afc Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-7.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-7.png new file mode 100644 index 000000000000..f77e89f54c54 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-7.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-8.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-8.png new file mode 100644 index 000000000000..19a4f0f3276e Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-8.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-9.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-9.png new file mode 100644 index 000000000000..b5582f255cd6 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/java-example-9.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-0.png new file mode 100644 index 000000000000..6e95ed74b246 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-1.png new file mode 100644 index 000000000000..ad9450487f21 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/nodejs-example-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-0.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-0.png new file mode 100644 index 000000000000..86c16c2d71df Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-1.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-1.png new file mode 100644 index 000000000000..b783a48a786c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-2.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-2.png new file mode 100644 index 000000000000..86144296b4aa Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-3.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-3.png new file mode 100644 index 000000000000..54ceb5547f34 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-4.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-4.png new file mode 100644 index 000000000000..3ffbe07560ba Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/python-example-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-0.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-0.png new file mode 100644 index 000000000000..648276d14120 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-0.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-1.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-1.png new file mode 100644 index 000000000000..e47a1b27115d Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-2.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-2.png new file mode 100644 index 000000000000..dab8d56621b8 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-3.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-3.png new file mode 100644 index 000000000000..9ac654f24398 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-4.png b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-4.png new file mode 100644 index 000000000000..b00209521090 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/examples/programming-languages/images/vue-example-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/examples/social-communication/install-tailchat.md b/docs/archived/5.0/docs/quick-start/examples/social-communication/install-tailchat.md new file mode 100644 index 000000000000..5f4f2f6f98eb --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/examples/social-communication/install-tailchat.md @@ -0,0 +1,172 @@ +--- +keywords: [Tailchat installation, open source IM, Sealos, MongoDB, Redis, Minio, enterprise application, microservice architecture, plugin architecture] +description: Quick installation guide for Tailchat, an open source IM application with microservice and plugin architecture, using Sealos with MongoDB, Redis, and Minio. +--- + +# Quick installation of Tailchat + +[Tailchat](https://tailchat.msgbyte.com/) is an open source IM application that is pluggable and easy to expand. Plugin +architecture gives Tailchat unlimited possibilities. + +Front-end micro-kernel architecture + back-end micro-service architecture makes Tailchat to control any +customized/privatized scenarios + +Created for enterprises and private domain users, highly free group management and customized panel display allow +private domain owners to better display their works, manage users, and build their own brand and circle. + +![](../images/tailchat/0.png) + +## First, enter Sealos and open "Application Management" + +![](../images/tailchat/1.png) + +## Create a new application + +![](../images/tailchat/2.png) + +### Create dependencies + +As an enterprise-level application, `tailchat` has the minimum dependencies of `mongodb`, `redis`, and `minio`. Let's +create them one by one. + +#### MongoDB + +For convenience, we will fix one instance and bind it to local storage. The image used is `mongo:4`. Note that because +we did not set a password for the database, do not provide network services to the public network. The container exposes +port 27017, which is the default database service port. The content is as follows: + +![](../images/tailchat/3.png) + +Click "Deploy Application" to submit the deployment. Wait patiently for a while, and you can see that the application +has started up. + +![](../images/tailchat/4.png) + +> Note: that the initial allocation of 64m is too small for MongoDB, so I changed it to 128m by modifying the +> application. Resource allocation can be changed at any time, which is also a convenient feature of Sealos/Kubernetes. + +#### Minio + +Next, we will create Minio, an open-source object storage service. We can also quickly create it through Sealos's UI. +The image used is `minio/minio`. Note that we need to make some adjustments: + +- Expose port: 9000 +- Change the run command to: `minio` +- Change the command parameters to: `server /data` +- Set environment variables: + - MINIO_ROOT_USER: tailchat + - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat +- Local storage: `/data` + +The final result is as follows: + +![](../images/tailchat/5.png) + +Click the "Deploy" button and you can see that the service has started up normally. + +#### Redis + +Finally, we need to deploy Redis as a content cache and message forwarding. The image used is `redis:alpine`, and the +exposed port is `6379`. The final result is as follows: + +![](../images/tailchat/6.png) + +### Create Tailchat itself + +At this point, all the dependencies required by Tailchat have been deployed, as shown below: + +![](../images/tailchat/7.png) + +Now we can deploy the Tailchat itself. The Tailchat itself will be relatively complex, but because Sealos is purely +UI-based, it will not be too complicated. + +- Use image: `moonrailgun/tailchat` +- Expose port: `11000` (remember to open external access) +- Configure environment variables as follows: + ``` + SERVICEDIR=services,plugins + TRANSPORTER=redis://redis:6379 + REDIS_URL=redis://redis:6379 + MONGO_URL=mongodb://mongo/tailchat + MINIO_URL=minio:9000 + MINIO_USER=tailchat + MINIO_PASS=com.msgbyte.tailchat + ``` + +The final effect is as follows: + +![](../images/tailchat/8.png) + +After waiting patiently for a while, you can see that the Tailchat service has started up. + +![](../images/tailchat/9.png) + +## Preview service + +First, we can check the availability of the Tailchat service by adding `/health` to the external address provided by the +service, such as `https://.cloud.sealos.io/health`. When it starts up, the Tailchat service will return +content like this: + +![](../images/tailchat/10.png) + +This JSON string contains the image version used, node name, system usage, and microservice loading status. Here we can +see that my common services, such as `user`/`chat.message`, and some services with plugin prefixes such as +`plugin.registry`, have all started up normally, indicating that our server is running normally. Now we can directly +access our external address and see that after a short loading time, the page opens normally and automatically jumps to +the login page. + +![](../images/tailchat/11.png) + +Register an account casually, and you can see that we can enter the main interface of Tailchat normally, as shown in the +following figure: + +![](../images/tailchat/12.png) + +At this point, our service has successfully landed in Sealos. + +## Scaling service + +Of course, as a distributed architecture system, Tailchat naturally supports horizontal scaling. In Sealos, scaling is +also very simple. Just modify the number of instances through the change operation: + +![](../images/tailchat/13.png) + +![](../images/tailchat/14.png) + +![](../images/tailchat/15.png) + +At this point, when we access `https://.cloud.sealos.io/health`, we can see that we can access different +nodes. + +![](../images/tailchat/16.png) + +## Add Tailchat entry to desktop + +Open Terminal, enter `vim app.yml` to create and edit a configuration file + +Enter the following content, note that the url should be replaced with the url deployed by yourself + +```yml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: tailchat-app-entry +spec: + name: Tailchat + icon: + type: iframe + data: + url: + desc: + icon: https://tailchat.msgbyte.com/img/logo.svg + menuData: + displayType: normal +``` + +Press `esc` to exit edit mode, press `:wq` to save and exit vim + +Type `kubectl apply -f app.yml` to start the configuration. + +After refreshing the page, we can see that our entry appears on the desktop of `sealos` + +![](../images/tailchat/17.png) diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-1.png b/docs/archived/5.0/docs/quick-start/images/quick-start-1.png new file mode 100644 index 000000000000..b1ddba883b74 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-1.png differ diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-2.png b/docs/archived/5.0/docs/quick-start/images/quick-start-2.png new file mode 100644 index 000000000000..4ed913f595c3 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-2.png differ diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-3.png b/docs/archived/5.0/docs/quick-start/images/quick-start-3.png new file mode 100644 index 000000000000..e9bf1494a858 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-3.png differ diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-4.png b/docs/archived/5.0/docs/quick-start/images/quick-start-4.png new file mode 100644 index 000000000000..056b2626b35c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-4.png differ diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-5.png b/docs/archived/5.0/docs/quick-start/images/quick-start-5.png new file mode 100644 index 000000000000..7f15f20f58c0 Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-5.png differ diff --git a/docs/archived/5.0/docs/quick-start/images/quick-start-6.png b/docs/archived/5.0/docs/quick-start/images/quick-start-6.png new file mode 100644 index 000000000000..bb2731c08a9c Binary files /dev/null and b/docs/archived/5.0/docs/quick-start/images/quick-start-6.png differ diff --git a/docs/archived/5.0/docs/quick-start/quick-start.md b/docs/archived/5.0/docs/quick-start/quick-start.md new file mode 100644 index 000000000000..c3fb91ce8cec --- /dev/null +++ b/docs/archived/5.0/docs/quick-start/quick-start.md @@ -0,0 +1,36 @@ +--- +keywords: [Flarum deployment, Sealos App Store, open source forum, deploy Flarum, Sealos desktop] +description: Quickly deploy Flarum using Sealos. Follow our step-by-step guide to get your open source forum up and running in no time. +--- + +# Quick Start + +Deploy Flarum quickly with Sealos. + +> Flarum is a very simple open source forum software. It's responsive, easy to use, and has all the features you need to +> build a successful community. It's also extremely extensible, allowing for ultimate customizability. + +Open the Sealos desktop and click App Store. + +![](./images/quick-start-1.png) + +Click on the Flarum app (the search bar in the upper left corner can quickly find the app). + +![](./images/quick-start-2.png) + +Click Deploy App. + +![](./images/quick-start-3.png) + +Wait for the status of all components to change to running, which means that the components have been started (you can +re-enter the current page refresh state). + +![](./images/quick-start-4.png) + +Return to the Sealos desktop and click on the Flarum app. + +![](./images/quick-start-5.png) + +Successfully accessed Flarum. + +![](./images/quick-start-6.png) diff --git a/docs/archived/5.0/docs/user-guide/ai-proxy/ai-proxy.md b/docs/archived/5.0/docs/user-guide/ai-proxy/ai-proxy.md new file mode 100644 index 000000000000..792d7648eded --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/ai-proxy/ai-proxy.md @@ -0,0 +1,144 @@ +--- +title: AI Proxy - 一站式 AI 模型调用解决方案 | Sealos +description: AI Proxy 是 Sealos 平台提供的统一 AI 模型调用服务,支持多平台 API Key 管理、统一计费和监控,让开发者轻松接入各类 AI 模型。 +keywords: ["AI Proxy", "Sealos", "AI模型调用", "API管理", "统一计费", "开发者工具"] +--- + +# AI Proxy 使用指南 + +## 简介 + +AI Proxy 是 [Sealos 平台](/docs/5.0/introduction/what-is-sealos.md)提供的一站式 AI 模型调用解决方案,让开发者能够在统一的平台中轻松调用和管理各类 AI 模型。无论是通义千问、文心一言还是其他 AI 模型,都可以通过统一的接口进行调用。 + +### 为什么选择 AI Proxy? + +- 🔑 **一键获取多平台密钥** - 无需分别注册各个 AI 平台 +- 💰 **统一计费更省心** - 告别多平台充值的烦恼 +- 📊 **集中管理更高效** - 一站式监控所有调用情况 + +## 功能特点 + +### 统一的 API 访问 + +- 提供标准化的 API 接口,支持多家主流 AI 模型厂商 +- 兼容 OpenAI API 格式,便于快速迁移和集成 +- 持续扩充支持的模型类型 + +### 简化的密钥管理 + +![密钥管理界面](images/ai-proxy-key-management.png) + +- 一键获取多平台 API Key +- 无需分别注册各个 AI 平台 +- 统一的密钥管理界面 + +### 集中化计费与监控 + +![计费系统界面](images/ai-proxy-billing.png) + +- 使用 Sealos 平台统一结算 +- 透明的按量计费模式 +- 详细的费用明细和账单查询 +- 详细的调用日志记录 + +## 快速开始 + +### 1. 获取 API Key + +1. 访问 [Sealos Cloud](https://hzh.sealos.run) +2. 打开 AI Proxy 应用 +3. 点击【新建】按钮创建新的 API Key +4. 复制生成的 API Endpoint 和 API Key + +### 2. API 调用示例 + +#### 使用 JavaScript 调用 + +```javascript +async function main() { + const apiKey = 'your-api-key' + const apiUrl = 'https://aiproxy.hzh.sealos.run/v1/chat/completions' + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: 'Doubao-lite-4k', + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: '你好,请介绍一下你自己。' } + ], + max_tokens: 2048, + temperature: 0.7, + }), + }) + + const data = await response.json() + console.log(data.choices[0].message.content) +} +``` + +### 3. 请求参数说明 + +| 参数 | 类型 | 说明 | 示例值 | +|------|------|------|--------| +| model | string | 要使用的模型名称 | 'Doubao-lite-4k' | +| messages | array | 对话消息列表 | [{"role": "user"}] | +| max_tokens | number | 最大生成的 token 数量 | 2048 | +| temperature | number | 生成文本的随机性,范围 0-1 | 0.7 | + +## 费用管理 + +### 查看费用明细 + +1. 在 AI Proxy 界面中点击【费用明细】 +2. 可查看各模型的调用次数和费用统计 +3. 支持按时间范围筛选费用记录 + +### 余额充值 +1. 进入 Sealos 费用中心 +2. 选择充值金额 +3. 完成支付后即可使用充值金额调用模型 + +## 调用日志 + +### 日志查看 + +- 支持查看详细的 API 调用记录 +- 包含调用时间、模型名称、输入输出 token 数量等信息 + +### 日志筛选 + +- 按时间范围筛选 +- 按模型类型筛选 +- 按 Token 筛选 + +## 最佳实践 + +### 开发建议 + +1. 合理设置 `max_tokens` 参数,避免生成过长的无效内容 +2. 根据场景调整 `temperature` 参数,对话场景建议使用较高的值 +3. 在生产环境中做好异常处理和重试机制 + +### 成本优化 + +1. 选择适合业务场景的模型 +2. 合理设置上下文长度,避免无效的 token 消耗 + +## 常见问题 + +### API 调用失败 + +- 检查 API Key 是否正确 +- 确认账户余额是否充足 +- 查看具体的错误信息进行排查 + +### 费用相关 + +- 费用按实际调用量计费 +- 不同模型的计费标准不同 +- 支持查看详细的计费规则 diff --git a/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-billing.png b/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-billing.png new file mode 100644 index 000000000000..9d163b444c85 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-billing.png differ diff --git a/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-key-management.png b/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-key-management.png new file mode 100644 index 000000000000..43c20d0366fd Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/ai-proxy/images/ai-proxy-key-management.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/add-domain.md b/docs/archived/5.0/docs/user-guide/app-launchpad/add-domain.md new file mode 100644 index 000000000000..6a3c61bfc27e --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/add-domain.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 2 +keywords: [custom domain, Sealos, app deployment, domain provider, public access] +description: Learn how to assign a custom domain to your project using Sealos for a tailored brand experience. Follow our step-by-step guide for seamless app deployment. +--- + +# Add a domain + +Assigning a custom domain to your project guarantees that visitors to your application will have a tailored experience +that aligns with your brand. + +## When Deploying + +Just enable "Public Access" when you're deploying, and [Sealos](https://cloud.sealos.io) will sorts you out with a +domain. + +![](images/app-launchpad-domain.png) + +Now, on your domain provider's end, link the 'CNAME' to the one Sealos provided. + +Once it's active, jump back to Sealos, click on "Custom Domain" to the side: + +![](images/app-launchpad-domain2.png) + +Enter your custom domain in the pop-up box and click confirm. + +![](images/app-launchpad-domain3.png) + +To wrap up, click the "Deploy" button. Once your app's live, click on the external address to access the app via the +custom domain. + +## Post Deployment + +For the apps you've deployed, just click "Update" top-right on the app details page. Then, follow the earlier steps to +integrate your custom domain. \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/app-launchpad.md b/docs/archived/5.0/docs/user-guide/app-launchpad/app-launchpad.md new file mode 100644 index 000000000000..e821ec28cd64 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/app-launchpad.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 0 +keywords: [App Launchpad, Sealos, application deployment, private images, horizontal pod autoscaling] +description: App Launchpad in Sealos simplifies application deployment with features like private images, HPA, custom domains, and real-time monitoring. +--- + +# App Launchpad + +**App Launchpad** is a feature within Sealos that serves as a single-image deployment tool. Its main goal is to +streamline and expedite the process of deploying applications, allowing you to launch your application in as little as 5 +minutes. + +The tool currently boasts a range of functionalities: + +- Capability to deploy applications using private images. +- Flexibility to tailor CPU and memory resources according to the specific needs of the application. +- Support for deploying multiple replicas. +- Horizontal Pod Autoscaling (HPA) for dynamic scaling. +- Provision of external URLs for easy access from the public network. +- Option to assign custom domain to applications, enhancing both brand visibility and the user experience. +- Utilization of ConfigMap for configuration file management. +- Persistent storage solutions for application data, ensuring both its security and continuity. +- Real-time monitoring features for applications and Pods to facilitate prompt issue detection and resolution. +- Comprehensive logging of application activities, aiding in troubleshooting and performance optimization. +- Analysis of system events (Events) to extract critical insights for enhancing application performance. +- A convenient one-click feature to access the container terminal, simplifying management and debugging tasks. +- Ability to expose several ports of an application to the external network. + +## [Quick Start](./use-app-launchpad.md) + +For quick and easy installation of commonly utilized applications. + +## [Update Application](./update-app.md) + +Guidance on modifying application configurations after initial deployment. + +## [Add a domain](./add-domain.md) + +Instructions for integrating a custom domain with your application. + +## [Exposing Multiple Ports](./expose-multi-ports.md) + +Details on how to make multiple ports of an application accessible externally. + +## [Environment](./environment.md) + +Directions for configuring applications through the use of environment variables. + +## [ConfigMap](./configmap.md) + +Guidelines for setting up application configurations via configuration files. + +## [Autoscaling](./autoscale.md) + +Strategy for autoscaling the number of application instances in response to varying workloads. + +## [Persistent Volume](./persistent-volume.md) + +Utilizing persistent storage for the long-term preservation of data. diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/autoscale.md b/docs/archived/5.0/docs/user-guide/app-launchpad/autoscale.md new file mode 100644 index 000000000000..d0a25500c18d --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/autoscale.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 6 +keywords: [Sealos, Autoscaling, Horizontal Pod Autoscaler, HPA, application scaling, CPU usage, memory usage, cloud scaling, Sealos controller, performance monitoring] +description: Learn how Sealos' Autoscaling feature dynamically adjusts application instances based on CPU and memory usage, ensuring optimal performance and resource utilization. +--- + +# Autoscaling + +In [Sealos](https://cloud.sealos.io), the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" feature enables the +automatic adjustment of application instance numbers to effectively respond to varying load conditions. This +functionality is known as "**Autoscaling**," or more technically, the Horizontal Pod Autoscaler (HPA). + +Autoscaling operates by dynamically altering the count of application instances based on specific metrics like CPU and +memory usage. This ensures that the applications run efficiently and resources are optimally utilized. + +> Key Point: In Autoscaling, "usage" typically refers to the average use across all instances of an application. For +> example, if an app runs on two instances, its average CPU usage is calculated as the mean of the usage of these two +> instances. + +The workings of Autoscaling are as follows: + +1. **Monitoring**: It continuously monitors crucial performance indicators like CPU and memory usage. +2. **Decision Making**: Based on predefined thresholds (e.g., maintaining CPU usage below 50%), it calculates the + required adjustments in the instance count. +3. **Adjustment**: Following this, Autoscaling automatically instructs the Sealos controller to modify the number of + instances, ensuring the usage stays within the desired range. + +For instance, if we set up an application with specific Autoscaling rules such as a maximum CPU usage of 50% and the +ability for instance numbers to vary between 1 and 5, Autoscaling will: + +- Increase the number of instances when the average CPU usage exceeds 50%, up to a maximum of 5. +- Decrease the number of instances when the average CPU usage drops below 50%, but always maintain at least one instance + in operation. + +![](images/autoscale.png) diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/configmap.md b/docs/archived/5.0/docs/user-guide/app-launchpad/configmap.md new file mode 100644 index 000000000000..28c71f009150 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/configmap.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 5 +keywords: [Sealos, configuration files, Nginx container, environment variables, application settings] +description: Learn how to effectively manage application settings in Sealos using configuration files, including Nginx container setup and environment variables. +--- + +# ConfgMap + +In [Sealos](https://cloud.sealos.io), the implementation of configuration files plays a vital role, particularly when +the application deals with numerous or complex configurations. Differing from environment variables, configuration files +are a more versatile and dependable means for managing settings. Environment variables are more apt for simple, +small-scale configuration tasks. + +The primary strength of configuration files is their capability to hold and control elaborate configuration data, +including aspects like configuration files, command-line arguments, and environment variables. These pieces of data can +be incorporated into the container upon the launch of the application container, facilitating adjustments to the +application's functionalities without the necessity of recompiling the image. + +Take, for example, the Nginx container. The utilization of configuration files in this context can be described as +follows: + +- **Filename**: This pertains to a file within the Nginx container, for which references can be drawn from the + instructions provided by the image supplier. +- **File Value**: This is the content corresponding to the file. In cases where the content is elaborate, it's + recommended to complete editing it offline and then paste it into the specified location. +- **Key Points**: The approach involves mounting an individual file, not an entire directory. It is imperative to + precisely identify the file to be mounted, rather than just a directory path. + +![](images/applaunchpad13.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/environment.md b/docs/archived/5.0/docs/user-guide/app-launchpad/environment.md new file mode 100644 index 000000000000..0d7408b4b95a --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/environment.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 4 +keywords: [Sealos, environment variables, container applications, configuration management, App Launchpad] +description: Learn how to manage configuration data for container applications in Sealos using environment variables for better maintainability and scalability. +--- + +# Environment + +In [Sealos](https://cloud.sealos.io), environment variables are pivotal in managing the configuration data for container +applications. These variables enable the provision of essential configuration information to applications without +necessitating changes to the application's code or image, thus bolstering their maintainability and scalability. + +The process of defining environment variables in the "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" interface +is streamlined through a bulk input method. Users can define multiple variables by entering them line by line. Each +variable comprises a key and a value, separated by either an equal sign (=) or a colon (:). The interface is designed to +automatically eliminate any invalid characters from the key, ensuring the accuracy and validity of the environment +variables. + +![](images/applaunchpad12.png) + +**Environment Variable Formats That Are Correctly Interpreted:** + +```shell +host=127.0.0.1 +port:3000 +name: sealos +- username=123 +- password:123 +# Comments like this line are ignored, as they don't include an equal sign (=) or a colon (:), which are the key markers. +``` + +**Environment Variable Formats That Cannot Be Interpreted:** + +```shell +host=127.0.0.1 # This line is interpreted because it contains an equal sign (=) or a colon (:). The comment here is also considered part of the variable due to the preceding equal sign (=). +``` \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/expose-multi-ports.md b/docs/archived/5.0/docs/user-guide/app-launchpad/expose-multi-ports.md new file mode 100644 index 000000000000..e24a7d637cc8 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/expose-multi-ports.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 3 +keywords: [multi-port application, expose multiple ports, Sealos platform, app launchpad, network configuration] +description: Learn how to expose multiple ports for complex applications using Sealos App Launchpad. Simplify network configuration and enhance accessibility. +--- + +# Exposing Multiple Ports + +In complex application environments, it's common for services to expose multiple ports simultaneously to cater to +diverse needs. These requirements can emerge from various scenarios: + ++ **Multi-protocol support**: For example, an application might support both HTTP and HTTPS, necessitating the exposure + of both ports 80 and 443. ++ **Multi-functional application**: An application might have a web service and an admin service, both of which listen + on different ports. ++ **Compatibility considerations**: To remain compatible with older versions or other services, you might need to expose + ports for both the new and old interfaces. ++ **Combined Database + App**: For instance, if you have an application and a database within the same Pod, you might + need to expose ports for both the application and the database. ++ **Prometheus Monitoring and App Service**: If your application has a business port and another port for Prometheus + monitoring via `/metrics`, you might need to expose both. ++ **Coexistence of GRPC and RESTful services**: If your application offers both GRPC and RESTful services, you might + need to expose separate ports for each type of service. + +When deploying applications using "[App Launchpad](/guides/applaunchpad/applaunchpad.md)" +on [Sealos](https://cloud.sealos.io), you can easily choose to expose multiple ports. During the deployment process, +users simply click on the "Network" option and then select "Add Port" to configure multiple ports. + +![](images/multi-ports1.png) + +Furthermore, the Sealos platform offers external access to these ports. Once exposed to the public network, each port +will be assigned a unique sub-domain, facilitating easier remote access and management. \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain.png new file mode 100644 index 000000000000..013dfd23003e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain2.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..840e42c1e952 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain2.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain3.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..7ff6bfd47d46 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad-domain3.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad.jpg b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad.jpg new file mode 100644 index 000000000000..1a64b79192e3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad.jpg differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad10.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad10.png new file mode 100644 index 000000000000..2a88dbdda281 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad10.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad11.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad11.png new file mode 100644 index 000000000000..acc586c337db Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad11.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad12.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad12.png new file mode 100644 index 000000000000..d8490adeccc8 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad12.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad3.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad3.png new file mode 100644 index 000000000000..e64eadecb89a Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad3.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad4.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad4.png new file mode 100644 index 000000000000..01c66a4b184d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad4.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad6.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad6.png new file mode 100644 index 000000000000..6019b61ca10f Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad6.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad7.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad7.png new file mode 100644 index 000000000000..57114556318f Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad7.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad8.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad8.png new file mode 100644 index 000000000000..b72c79adb57d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad8.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad9.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad9.png new file mode 100644 index 000000000000..8a111d8424a8 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-launchpad9.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-list.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-list.png new file mode 100644 index 000000000000..a9461aae6b7e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/app-list.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad12.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad12.png new file mode 100644 index 000000000000..b92744e027a7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad12.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad13.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad13.png new file mode 100644 index 000000000000..5e0307ba334a Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/applaunchpad13.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/autoscale.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/autoscale.png new file mode 100644 index 000000000000..0c59a5f3bfb0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/autoscale.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/multi-ports1.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/multi-ports1.png new file mode 100644 index 000000000000..74e5bd569bf1 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/multi-ports1.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/persistent-volume.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/persistent-volume.png new file mode 100644 index 000000000000..af83504c71ca Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/persistent-volume.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert1.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert1.png new file mode 100644 index 000000000000..a4ba01524001 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert1.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert2.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert2.png new file mode 100644 index 000000000000..b7ea59eed18a Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert2.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert3.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert3.png new file mode 100644 index 000000000000..0fddf4574890 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert3.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert4.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert4.png new file mode 100644 index 000000000000..aa4fc627bc11 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/set-cert4.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-1.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-1.png new file mode 100644 index 000000000000..4dd8663d7dd7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-2.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-2.png new file mode 100644 index 000000000000..3d3819f431d0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-3.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-3.png new file mode 100644 index 000000000000..79a1815c53e4 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-4.png b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-4.png new file mode 100644 index 000000000000..834cb08f40d3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/app-launchpad/images/use-app-launchpad-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/persistent-volume.md b/docs/archived/5.0/docs/user-guide/app-launchpad/persistent-volume.md new file mode 100644 index 000000000000..2c99ea7a789e --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/persistent-volume.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 7 +keywords: [persistent storage, data persistence, container storage, Sealos, external storage, container deployment, data retention, Nextcloud, application container] +description: Ensure data persistence in Sealos with external storage solutions, maintaining data continuity even through container restarts or redeployments. +--- + +# Persistent Volume + +[Sealos](https://cloud.sealos.io) offers a flexible environment where containers can be effortlessly created and +destroyed. This flexibility is advantageous for application deployment and management, but it also raises the issue of +maintaining data persistence. In scenarios where data is stored within an application container, its destruction leads +to the loss of all stored data. + +To counter this problem, the use of persistent storage is essential. Persistent storage ensures that data is stored +externally, thereby preserving it even through container restarts or redeployments. This is particularly vital for +applications requiring data retention, like databases, file storage systems, or any services involving user data. + +For instance, in deploying Nextcloud, all data associated with its container is located in the `/var/www/html` +directory. To maintain data continuity, it's necessary to use external storage solutions for persisting data in this +directory. + +![](images/persistent-volume.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/app-launchpad/set-cert.md b/docs/archived/5.0/docs/user-guide/app-launchpad/set-cert.md new file mode 100644 index 000000000000..ee7aa3c8804d --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/app-launchpad/set-cert.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 8 +keywords: [custom domain certificate, cert-manager, Kubernetes secret, ingress patch, App Launchpad] +description: Learn how to manually set up a custom domain certificate in App Launchpad using Kubernetes secrets and ingress patching. +--- + +# Set a Custom Domain Certificate + +Successfully set up a custom domain in "App Launchpad" but cannot access the domain, and it shows the certificate is not +secure. This is because cert-manager did not successfully issue the certificate. To resolve this issue, we can manually +set up the certificate. + +First, make sure you have successfully set up CNAME in your cloud provider and have downloaded the certificate +corresponding to your domain. + +Open "App Launchpad", set a custom domain. + +![](images/set-cert1.png) + +Open the "Terminal", and execute the following commands in sequence. + +```bash +# Create tls.crt using the certificate file information (replace xxxx with the actual certificate file information). +cat > tls.crt < tls.key <established with the database simultaneously. | +| max_wal_size | Sets the maximum size of WAL (Write-Ahead Logging) files. | +| min_wal_size | Sets the minimum size of WAL files. | +| max_worker_processes | Sets the maximum number of background processes that
PostgreSQL can start. | +| shared_buffers | The size of memory used for data caching. | + +**Common MySQL Parameters**: + +| Parameter Name | Description | +|-------------------------|-------------------------------------------------------| +| innodb_buffer_pool_size | Sets the size of the InnoDB buffer pool. | +| max_connections | The maximum number of concurrent connections allowed. | +| query_cache_size | The size of the query cache. | +| thread_cache_size | The size of the thread cache. | +| max_allowed_packet | The maximum packet size. | +| innodb_log_file_size | The size of the InnoDB log file. | + +**Common MongoDB Parameters**: + +| Parameter Name | Description | +|-------------------------|-----------------------------------------------------------| +| storage.dbPath | The path for storing data files. | +| storage.journal.enabled | Sets the maximum size of WAL (Write-Ahead Logging) files. | +| min_wal_size | Enables logging. | +| net.port | The server port. | +| net.bindIp | The bound IP address. | + +**Common Redis Parameters**: + +| Parameter Name | Description | +|------------------|-------------------------------------------| +| maxclients | The maximum number of client connections. | +| maxmemory | The maximum amount of memory usage. | +| maxmemory-policy | The memory eviction policy. | +| appendonly | Switch for AOF persistence. | +| appendfsync | The frequency of AOF file flushing. | + +4. **Apply pg-config.yaml**: + +```bash +$ kubectl apply -f pg-config.yaml +``` +![config_4](./imgs/config_4.png) + +5. **Check whether pg-config.yaml is successfully applied**: +```bash +# If the status of OpsRequest is Succeed and the status of pod is Running, the application is successfully configured +$ kubectl get OpsRequest +$ kubectl get pod +``` +![config_5](./imgs/config_5.png) + +6. **Access the database to check whether the configuration takes effect**: +```bash +$ show max_connections; +``` +![config_6](./imgs/config_6.png) +![config_7](./imgs/config_7.png) + diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_1.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_1.png new file mode 100644 index 000000000000..d8c8c62be3a6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_1.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_2.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_2.png new file mode 100644 index 000000000000..4bd234485bf3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_2.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_3.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_3.png new file mode 100644 index 000000000000..1f6d2ff3383b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_3.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_4.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_4.png new file mode 100644 index 000000000000..02f9a61edddf Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_4.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_5.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_5.png new file mode 100644 index 000000000000..412e0bbe7fc7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_5.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_6.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_6.png new file mode 100644 index 000000000000..5f49dd84f12e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_6.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_7.png b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_7.png new file mode 100644 index 000000000000..88f930a876f0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/config-docs/imgs/config_7.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/database.md b/docs/archived/5.0/docs/user-guide/database/database.md new file mode 100644 index 000000000000..51e68d30e8cd --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/database/database.md @@ -0,0 +1,217 @@ +--- +sidebar_position: 0 +keywords: [database cluster deployment, Sealos database management, MySQL PostgreSQL MongoDB, database backup, database migration] +description: Learn how to deploy and manage database clusters with Sealos. Supports MySQL, PostgreSQL, MongoDB, and more. Features include elastic expansion, backup, and migration. +--- + +# Database + +Database is the core component of an application, Sealos provides a database cluster deployment management tool, +can help you quickly deploy database clusters. It supports MySQL, PostgreSQL, MongoDB, Redis, Kafka, and more. Currently +the database supports the following features. + ++ Elastic expansion ++ Support multi-version and multi-type databases ++ Database connection: one-click connection, external network access ++ Database connection: manual backup, automatic backup ++ Database monitoring: resource, status, and performance monitoring ++ Database migration: public network migration and file migration ++ Database high availability: Supports multiple database instances ++ Visual database management: Create, delete, update, pause, restart the database + +## Quick start + +Deploy and access a PostgreSQL database in just 6 steps. + +1、Open Sealos desktop and click Database. + +![start_1](./imgs/start_1.png) + +2、Click on the creation of a new cluster. + +![start_2](./imgs/start_2.png) + +3、Select the corresponding database and parameters. + +![start_3](./imgs/start_3.png) + +4、Click on deployment. + +![start_4](./imgs/start_4.png) + +5、Enter DB to view details. + +![start_5](./imgs/start_5.png) + +6、Database details. + +![start_6](./imgs/start_6.png) + +![start_7](./imgs/start_7.png) + +![start_8](./imgs/start_8.png) + +![start_9](./imgs/start_9.png) + +![start_10](./imgs/start_10.png) + +![start_11](./imgs/start_11.png) + +![start_12](./imgs/start_12.png) + +## Database connection + +### One-click connection + +1、Enter database, click Connect. + +![connect_1](./imgs/connect_1.png) + +2、Manipulate the database at the terminal. + +![connect_2](./imgs/connect_2.png) + +### Extranet access + +1、Enter database, click Connect. + +![connect_3](./imgs/connect_3.png) + +2、Confirm open database. + +![connect_4](./imgs/connect_4.png) + +3、Copy the database connection information. + +![connect_5](./imgs/connect_5.png) + +4、Connect to the database in the Database Connection tool. + +![connect_6](./imgs/connect_6.png) + +![connect_7](./imgs/connect_7.png) + +## Database backup + +### Manual backup + +1、Enter the database backup page, click Backup. + +![backup_1](./imgs/backup_1.png) + +2、Enter the backup information to start the backup. + +![backup_2](./imgs/backup_2.png) + +3、Check Backup status. + +![backup_3](./imgs/backup_3.png) + +![backup_4](./imgs/backup_4.png) + +### Automatic backup + +1、Enter the database backup page, click Backup. + +![backup_5](./imgs/backup_5.png) + +2、Enable automatic backup and enter backup information. + +![backup_6](./imgs/backup_6.png) + +## Database migration + +This section uses the MySQL database as an example to describe how to migrate a database. + +### Public network migration + +Public network migration involves two databases: the source database and the target database. The source database is the +data source for migration, and the target database is the data destination for migration. The following introduction +demonstrates the local database as the source database. + +1、Enter the target database and connect to the target database. + +![migration_1](./imgs/migration_1.png) + +2、Create the corresponding database in the terminal interface. (skip this step if the corresponding database already +exists) + +![migration_2](./imgs/migration_2.png) + +```bash +# Example of creating a database sql statement: +$ create database testmysql; +``` + +3、Enter the public network migration page of the target database, click the Migrate button. + +![migration_3](./imgs/migration_3.png) + +4、View migration configuration information. + +![migration_4](./imgs/migration_4.png) + +Copy the migration configuration information as follows. + +```bash +# Example of an sql statement for setting configuration information: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` + +5、Set the configuration information in the source database (MySQL and Postgres require manual configuration +information, Mongo does not require configuration). + +```bash +# xample of an sql statement for setting configuration information: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` +![migration_5](./imgs/migration_5.png) + +6、Enter the target database, fill in the migration information of the source database, which tables to migrate, and +fill in which database in the target database to migrate. + +![migration_6](./imgs/migration_6.png) + +If you need to continuously migrate data from the source database, enable continuous migration in the advanced +configuration as follows. + +![migration_7](./imgs/migration_7.png) + +7、View migration task information. + +![migration_8](./imgs/migration_8.png) + +8、Enter the target database, connect to the target database, and check whether the migration data is complete. + +![migration_9](./imgs/migration_9.png) + +### File migration + +1、Enter the target database and connect to the target database. + +![migration_10](./imgs/migration_10.png) + +2、Create the corresponding database in the terminal interface. (skip this step if the corresponding database already +exists) + +```bash +# Example of creating a database sql statement: +$ create database testmysql; +``` +![migration_11](./imgs/migration_11.png) + +3、Upload the migration file, enter the database name, and start the migration. + +![migration_12](./imgs/migration_12.png) + +4、File migrating, Wait for the migration result. + +![migration_13](./imgs/migration_13.png) + +![migration_14](./imgs/migration_14.png) + +5、Connect to the database to check whether the migrated data is complete. + +![migration_15](./imgs/migration_15.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_1.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_1.png new file mode 100644 index 000000000000..1755f5622f18 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_1.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_2.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_2.png new file mode 100644 index 000000000000..44917f8d0a63 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_2.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_3.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_3.png new file mode 100644 index 000000000000..0ddf8b9aa425 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_3.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_4.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_4.png new file mode 100644 index 000000000000..ef9cbf5e9829 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_4.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_5.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_5.png new file mode 100644 index 000000000000..297b3965f89e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_5.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/backup_6.png b/docs/archived/5.0/docs/user-guide/database/imgs/backup_6.png new file mode 100644 index 000000000000..aff13dcc9b0d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/backup_6.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_1.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_1.png new file mode 100644 index 000000000000..7d7152c94a1d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_1.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_2.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_2.png new file mode 100644 index 000000000000..46f35589f728 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_2.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_3.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_3.png new file mode 100644 index 000000000000..3b1c76fe6024 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_3.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_4.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_4.png new file mode 100644 index 000000000000..eb861ab4b7f9 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_4.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_5.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_5.png new file mode 100644 index 000000000000..408532d632ba Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_5.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_6.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_6.png new file mode 100644 index 000000000000..92f075af1fb0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_6.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/connect_7.png b/docs/archived/5.0/docs/user-guide/database/imgs/connect_7.png new file mode 100644 index 000000000000..c45553504155 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/connect_7.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_1.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_1.png new file mode 100644 index 000000000000..fc27c6626da6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_1.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_10.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_10.png new file mode 100644 index 000000000000..fc27c6626da6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_10.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_11.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_11.png new file mode 100644 index 000000000000..9ddbee463462 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_11.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_12.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_12.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_12.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_13.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_13.png new file mode 100644 index 000000000000..137e4ea3d8d5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_13.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_14.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_14.png new file mode 100644 index 000000000000..b7a2267dd5eb Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_14.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_15.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_15.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_15.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_2.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_2.png new file mode 100644 index 000000000000..9b2b39d5df8b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_2.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_3.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_3.png new file mode 100644 index 000000000000..c7f7411bd67f Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_3.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_4.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_4.png new file mode 100644 index 000000000000..214e9f5477c3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_4.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_5.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_5.png new file mode 100644 index 000000000000..f86eeb99def4 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_5.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_6.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_6.png new file mode 100644 index 000000000000..8c0949d5da92 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_6.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_7.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_7.png new file mode 100644 index 000000000000..0f2814956c68 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_7.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_8.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_8.png new file mode 100644 index 000000000000..d15a9d1ba453 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_8.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/migration_9.png b/docs/archived/5.0/docs/user-guide/database/imgs/migration_9.png new file mode 100644 index 000000000000..3f814eee38e6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/migration_9.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_1.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_1.png new file mode 100644 index 000000000000..c060483b38ad Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_1.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_10.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_10.png new file mode 100644 index 000000000000..544b065a2be5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_10.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_11.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_11.png new file mode 100644 index 000000000000..2ef27b4ecb4f Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_11.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_12.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_12.png new file mode 100644 index 000000000000..c1519b2d62fb Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_12.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_2.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_2.png new file mode 100644 index 000000000000..f83990d4edb8 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_2.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_3.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_3.png new file mode 100644 index 000000000000..971e368049b5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_3.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_4.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_4.png new file mode 100644 index 000000000000..058b39327035 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_4.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_5.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_5.png new file mode 100644 index 000000000000..333ffbed2bb5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_5.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_6.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_6.png new file mode 100644 index 000000000000..42acc0ad91f7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_6.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_7.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_7.png new file mode 100644 index 000000000000..63f79d1ac789 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_7.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_8.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_8.png new file mode 100644 index 000000000000..b3ab09bf8c2e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_8.png differ diff --git a/docs/archived/5.0/docs/user-guide/database/imgs/start_9.png b/docs/archived/5.0/docs/user-guide/database/imgs/start_9.png new file mode 100644 index 000000000000..40b2a7308f15 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/database/imgs/start_9.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/devbox.md b/docs/archived/5.0/docs/user-guide/devbox/devbox.md new file mode 100644 index 000000000000..9410b058d26d --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/devbox/devbox.md @@ -0,0 +1,35 @@ +# Devbox + +## Overview + +> A platform for instant collaborative development, seamless deployment, and strict environment isolation. Streamline your workflow with our all-in-one solution. + +Sealos Devbox is an all-in-one platform designed for integrated online development, testing, and production. It offers a seamless solution for creating environments and database dependencies with just a single click. This innovative platform allows developers to work locally using their preferred IDEs while streamlining setup processes and enabling automatic application deployment. + +![overview-1](./images/quick-start-1.png) + +### Key Features and Advantages + +#### Instant collaborative environments + +Sealos Devbox provides quick and easy setup of development environments for a wide range of programming languages and frameworks, including less common ones. This feature enables teams to start collaborating instantly, regardless of the technology stack they're using. + +#### Cloud development environment + +One of the primary advantages of Sealos Devbox is its ability to eliminate environment inconsistencies. By offering a unified cloud platform, it allows teams to share code, configurations, and test data effortlessly. This streamlined approach accelerates development processes, enhances efficiency, and promotes seamless collaboration within a single, harmonious environment. + +#### Headless development experience + +Sealos Devbox simplifies the development process by unifying development, testing, and production environments. It automates environment creation and integrates smoothly with local IDEs, providing a hassle-free setup experience for developers. + +#### Effortless continuous delivery + +With Sealos Devbox, teams can deliver applications smoothly without requiring expertise in Docker or Kubernetes. Developers simply need to specify the version, and Devbox handles all the complex tasks, including building containers. + +#### Strict environment isolation + +Sealos Devbox offers isolated development environments, helping teams avoid dependency conflicts. Each project can have its own consistent and reproducible workspace, allowing developers to focus on relevant tasks without worrying about environmental inconsistencies. + +#### Access from any network + +Sealos Devbox provides access to applications from both internal networks and the Internet, with automatic TLS configuration. This feature ensures secure and flexible development capabilities, allowing teams to work from any network, anywhere in the world. \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/devbox/faq.md b/docs/archived/5.0/docs/user-guide/devbox/faq.md new file mode 100644 index 000000000000..0e55a5164aff --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/devbox/faq.md @@ -0,0 +1,117 @@ +# FAQ + +## 1. Cursor connection problem but VSCode can connect + +Cursor Since the plugin version synchronization with VSCode is slow, outdated versions may cause connection problems. + +Solution: Manually install the Devbox plugin. Install Remote-SSH in the Cursor extension market. Note that the current version should be v0.113.x. (Note that this version must be installed in Cursor. If you download Remote-SSH from the webpage and then import it into Cursor, there is a high probability that the versions do not correspond and lead to incompatibility.) + +1. Download the vsix file of the [Devbox](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio) plugin from the VSCode plugin market. + +![devbox-1](./images/faq-1.png) + +2. Open the Cursor's extension window. + +3. Drag the downloaded file into the extension window. + +![devbox-2](./images/faq-2.png) + +4. Install Remote-SSH v0.113.x in Cursor. + +![devbox-3](./images/faq-3.png) + +## 2. Cursor and VSCode cannot connect + +First, understand the principle of the Devbox plugin: add remote environment information by modifying the ssh config file, and connect to the remote environment through the Remote-SSH plugin. The plugin first writes the following line of code in `~/.ssh/config` (some older versions may write other similar content): + +```bash +Include ~/.ssh/sealos/devbox_config +``` + +This line of code imports the contents of the file `~/.ssh/sealos/devbox_config` into the current file. And `devbox_config` contains normal SSH configuration content, for example: + +```config +Host usw.sailos.io_ns-rqtny6y6_devbox1234 + HostName usw.sailos.io + User devbox + Port 40911 + IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 + IdentitiesOnly yes + StrictHostKeyChecking no +``` + +So if there is a problem, it is most likely a plugin bug that causes errors in reading and writing files. You can feedback this to us or try to adjust the SSH file yourself. + +## 3. Always stuck in downloading vscode-server or keep retrying + +Cause: Due to some operation (such as restarting Devbox during this process), the download cursor is suspended, and re-downloading causes conflicts. + +Solution: + +1. Enter the web terminal and delete the `.cursor-server` folder. + 1. Click "Terminal" in the operation button on the right side of the Devbox webpage list item. + 2. Enter the terminal and go to the user directory first, `cd ..`, then use `ls -a ` to view all files and you can see `.cursor-server`. + 3. Remove `rm -rf .cursor-server`. + 4. Just retry the connection. +2. If there is no content in the newly created Devbox, you can directly delete it and rebuild it. + +## 4. Report the following error + +```bash +upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: delayed connect error: 111 +``` + +First of all, you should understand that your current environment is a development environment. The URL you are connecting to is a test URL, which is only used in the development environment. This URL corresponds to the port of the development environment. In other words, you must run the development environment, such as `npm run dev` to run your program first, before you can see the content through the URL, otherwise this error will be reported. + +Another possible situation is that you just need to wait for a while, maybe the network is slow. + +## 5. Click the link Cursor and enter the cursor interface, and an error message "Failed to fetch" is reported + +Try to open Cursor's extension market. If the extension market cannot be loaded normally and the error `Error while fetching extensions.Failed to fetch` is reported, it is a network problem that cannot load Cursor's plug-in market. Please refer to the manual installation tutorial above to manually install the Devbox plug-in or try to change your network environment. + +## 6. The local localhost can open the project but the public network address cannot be opened + +The exposed address in the code must be changed from `localhost` to `0.0.0.0` due to network reasons. + +## 7. The Program Runs Normally in Devbox, but Fails to Run After Deployment + +Before deploying, please ensure you can execute `entrypoint.sh` in the terminal (this is the recommended startup script +post-deployment). If public services are required, also check that the public address is accessible. + +```bash +./entrypoint.sh +``` + +If you encounter the following issue: + +```bash +bash: ./entrypoint.sh: Permission denied +``` + +You can run the following command in the terminal to modify the script's permissions, and then attempt to execute the +`entrypoint` script again, ensuring the outcome aligns with your expectations. + +```bash +sudo chmod +x entrypoint.sh +``` + +Additionally, it is advisable to build and test the project code in Devbox before deployment. Once testing is +successful, you can proceed with the deployment. This can effectively prevent errors and out-of-memory (OOM) issues +during the startup phase after deployment. + +## 8. Application Startup Fails When Listening on Port 80 + +When using Devbox, the default user for local editors (such as VSCode, Cursor, etc.) is devbox. +Ports in the range of 1-1023 are restricted to the administrator user. +If you need to use these ports normally, please run your program with administrator privileges. For example, in the Go +Devbox, you can use the following command: + +```bash +sudo go run main.go +``` + +Alternatively, you could change your application's port. + +## 9. How to Change the Default User to Root in Devbox + +Run 'sudo su' in the terminal to switch to the root user. diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/faq-1.png b/docs/archived/5.0/docs/user-guide/devbox/images/faq-1.png new file mode 100644 index 000000000000..a04895dd2dff Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/faq-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/faq-2.png b/docs/archived/5.0/docs/user-guide/devbox/images/faq-2.png new file mode 100644 index 000000000000..699e988dfd91 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/faq-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/faq-3.png b/docs/archived/5.0/docs/user-guide/devbox/images/faq-3.png new file mode 100644 index 000000000000..66a94f7d1a7b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/faq-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-1.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-1.png new file mode 100644 index 000000000000..af6f4698767c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-10.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-10.png new file mode 100644 index 000000000000..e390f1fdca9c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-10.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-2.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-2.png new file mode 100644 index 000000000000..7da4e09c31d2 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-3.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-3.png new file mode 100644 index 000000000000..c0fdb9554e94 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-4.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-4.png new file mode 100644 index 000000000000..ab7e99b80956 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-5.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-5.png new file mode 100644 index 000000000000..da15be428b69 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-6.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-6.png new file mode 100644 index 000000000000..15847a6d53bc Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-6.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-7.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-7.png new file mode 100644 index 000000000000..c7860131e6d5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-7.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-8.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-8.png new file mode 100644 index 000000000000..a58ebd3fc763 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-8.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-9.png b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-9.png new file mode 100644 index 000000000000..d26bcb084451 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/quick-start-9.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-1.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-1.png new file mode 100644 index 000000000000..a2b8ad66059c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-10.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-10.png new file mode 100644 index 000000000000..d6724dd2908a Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-10.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-11.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-11.png new file mode 100644 index 000000000000..1b6d1c13e696 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-11.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-2.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-2.png new file mode 100644 index 000000000000..ced2f197b1e8 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-3.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-3.png new file mode 100644 index 000000000000..9c6df1e7da26 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-4.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-4.png new file mode 100644 index 000000000000..9cbc4361c4d0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-5.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-5.png new file mode 100644 index 000000000000..06aa6e9d8665 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-6.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-6.png new file mode 100644 index 000000000000..adee170669f7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-6.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-7.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-7.png new file mode 100644 index 000000000000..2e7cd1052c25 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-7.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-8.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-8.png new file mode 100644 index 000000000000..6a4e0077e6b8 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-8.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-9.png b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-9.png new file mode 100644 index 000000000000..9ef04d952612 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/devbox/images/use-jb-ide-9.png differ diff --git a/docs/archived/5.0/docs/user-guide/devbox/quick-start.md b/docs/archived/5.0/docs/user-guide/devbox/quick-start.md new file mode 100644 index 000000000000..106d1c530335 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/devbox/quick-start.md @@ -0,0 +1,169 @@ +# Quick Start + +> Learn how to create, develop, and deploy a Next.js app using Sealos DevBox. This guide covers project setup, remote development with Cursor IDE, and cloud deployment. + +Sealos Devbox is an all-in-one platform designed for integrated online development, testing, and production. It offers a seamless solution for creating environments and database dependencies with just a single click, allows developers to work locally using their preferred IDEs while streamlining setup processes and enabling automatic application deployment. + +In this guide We'll demonstrate how to create a minimal Next.js demo project with Sealos Devbox. + +## Create a Devbox Project + +1. Click on the "Devbox" icon on Sealos Desktop, then click on the "Create New Project" button to create a new project. + +2. In the "Runtime" section, choose "Next.js" as the development framework. Use the sliders to set the CPU cores and memory for the project. + +![quick-start-1](./images/quick-start-1.png) + +3. After setting up the basic environment, you'll need to configure the network settings for your project: + +- Scroll down to the "Network" section of the configuration page. +- Container Port: + - Enter "3000" in the Container Port field. This is the default port that Next.js uses for development. + - If you need additional ports, click the "Add Port" button and specify them. +- Enable Internet Access: + - Toggle the switch to enable internet access for your Devbox. This allows external users to access your Next.js + application through the public internet using the provided domain. +- Domain: + - By default, Sealos provides a subdomain for your application. + - If you want to use a custom domain, click on "Custom Domain" and follow the instructions to set it up. + +Remember that the container port (3000) should match the port your Next.js application is configured to run on. If you change the port in your Next.js configuration, make sure to update it here as well. + +![quick-start-2](./images/quick-start-2.png) + +4. Click on the "Create" button to create your project. + +![quick-start-3](./images/quick-start-3.png) + +## Connect with Cursor IDE + +After creating your project, you'll see it listed in the Devbox List. Each project has an "Operation" column with various options. + +![quick-start-4](./images/quick-start-4.png) + +1. To connect to your project's Devbox runtime using Cursor IDE: + +- Locate your project in the Devbox List. +- In the "Operation" column, click on the dropdown arrow next to the VSCode icon. +- From the dropdown menu, select "Cursor". +- Click on the "Cursor" option that appears. + +2. When you click on "Cursor", it will launch the Cursor IDE application on your local machine. Within Cursor, a popup window will appear, prompting you to install the Devbox plugin for Cursor. This plugin enables SSH remote connection to the Devbox runtime. + +- Follow the instructions in the Cursor popup to install the Devbox plugin. +- Once installed, Cursor will establish a remote connection to your Devbox runtime. + +> You can switch between different IDE options (VSCode, Cursor, or VSCode Insiders) at any time by using the dropdown menu in the "Operation" column. + +## Develop + +1. After the connection is established, you'll be able to access and edit your project files directly within the Cursor IDE environment. + +![quick-start-5](./images/quick-start-5.png) + +This remote connection allows you to develop your Next.js application using Cursor IDE, with all the benefits of a cloud-based development environment: + +- Your code runs in the Devbox runtime, ensuring consistency across development and production environments. +- You can access your project from anywhere, on any device with Cursor installed. +- Collaboration becomes easier as team members can connect to the same Devbox runtime. + +2. You can start debugging your Next.js application: + +- Open the terminal within Cursor IDE. +- Navigate to your project directory if you're not already there. +- Run the following command to start the Next.js development serve: + +```bash +npm run dev +``` + +- This command will start your Next.js application in development mode. + +3. To access your running application: + +- Return to the Sealos Devbox List in your browser. +- Find the project you just created. +- Click on the "Detail" button on the right side of your project's row. + +4. In the project details page: + +- Look for the "Network" section. +- You'll see an "External Address" field. +- Click on this external address. + +![quick-start-6](./images/quick-start-6.png) + +5. This will open your Next.js application in a new browser tab, allowing you to view and interact with your running service. + +![quick-start-7](./images/quick-start-7.png) + +## Release + +After you've developed and tested your Next.js application, you can release it as an OCI (Open Container Initiative) image. This allows you to version your application and prepare it for deployment. + +1. In the Cursor IDE terminal, navigate to your project directory and run the build command: + +```bash +npm run build +``` + +This command creates a production-ready build of your Next.js application in the '.next' directory. + +2. Navigate to your project's details page: + +- Go to the Sealos Devbox List in your browser. +- Find your project and click on the "Detail" button on the right side of your project's row. + +3. On the project details page, look for the "Version" section. + +4. Click on the "Release" button located in the top right corner of the "Version" section. + +5. A "Release" dialog box will appear. Here, you need to provide the following information: + +- Image Name: This field is pre-filled with your project's image name. +- Tag: Enter a version tag for your release (e.g., v1.0). +- Description: Provide a brief description of this release (e.g., "Initial release" or "Bug fixes for login feature"). + +![quick-start-8](./images/quick-start-8.png) + +6. After filling in the required information, click the "Release" button at the bottom of the dialog box. + +7. The system will process your release. Once completed, you'll see a new entry in the "Version" section of your project + details page, showing the tag, status, creation time, and description of your release. + +![quick-start-9](./images/quick-start-9.png) + +By following these steps, you've successfully created an OCI image of your Next.js application. This image can now be used for deployment or shared with other team members. Each release creates a snapshot of your current code, allowing you to maintain different versions of your application and easily roll back if needed. + +> Remember to create new releases whenever you make significant changes or reach important milestones in your project. This practice helps in maintaining a clear history of your application's development and facilitates easier deployment and collaboration. + +## Deploy + +After releasing your Next.js application as an OCI image, you can deploy it to Sealos Cloud for production use. Here's how to do it: + +1. In your project's details page, locate the "Version" section. + +2. Find the release you want to deploy and click the "Deploy" button in the "Operation" column. + +3. This will redirect you to the App Launchpad application within Sealos. + +4. In the App Launchpad application, follow the deployment wizard to configure your application settings. This may include: + +- Selecting the appropriate environment +- Setting resource limits (CPU, memory) +- Configuring environment variables if needed +- Setting up any required volumes or persistent storage + +![quick-start-10](./images/quick-start-10.png) + +5. Once you've configured all necessary settings, click the "Deploy Application" button in the top right corner to start the deployment process. + +6. You'll be taken to the application details view within App Launchpad. + +7. Once the status is "Running", Click on the address provided under "Public Address". This will open your deployed Next.js application in a new browser tab. + +By following these steps, you've successfully deployed your Next.js application to Sealos Cloud using the App Launchpad application. Your application is now accessible via the public address, allowing users to interact with it from anywhere on the internet. + +> You can always update your application by creating a new release in Devbox and repeating this deployment process with the new version using App Launchpad. + +This workflow allows you to develop and debug your Next.js application in a cloud environment while still using your preferred local IDE. The external address makes it easy to share your work with team members or clients, as they can access your running application from anywhere with an internet connection. \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/devbox/use-jetbrains-ide.md b/docs/archived/5.0/docs/user-guide/devbox/use-jetbrains-ide.md new file mode 100644 index 000000000000..340b9f35cdd2 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/devbox/use-jetbrains-ide.md @@ -0,0 +1,50 @@ +# Develop with JetBrains IDE + +> This guide describes how to use IntelliJ IDEA in the JetBrains IDE to develop a Java-based Devbox. + +## Prerequisites + +1. Download the [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/) application. + +2. Start a Devbox project. + +3. Download the private key of the Devbox project to your local computer. View the SSH configuration in the Devbox project detail (Username: devbox, Host: hzh.sealos.run, Port: 30566). + +![use-jb-ide-2](./images/use-jb-ide-2.png) + +## Get Started + +1. Open Devbox, select JetBrains IDE and click on it. + +![use-jb-ide-3](./images/use-jb-ide-3.png) + +2. Automatically invoke the local JetBrains Gateway and click `New Connection`. + +![use-jb-ide-4](./images/use-jb-ide-4.png) + +3. Open JetBrains Gateway, fill in Username, Host and Port, check Specify private key, and select the path to the private key. Click `Check Connection and Continue` to test the SSH connection. + +![use-jb-ide-5](./images/use-jb-ide-5.png) + +4. Select `IntelliJ IDEA 2024.3.1 Preview` for the IDE version and `/home/devbox/project` for the project path. Click Download IDE and Connect to download the IDE and connect. + +![use-jb-ide-6](./images/use-jb-ide-6.png) + +You need to wait for the IDE to download. + +![use-jb-ide-7](./images/use-jb-ide-7.png) + +5. Automatically invoke the local IntelliJ IDEA, select English as the language, open the project file, and click the green arrow to run the Java service. + +![use-jb-ide-8](./images/use-jb-ide-8.png) + +![use-jb-ide-9](./images/use-jb-ide-9.png) + +6. Open the detail of the Devbox project and click the public address to access the Java service. + +![use-jb-ide-10](./images/use-jb-ide-10.png) + +7. Successfully accessed the Java service. + +![use-jb-ide-11](./images/use-jb-ide-11.png) + diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-1.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-1.png new file mode 100644 index 000000000000..e1cb4ea789d0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-2.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-2.png new file mode 100644 index 000000000000..fd26d149699b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-3.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-3.png new file mode 100644 index 000000000000..bf6d3a713e1e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-4.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-4.png new file mode 100644 index 000000000000..1d051c97977c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-5.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-5.png new file mode 100644 index 000000000000..2c090d3eb5cc Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-6.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-6.png new file mode 100644 index 000000000000..80f11b12eccf Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-6.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-7.png b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-7.png new file mode 100644 index 000000000000..f3446edd301c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/kubepanel/images/kubepanel-7.png differ diff --git a/docs/archived/5.0/docs/user-guide/kubepanel/kubepanel.md b/docs/archived/5.0/docs/user-guide/kubepanel/kubepanel.md new file mode 100644 index 000000000000..3f01581432c6 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/kubepanel/kubepanel.md @@ -0,0 +1,44 @@ +--- +keywords: [Kubernetes IDE, Kubernetes management, Kubepanel features, Kubernetes monitoring, Kubernetes resource visualization] +description: Kubepanel is a user-friendly Kubernetes IDE offering real-time monitoring, resource visualization, and easy management of Kubernetes clusters. +--- + +# Kubepanel + +Kubepanel is a Kubernetes IDE (Integrated Development Environment). It provides a user-friendly graphical interface for +managing Kubernetes clusters, offering features such as real-time monitoring and resource visualization. It aims to make +Kubernetes more accessible and manageable, even for those who are not very familiar with Kubernetes command-line tools. + +## Quick Start + +Open the Sealos desktop and click on Kubepanel. + +![](images/kubepanel-1.png) + +### Viewing Resources + +In Kubepanel, you can view Workload, Config, Network, and Storage resources. + +![](images/kubepanel-2.png) + +### Deploying Resources + +Click on the plus sign. + +![](images/kubepanel-3.png) + +You need to select a template before you can write the resource YAML. + +![](images/kubepanel-4.png) + +Here, the Deployment template is selected. Click Create to create the Deployment resource. + +![](images/kubepanel-5.png) + +After creation, you can see the Deployment resource under Workload/Deployments. + +![](images/kubepanel-6.png) + +Kubepanel also supports deleting or updating resources. + +![](images/kubepanel-7.png) diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/1.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/1.png new file mode 100644 index 000000000000..f959ae40c223 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/1.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/10.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/10.png new file mode 100644 index 000000000000..e636144a8eeb Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/10.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/11.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/11.png new file mode 100644 index 000000000000..771f415a78f2 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/11.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/12.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/12.png new file mode 100644 index 000000000000..76c1487cf46e Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/12.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/13.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/13.png new file mode 100644 index 000000000000..5abeca82fcee Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/13.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/14.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/14.png new file mode 100644 index 000000000000..0c4f82ee2374 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/14.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/15.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/15.png new file mode 100644 index 000000000000..7884c768e0d3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/15.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/16.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/16.png new file mode 100644 index 000000000000..3f76223396a7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/16.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/17.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/17.png new file mode 100644 index 000000000000..21c2d258ae63 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/17.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/2.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/2.png new file mode 100644 index 000000000000..25d649f88c7c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/2.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/3.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/3.png new file mode 100644 index 000000000000..176138f21cc5 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/3.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/4.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/4.png new file mode 100644 index 000000000000..acbf8f0358b1 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/4.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/5.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/5.png new file mode 100644 index 000000000000..52bb97ac5538 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/5.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/6.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/6.png new file mode 100644 index 000000000000..d9f63a24dd9a Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/6.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/7.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/7.png new file mode 100644 index 000000000000..b5a7d5ea362d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/7.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/8.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/8.png new file mode 100644 index 000000000000..e0bb62a7b74b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/8.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/images/9.png b/docs/archived/5.0/docs/user-guide/objectstorage/images/9.png new file mode 100644 index 000000000000..f2284d0f79b9 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/objectstorage/images/9.png differ diff --git a/docs/archived/5.0/docs/user-guide/objectstorage/objectstorage.md b/docs/archived/5.0/docs/user-guide/objectstorage/objectstorage.md new file mode 100644 index 000000000000..4344f21ec852 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/objectstorage/objectstorage.md @@ -0,0 +1,213 @@ +--- +keywords: [Object Storage, Sealos, unstructured data, bucket permissions, SDK access] +description: Learn how to use Sealos Object Storage to manage unstructured data, set bucket permissions, and access via SDKs. Quick start guide included. +--- + +# Object Storage + +**Object Storage** is Sealos' built-in object storage service, which is primarily used to store and manage unstructured +data. + +Currently, **Object Storage** has the following features: + +- Upload files to bucket. +- Download files from bucket. +- Expose the access permission of the bucket. +- Use SDK to access bucket. +- Monitors bucket resource metrics. +- Static host. + +## Quick start + +### Upload files to bucket + +Open the Sealos desktop and click on Object Storage. + +![](./images/1.png) + +Create a bucket. + +![](./images/2.png) + +Set bucket name to test and permission to private. + +Bucket Permissions Explanation: + +- private: Private bucket, no public access. +- publicRead: Shared bucket with public read access. +- publicReadwrite: Shared bucket with public read and write access. + +![](./images/3.png) + +Bucket is created successfully. + +![](./images/4.png) + +Upload file. + +![](./images/5.png) + +File uploaded successfully. + +![](./images/6.png) + +### Expose the access permission of the bucket + +Click the Edit button. + +![](./images/7.png) + +Set Bucket Permission to publicRead and click the Application button. + +![](./images/8.png) + +Copy file link. + +![](./images/9.png) + +Paste to browser address bar to access files. + +![](./images/10.png) + +### View the access key configuration + +An Object Storage user consists of a unique access key (username) and corresponding secret key (password). Internal is +the internal access address of Object Storage, and External is the external access address of Object Storage. + +![](./images/11.png) + +### Use SDK to access bucket + +The SDK requires three parameters to access bucket: AccessKey, SecretKey, and Endpoint (Internal or External). If the +Region parameter is required, us-east-1 is used by default. + +#### Go Client SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/go/API.html + +Example: Use the Go Client SDK to upload the style.css file to the sv3dd7u4-test bucket, and set the endpoint to the +external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address. + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" +) +import "github.com/minio/minio-go/v7" +import "github.com/minio/minio-go/v7/pkg/credentials" + +func main() { + endpoint := "objectstorageapi.xxx.xxx.xxx" + accessKey := "xxxxxxxx" + secretKey := "xxxxxxxxxxxxxxxx" + // init minio client + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + }) + if err != nil { + log.Fatalln(err) + } + // get local file + file, err := os.Open("./style.css") + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + fmt.Println(err) + return + } + // put object + uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"}) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Successfully uploaded bytes: ", uploadInfo) +} +``` + +File uploaded successfully. + +![](./images/12.png) + +#### Java Client SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/java/API.html + +Example: Use the Java Client SDK to upload the style1.css file to the sv3dd7u4-test bucket, and set the endpoint to the +external address. If the service is deployed in the K8s cluster, you can change the endpoint to the internal address. + +```xml + + + io.minio + minio + 8.5.9 + +``` + +```javascript +package org.example; + +import io.minio.MinioClient; +import io.minio.UploadObjectArgs; + +public class FileUploader { + public static void main(String[] args) throws Exception { + + MinioClient minioClient = + MinioClient.builder() + .endpoint("https://objectstorageapi.xxx.xxx.xxx") + .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx") + .build(); + + + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket("sv3dd7u4-test") + .object("style1.css") + .filename("src/main/java/org/example/style1.css") + .build()); + + System.out.println("Successfully uploaded bytes."); + } +} +``` + +File uploaded successfully. + +![](./images/13.png) + +#### Omit other language SDK + +Detailed documentation reference: https://min.io/docs/minio/linux/developers/minio-drivers.html + +### Static host + +Create a permission for publicRead/publicReadwrite bucket, click Enable Hosting. + +![](./images/14.png) + +Click on the address to jump to access. + +![](./images/15.png) + +Click Custom Domain. + +![](./images/16.png) + +In the Update area of App Launchpad, you can customize the domain. + +![](./images/17.png) + + + + diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/cost-center.md b/docs/archived/5.0/docs/user-guide/system-function/cost-center/cost-center.md new file mode 100644 index 000000000000..860654e6129d --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/system-function/cost-center/cost-center.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 1 +keywords: [Cost Center, Billing Details, Resource Analysis, Pricing Information, Invoice Generation] +description: Discover Sealos Cost Center with modules for cost overview, billing details, resource analysis, pricing information, and invoice generation. +--- + +# Cost Center + +The Cost Center consists of five modules: Cost Overview, Billing Details, Resource Analysis, Standard, and Create +Invoice. + +Cost Overview and Billing Details display the overall income and expenditure of the account and the expenses of various +applications, Resource Analysis shows the expenditure distribution of each resource, Standard explains pricing +information, and the Create Invoice module supports generating invoices for recharges. + +## Quick Start + +Open the Sealos desktop and click on the Cost Center. + +![](./images/cost-center-1.png) + +### Home Page + +Displays cost trend charts for all available zones and annual income and expenditure charts. + +![](./images/cost-center-2.png) + +### Cost Overview and Billing Details + +Supports quick bill lookup and filtering by setting time range, available zones, workspaces, application types, and +application names. + +![](./images/cost-center-3.png) + +### Resource Analysis + +Shows resource quota and cost distribution charts, with filtering options by setting time range, available zones, +workspaces, application types, and application names. + +![](./images/cost-center-4.png) + +### Standard + +Displays resource pricing and supports filtering by setting available zones. + +![](./images/cost-center-5.png) + +### Create invoice + +Invoice Generation. + +![](./images/cost-center-6.png) \ No newline at end of file diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-1.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-1.png new file mode 100644 index 000000000000..1b5c71858316 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-2.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-2.png new file mode 100644 index 000000000000..ba3484d5ba87 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-3.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-3.png new file mode 100644 index 000000000000..861d2d689f2b Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-4.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-4.png new file mode 100644 index 000000000000..3d65e3543e37 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-5.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-5.png new file mode 100644 index 000000000000..769290de9ac7 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-6.png b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-6.png new file mode 100644 index 000000000000..6d325929ee5c Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/cost-center/images/cost-center-6.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/system-function.md b/docs/archived/5.0/docs/user-guide/system-function/system-function.md new file mode 100644 index 000000000000..920cc943eec4 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/system-function/system-function.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 0 +keywords: [Sealos, system functions, cost center, workspace, work order, team collaboration] +description: Sealos offers system functions like cost center, workspace, and work order to enhance team collaboration and manage billing efficiently. +--- + +# Overview + +Sealos offers system functions such as cost center, workspace, and work order. + +- Cost Center: View bills, pricing standards, and issue invoices. + +- Workspace: Facilitate team collaboration and isolation through different workspaces. + +- Work order: Submit issue feedback. diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-1.png b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-1.png new file mode 100644 index 000000000000..17c998b9077d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-2.png b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-2.png new file mode 100644 index 000000000000..4a67c1e36ba6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-3.png b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-3.png new file mode 100644 index 000000000000..bda58e0da424 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-4.png b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-4.png new file mode 100644 index 000000000000..73d6f1e86aa4 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-5.png b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-5.png new file mode 100644 index 000000000000..d0c30fb6c3b0 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/work-order/images/order-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/work-order/work-order.md b/docs/archived/5.0/docs/user-guide/system-function/work-order/work-order.md new file mode 100644 index 000000000000..396538c339a9 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/system-function/work-order/work-order.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 3 +keywords: [Sealos, Work Order, Sealos desktop, issue submission, customer support] +description: Learn how to use Sealos Work Order to submit issues and get support quickly. Follow our step-by-step guide for a seamless experience. +--- + +# Work Order + +If you encounter any issues while using Sealos, you can use the Work Order to submit your questions and feedback. We +will address them as soon as possible. + +## Quick Start + +Open the Sealos desktop and click on Work Order. + +![](./images/order-1.png) + +Click on New Order. + +![](./images/order-2.png) + +Select the type of issue, upload relevant appendix, describe the details of the issue, and then click Submit Order. + +![](./images/order-3.png) + +After submitting the order, a 🤖 chatbot will respond to your queries. If you find that the chatbot’s response does not +resolve the issue, you can click Switch to Manual to connect with a human agent. + +![](./images/order-4.png) + +Click on the Order details to re-enter the chat interface. If the issue is resolved, you can click Close to close the +order. + +![](./images/order-5.png) + diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-1.png b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-1.png new file mode 100644 index 000000000000..7619f27cacec Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-2.png b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-2.png new file mode 100644 index 000000000000..557cbd219f72 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-2.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-3.png b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-3.png new file mode 100644 index 000000000000..a3a2ccac9a39 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-3.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-4.png b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-4.png new file mode 100644 index 000000000000..4f7af0801748 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-4.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-5.png b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-5.png new file mode 100644 index 000000000000..6cc985fa8ffa Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/system-function/workspace/images/workspace-5.png differ diff --git a/docs/archived/5.0/docs/user-guide/system-function/workspace/workspace.md b/docs/archived/5.0/docs/user-guide/system-function/workspace/workspace.md new file mode 100644 index 000000000000..5e8e7d858c65 --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/system-function/workspace/workspace.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 2 +keywords: [workspace management, multi-tenant resource isolation, Sealos desktop, invite users, workspace roles] +description: Learn how to manage workspaces in Sealos, including creating, switching, and inviting users with different roles and permissions. +--- + +# Workspace + +A workspace is a multi-tenant resource isolation mechanism that divides a cluster into multiple independent workspaces, +each with its own resource quotas and permissions. This mechanism supports team collaboration and resource allocation. + +A personal workspace is a special form of a workspace where each user has a personal space with the same name as their +username. Although the resource quotas and permissions for a personal space are the same as those of a regular +workspace, it cannot be deleted or have other users added. + +## Quick Start + +### Creating a Workspace + +Open the Sealos desktop, click on the Personal Workspace, and then click on Manage Workspace. + +![](./images/workspace-1.png) + +Click on Create Workspace. + +![](./images/workspace-2.png) + +### Switching Workspaces + +Return to the Sealos desktop, click on the Personal Workspace, and select the hello workspace to switch to it. + +![](./images/workspace-3.png) + +### Inviting Users + +In the Manage Workspaces section, click on Invite Member, select the members' roles and permissions, click on Generate +Invitation Link, and then send the link to other users. + +There are three different roles for users within a workspace, with the following descriptions of roles and permissions: + +- Owner: The owner has full permissions for the workspace, including deleting the workspace, viewing/creating/modifying + all resources within the workspace, and inviting users to join the workspace as managers or developers. + +- Manager: The manager has administrative permissions for the workspace, including viewing/creating/modifying all + resources within the workspace and inviting users to join the workspace as developers. + +- Developer: The developer has development permissions, including viewing the status of resources within the workspace. + +![](./images/workspace-4.png) + +### Accepting an Invitation + +When a user receives an invitation link, they should visit the link and click on Accept Invitation to join the +workspace. + +![](./images/workspace-5.png) diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal-1.png b/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal-1.png new file mode 100644 index 000000000000..f1fcc577c7f2 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal.png b/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal.png new file mode 100644 index 000000000000..ac1a69448ca6 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/database-terminal.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal-1.png b/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal-1.png new file mode 100644 index 000000000000..a94f8582d11d Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal.png b/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal.png new file mode 100644 index 000000000000..44fad2374a07 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/nginx-terminal.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/terminal-1.png b/docs/archived/5.0/docs/user-guide/terminal/images/terminal-1.png new file mode 100644 index 000000000000..78ce7eb3f242 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/terminal-1.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/terminal-pod.png b/docs/archived/5.0/docs/user-guide/terminal/images/terminal-pod.png new file mode 100644 index 000000000000..bc47999cf4f3 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/terminal-pod.png differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/images/terminal.webp b/docs/archived/5.0/docs/user-guide/terminal/images/terminal.webp new file mode 100644 index 000000000000..151254f046c2 Binary files /dev/null and b/docs/archived/5.0/docs/user-guide/terminal/images/terminal.webp differ diff --git a/docs/archived/5.0/docs/user-guide/terminal/terminal.md b/docs/archived/5.0/docs/user-guide/terminal/terminal.md new file mode 100644 index 000000000000..efc44a56a2af --- /dev/null +++ b/docs/archived/5.0/docs/user-guide/terminal/terminal.md @@ -0,0 +1,99 @@ +--- +keywords: [Sealos Terminal, Kubernetes Pod, Kubernetes Terminal, Application Container Terminal, Direct Database Connection] +description: Discover the powerful Sealos Terminal for direct interaction with Kubernetes Pods, application containers, and databases, enhancing efficiency and control. +--- + +# Terminal + +Integrated within the [Sealos](https://cloud.sealos.io) Cloud Operating System is the Terminal, an application of +profound potency. Mirroring the terminal in standalone operating systems, it facilitates users to interact directly with +the operating system, executing various operations via command entry. + +![](images/terminal.webp) + +The value of the Terminal rests in its efficiency, flexibility, and powerful capabilities. For a plethora of tasks, +employing the Terminal proves swifter and more concise than utilizing a Graphical User Interface (GUI). It is an +indispensable instrument for developers, permitting users to gain a profound understanding and control of the Sealos +Cloud Operating System. + +**Every user's Terminal is in fact operating within a Pod of Kubernetes**, which can be observed in the Terminal using +the following command: + +![](images/terminal-pod.png) + +## Accessing the Terminal of an Application Container + +One may directly access the Terminal of any application container via the Terminal App. Suppose you have deployed an +application such as Nginx via application management, one can navigate directly to the Nginx application's detail page, +click on the three dots on the right side of the details, and then click on 'Terminal' to access the Terminal of the +Nginx application. + +![](images/nginx-terminal.png) + +![](images/nginx-terminal-1.png) + +## 终端一键直连数据库 + +It is also possible to establish a direct connection to the database created in +the[Database App](../dbprovider/dbprovider.md) with a single click via the Terminal. + +Navigate to the database details page and click on '一键连接' on the left: + +![](images/database-terminal.png) + +This will take you directly to the Terminal App and connect to the database: + +![](images/database-terminal-1.png) + +## Direct Manipulation of Kubernetes Resources via Terminal + +:::danger + +The following operations necessitate foundational knowledge of Kubernetes and container-related concepts, avoid casual +execution without this understanding. + +::: + +Veteran Kubernetes users can utilize the Terminal to manipulate Kubernetes resources directly, affording many advantages +for such seasoned users: + ++ Enables batch operations and automation ++ Offers high flexibility ++ Allows in-depth troubleshooting + +Below are examples of common operations with Kubernetes resources within the Terminal: + ++ View the list of user's Pods + +```bash +kubectl get pods +``` + ++ Create and manage resource + +```bash +# Create resources using YAML or JSON files +$ kubectl create -f + +# Create or update resources using YAML or JSON files +$ kubectl apply -f + +# Delete specified resource type and name +$ kubectl delete + +# View the status of a specified Deployment's rolling update +$ kubectl rollout status deployment/ +``` + ++ Troubleshooting + +```bash +# Obtain detailed information about a specific resource, including events and status +$ kubectl describe + +# Obtain logs of a specific container in a specific Pod +$ kubectl logs -c + +# Open a terminal in a specific Pod for debugging purposes +$ kubectl exec -it +``` \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/Intro.md b/docs/archived/5.0/i18n/zh-Hans/Intro.md new file mode 100644 index 000000000000..0d617755300f --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/Intro.md @@ -0,0 +1,58 @@ +--- +description: Sealos是基于Kubernetes的云操作系统,简化云原生部署。它支持公私云环境,提供应用和数据库管理,实现高效经济的云计算体验。探索Sealos,开启简单云计算之旅。 +keywords: [Sealos, 云操作系统, Kubernetes, 云原生, 公有云, 私有云, 分布式应用, 应用管理, 数据库管理, 云计算] +--- + +import Highlight from '@site/src/components/Highlight' + +# 什么是 Sealos? + + + +云原生生态之庞大与复杂,无疑让许多企业感到手足无措,这个生态依然缺乏好用的、开箱即用的发行版。在云原生市场还急需一款云操作系统以进一步降低云原生门槛与成本。 + +Sealos 的问世为企业开启了一扇新窗。它为企业与开发者提供了一种全新的选择,无论在公有云还是私有云中,都只需安装一个云操作系统。它能让各类应用在操作系统上稳定安全地运行,一并解决了应用所需的各种依赖问题。 + +Sealos 坚守的理念是:**云可以像 Linux 一样简单**。你可以像使用 Linux 一样安装后立即投入使用,使用过程无需过多纠结。唯一的差别仅在于,Linux 是安装在单一服务器上,而 Sealos 则是安装在整个数据中心。 + +在 Sealos 的架构中,**公有云与私有云并无本质上的差异**,他们应是同一套代码的不同实例,唯一的区别仅在于配置与所安装应用的差异。在内网安装即是私有云,而在公网对外提供服务则成为公有云。 + +我们坚信,随着 Sealos 的不断发展与进步,未来的云将更加开放,更加简单。云计算的美好将属于所有算力的提供者,云的价值将由所有云计算的参与者共享。**他们将能够在多元化的市场环境中,以更经济的方式享受云计算所带来的种种便利。** + +## 概述 + +Sealos 是一款以 Kubernetes 为内核的**云操作系统发行版**。它以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,使企业能够**像使用个人电脑一样**简单地使用云。 + +用户将可以像使用个人电脑一样在 Kubernetes 上一键安装任意高可用分布式应用,几乎不需要任何专业的交付和运维成本。同时,利用独特的集群镜像能力,用户可将任意分布式应用打包成 OCI 镜像,自由组合各种分布式应用,轻松订制所需的云。通过强大且灵活的应用商店功能,可满足各类用户的多样化需求。 + +![](./images/sealos-desktop-zh.webp) + +## 适用场景 & 优势 + +Sealos 是一款强大的业务运行平台,它能完美支持 Java、Go、Python、PHP 等各类应用,不受编程语言限制。该平台为应用提供稳定的运行环境,并解决后端依赖问题,例如数据库、对象存储和消息队列等。更进一步,它还能灵活处理应用配置管理、服务发现、公网暴露,以及自动伸缩等问题。 + +### 公有云 + +如果您的业务需要运行在公有云环境中,那么您可以直接使用 [Sealos 提供的公有云服务](https://cloud.sealos.io)。 + +#### Sealos 的能力 + +- 🚀 **应用管理**:在应用商店中轻松管理并快速发布可公网访问的分布式应用。 +- 🗄️ **数据库管理**:秒级创建高可用数据库,支持 MySQL、PostgreSQL、MongoDB 和 Redis。 +- 🌥️ **公私一致**:即是公有云也是私有云,支持传统应用无缝迁移到云环境。 + +#### Sealos 的优势 + +- 💰 **高效 & 经济**:仅需为容器付费,自动伸缩杜绝资源浪费,大幅度节省成本。 +- 🌐 **通用性强,无心智负担**:专注于业务本身,无需担心复杂性,几乎没有学习成本。 +- 🛡️ **灵活 & 安全**:多租户共享机制在确保安全的同时,实现资源隔离与高效协作。 + +### 私有云 + +Sealos 云操作系统 **100% 开源**,您可在 [GitHub](https://github.com/labring/sealos) 上找到所有源代码,因此 **Sealos 公有云**的全部功能也可以部署在私有云环境中,以满足您更多样化的业务需求。 + +### 强大的应用交付能力 + +- Sealos 具备优秀的 Kubernetes 生命周期管理能力,还可以自由定制 Kubernetes 环境。 +- Sealos 可以将整个集群打包,并一键交付到客户环境中。所有服务和业务都可以整体交付。 +- 不同于 Docker 主要关注单机镜像,Sealos 更进一步,可以将**整个集群**或者某个分布式应用进行打包。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/community.md b/docs/archived/5.0/i18n/zh-Hans/community.md new file mode 100644 index 000000000000..274cb076d243 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/community.md @@ -0,0 +1,16 @@ +--- +keywords: [Sealos社区, 开源项目, 微信交流群, Discord服务器, GitHub Issue, 技术支持, 用户交流, 问题反馈] +description: 探索Sealos开源社区:加入微信群、Discord服务器与开发者交流,关注Twitter获取最新动态,通过GitHub反馈问题。我们致力于为您提供全方位支持。 +--- + +# 社区 + +Sealos 是一个由用户和贡献者参与推动的开源项目,如果您对产品使用存在疑问和建议,可尝试以下方式寻求支持。我们的团队与社区会竭尽所能为您提供帮助。 + ++ 📱 扫码加入社区微信交流群👇 + +![](https://oss.laf.run/htr4n1-images/sealos-qr-code.jpg) + ++ 💬 加入我们的 [Discord 服务器](https://discord.gg/qzBmGGZGk7),与 Sealos 开发者和终端用户进行交流。 ++ 🐦 在 [Twitter](https://twitter.com/Sailos_io) 上关注我们。 ++ 🐞 请将任何 Sealos 的 Bug、问题和需求提交到 [GitHub Issue](https://github.com/labring/sealos/issues/new/choose)。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/QA.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/QA.md new file mode 100644 index 000000000000..26d1329224cd --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/QA.md @@ -0,0 +1,226 @@ +--- +sidebar_position: 1 +keywords: [Sealos, 镜像构建, Kubernetes运行时, 版本兼容性, 文件目录位置] +description: 了解Sealos常见问题及解决方案,包括镜像构建、Kubernetes运行时选择、版本兼容性和文件目录位置调整。 +--- + +# 常见问题 + +使用Sealos时,您可能会遇到一些问题。以下是一些常见问题的答案和解决方法。 + +## 镜像构建问题 + +### Q1: 在构建阶段如何设置代理服务? + +在执行构建命令时,可以通过设置HTTP_PROXY环境变量来配置代理服务。 + +```shell +HTTP_PROXY=socket5://127.0.0.1:7890 sealos build xxxxx +``` + +### Q2:如何启用buildah的调试日志? + +若需要查看buildah的调试日志,可以通过设定`BUILDAH_LOG_LEVEL`环境变量实现。 + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +### Q3:如何在Pod中执行Sealos构建? + +若在Pod中执行Sealos构建,请按以下步骤操作: + +1. 在Pod中构建镜像,可用以下YAML配置创建Deployment。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sealoscli + name: sealoscli +spec: + replicas: 1 + selector: + matchLabels: + app: sealoscli + strategy: {} + template: + metadata: + labels: + app: sealoscli + spec: + containers: + - image: #用你的sealos镜像替换 + name: sealoscli + stdin: true + stdinOnce: true + securityContext: + privileged: true +``` + +2. 创建Dockerfile。以下是一个例子,根据需要进行修改。 + +```dockerfile +FROM bitnami/minideb:buster + +ARG TARGETOS +ARG TARGETARCH + +LABEL from=bitnami/minideb:buster platform=rootcloud team=oam tag=buster name=base + +RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && sed -i "s@http://security.debian.org@http://mirrors.aliyun.com/debian-security@g" /etc/apt/sources.list +RUN install_packages curl iputils-ping net-tools telnet procps vim wget jq + +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV TZ=Asia/Shanghai +``` + +3. 在Pod中执行构建命令。 + +```shell +sealos build --arch arm64 --build-arg TARGETOS=linux --build-arg TARGETARCH=arm64 -t test -f Dockerfile . +``` + +### Q4:如何使用其他构建工具构建集群镜像? + +如果您想使用 Docker 或 Podman 等其他容器工具来构建集群镜像,您可以借助 [sreg](https://github.com/labring/sreg) 来缓存镜像。 + +以下是操作步骤: + +1. 安装 sreg: + ```shell + wget https://github.com/labring/sreg/releases/download/v0.1.1/sreg_0.1.1_linux_amd64.tar.gz + tar -xzf sreg_0.1.1_linux_amd64.tar.gz sreg + mv sreg /usr/bin/ + ``` +2. 缓存镜像: + ```shell + sreg save --registry-dir=registry . + ``` +3. 构建集群镜像: + ```shell + docker build -t xxxx -f Sealfile . + ``` + +### Q5:执行Sealos构建时遇到“lgetxattr /var/lib/containers/storage/overlay/0c2afe770ec7870ad4639f18a1b50b3a84718f95c8907f3d54e14dbf0a01d50d/merged/dev/ptmx: no such device”错误? + +这个问题可能与`fuse-overlayfs`的版本有关。建议您从[这里](https://github.com/containers/fuse-overlayfs/releases)下载最新版本下载并替换`/bin/fuse-overlayfs`。 + +## 运行时选择问题 + +### Q1:如何选择Kubernetes运行时? + +Sealos会根据您选择的镜像决定使用哪种运行时。如果选择了kubernetes-docker镜像,Sealos将使用Docker作为运行时;如果选择了kubernetes-crio镜像,Sealos将使用CRI-O作为运行时。 + +## 版本兼容性问题 + +### Q1:报错:"Applied to cluster error: failed to init exec auth.sh failed exit status 127"? + +此问题常因您使用的sealos版本和镜像版本不匹配造成。请确认您的镜像版本和sealos的版本是匹配的。 +例如,若您正使用形如kubernetes:v1.xx.x的版本,可能需要升级sealos,特别是在使用较老版本的sealos,而sealos集群镜像则使用了最新版时。 +另一种解决方法是选择对应版本的sealos镜像。比如,如果您的sealos版本是4.1.3,那么集群镜像应选择形如kuberntes:v1.24.0-4.1.3的版本。 +确保镜像版本和sealos版本的匹配,可以帮助避免此类问题。 + +### Q2: 如果您在集群中新增了其他域名,或者修改了 service 的 CIDR,并且在添加 master 时出现了错误 + +为了解决这个问题,Sealos 团队在 4.2.0 版本进行了相应的修复。具体的修复内容和讨论可以在这个 pull request 中查看:https://github.com/labring/sealos/pull/2943 。 + +所以,如果您遇到了这个问题,我们建议您升级到 Sealos 4.2.0 版本。更新后的版本应该能够正确处理这些变更,并且在添加 master 时不会出现错误。 + +## 文件和目录位置问题 + +### Q1:如何修改`/root/.sealos`默认目录的存储位置? + +若需修改默认的存储位置,可以设置SEALOS_RUNTIME_ROOT环境变量,然后运行sealos命令。建议您将这个环境变量设置为全局的,这样在其他命令或场景中也可以方便使用。 + +```shell +export SEALOS_RUNTIME_ROOT=/data/.sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q2:如何修改`/var/lib/sealos`默认目录的存储位置? + +若需修改默认的存储位置,可以设置SEALOS_DATA_ROOT环境变量,然后运行sealos命令。同样,建议您将这个环境变量设置为全局的。 + +```shell +export SEALOS_DATA_ROOT=/data/sealos +sealos run labring/kubernetes:v1.24.0 +``` + +### Q3: 如何修改 Sealos 镜像数据和状态的存储路径? + +> 在使用 Sealos 集群时,可能需要改变默认的镜像数据存储路径和状态数据的存储路径。默认情况下,这些数据被存储在 `/etc/containers/storage.conf` 文件定义的位置。 + +1. **查看当前存储配置** + 首先,我们可以使用下面的命令来查看当前的镜像存储配置: + ``` + sealos images --debug + ``` + 这个命令会打印出包含当前存储配置的文件,例如: + ``` + 2023-06-07T16:27:02 debug using file /etc/containers/storage.conf as container storage config + REPOSITORY TAG IMAGE ID CREATED SIZE + ``` +2. **修改镜像数据存储路径** + 如果你希望更改镜像数据的存储路径,你可以编辑 `/etc/containers/storage.conf` 文件。在这个文件中,找到并修改 `graphroot` 字段设置为新的路径。例如: + ``` + vim /etc/containers/storage.conf + ``` + 在编辑器中,将 `graphroot` 字段的值修改为你希望的新路径。 +3. **修改状态数据存储路径** + 参考 Buildah 的设计,Sealos 同样提供了状态数据存储路径的设置。在同样的配置文件 `/etc/containers/storage.conf` 中,找到并修改 `runroot` 字段为新的路径。 + +通过以上步骤,你可以将 Sealos 集群的镜像数据和状态数据保存到新的地址。每次运行 Sealos 命令时,它都将使用你在 `graphroot` 和 `runroot` 中设置的新路径来分别存储镜像数据和状态数据。 + +### Q4:ssh传输文件时,如何禁止检查文件的md5? + +在网络环境良好时,禁用md5检查可以极大提升传输速度。若不想在ssh传输文件时检查文件的md5,可将SEALOS_SCP_CHECKSUM环境变量设置为false以禁用此功能。建议将此环境变量设为全局,以便在多场景下使用。 + +```shell +export SEALOS_SCP_CHECKSUM=false +sealos run labring/kubernetes:v1.24.0 +``` + + +## 其他问题 + +### Q1:image-cri-shim导致端口大量占用,耗尽服务器socket资源? + +出现此问题时,可通过以下命令解决: + +```shell +wget https://github.com/labring/sealos/releases/download/v4.2.0/sealos_4.2.0_linux_amd64.tar.gz && tar xvf sealos_4.2.0_linux_amd64.tar.gz image-cri-shim +sealos exec -r master,node "systemctl stop image-cri-shim" +sealos scp "./image-cri-shim" "/usr/bin/image-cri-shim" +sealos exec -r master,node "systemctl start image-cri-shim" +sealos exec -r master,node "image-cri-shim -v" +``` + +### Q2:报错"[ERROR FileAvailable--etc-kubernetes-kubelet.conf]: /etc/kubernetes/kubelet.conf already exists" + +此问题可通过升级至Sealos 4.1.7+来解决。 + +### Q3:报错:"function "semverCompare" not defined" + +此问题可通过升级至Sealos 4.1.4+来解决。 + +我们希望这些解答能帮助您解决在使用Sealos过程中遇到的问题。如果还有其他问题,欢迎随时提问。 + +## 使用技巧 + +### Q1: 如何清理 Sealos 集群的缓存文件 + +> 在使用 Sealos 集群时,安装过程中可能会在本地存储一些缓存文件,这些文件会重复占用磁盘空间。那么,如何清理这些缓存文件以释放磁盘空间呢? + +我们提供了一个非常简单的解决方案,只需要执行以下命令: + +```shell +sealos unmount --all && sealos rm --all +``` +这个命令的作用是移除所有缓存的 Sealos 集群镜像文件,以及所有的相关挂载点。--all 选项表示处理所有相关文件和挂载点。 +执行这个命令后,所有 Sealos 集群的缓存文件就会被清理掉,从而释放出被它们占用的磁盘空间。 +这是一个非常有用的技巧,特别是对于在磁盘空间有限的环境中运行 Sealos 集群的用户来说。在你感觉磁盘空间被占用过多时,不妨尝试执行这个命令来释放一些空间。 +请注意,这个命令只会删除缓存文件,不会影响已经运行的集群。也就是说,执行这个命令后,你的集群仍然可以正常运行。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md new file mode 100644 index 000000000000..b00e901c2d33 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/build-image-using-registry-sync.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 2 +keywords: [Sealos镜像构建, 镜像缓存, registry-proxy, skopeo copy, 镜像仓库同步] +description: 了解Sealos镜像构建的改进指南,提升构建效率,使用registry-proxy和skopeo copy实现镜像仓库同步,优化镜像管理。 +--- +--- + +# 镜像构建改进指南 + +## 深入理解Sealos镜像构建 + +为了了解Sealos镜像构建的背后所做的工作,我们将首先揭示它在底层究竟进行了哪些操作。以下是一个直观的架构图: + +![](images/build.png) + +Sealos在构建镜像过程中涵盖了以下几个核心步骤: + +- **缓存镜像**:解析构建执行时的工作目录(在这里我们称之为"context"目录),将缓存镜像保存到registry结构并存储在./registry目录下。 +- **构建镜像**:在context目录中进行镜像构建,生成新的镜像。(请注意,构建镜像时需要将./registry目录进行复制。) + +## 提升镜像构建效率 + +当前项目中,我们借用了`github.com/distribution/distribution`的源代码,在执行缓存镜像的过程中直接调用了registry的sdk并启动了registry-proxy功能。借助于镜像仓库的缓存能力,我们将镜像缓存并存入context/registry目录。 + +这个过程的关键就在于调用了distribution仓库的方法进行保存镜像: + +- 启动 registry-proxy 功能。 +- 保存镜像摘要及索引相关数据(通过调用saveManifestAndGetDigest方法)。 +- 保存镜像文件数据(通过调用saveBlobs方法)。 + +这种方法确实具有一些显著的优点: + +- 轻量化:无需依赖其他组件即可保存镜像。 +- 自由控制:可以自由控制保存逻辑,无需依赖第三方组件。 + +然而,我们也注意到了一些潜在的问题: + +- 对新手来说,代码理解难度较高,不易清晰了解这里的逻辑。 +- 无法缓存使用token认证的方式。 +- 需要依赖一些临时存储空间,对空间有要求。 + +考虑到这些问题,我们决定尝试一种新的模式:在本地启动一个轻量的registry,使用`skopeo copy`的sdk进行代码复用。这一改变直接解决了之前所有的问题。 + +![](images/registry-build.png) + +**所以,新的构建方式 ✨镜像仓库同步✨ 优雅登场 🎉🎉** + +官方仓库中的[#3154](https://github.com/labring/sealos/pull/3154)这个PR已经完成了这个功能的实现。目前,Sealos支持这两种方式进行镜像构建。接下来,我会介绍如何启动新功能(如果新功能表现稳定,我们可能会废弃旧的构建方式)。 + +## 如何启动新功能 + +> Sealos v4.3.0 以上版本默认支持此功能。 + +启动新功能非常简单,只需在你构建镜像之前添加一个环境变量即可。这个功能同时支持build和merge两个命令。 + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +``` + +以下是执行上述命令后的预期输出: + +```tex +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t test . +Getting image source signatures +Copying blob fee740108510 done +Copying config f92f3ea6e4 done +Writing manifest to image destination +Storing signatures +Getting image source signatures +Copying blob 08409d417260 done +Copying config 44dd6f2230 done +Writing manifest to image destination +Storing signatures +2023-06-01T13:16:07 info saving images busybox, alpine +STEP 1/2: FROM scratch +STEP 2/2: COPY registry ./registry +COMMIT test +Getting image source signatures +Copying blob 13ab73c881c8 done +Copying config 4e22d16b36 done +Writing manifest to image destination +Storing signatures +--> 4e22d16b366 +Successfully tagged localhost/test:latest +4e22d16b366e9fec25641522a74cbd73a7db67dc0516b8f8e00200c4d0551592 +``` + +希望以上内容可以帮助您更好地理解并使用Sealos的新镜像构建方式。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md new file mode 100644 index 000000000000..8d048f8a9847 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/dual-stack-cluster.md @@ -0,0 +1,793 @@ +--- +sidebar_position: 5 +keywords: [双栈集群, calico安装, sealos, Kubernetes, IPv6配置, Clusterfile, k8s双栈, 网络配置, Kubernetes集群, Calico双栈] +description: 使用calico安装双栈集群,详细步骤包括生成和编辑Clusterfile,添加IPv6的pod和svc的CIDR范围,确保Kubernetes集群的双栈网络配置。 +--- + +# 使用calico安装双栈集群 + +1. 前置条件 + - sealos 版本 >=4.3.0 + - 每个主机都有一个IPv4主机和IPv6地址,并且可以互通通过IPv4和IPv6地址 + - calico采用vxlan模式, 内核版本必须大于 3.12。 可以参考[官方文档](https://github.com/cyclinder/kubespray/blob/042c960c6617f8a360a8281464ff63f99ee2471c/docs/calico.md) +2. 运行 `sealos gen` 生成一个 Clusterfile,例如: + +```shell +$ sealos gen labring/kubernetes:v1.26.1 labring/helm:v3.10.3 labring/calico:v3.25.0 --masters 192.168.0.10 --nodes 192.168.0.11 --passwd "xxx" >Clusterfile +``` + +注意:labring/helm 应当在 labring/calico 之前。 + +生成的 Clusterfile 如下: + +
+Clusterfile + + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + ExtraArgs: + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10 + ServiceSubnet: 10.96.0.0/22 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +``` + +
+ +3. 生成 Clusterfile 后,编辑Clusterfile,然后添加IPv6 的 pod 和svc 的 CIDR 范围。这里使用fd85:ee78:d8a6:8607::1:0000/112、fd85:ee78:d8a6:8607::1000/116作为参考示例。主要修改以下信息。 + +
+Clusterfile + + +```yaml +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 #增加pod IPv6地址段 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #增加svc IPv6地址段 +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a #增加控制节点的ipv6地址,如果你需要使用此IP访问apiserver + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 #增加svc IPv6地址段 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 #默认为64 + node-cidr-mask-size-ipv4: 24 #默认为24 +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" #增加pod IPv6地址段 +--- +# 添加Calico双栈配置 +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 #增加pod IPv6地址段 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +最终的Clusterfile会是这样。 + +
+Clusterfile + + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.10:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.11:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.26.1 + - labring/helm:v3.10.3 + - labring/calico:v3.25.0 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 +status: {} + +--- +BootstrapTokens: null +CertificateKey: "" +LocalAPIEndpoint: + AdvertiseAddress: 192.168.0.10 + BindPort: 6443 +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: InitConfiguration + +--- +APIServer: + CertSANs: + - 127.0.0.1 + - apiserver.cluster.local + - 10.103.97.2 + - 192.168.0.10 + - 2001:db8::f816:3eff:fe8c:910a + ExtraArgs: + service-cluster-ip-range: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 + audit-log-format: json + audit-log-maxage: "7" + audit-log-maxbackup: "10" + audit-log-maxsize: "100" + audit-log-path: /var/log/kubernetes/audit.log + audit-policy-file: /etc/kubernetes/audit-policy.yml + enable-aggregator-routing: "true" + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/kubernetes + MountPath: /etc/kubernetes + Name: audit + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /var/log/kubernetes + MountPath: /var/log/kubernetes + Name: audit-log + PathType: DirectoryOrCreate + ReadOnly: false + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + TimeoutForControlPlane: null +CIImageRepository: "" +CIKubernetesVersion: "" +CertificatesDir: "" +ClusterName: "" +ComponentConfigs: null +ControlPlaneEndpoint: apiserver.cluster.local:6443 +ControllerManager: + ExtraArgs: + node-cidr-mask-size-ipv6: 120 + node-cidr-mask-size-ipv4: 24 + bind-address: 0.0.0.0 + cluster-signing-duration: 876000h + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +DNS: + ImageRepository: "" + ImageTag: "" + Type: "" +Etcd: + External: null + Local: + DataDir: "" + ExtraArgs: + listen-metrics-urls: http://0.0.0.0:2381 + ImageRepository: "" + ImageTag: "" + PeerCertSANs: null + ServerCertSANs: null +FeatureGates: null +ImageRepository: "" +KubernetesVersion: v1.26.1 +Networking: + DNSDomain: "" + PodSubnet: 100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112 + ServiceSubnet: 10.96.0.0/22,fd85:ee78:d8a6:8607::1000/116 +Scheduler: + ExtraArgs: + bind-address: 0.0.0.0 + feature-gates: "" + ExtraVolumes: + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true + - HostPath: /etc/localtime + MountPath: /etc/localtime + Name: localtime + PathType: File + ReadOnly: true +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration + +--- +CACertPath: /etc/kubernetes/pki/ca.crt +ControlPlane: + CertificateKey: "" + LocalAPIEndpoint: + AdvertiseAddress: "" + BindPort: 6443 +Discovery: + BootstrapToken: null + File: null + TLSBootstrapToken: "" + Timeout: 5m0s +NodeRegistration: + CRISocket: /run/containerd/containerd.sock + IgnorePreflightErrors: null + KubeletExtraArgs: null + Name: "" + Taints: null +Patches: null +SkipPhases: null +apiVersion: kubeadm.k8s.io/v1beta3 +kind: JoinConfiguration + +--- +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +bindAddress: 0.0.0.0 +bindAddressHardFail: false +clientConnection: + acceptContentTypes: "" + burst: 10 + contentType: application/vnd.kubernetes.protobuf + kubeconfig: "" + qps: 5 +clusterCIDR: "" +configSyncPeriod: 15m0s +conntrack: + maxPerCore: 32768 + min: 131072 + tcpCloseWaitTimeout: 1h0m0s + tcpEstablishedTimeout: 24h0m0s +detectLocal: + bridgeInterface: "" + interfaceNamePrefix: "" +detectLocalMode: "" +enableProfiling: false +healthzBindAddress: 0.0.0.0:10256 +hostnameOverride: "" +iptables: + masqueradeAll: false + masqueradeBit: 14 + minSyncPeriod: 1s + syncPeriod: 30s +ipvs: + excludeCIDRs: + - 10.103.97.2/32 + minSyncPeriod: 0s + scheduler: "" + strictARP: false + syncPeriod: 30s + tcpFinTimeout: 0s + tcpTimeout: 0s + udpTimeout: 0s +kind: KubeProxyConfiguration +clusterCIDR: "100.64.0.0/10,fd85:ee78:d8a6:8607::1:0000/112" +metricsBindAddress: 0.0.0.0:10249 +mode: ipvs +nodePortAddresses: null +oomScoreAdj: -999 +portRange: "" +showHiddenMetricsForVersion: "" +udpIdleTimeout: 250ms +winkernel: + enableDSR: false + forwardHealthCheckVip: false + networkName: "" + rootHnsEndpointName: "" + sourceVip: "" + +--- +address: 0.0.0.0 +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 2m0s + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 5m0s + cacheUnauthorizedTTL: 30s +cgroupDriver: cgroupfs +cgroupsPerQOS: true +clusterDomain: cluster.local +configMapAndSecretChangeDetectionStrategy: Watch +containerLogMaxFiles: 5 +containerLogMaxSize: 10Mi +contentType: application/vnd.kubernetes.protobuf +cpuCFSQuota: true +cpuCFSQuotaPeriod: 100ms +cpuManagerPolicy: none +cpuManagerReconcilePeriod: 10s +enableControllerAttachDetach: true +enableDebugFlagsHandler: true +enableDebuggingHandlers: true +enableProfilingHandler: true +enableServer: true +enableSystemLogHandler: true +enforceNodeAllocatable: +- pods +- pods +eventBurst: 10 +eventRecordQPS: 5 +evictionHard: + imagefs.available: 15% + memory.available: 100Mi + nodefs.available: 10% + nodefs.inodesFree: 5% +evictionPressureTransitionPeriod: 5m0s +failSwapOn: true +fileCheckFrequency: 20s +hairpinMode: promiscuous-bridge +healthzBindAddress: 0.0.0.0 +healthzPort: 10248 +httpCheckFrequency: 20s +imageGCHighThresholdPercent: 85 +imageGCLowThresholdPercent: 80 +imageMinimumGCAge: 2m0s +iptablesDropBit: 15 +iptablesMasqueradeBit: 14 +kind: KubeletConfiguration +kubeAPIBurst: 10 +kubeAPIQPS: 5 +localStorageCapacityIsolation: true +logging: + flushFrequency: 5000000000 + format: text + options: + json: + infoBufferSize: "0" + verbosity: 0 +makeIPTablesUtilChains: true +maxOpenFiles: 1000000 +maxPods: 110 +memoryManagerPolicy: None +memorySwap: {} +memoryThrottlingFactor: 0.8 +nodeLeaseDurationSeconds: 40 +nodeStatusMaxImages: 50 +nodeStatusReportFrequency: 10s +nodeStatusUpdateFrequency: 10s +oomScoreAdj: -999 +podPidsLimit: -1 +port: 10250 +registerNode: true +registryBurst: 10 +registryPullQPS: 5 +rotateCertificates: true +runtimeRequestTimeout: 2m0s +seccompDefault: false +serializeImagePulls: true +shutdownGracePeriod: 0s +shutdownGracePeriodCriticalPods: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 4h0m0s +syncFrequency: 1m0s +topologyManagerPolicy: none +topologyManagerScope: container +volumePluginDir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ +volumeStatsAggPeriod: 1m0s +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + bgp: Disabled + ipPools: + - blockSize: 22 + cidr: 100.64.0.0/10 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + - blockSize: 122 + cidr: fd85:ee78:d8a6:8607::1:0000/112 + encapsulation: VXLAN + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*|em.*" + nodeAddressAutodetectionV6: + interface: "eth.*|en.*|em.*" +``` + +
+ +4. 运行 `sealos apply -f Clusterfile` 部署集群。 + +5. 更多参考[Calico官网](https://docs.tigera.io/calico/latest/networking/ipam/ipv6) 和 [k8s官方文档](https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/dual-stack-support/) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md new file mode 100644 index 000000000000..4762fc677ad5 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/image-build-standardized.md @@ -0,0 +1,108 @@ +--- +sidebar_position: 1 +keywords: [Sealos镜像构建, Kubernetes部署, 目录结构标准化, Kubefile参数, 容器镜像管理, 集群初始化, 应用配置, Helm chart, 环境变量设置, 镜像仓库配置] +description: 学习Sealos镜像构建的标准化目录配置,掌握Kubefile参数使用,优化Kubernetes部署流程。本指南助您规范化构建过程,提高效率,降低错误率。 +--- + +# 镜像构建与标准化目录配置 + +在开展 Sealos 镜像构建任务前,我们建议先构建一个符合规范的目录结构。这样能够使构建过程更加规范,易于管理,同时也能降低出错率。这篇文章将详细指导你如何创建这样一个目录结构,并解释每个目录的用途。 + +## 目录结构示例 + +一个完整的、符合规范的目录结构示例如下: + +```shell +. +├── charts +│   └── nginx +│   ├── Chart.lock +│   ├── charts +│   ├── Chart.yaml +│   ├── README.md +│   ├── templates +│   ├── values.schema.json +│   └── values.yaml +├── images +│   └── shim +│   └── nginxImages +├── init.sh +├── Kubefile +├── manifests +│   └── nginx +│   ├── deployment.yaml +│   ├── ingress.yaml +│   └── service.yaml +├── opt +│   └── helm +└── registry +``` + +## 目录描述 + +每个目录在构建过程中都扮演着特定的角色,以下是他们的详细描述: + +- `Kubefile` (必需):这个文件类似于 Dockerfile,是构建镜像的核心文件。它定义了构建过程中的各个步骤,如基础镜像选择、环境变量设置、文件复制等。 +- `manifests`:这个目录用于存放 Kubernetes 的 yaml 文件,这些文件描述了你的应用的配置信息,如 Pod、Service、Deployment 的配置。 +- `charts`:这个目录用于存放 Helm chart 文件,Helm chart 是 Kubernetes 的一个包管理工具,可以简化 Kubernetes 应用的部署和管理。 +- `images/shim`:这个目录用于存放无法从 yaml 文件或 Helm chart 中自动提取的镜像。在构建过程中,sealos 将自动拉取这些镜像。 +- `opt`:二进制文件存储在这里。 +- `registry`:这个目录用于存放构建过程中拉取到本地的镜像。在构建过程中,该目录将自动生成,无需手动创建。 +- `init.sh`:这个脚本在构建过程中由 GitHub Action 自动运行,你可以在这个脚本中编写一些自动化的工作,如初始化环境、预处理数据等。([cluster-image](https://github.com/labring-actions/cluster-image)的规则) + +## Kubefile 参数 + +`Kubefile` 文件是镜像构建的核心,它支持多种参数,以下是这些参数的详细解析: + +```shell +FROM labring/kubernetes:v1.24.0 +ENV version v1.1.0 +COPY manifests ./manifests +COPY registry ./registry +ENTRYPOINT ["kubectl apply -f manifests/tigera-operator.yaml"] +CMD ["kubectl apply -f manifests/custom-resources.yaml"] +``` + +各个参数的描述: + +- `FROM`:这个指令用于设置构建的基础镜像,所有的构建步骤都基于这个镜像进行。 +- `LABEL`: `LABEL`定义一些sealos集群镜像的内部配置。 + + - `check` 集群镜像运行前的一些检查脚本操作 + - `clean` 集群reset或者节点删除的清理脚本 + - `clean-registry` 集群reset时候的清理镜像仓库的脚本 + - `image` 集群的lvscare镜像地址(sealos的IPVS镜像) + - `init` 集群初始化的脚本 + - `init-registry` 集群初始化时启动容器镜像仓库的脚本 + - `sealos.io.type` 集群镜像类型,目前主要是rootfs、application和patch。 + - rootfs 是运行集群的基础镜像,比如kubernetes、kubernetes-docker这种包含镜像、二进制等集群所需的。(**每个节点都需要存在**) + - application 是应用镜像,比如calico、helm、istio等应用服务的镜像。(**只存储到master0节点**) + - patch是在rootfs镜像后需要调整的,是另一种修改rootfs镜像的方式(**还有一种方式是Config方式**),它会覆盖默认的集群运行的第一个镜像。 + + - `sealos.io.version` 镜像的版本号,目前开启的是v1beta1 + - `version` 集群的版本号,当前是kubernetes的版本号 + - `vip` 是VIP的地址,为修改IPVS的虚IP使用 + +- `ENV`:`ENV`指令将环境变量``设置为值``。(rootfs中默认会有一些默认的环境变量,可以修改rootfs中一些默认参数,比如镜像仓库的账号密码、docker、containerd的存储目录等等) + + 具体的集群镜像需要具体查看,`sealos inspect`镜像看一下对应的环境变量,不同版本的镜像略有不同。 + + - SEALOS_SYS_CRI_ENDPOINT: 当前集群镜像的criSocket (不同类型集群镜像可能不同) + - criData: cri的数据目录 + - defaultVIP: 默认的VIP地址 + - disableApparmor: 是否禁用apparmor (containerd有这个问题) + - registryConfig: 容器镜像仓库的配置目录 + - registryData: 容器镜像仓库的数据目录(因为是目录进行了挂载,其实这个配置没有实际意义,它实际还是存储在/var/lib/sealos下面) + - registryDomain: 默认镜像仓库的域名 + - registryPassword: 默认镜像仓库的密码 + - registryPort: 默认镜像仓库的端口 + - registryUsername: 默认镜像仓库的账户 + - sandboxImage: 默认cri启动的sandbox_image。(无需写repo只需要写镜像名称,eg: pasue:3.7) +- `COPY`:`COPY`指令从``复制新的文件或目录,并将它们添加到容器的文件系统路径``上。(**注意,需要把registry目录进行拷贝,否则集群没有容器镜像**) +- `ENTRYPOINT`:这个指令用于设置镜像的启动命令,当镜像启动时,这条命令会被执行。 +- `CMD`:这个指令也用于设置镜像的启动命令,但它与 ENTRYPOINT 指令的区别在于,如果用户在运行镜像时(`sealos run --cmd`)提供了启动命令,CMD 指令中的命令将会被覆盖。 + +在构建过程中,Sealos 还会自动设置一些内置的环境变量,包括(前缀为'SEALOS_SYS'的环境变量无法被修改): + +- SEALOS_SYS_KUBE_VERSION:Kubernetes的版本号,例如 v1.26.0 +- SEALOS_SYS_SEALOS_VERSION:Sealos的版本号,例如 4.1.3 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/build.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/build.png new file mode 100644 index 000000000000..d1ae7d6aeadd Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/build.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png new file mode 100644 index 000000000000..485b531d28f8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/registry-build.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png new file mode 100644 index 000000000000..70ef65ffa13f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/images/sealos-run.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/sealos-run.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/sealos-run.md new file mode 100644 index 000000000000..fa011d475d28 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/sealos-run.md @@ -0,0 +1,138 @@ +--- +sidebar_position: 2 +keywords: [Sealos Run, Kubernetes集群部署, 容器编排, 节点管理, 镜像分发, 集群初始化, 证书管理, IPVS规则, Kubeadm配置, Clusterfile] +description: 深入解析Sealos Run命令的工作原理,包括节点检查、镜像分发、集群初始化等九大步骤,助您轻松部署和管理Kubernetes集群。 +--- + +# Sealos Run 的工作原理 + +以下是 `sealos run` 命令的详细工作原理。为了帮助您更好地理解,我们将其分解为九个主要步骤。 + +## 1. 执行命令 + +首先,用户通过执行以下命令启动 Sealos: + +```bash +sealos run kubernetes:v1.23.8 +``` + +## 2. 执行 Pipeline + +命令执行后,Sealos 开始执行一系列相关的操作步骤,我们称之为 "Pipeline"。这个过程包括以下几个子步骤: + +### 2.1 节点检查 + +Sealos 会对所有节点进行检查,以确保它们都是可用的。例如,检查是否存在重复的主机名,以及所有节点的时间是否已经同步。 + +### 2.2 拉取镜像 + +接着,Sealos 从远程仓库拉取 Kubernetes 的镜像,并将其加载到本地存储。 + +### 2.3 渲染配置 + +然后,Sealos 会对拉取到的镜像的 rootfs(文件系统)进行配置渲染,这是为了准备后续的节点分发。 + +### 2.4 分发镜像文件 + +配置渲染完成后,Sealos 会将渲染后的 rootfs 文件分发到各个节点。 + +### 2.5 分发 Registry 目录 + +此外,Sealos 也会将镜像中的 registry 目录分发到对应的 registry 角色节点。Sealos 支持两种模式: + +- **scp模式**:直接通过 scp 将目录复制到每个节点,这种方式会占用大量的网络带宽。 +- **镜像同步模式**:使用 skopeo sdk 的镜像同步机制,能够实现增量镜像同步,从而节省网络带宽。该功能可通过环境变量 `SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true` 来开启。 + +### 2.6 执行 Bootstrap + +Bootstrap 是关键步骤,包括以下操作: + +#### 2.6.1 添加 Host 解析 + +Sealos 会在每个节点上添加 registry 的 host 解析。 + +#### 2.6.2 执行 Registry-Init 脚本 + +在添加完 host 解析后,Sealos 会执行 registry-init 脚本。 + +#### 2.6.3 执行 Init 脚本 + +最后,Sealos 会执行 init 脚本,启动 Kubernetes 服务。 + +## 3. 执行 InitMaster0 + +Pipeline 执行完成后,Sealos 进入 InitMaster0 阶段。在此阶段,Sealos 会执行以下操作: + +### 3.1 生成 Kubeadm 配置 + +首先,Sealos 生成 Kubeadm 的初始化配置文件。 + +### 3.2 生成并分发证书文件 + +然后,Sealos 生成集群的所有 + +证书文件,并将它们分发到各个节点。 + +### 3.3 初始化 Master0 + +最后,Sealos 执行`kubeadm init`初始化 Master0 节点。 + +## 4. 执行 JoinMaster + +初始化完 Master0 节点后,Sealos 进入 JoinMaster 阶段。在此阶段,Sealos 会执行以下操作: + +### 4.1 同步 Kubeconfig + +首先,Sealos 将 Kubeconfig 文件同步到各个节点。 + +### 4.2 同步证书文件 + +接下来,Sealos 同步证书文件到各个节点。 + +### 4.3 生成 Join 配置 + +然后,Sealos 生成 JoinMaster 配置文件。 + +### 4.4 生成 Join 令牌 + +接着,Sealos 生成 Join 令牌。 + +### 4.5 执行 Join Master 流程 + +最后,Sealos 执行`kubeadm join`流程,让其他 Master 节点加入集群。 + +## 5. 执行 JoinNode + +所有 Master 节点加入集群后,Sealos 进入 JoinNode 阶段,执行以下操作: + +### 5.1 生成 Join 令牌 + +首先,Sealos 再次生成 Join 令牌。 + +### 5.2 生成 IPVS 规则 + +然后,Sealos 生成 IPVS 规则。 + +### 5.3 生成 Join 配置 + +接下来,Sealos 再次生成 JoinNode 配置。 + +### 5.4 执行 Join Node 流程 + +最后,Sealos 执行 Join Node 流程,让 Worker 节点加入集群。 + +## 6. 同步 IPVS + +所有节点加入集群后,Sealos 将 Master 的 IPVS 规则同步到所有节点。这里主要是通过 lvscare 的静态 pod,根据 Master 节点的数量同步静态 pod 的配置。 + +## 7. 执行 Guest 阶段 + +IPVS 规则同步完成后,Sealos 执行 Guest 阶段。这是执行集群镜像命令的阶段。 + +## 8. 写入 Clusterfile + +最后,Sealos 将最终的执行结果写入 Clusterfile。 + +这就是 `sealos run` 命令的工作原理。希望通过这篇文章,您能对 `sealos run` 命令有更深入的理解。以下是我们提到的全景图,以帮助您更好地理解整个流程。 + +![](images/sealos-run.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/template-function.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/template-function.md new file mode 100644 index 000000000000..7f8f93e157fd --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/advanced-guide/template-function.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 9 +keywords: [Sealos模板渲染, 集群镜像构建, 模板函数增强, Kubernetes配置管理, 动态生成资源清单, semverCompare, 多版本Kubernetes支持, 镜像构建灵活性] +description: 探索Sealos的模板渲染功能,了解如何在集群镜像构建中动态生成配置、脚本和Kubernetes资源清单,以及如何使用增强的模板函数实现多版本Kubernetes支持。 +--- + +# 模板引入与函数增强 + +在 Sealos 的镜像构建过程中,"tmpl" 后缀的文件在 "etc"、"scripts" 和 "manifests" 这三个目录下的文件会被特殊处理。 + +1. "etc" 目录:这个目录通常用于存放配置文件。在构建过程中,Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,渲染完成后的文件将被拷贝到集群镜像的相应目录下。 + +2. "scripts" 目录:这个目录通常用于存放执行脚本。Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,生成的脚本将在集群镜像构建过程中被执行。 + +3. "manifests" 目录:这个目录通常用于存放 Kubernetes 资源清单文件。Sealos 会对该目录下的 "tmpl" 后缀的文件进行模板渲染,生成的清单文件将在集群镜像构建过程中被应用到 Kubernetes 集群中。 + +总的来说,这三个目录下的 "tmpl" 后缀文件在 Sealos 的镜像构建过程中都会被视为模板文件进行处理。这种处理方式提供了更多的灵活性,让我们可以在构建过程中动态生成配置、脚本或者 Kubernetes 资源清单。 + +在构建镜像时,我们支持 `template` 来让维护者完全控制生成的配置文件(模块渲染)。例如: + +```yaml +service: + name: {{ .serviceName }} + port: + {{- if typeIs "string" .servicePort }} + name: {{ .servicePort }} + {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} + number: {{ .servicePort | int }} + {{- end }} +``` + +请参考 Golang 的 [text/template](https://pkg.go.dev/text/template) 了解基础介绍和更多细节。 + +## 模板函数增强 + +此外,我们支持 `templateFunc` 来增强模板函数。例如: + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://{{ .registryDomain }}:{{ .registryPort }} +force: true +debug: false +image: /var/lib/image-cri-shim +{{ if and (ne .SEALOS_SYS_KUBE_VERSION "") (semverCompare "^1.26.0" .SEALOS_SYS_KUBE_VERSION) }}version: v1{{ else }}version: v1alpha2{{ end }} +timeout: 15m +auth: {{ .registryUsername }}:{{ .registryPassword }} +``` + +在这里我们使用 `semverCompare` 来检查用户是否运行在 k8s 版本 v1.26.0 或以上,如果是,生成 `version: v1`,否则生成 `version: v1alpha2`。 +有了这个支持,我们可以很轻松地用一个集群镜像文件管理多版本的 Kubernetes 支持。 + +### 一些最常用的模板函数 + +* [semverCompare](http://masterminds.github.io/sprig/semver.html) 比较语义版本,而不是字符串比较。 +* [default](http://masterminds.github.io/sprig/defaults.html) default 可以在值为空或未定义时提供一个默认值。 +* [toYaml](https://github.com/labring/sealos/blob/main/lifecycle/pkg/template/funcmap.go#L66) 将当前值(对象,映射,数组)显示为 yaml 格式的字符串。 + +完整的支持函数列表,请[点击这里](http://masterminds.github.io/sprig/)。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/images/sealos.webp b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/images/sealos.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/images/sealos.webp differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md new file mode 100644 index 000000000000..9ade8de8d263 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 0 +keywords: [Kubernetes集群, Sealos工具, 集群生命周期管理, 分布式应用, Kubernetes安装] +description: 使用Sealos工具轻松管理Kubernetes集群生命周期,支持分布式应用和自定义集群镜像,提供高可用性和离线安装功能。 +--- + +# K8s 集群生命周期管理 + +Sealos 提供一套强大的工具,使得用户可以便利地管理整个集群的生命周期。 + +## 功能介绍 + +使用 Sealos,您可以安装一个不包含任何组件的裸 Kubernetes 集群。此外,Sealos 还可以在 Kubernetes 之上,通过集群镜像能力组装各种上层分布式应用,如数据库、消息队列等。 + +Sealos 不仅可以安装一个单节点的 Kubernetes 开发环境,还能构建数千节点的生产高可用集群。 + +Sealos 具有自由伸缩集群、备份恢复、释放集群等功能,即使在离线环境中,Sealos 也能提供出色的 Kubernetes 运行体验。 + +## 主要特性 + +- 支持 ARM,v1.20 以上版本离线包支持 containerd 与 docker 集成 +- 提供 99 年证书,支持集群备份,升级 +- 不依赖 ansible、haproxy、keepalived,一个二进制工具,零依赖 +- 提供离线安装,不同 Kubernetes 版本只需使用不同的集群镜像 +- 高可用性由 ipvs 实现的 localLB 提供,占用资源少,稳定可靠,类似 kube-proxy 的实现 +- 使用 image-cri-shim 自动识别镜像名称,使离线交付更方便 +- 几乎兼容所有支持 systemd 的 x86_64 架构的环境 +- 轻松实现集群节点的增加/删除 +- 已有数万用户在线上环境使用 Sealos,稳定可靠 +- 支持集群镜像,自由组合定制你需要的集群,如 openebs 存储+数据库+minio 对象存储 +- 使用 buildah 的 sdk 实现对镜像标准统一,完全兼容 OCI 的标准 + +## 使用 Sealos 运行 Kubernetes 集群 + +使用 Sealos 运行一个 Kubernetes 集群非常简单,只需以下步骤: + +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v4.3.0/scripts/install.sh \ + | sh -s v4.3.0 labring/sealos +# 创建一个集群 +$ sealos run labring/kubernetes:v1.25.0-4.2.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +[![asciicast](https://asciinema.org/a/519263.svg)](https://asciinema.org/a/519263?speed=3) + +## 在集群上运行分布式应用 + +通过 `sealos run` 命令,您可以在集群上运行各种分布式应用,如数据库、消息队列、AI 能力,甚至企业级 SaaS 软件。例如: + +```shell +# MySQL 集群 +$ sealos run labring/mysql-operator:8.0.23-14.1 + +# Clickhouse 集群 +$ sealos run labring/clickhouse:0.18.4 + +# Redis 集群 +$ sealos run labring/redis-operator:3.1.4 +``` + +## 自定义集群 + +对于 Sealos 生态没有的集群镜像,用户可以方便地自己构建和定制属于自己的集群镜像。例如: + +[构建一个 ingress 集群镜像](/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md) + +您还可以定制一个完全属于自己的 Kubernetes: + +Sealfile: + +```shell +FROM kubernetes:v1.25.0 +COPY flannel-chart . +COPY mysql-chart . +CMD ["helm install flannel flannel-chart", "helm install mysql mysql-chart"] +``` + +```shell +sealos build -t my-kuberentes:v1.25.0 . +sealos run my-kuberentes:v1.25.0 ... +``` + +## 常见问题 + +**Sealos 是 Kubernetes 安装工具吗?** + +安装部署只是 Sealos 的一个基本功能,如同单机操作系统有 Boot 模块一样,Sealos 的 Boot 模块可以很好地管理 Kubernetes 在任何场景下的生命周期。 + +**Sealos 和 Rancher、KubeSphere 有什么区别?** + +Sealos 的设计理念是 "化整为零,自由组装,大道至简"。Sealos 利用 Kubernetes 的能力,以简单的方式提供给用户真正需要的东西。用户需要的不一定是 Kubernetes,用户需要的是具体的能力。 + +Sealos 是极其灵活的,不会给用户带来额外负担。它的形态取决于用户的需求和安装的应用。Sealos 的核心是分布式应用,所有应用都是一等公民。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md new file mode 100644 index 000000000000..926f58549b56 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 4 +keywords: [集群镜像构建, 二进制文件打包, sealos工具, helm安装, 镜像推送] +description: 学习如何使用sealos工具将二进制文件(如helm)打包为集群镜像,并通过主节点部署实现安装。包含详细步骤和命令示例。 +--- + +# 构建基于二进制文件的集群镜像 + +此文档主要介绍了如何使用 `sealos` 工具将单一二进制文件(例如 `helm` 或 `kustomize`)打包为集群镜像,并将它们通过在主节点上部署集群镜像进行安装。以 `helm` 为例,我们将详细介绍如何将二进制文件打包成集群镜像。 + +## 创建构建工作空间 + +首先,创建一个基础目录作为构建工作空间: + +```shell +$ mkdir ~/cluster-images +``` + +在工作空间中,创建一个 `opt` 目录用于存储二进制文件: + +```shell +$ cd cluster-images +$ mkdir opt/ +``` + +## 准备二进制文件 + +接下来,我们准备 `helm` 二进制文件。在此,我们从 [github release](https://github.com/helm/helm/releases) 中下载: + +```shell +wget https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz +tar -zxvf helm-v3.10.1-linux-amd64.tar.gz +chmod a+x linux-amd64/helm +mv linux-amd64/helm opt/ +``` + +## 创建构建镜像所需的 `Sealfile` 文件 + +创建一个名为 `Sealfile` 的文件,内容如下: + +```shell +FROM scratch +COPY opt ./opt +CMD ["cp opt/helm /usr/bin/"] +``` + +目前的目录结构如下: + +``` +. +├── Sealfile +└── opt + └── helm +``` + +## 构建集群镜像 + +现在,一切准备就绪,你可以开始构建集群镜像了: + +```shell +sealos build -t labring/helm:v3.10.1 . +``` + +**注意:** 首先你需要在本地主机上安装 `sealos` 命令。 + +你可以查看构建日志来了解构建过程。 + +```shell +root@ubuntu:~/cluster-images# sealos build -t labring/helm:v3.10.1 . +... +``` + +查看构建的镜像,现在所有依赖的二进制文件都已经构建进集群镜像中: + +```shell +root@ubuntu:~/cluster-images# sealos images +labring/helm v3.10.1 19ed4a24f0fe 3 minutes ago 45.1 MB +``` + +## 推送镜像 + +你可以将镜像推送至任何 Docker 镜像仓库,下面的命令将镜像推送到 dockerhub: + +```shell +sealos push labring/helm:v3.10.1 +``` + +**注意:** 请使用 `sealos` 命令来操作集群镜像,不支持 Docker 命令。 + +如果你使用的是私有镜像仓库,可以使用 `sealos login` 命令登录你的镜像仓库,然后再推送或者拉取镜像 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md new file mode 100644 index 000000000000..4dd260d385c5 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md @@ -0,0 +1,108 @@ +--- +sidebar_position: 5 +keywords: [集群镜像构建, go-template, Kubefile, 环境变量, sealos, yaml模板, 服务类型自定义, 镜像运行, kubectl, helm] +description: 学习如何使用go-template构建集群镜像,通过Kubefile和yaml模板利用环境变量自定义服务配置,掌握sealos运行镜像的高级技巧。 +--- + +# 构建基于 go-template 的集群镜像 + +在构建集群镜像的过程中,我们可以使用 `--env` 选项通过sealos命令行传递一些变量。这些环境变量可以被Kubefile的 `CMD` 命令或者yaml文件模板所使用。 + +## 在 Kubefile 中使用环境变量 + +这个示例定义了一个 `SERVICE_TYPE` 变量,它允许用户在安装应用程序时自定义服务暴露类型,并将参数传递给CMD中的helm命令。 + +Kubefile 示例: + +```shell +FROM scratch +ENV SERVICE_TYPE "NodePort" +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=$(SERVICE_TYPE)"] +``` + +运行集群应用并设置一个自定义的 `SERVICE_TYPE=LoadBalancer`,如果不设置,它将默认为 NodePort。 + +```shell +sealos run labring/nginx:v1.23.1 --env SERVICE_TYPE=LoadBalancer +``` + +## 在Yaml文件中使用环境变量 + +准备一个简单的nginx服务的yaml文件,这个文件必须是 `*.tmpl` 扩展名,以便在运行 `sealos run --env` 命令时渲染。 + +```shell +$ cat manifests/service.yaml.tmpl +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: {{ .serviceType }} + ports: + - port: 80 + nodePort: {{ .http_NodePort }} + name: http + - port: 443 + nodePort: {{ .https_NodePort }} + name: https + selector: + name: nginx +``` + +下面是一个Kubefile样例,你可以在这里设置默认的环境变量。 + +```shell +FROM scratch +ENV serviceType NodePort +ENV http_NodePort 30080 +ENV https_NodePort 30443 + +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/service.yaml"] +``` + +当你构建镜像时,什么都不会发生,只有在运行应用程序时,它才会渲染。如果没有设置 `--env`,它将使用 Kubefile 的默认 ENV。 + +```shell +sealos run labring/nginx:1.23.1 --env serviceType=LoadBalancer --env http_NodePort=30080 --env https_NodePort=30443 +``` + +你会发现 sealos 会在主节点的本地路径上基于 `service.yaml.tmpl` 渲染一个新的yaml文件 `service.yaml`。 + +**注意** 新版本的应用的rootfs放到了`/var/lib/sealos/data/default/applications`目录,每个应用都有独立的目录。 + +```shell +root@node1:~# ls /var/lib/sealos/data/default/rootfs/manifests |grep service +service.yaml +service.yaml.tmpl +``` + +检查 yaml 内容: + +```shell +root@node1:~# cat /var/lib/sealos/data/default/rootfs/manifests/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + name: nginx +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30080 + name: http + - port: 443 + nodePort: 30443 + name: https + selector: + name: nginx +``` + +**注意:**所有类型的文件都支持这个特性(文件名后缀是.tmpl且构建目录在etc、scripts和manifests),你可以自己尝试一下。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md new file mode 100644 index 000000000000..1c1a20cffd53 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md @@ -0,0 +1,237 @@ +--- +sidebar_position: 3 +keywords: [Helm Charts, 集群镜像, Sealos, Kubernetes, nginx, 镜像构建, 容器化, 云原生, DevOps, CI/CD] +description: 学习如何使用Sealos和Helm Charts构建基于nginx的Kubernetes集群镜像。本教程涵盖准备工作、Charts配置、Kubefile创建、镜像构建和安装等步骤。 +--- + +# 构建基于 Helm Charts 的集群镜像 + +让我们以最简单的 nginx 应用程序为例,介绍如何基于 Helm Charts 构建一个基于 nginx 的集群镜像。 + +## 一、准备工作 + +创建一个用于构建工作的基础目录。 + +```shell +$ mkdir ~/cloud-images +``` + +创建一个 `charts` 目录来存储 Kubernetes nginx Helm Charts 文件。 + +```shell +$ cd cloud-images +$ mkdir charts +``` + +## 二、准备Helm Charts + +准备 nginx Helm Charts,这里我们使用 [bitnami 官方的 nginx Helm Charts](https://bitnami.com/stack/nginx),让我们将 Helm Chart 文件拉取到本地并解压到 `charts` 目录中。 + +```shell +helm repo add bitnami https://charts.bitnami.com/bitnami +helm search repo bitnami/nginx +helm pull bitnami/nginx --version=13.2.13 -d charts/ --untar +``` + +**注意:** 首先你应该安装 Helm 命令工具到本地主机。 + +现在,charts 目录的结构如下所示。 + +``` +charts/ +└── nginx + ├── Chart.lock + ├── charts + ├── Chart.yaml + ├── README.md + ├── templates + ├── values.schema.json + └── values.yaml +``` + +## 三、创建Kubefile + +创建一个名为 `Kubefile` 的文件用于镜像构建: + +```shell +$ cat Kubefile +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm install nginx charts/nginx --namespace=nginx --create-namespace"] +``` + +建议使用 `helm upgrade --install` 而不是 `helm install`,这样可以在以后更新应用程序时重复运行相同的命令。 + +你可以根据需要添加其他选项,例如通过 NodePort 暴露服务。 + +```shell +FROM scratch +COPY charts charts +COPY registry registry +CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +``` + +## 四、构建集群镜像 + +现在一切准备就绪,你可以开始构建集群镜像。 + +```shell +sealos build -t labring/nginx:v1.23.2 . +``` + +**注意:** 你应该首先将 sealos 命令安装到本地主机。 + +你可以查看构建日志。 + +```shell +root@ubuntu:~/cloud-images# sealos build -t labring/nginx:v1.23.2 . +2022-11-06T15:58:33 info lookup in path charts +2022-11-06T15:58:33 info sub chart is nginx +2022-11-06T15:58:33 warn if you access private registry,you must be 'sealos login' or 'buildah login' +2022-11-06T15:58:33 info pull images [docker.io/bitnami/nginx:1.23.2-debian-11-r29] for platform is linux/amd64 +Pulling image: docker.io/bitnami/nginx:1.23.2-debian-11-r29 +1d8866550bdd: Download complete +cbbfe6232a5b: Download complete +ed342369e859: Download complete +Status: images save success +2022-11-06T15:58:43 info output images [docker.io/bitnami/nginx:1.23.2-debian-11-r29] for platform is linux/amd64 +STEP 1/3: FROM scratch +STEP 2/3: COPY . . +STEP 3/3: CMD ["helm upgrade --install nginx charts/nginx --namespace=nginx --create-namespace --set service.type=NodePort"] +COMMIT labring/nginx:v1.23.2 +Getting image source signatures +Copying blob 9f5a861e0f8d done +Copying config 1b89695273 done +Writing manifest to image destination +Storing signatures +--> 1b896952734 +Successfully tagged localhost/labring/nginx:v1.23.2 +1b8969527343939d60859469708e5420758f7419a421304f81b5132669982de7 +2022-11-06T15:58:44 info + ___ ___ ___ ___ ___ ___ + /\ \ /\ \ /\ \ /\__\ /\ \ /\ \ + /::\ \ /::\ \ /::\ \ /:/ / /::\ \ /::\ \ + /:/\ \ \ /:/\:\ \ /:/\:\ \ /:/ / /:/\:\ \ /:/\ \ \ + _\:\~\ \ \ /::\~\:\ \ /::\~\:\ \ /:/ / /:/ \:\ \ _\:\~\ \ \ + /\ \:\ \ \__\ /:/\:\ \:\__\ /:/\:\ \:\__\ /:/__/ /:/__/ \:\__\ /\ \:\ \ \__\ + \:\ \:\ \/__/ \:\~\:\ \/__/ \/__\:\/:/ / \:\ \ \:\ \ /:/ / \:\ \:\ \/__/ + \:\ \:\__\ \:\ \:\__\ \::/ / \:\ \ \:\ /:/ / \:\ \:\__\ + \:\/:/ / \:\ \/__/ /:/ / \:\ \ \:\/:/ / \:\/:/ / + \::/ / \:\__\ /:/ / \:\__\ \::/ / \::/ / + \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ + + Website :https://www.sealos.io/ + Address :github.com/labring/sealos +``` + +sealos 将自动从 charts 目录中提取镜像,将其拉取到本地并存储在 registry 目录中。 + +现在的目录结构如下所示: + +```shell +. +├── charts +│ └── nginx +│ ├── Chart.lock +│ ├── charts +│ ├── Chart.yaml +│ ├── README.md +│ ├── templates +│ ├── values.schema.json +│ └── values.yaml +├── Kubefile +└── registry + └── docker + └── registry +``` + +在本地查看构建的镜像,现在所有依赖的部署清单和镜像缓存都构建到了集群镜像中。 + +```shell +root@ubuntu:~/cloud-images# + + sealos images +labring/nginx v1.23.2 521c85942ee4 4 minutes ago 56.8 MB +``` + +你可以将镜像推送到任何 Docker 镜像仓库,下面的命令将其推送到 Docker Hub。 + +```shell +sealos push labring/nginx:v1.23.2 +``` + +**注意:** 请使用 sealos 命令操作集群镜像,不支持 Docker 命令。 + +如果你使用私有镜像仓库,只需在拉取或推送镜像之前使用 `sealos login` 命令登录到注册表。 + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +## 五、安装集群镜像 + +然后你可以在你的集群中运行集群镜像。 + +```shell +sealos run labring/nginx:v1.23.2 +``` + +helm 二进制命令将安装到你的 Kubernetes 集群的主节点上。 + +```shell +root@ubuntu:~# helm -n nginx ls +``` + +## 六、说明 + +默认情况下,在构建镜像时,sealos 只解析默认的 values.yml 文件,但是你也可以为 sealos 提供自定义的 values.yaml 文件。 + +**自定义 values 文件必须放在与你的 Chart 相同的目录中,并且必须以 `.values.yaml` 的形式命名,例如 `loki-stack.values.yaml`。** + +```shell +. +├── charts +│ ├── loki-stack +│ │ ├── charts +│ │ ├── Chart.yaml +│ │ ├── README.md +│ │ ├── requirements.lock +│ │ ├── requirements.yaml +│ │ ├── templates +│ │ └── values.yaml +│ └── loki-stack.values.yaml +├── init.sh +├── Kubefile +``` + +`loki-stack.values.yaml` 文件内容如下: + +```shell +$ cat charts/loki-stack.values.yaml +promtail: + enabled: false +fluent-bit: + enabled: true +grafana: + enabled: true +``` + +不同的 values 文件可能会输出不同的镜像列表,以使 sealos 能够在 `sealos build` 过程中自动解析镜像。 + +```shell +$ helm template charts/loki-stack/ -f charts/loki-stack/values.yaml|grep image: + image: "grafana/promtail:2.0.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + +$ helm template charts/loki-stack/ -f charts/loki-stack.values.yaml|grep image: + image: "grafana/fluent-bit-plugin-loki:1.6.0-amd64" + image: "kiwigrid/k8s-sidecar:0.1.209" + image: "grafana/grafana:6.7.0" + image: "grafana/loki:2.0.0" + image: "bats/bats:v1.1.0" + image: bats/bats:v1.1.0 +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md new file mode 100644 index 000000000000..095a373b745d --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-image-list.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 1 +keywords: [集群镜像构建, 镜像列表, Kubefile, 容器镜像, kubectl apply, Calico, 镜像tar包, docker-archive, oci-archive, Sealos] +description: 学习如何使用镜像列表或docker存储的tar包构建Sealos集群镜像。本指南涵盖了Kubefile配置、镜像列表格式和tar包构建方法,助您轻松管理容器化应用。 +--- + +# 构建基于镜像清单的集群镜像 + +本文将指导你如何使用镜像列表构建集群镜像,或使用现有的docker存储的tar包进行构建应用镜像。 + +## 镜像列表 构建 + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── shim +│   └── CalicoImageList +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM labring/kubernetes:v1.24.0 +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +说明: + +CalicoImageList 中的镜像列表将被拉取到本地,然后使用 `kubectl apply -f` 命令将其应用到集群中。 + +镜像列表目前支持: +- docker.io/calico/cni:v3.20.0 这种远程的镜像 +- containers-storage:docker.io/labring/coredns:v0.0.1 这种本地的OCI容器镜像 +- docker-daemon:docker.io/library/nginx:latest 这种本地的docker容器镜像 + + +## 镜像tar包 构建 + +``` +. +├── Kubefile +├── cni +│   ├── custom-resources.yaml +│   └── tigera-operator.yaml +├── images +│   └── skopeo +│   ├── calico.tar +│   └── tar.txt +└── registry + └── docker + └── registry +``` + +```dockerfile +FROM scratch +COPY cni ./cni +COPY images ./images +COPY registry ./registry +CMD ["kubectl apply -f cni/tigera-operator.yaml","kubectl apply -f cni/custom-resources.yaml"] +``` + +说明: + +tar.txt 中的配置会被拉取到本地并重定向镜像列表,然后使用 `kubectl apply -f` 命令将其应用到集群中。 +配置文件格式: + +``` +docker-archive:calico.tar@calico/cni:v3.20.0 +``` + +镜像列表目前支持: +- docker-archive 这种docker存储的镜像,仅支持单个镜像 +- oci-archive 这种oci存储的镜像,仅支持单个镜像 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md new file mode 100644 index 000000000000..35a2c79868f3 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md @@ -0,0 +1,103 @@ +--- +sidebar_position: 2 +keywords: [集群镜像构建, Sealos, Kubernetes, 部署清单, Kubefile, Docker镜像, nginx应用, 镜像推送, 私有镜像仓库] +description: 学习如何使用Sealos构建基于部署清单的Kubernetes集群镜像。本教程涵盖从准备工作到镜像推送的完整流程,包括创建Kubefile和验证镜像等关键步骤。 +--- + +# 构建基于部署清单的集群镜像 + +本文档将详细介绍如何构建基于部署清单(Deployment Manifest)的集群镜像。我们将以一个简单的nginx应用为例来进行说明。 + +## 一、准备工作 + +1. 首先,创建一个基础目录作为构建工作区。 + +```shell +$ mkdir ~/cloud-images +``` + +2. 创建一个名为 `manifests` 的目录来存储 kubernetes nginx 部署 yaml 文件。 + +```shell +$ cd cloud-images +$ mkdir manifests +``` + +## 二、准备清单文件 + +在这个阶段,我们将准备一个简单的nginx kubernetes yaml文件。 + +```shell +$ cat manifests/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 2 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.23.1 + ports: + - containerPort: 80 +``` + +## 三、创建Kubefile + +在这个阶段,我们需要创建一个Kubefile文件,该文件将用于构建镜像。 + +```shell +FROM scratch +COPY manifests manifests +COPY registry registry +CMD ["kubectl apply -f manifests/deployment.yaml"] +``` + +## 四、构建集群镜像 + +在准备好所有必需的文件和目录后,我们可以开始构建集群镜像。 + +```shell +sealos build -t labring/nginx:v1.23.1 . +``` + +**注意:** 在开始构建前,您需要先在本地主机上安装 sealos 命令。 + +构建过程中,您可以查看构建日志。 + +## 五、验证镜像 + +在构建完毕后,可以通过下列命令查看构建的镜像: + +```shell +root@ubuntu:~/cloud-images# sealos images +labring/nginx v1.23.1 521c85942ee4 4 minutes ago 56.8 MB +``` + +## 六、推送镜像 + +最后,我们可以将构建好的镜像推送至任何Docker镜像仓库,以下命令将其推送至DockerHub。 + +```shell +sealos push labring/nginx:v1.23.1 +``` + +**注意:** 请使用 sealos 命令来操作集群镜像,Docker 命令不受支持。 + +如果你使用的是私有镜像仓库,只需要在拉取或推送镜像前使用 `sealos login` 登录仓库即可。 + +```shell +sealos login docker.io -u xxx -p xxx + +sealos login registry.cn-hangzhou.aliyuncs.com -u xxx -p xxx +``` + +至此,基于部署清单的集群镜像已经构建完成。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md new file mode 100644 index 000000000000..0018b0505c01 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md @@ -0,0 +1,129 @@ +--- +sidebar_position: 6 +keywords: [sealos构建集群镜像, sealos exec, sealos scp, 集群镜像构建, Kubefile, openebs mayastor, flannel部署] +description: 学习如何使用sealos exec和scp命令构建自定义集群镜像。本文详细介绍了openebs mayastor和flannel的部署过程,帮助开发者轻松创建高效的Kubernetes集群。 +--- + +# 使用 exec 和 scp 命令构建集群镜像 + +默认情况下,`sealos run xx` 只会在第一个主节点上运行命令和复制文件。当你希望在特定节点或所有节点上运行命令或复制文件时,你可以在构建集群镜像时使用 `sealos exec` 或 `sealos scp` 命令。 + +- sealos exec: 连接到一个或多个节点并运行任何 shell 命令; +- sealos scp: 连接到一个或多个节点并将本地文件复制到远程节点。 + +虽然你可以直接在宿主机上使用这些命令,但本文主要描述的是在使用 sealos build 构建集群镜像时如何使用这两个命令。 + +## sealos exec 示例 + +以下是构建一个 openebs 集群镜像的示例。在安装 openebs maystor 之前,需要在节点上执行一些初始化操作,你可以使用 sealos exec 来实现这一点。 + +首先,创建一个用于构建工作的基础目录。 + +```shell +$ mkdir ~/cloud-images +``` + +创建一个 `charts` 目录,用来存储 kubernetes nginx helm charts 文件。 + +```shell +$ cd cloud-images +``` + +创建一个名为 `Kubefile` 的文件,用于镜像构建: + +```shell +$ cat Kubefile +FROM scratch +COPY manifests manifests +COPY registry registry +COPY opt opt +COPY mayastor.sh mayastor.sh +CMD ["bash mayastor.sh"] +``` + +创建一个名为 `mayastor.sh` 的脚本文件,sealos exec 后面的 shell 命令将在所有节点上执行(在所有节点上创建 hugepage、加载内核模块),但其他命令只会在主节点上运行。 + +```shell +$ cat mayastor.sh +#!/usr/bin/env bash +set -e + +sealos exec " +echo vm.nr_hugepages = 1024 | sudo tee -a /etc/sysctl.d/mayastor.conf +sysctl -p +sudo modprobe -- nbd +sudo modprobe -- nvmet +sudo modprobe -- nvmet_rdma +sudo modprobe -- nvme_fabrics +sudo modprobe -- nvme_tcp +sudo modprobe -- nvme_rdma +sudo modprobe -- nvme_loop +cat < /tmp/buildimage/kubeadm.yml < /tmp/buildimage/Kubefile < +Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.24.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +``` + + + +2. 生成 Clusterfile 后,然后更新集群配置。例如,要修改 pods 的 CIDR 范围,就可以修改 `networking.podSubnet` 和 `spec.data.spec.calicoNetwork.ipPools.cidr` 字段。最终的 Clusterfile 会像是这样: + +
+Clusterfile + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + creationTimestamp: null + name: default +spec: + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +status: {} +--- +apiVersion: kubeadm.k8s.io/v1beta2 +kind: ClusterConfiguration +networking: + podSubnet: 10.160.0.0/12 +--- +apiVersion: apps.sealos.io/v1beta1 +kind: Config +metadata: + name: calico +spec: + path: charts/calico/values.yaml + strategy: merge + data: | + installation: + enabled: true + kubernetesProvider: "" + calicoNetwork: + ipPools: + - blockSize: 26 + cidr: 10.160.0.0/12 + encapsulation: IPIP + natOutgoing: Enabled + nodeSelector: all() + nodeAddressAutodetectionV4: + interface: "eth.*|en.*" +``` + +
+ +3. 运行 `sealos apply -f Clusterfile` 启动集群。集群运行成功后会把 Clusterfile 保存到 `.sealos/default/Clusterfile` 文件中,可以修改其中字段来重新 apply 对集群进行变更。 + +**注意:** + +- 可以参考[官方文档](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-config/)或运行 `kubeadm config print init-defaults` 命令来打印 kubeadm 配置。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md new file mode 100644 index 000000000000..34917a878b49 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/run-cluster.md @@ -0,0 +1,16 @@ +--- +sidebar_position: 0 +keywords: [Sealos集群, 集群初始化, 节点管理, 服务管理, 实战示例] +description: 了解如何有效运行和管理Sealos集群,包括集群初始化、节点管理、服务管理和实战示例的详细教程和资源。 +--- + +# Sealos 集群镜像运行 + +在这个目录中,我们提供了一系列详细的教程和资源,帮助您有效地运行和管理 Sealos 集群。以下是每个文件或子目录的简短概述: + +1. **集群初始化**:这部分提供了所有必需的步骤,以启动并初始化一个新的 Sealos 集群。它包括如何配置 Sealos,以及如何运行初始化命令等信息。 +2. **节点管理**:这个部分包括如何添加和移除集群节点,以及如何维护和管理集群节点的详细信息。 +3. **服务管理**:这部分包含了如何在 Sealos 集群上部署和管理各种服务的指南。它包括如何使用 Sealos 的命令行工具来管理服务,以及如何配置服务以满足你的特定需求。 +4. **实战示例**:这部分包含了各种实战示例,包括如何运行和管理各种应用,以及如何处理特定的运维问题。 + +我们希望这个目录可以为您在运行和管理 Sealos 集群时提供全方位的帮助。如果有任何问题或建议,欢迎随时联系我们。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md new file mode 100644 index 000000000000..77ec0154b684 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/template-apply-cluster.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 2 +keywords: [Go模板语法, Clusterfile, Kubernetes集群部署, sealos, 自定义values文件] +description: 了解如何使用Go模板语法编写Clusterfile并通过sealos部署Kubernetes集群,包含自定义values文件示例和详细步骤。 +--- + +# 模板配置安装 + +我们也可以使用 Go 模版语法来编写 Clusterfile(就像 [Helm](https://helm.sh/) 一样,但暂不支持部分模版函数,如 `include`/`tpl`/`require`/`lookup`)。 例如,创建 `Clusterfile.yaml` 如下: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: {{ .Values.clusterName }} +spec: + hosts: + - ips: {{ .Values.masters | toYaml | nindent 8 }} + roles: ["master", "amd64"] + {{- with .Values.nodes }} + - ips: {{ . | toYaml | nindent 8 }} + roles: ["node", "amd64"] + {{- end }} + image: {{ .Values.images | toYaml | nindent 4 }} + ssh: + passwd: {{ env "SSH_PASSWORD" .Values.ssh.passwd }} + pk: {{ default "~/.ssh/id_rsa" .Values.ssh.pk }} + port: {{ default 22 .Values.ssh.port | int }} + user: {{ default "root" .Values.ssh.user }} +--- +apiVersion: kubeadm.k8s.io/v1beta3 +kind: ClusterConfiguration +networking: + dnsDomain: {{ default "cluster.local" .Values.networking.dnsDomain }} + serviceSubnet: {{ default "10.96.0.0/18" .Values.networking.serviceSubnet }} + podSubnet: {{ default "100.64.0.0/17" .Values.networking.podSubnet }} +``` + +随后,创建一个自定义的 values 文件 `example.values.yaml`: + +```yaml +clusterName: default +images: + - dockerhub.tencentcloudcr.com/labring/kubernetes:v1.23.8 + - dockerhub.tencentcloudcr.com/labring/calico:v3.24.1 +masters: + - 10.74.16.27:22 + - 10.74.16.140:22 + - 10.74.16.101:22 +nodes: [] +ssh: + # passwd: notSetYet + pk: /path/to/private/key/file + port: 22 + user: root +networking: + dnsDomain: cluster.local + serviceSubnet: 10.96.0.0/18 + podSubnet: 100.64.0.0/17 +``` + +然后就可以像这样部署集群了: + +```shell +$ sealos apply -f Clusterfile.yaml --values example.values.yaml --set clusterName=testlocal --env SSH_PASSWORD=s3cret +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md new file mode 100644 index 000000000000..878caae46bb2 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/operations/run-cluster/upgrade-cluster.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 3 +keywords: [Kubernetes升级, sealos, 集群管理, Kubernetes版本, Kubernetes集群] +description: 了解如何使用sealos命令轻松升级Kubernetes集群,确保集群稳定运行并保持最新版本。 +--- + +# 如何升级集群 + +如果你想要升级你的 Kubernetes 集群,你只需要运行以下命令: + +```sh +sealos run labring/kubernetes:<新版本号> +``` + +确保你已经建立了集群。 + +## 实例说明 + +1. 假设你已经运行过以下命令: + +```sh +sealos run labring/kubernetes:v1.24.0 labring/calico:v3.22.1 --masters 192.168.64.8 --nodes 192.168.64.7 +``` + +2. 现在你想要升级集群到 v1.25.0,你可以这样操作: + +```sh +sealos run labring/kubernetes:v1.25.0 +``` + +在运行到 'kubeadm upgrade v1.25.0' 的时候,你将看到: + +```txt +[upgrade/version] You have chosen to change the cluster version to "v1.25.0" +[upgrade/versions] Cluster version: v1.24.0 +[upgrade/versions] kubeadm version: v1.25.0 +[upgrade] Are you sure you want to proceed? [y/N]: +``` + +输入 'y' 来继续升级。 + +如果**出现错误**,你可以再次运行命令 'sealos run labring/kubernetes:v1.25.0'。即使失败,它也能保证得到相同的结果。 + +## 注意事项 + +1. **升级不能跨过次版本号**。比如从 'v1.23.0' 升级到 'v1.25.0' 是不允许的。如果你确实需要从 'v1.23.0' 升级到 'v1.25.0',你可以分成两步来操作,比如先从 'v1.23.0' 升级到 'v1.24.0',然后再从 'v1.24.0' 升级到 'v1.25.0'。 + +2. 一旦升级成功,集群挂载的旧版本镜像就会被替换。添加主节点或工作节点将会应用新版本。 + +这就是升级 Kubernetes 集群的整个过程。如果你在升级过程中遇到任何问题,不要犹豫,尽快查阅相关文档或者寻求帮助。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md new file mode 100644 index 000000000000..fc3f1066a6b9 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 3 +keywords: [Ingress集群镜像, sealos, helm, nginx-ingress, 镜像缓存代理, Dockerfile, 集群镜像构建, 镜像列表, 镜像registry] +description: 学习如何使用sealos和helm构建Ingress集群镜像,包括下载chart、添加镜像列表、编写Dockerfile、构建和推送镜像,以及运行集群镜像的完整流程。 +--- + +# 构建一个 Ingress 集群镜像 + +这里展示了如何用 helm 构建一个 nginx-ingress 集群镜像。 + +## 下载 helm chart + +```shell +$ mkdir ingress-nginx && cd ingress-nginx +$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +$ helm pull ingress-nginx/ingress-nginx +``` + +随后就能找到下载的 chart: + +```shell +$ ls +ingress-nginx-4.1.0.tgz +``` + +## 添加镜像列表 + +sealos 会下载镜像列表中的镜像并缓存到 registry 目录。 + +目录必须形如 `images/shim/[your image list filename]`: + +```shell +$ cat images/shim/nginxImages +k8s.gcr.io/ingress-nginx/controller:v1.2.0 +k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1 +``` + +## 编写 Dockerfile + +```Dockerfile +FROM scratch +COPY ../examples . +CMD ["helm install ingress-nginx ingress-nginx-4.1.0.tgz --namespace ingress-nginx --create-namespace"] +``` + +## 构建集群镜像 + +```shell +$ sealos build -f Dockerfile -t docker.io/fanux/ingress-nginx:v1.2.0 . +``` + +sealos 在构建的时候会自动添加镜像列表中的镜像依赖到集群镜像中,通过神奇的方式保存了里面依赖的 Docker 镜像。 +并且在到别的环境中运行的时候更神奇的自动检测集群中是否有 Docker 镜像,有的话自动下载,没有的话才会去 k8s.gcr.io 下载。 +用户无需修改 helm chart 中的 docker 镜像地址,这里用到了镜像缓存代理的黑科技。 + +## 推送到镜像 registry + +```shell +$ sealos login docker.io +$ sealos push docker.io/fanux/ingress-nginx:v1.2.0 +``` + +## 运行集群镜像 + +```shell +$ sealos run docker.io/fanux/ingress-nginx:v1.2.0 +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md new file mode 100644 index 000000000000..df4cc7b4a1ff --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md @@ -0,0 +1,194 @@ +--- +sidebar_position: 2 +keywords: [Kubernetes安装, Sealos, K8s集群部署, 离线安装K8s, Containerd, 集群镜像, 高可用K8s, 节点管理] +description: 使用Sealos快速部署Kubernetes集群,支持在线和离线安装,适用于amd64和arm64架构。轻松管理节点,安装分布式应用,支持Containerd和Docker运行时。 +--- + +# 安装 K8s 集群 + +Sealos 支持安装 `amd64` 和 `arm64` 架构的 K8s 集群。 + +## 先决条件 + +首先需要[下载 Sealos 命令行工具](/developer-guide/lifecycle-management/quick-start/install-cli.md),sealos 是一个简单的 Golang 二进制文件,可以安装在大多数 Linux 操作系统中。 + +以下是一些基本的安装要求: + ++ 每个集群节点应该有不同的主机名。主机名不要带下划线。 ++ 所有节点的时间需要同步。 ++ 需要在 K8s 集群的**第一个 master 节点**上运行 `sealos run` 命令,目前**集群外的节点不支持集群安装**。 ++ 建议使用干净的操作系统来创建集群。**不要自己装 Docker!** ++ 支持大多数 Linux 发行版,例如:Ubuntu、CentOS、Rocky linux。 ++ 支持 [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags) 中的所有 Kubernetes 版本。 ++ 支持使用 Containerd 作为容器运行时。 ++ 在公有云上安装请使用**私有 IP**。 + +## 查看集群镜像 + +Sealos 所有的集群镜像都可以在 [cluster-image-docs](https://github.com/labring-actions/cluster-image-docs) 仓库里找到。除了推送到 Docker Hub 之外,这些镜像还被同步到了阿里云的镜像仓库。 + +Docker Hub 上可以通过以下链接查看 Sealos 所有的集群镜像:[https://hub.docker.com/u/labring](https://hub.docker.com/u/labring). + +使用 [Registry Explorer](https://explore.ggcr.dev/) 可以查看 K8s 集群镜像的所有版本,直接输入 `registry.cn-shanghai.aliyuncs.com/labring/kubernetes`,然后点击 “Submit Query”: + +![](images/registry-explorer.png) + +就会看到这个集群镜像的所有 tag。 + +Docker Hub 同理,输入 `docker.io/labring/kubernetes` 即可查看所有 tag。 + +:::info注意 + +K8s 的小版本号越高,集群越稳定。例如 v1.28.x,其中的 x 就是小版本号。建议使用小版本号比较高的 K8s 版本。到本文截止时间为止,v1.27 最高的版本号是 v1.27.7,而 v1.28 最高的版本号是 v1.28.3,所以**建议使用 v1.27.7**。你需要根据实际情况来选择最佳的 K8s 版本 + +::: + +## 安装 K8s 单机版 + +```shell +# sealos version must >= v4.1.0 +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 registry.cn-shanghai.aliyuncs.com/labring/cilium:v1.13.4 --single +``` + +## 安装 K8s 集群 + +```shell +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 registry.cn-shanghai.aliyuncs.com/labring/cilium:v1.13.4 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 \ + --nodes 192.168.64.21,192.168.64.19 -p [your-ssh-passwd] +``` + +注意:labring/helm 应当在 labring/cilium 之前。 + +参数说明: + +| 参数名 | 参数值示例 | 参数说明 | +| --- | --- | --- | +| --masters | 192.168.0.2 | K8s master 节点地址列表 | +| --nodes | 192.168.0.3 | K8s node 节点地址列表 | +| --ssh-passwd | [your-ssh-passwd] | ssh 登录密码 | +|kubernetes | labring/kubernetes:v1.25.0 | K8s 集群镜像 | + +在干净的服务器上直接执行上面命令,不要做任何多余操作即可安装一个高可用 K8s 集群。 + +## 安装各种分布式应用 + +```shell +sealos run registry.cn-shanghai.aliyuncs.com/labring/helm:v3.9.4 # install helm +sealos run registry.cn-shanghai.aliyuncs.com/labring/openebs:v3.9.0 # install openebs +sealos run registry.cn-shanghai.aliyuncs.com/labring/minio-operator:v4.5.5 registry.cn-shanghai.aliyuncs.com/labring/ingress-nginx:4.1.0 +``` + +这样高可用的 Minio 等应用都有了,不用关心所有的依赖问题。 + +## 增加 K8s 节点 + +增加 node 节点: + +```shell +$ sealos add --nodes 192.168.64.21,192.168.64.19 +``` + +增加 master 节点: + +```shell +$ sealos add --masters 192.168.64.21,192.168.64.19 +``` + +## 删除 K8s 节点 + +删除 node 节点: + +```shell +$ sealos delete --nodes 192.168.64.21,192.168.64.19 +``` + +删除 master 节点: + +```shell +$ sealos delete --masters 192.168.64.21,192.168.64.19 +``` + +## 清理 K8s 集群 + +```shell +$ sealos reset +``` + + + +## 离线安装 K8s + +离线环境只需要提前导入镜像,其它步骤与在线安装一致。 + +首先在有网络的环境中导出集群镜像: + +```shell +$ sealos pull registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +$ sealos save -o kubernetes.tar registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 +``` + +### 导入镜像并安装 + +将 kubernetes.tar 拷贝到离线环境, 使用 load 命令导入镜像即可: + +```shell +$ sealos load -i kubernetes.tar +``` + +剩下的安装方式与在线安装的步骤一致: + +```shell +$ sealos images # 查看集群镜像是否导入成功 +$ sealos run registry.cn-shanghai.aliyuncs.com/labring/kubernetes:v1.27.7 # 单机安装,集群安装同理 +``` + +### 快速启动 K8s 集群 + +也可以不用 load 命令导入镜像,直接使用以下命令即可安装 K8s: + +```shell +$ sealos run kubernetes.tar # 单机安装,集群安装同理 +``` + +## 集群镜像版本支持说明 + +### 支持 Containerd 的 K8s + +推荐使用 Containerd 作为容器运行时 (CRI) 的集群镜像版本,Containerd 是一种轻量级、高性能的容器运行时,与 Docker 兼容。使用 Containerd 的 Kubernetes 镜像可以提供更高的性能和资源利用率。以下是支持 Containerd 的集群镜像版本支持说明: + +| K8s 版本 | Sealos 版本 | CRI 版本 | 集群镜像版本 | +|----------|-------------------| -------- | -------------------------- | +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes:v1.28.0 | +| `>=1.30` | `>=v5.1.0` | v1 | labring/kubernetes:v1.28.0 | + +根据 Kubernetes 版本的不同,您可以选择不同的 Sealos 版本和 CRI 版本。例如,如果您要使用 Kubernetes v1.26.0 版本,您可以选择 sealos v4.1.4-rc3 及更高版本,并使用 v1 CRI 版本。 + +### 支持 Docker 的 K8s + +当然,你也可以选择使用 Docker 作为容器运行时,以下是支持 Docker 的集群镜像版本支持说明: + +| K8s 版本 | Sealos 版本 | CRI 版本 | 集群镜像版本 | +|----------|-------------------| -------- | --------------------------------- | +| `<1.25` | `>=v4.0.0` | v1alpha2 | labring/kubernetes-docker:v1.24.0 | +| `>=1.25` | `>=v4.1.0` | v1alpha2 | labring/kubernetes-docker:v1.25.0 | +| `>=1.26` | `>=v4.1.4-rc3` | v1 | labring/kubernetes-docker:v1.26.0 | +| `>=1.27` | `>=v4.2.0-alpha3` | v1 | labring/kubernetes-docker:v1.27.0 | +| `>=1.28` | `>=v5.0.0` | v1 | labring/kubernetes-docker:v1.28.0 | +| `>=1.30` | `>=v5.1.0` | v1 | labring/kubernetes-docker:v1.28.0 | + +与支持 Containerd 的 Kubernetes 镜像类似,您可以根据 Kubernetes 版本的不同选择不同的 Sealos 版本和 CRI 版本。例如,如果您要使用 Kubernetes v1.26.0 版本,您可以选择 sealos v4.1.4-rc3 及更高版本,并使用 v1 CRI 版本。 + +### 支持 Containerd 的 k3s + +| K3s 版本 | Sealos 版本 | 集群镜像版本 | +|----------|------------|---------------------| +| `>=1.24` | `>=v5.0.0` | labring/k3s:v1.24.0 | + +## 总结 + +您可以根据自己的需求和偏好,在不同的镜像类型和版本中进行选择。同时,不要忘记查看 [更新日志](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md),以了解各个版本的更新内容和修复问题。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png new file mode 100644 index 000000000000..a9e28004026c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/images/registry-explorer.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md new file mode 100644 index 000000000000..59a788229224 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 1 +keywords: [Sealos命令行工具, Sealos安装, Kubernetes集群部署, 二进制下载, 包管理工具安装, 源码安装, 版本选择, Linux系统] +description: 本文详细介绍了如何下载和安装Sealos命令行工具,包括版本选择、二进制下载、包管理工具安装和源码安装等多种方法,助您快速部署Kubernetes集群。 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# 下载 Sealos 命令行工具 + +你可以通过运行命令来获取版本列表: + +```shell +$ curl --silent "https://api.github.com/repos/labring/sealos/releases" | jq -r '.[].tag_name' +``` + +> 注意:在选择版本时,建议使用稳定版本例如 `v4.3.0`。像 `v4.3.0-rc1`、`v4.3.0-alpha1` 这样的版本是预发布版,请谨慎使用。 + +设置 `VERSION` 环境变量为 latest 版本号,或者将 `VERSION` 替换为您要安装的 Sealos 版本: + +```shell +$ VERSION=`curl -s https://api.github.com/repos/labring/sealos/releases/latest | grep -oE '"tag_name": "[^"]+"' | head -n1 | cut -d'"' -f4` +``` + +## 二进制自动下载 + +```shell + +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/main/scripts/install.sh | PROXY_PREFIX=https://mirror.ghproxy.com sh -s ${VERSION} labring/sealos + +``` + +## 二进制手动下载 + + + + +```shell +$ wget https://mirror.ghproxy.com/https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_amd64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +```shell +$ wget https://mirror.ghproxy.com/https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_arm64.tar.gz \ + && tar zxvf sealos_${VERSION#v}_linux_arm64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin +``` + + + + +## 包管理工具安装 + +### DEB 源 + +```shell +$ echo "deb [trusted=yes] https://apt.fury.io/labring/ /" | sudo tee /etc/apt/sources.list.d/labring.list +$ sudo apt update +$ sudo apt install sealos +``` + +### RPM 源 + +```shell +$ sudo cat > /etc/yum.repos.d/labring.repo << EOF +[fury] +name=labring Yum Repo +baseurl=https://yum.fury.io/labring/ +enabled=1 +gpgcheck=0 +EOF +$ sudo yum clean all +$ sudo yum install sealos +``` + +## 源码安装 + +### 前置依赖 +1. `linux` +2. `git` +3. `golang` 1.20+ +4. `libgpgme-dev libbtrfs-dev libdevmapper-dev` + +如果在 `arm64` 环境下需要添加 `:arm64` 后缀。 + +### 构建 + +```shell +# git clone the repo +$ git clone https://github.com/labring/sealos.git +# just make it +$ make build BINS=sealos +``` + +## 下一步 + +[安装 K8s 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md)。 + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json new file mode 100644 index 000000000000..950b839be0aa --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 3 +} diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md new file mode 100644 index 000000000000..fae5196d188d --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/image-cri-shim.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 3 +keywords: [image-cri-shim, Kubernetes, 容器运行时, CRI, kubelet, 镜像自动识别, 容器部署, 中间件, 镜像仓库] +description: image-cri-shim 使用指南,简化Kubernetes容器部署,自动识别镜像名称,提高操作便利性,支持CRI API v1alpha2和v1。 +--- + +# image-cri-shim 使用指南 + +## 工作原理 + +image-cri-shim 是一个基于 CRI (Container Runtime Interface) 和 kubelet 的 gRPC (Google Remote Procedure Call) shim。CRI 是 Kubernetes 中用于与容器运行时进行交互的接口,而 kubelet 是负责维护容器运行状态和节点级别的资源管理的 Kubernetes 组件。 + +image-cri-shim 的主要功能是自动识别镜像名称,让用户在使用 Kubernetes 部署容器时无需手动指定镜像名称。这样可以降低用户的操作难度,提高部署容器的便利性。 + +在实际使用中,image-cri-shim 可以作为一个中间件,接收来自 kubelet 的请求,然后将请求转发给容器运行时。通过自动识别镜像名称,image-cri-shim 可以简化容器镜像的部署流程,减轻用户的操作负担。 + +``` ++------------+ +----------------+ +-------------------+ +| User | | Kubelet | | image-cri-shim | +| (Kubernetes| | (Node agent) | | (Middleware) | +| Manifest) | | | | | ++-----+------+ +-------+--------+ +-------+-----------+ + | | | + | YAML Manifest | | + |---------------> | | + | | | + | | | + | | CRI Request | + | |------------------------> | + | | | + | | Image Name | + | | Auto-Recognition | + | | | + | | | + | | CRI Response | + | | <------------------------+ + | | | + | | | + | Container | | + | Deployment | | + | <----------------------| | + | | | + | | | ++------------+ +-------+--------+ +-------+-----------+ + +``` + +从上述流程图可以看出,用户创建一个包含容器信息的 Kubernetes YAML 清单,然后将该清单提交给 kubelet。kubelet 是 Kubernetes 节点上的代理,负责管理容器。 +接着,kubelet 将 CRI 请求发送给 image-cri-shim 中间件。image-cri-shim 的主要任务是自动识别镜像名称,它会处理这个 CRI 请求并获取相关的镜像信息。当 image-cri-shim 识别到镜像名称后,它会将 CRI 响应返回给 kubelet。 + +最后,kubelet 使用从 image-cri-shim 获取的镜像名称来部署容器。这个过程对用户是透明的,用户无需手动指定镜像名称,从而简化了容器部署流程并提高了便利性。 + +## 架构图 + +image-cri-shim 的架构如下图所示: + +![](images/image-cri-shim.png) + + +## 使用说明 + +```yaml +shim: /var/run/image-cri-shim.sock +cri: /run/containerd/containerd.sock +address: http://sealos.hub:5000 +force: true +debug: true +timeout: 15m +auth: admin:passw0rd + +registries: +- address: http://172.18.1.38:5000 + auth: admin:passw0rd +``` +这段配置文件是一个用于设置 image-cri-shim 的 YAML 格式文件。配置文件中包含了一些关键的参数,以下是每个参数的解释: + +1. shim: 指定 image-cri-shim 的 UNIX 套接字文件路径。这个路径用于与 kubelet 之间的通信。 +2. cri: 指定容器运行时(如 containerd)的 UNIX 套接字文件路径。image-cri-shim 会使用这个路径与容器运行时进行通信。 +3. address: 定义镜像仓库的地址。在本例中,镜像仓库地址为 http://sealos.hub:5000。 +4. force: 设置为 true 时,image-cri-shim 会在强制启动shim,无需等待cri启动后启动。 +5. debug: 设置为 true 时,启用调试模式,输出更多的日志信息。 +6. timeout: 定义镜像操作的超时时间。在本例中,超时时间为 15 分钟(15m)。 +7. auth: 定义用于访问镜像仓库的身份验证凭据。在本例中,用户名为 admin,密码为 passw0rd。 + +此外,配置文件还包含了一个 registries 列表,用于定义其他镜像仓库及其身份验证凭据。在这个例子中,只有一个其他仓库: +- address: 该仓库的地址为 http://172.18.1.38:5000。 +- auth: 用于访问该仓库的身份验证凭据。在本例中,用户名为 admin,密码为 passw0rd。 +这个配置文件为 image-cri-shim 提供了所需的信息,以便正确地与 kubelet 和容器运行时(如 containerd)进行通信,以及访问和管理镜像仓库。 + +注意: image-cri-shim 能够同时兼容 CRI API v1alpha2 和 v1。 + +### 管理服务 + +image-cri-shim 通常作为一个系统服务运行。要管理 image-cri-shim,您可以使用系统服务管理工具(如 systemctl)来启动、停止、重启或查看服务状态。首先,确保您已经正确地安装了 image-cri-shim 并将其配置为一个系统服务。 + +1. 启动服务: `systemctl start image-cri-shim` +2. 停止服务: `systemctl stop image-cri-shim` +3. 重启服务: `systemctl restart image-cri-shim` +4. 查看服务状态: `systemctl status image-cri-shim` + +### 日志管理 + +要查看 image-cri-shim 服务的日志,您可以使用 journalctl 命令。journalctl 是一个用于查询和显示系统日志的工具,它与 systemd 服务管理器一起使用。 + +以下是使用 journalctl 查看 image-cri-shim 服务日志的命令: + +```shell +journalctl -u image-cri-shim +``` + +这将显示 image-cri-shim 服务的全部日志。如果您希望实时查看日志,可以添加 -f 参数: + +```shell +journalctl -u image-cri-shim -f +``` + +此外,您还可以根据时间过滤日志。例如,如果您只想查看过去一小时的日志,可以使用以下命令: + +```shell +journalctl -u image-cri-shim --since "1 hour ago" +``` + +这些命令应该能帮助您查看和分析 image-cri-shim 服务的日志,从而更好地了解服务的运行状态和可能出现的问题。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png new file mode 100644 index 000000000000..5c8d5f81b23a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/Image-cri-shim/images/image-cri-shim.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/_category_.json b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/_category_.json new file mode 100644 index 000000000000..e47d9e07a6d5 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 99 +} \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/images/01.webp b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/images/01.webp new file mode 100644 index 000000000000..0cc05e478e9e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/images/01.webp differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/lvscare.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/lvscare.md new file mode 100644 index 000000000000..8d3f804c5a3c --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/lvscare/lvscare.md @@ -0,0 +1,79 @@ +--- +keywords: [LVScare, Sealos, Kubernetes高可用性, IPVS负载均衡, 健康检查工具] +description: LVScare是一款基于IPVS的轻量级负载均衡和健康检查工具,与Sealos集成,提升Kubernetes集群的高可用性和稳定性。 +--- + +# LVScare 使用指南 + +## 介绍 + +LVScare是一款基于IPVS技术的轻量级负载均衡和健康检查工具,可以实时监控和管理后端服务。Sealos,一个云操作系统,它的生命周期管理是一个基于kubeadm的Kubernetes HA安装工具,适用于在任何Linux系统中安装和升级高可用Kubernetes集群。两者结合能实现Kubernetes master节点的高可用性。Sealos利用其生命周期管理能力,通过配合LVScare的轻量级,0依赖,高可用的特性,可以非常有效地保证Kubernetes集群的稳定性和可靠性。 + +### LVScare的工作原理与特点 + +LVScare通过IPVS实时监控后端服务(real servers)的健康状态。如果某个服务变得不可用,LVScare会立即将其权重设为0(用于TCP优雅终止),并在下一次检查期间从服务列表中移除。服务恢复正常后,LVScare会自动将其重新加入到服务列表。LVScare的这种设计使得它具备轻量级,0依赖,高可用的特性。占用资源少,稳定可靠,类似于kube-proxy的实现,可以通过ipvs实现的localLB保证服务的持续可用。 + +## Sealos与LVScare的集成 + +在Sealos中,我们利用了官方推荐的静态Pod的方式,自动配置和管理LVScare,以实现Kubernetes集群的高可用性。Sealos会在安装Kubernetes集群的过程中自动使用LVScare进行master节点的健康检查和负载均衡。这意味着,即使某个master节点出现故障,也不会影响Kubernetes集群的整体功能。 + +![](images/01.webp) + +### 如何运行LVScare静态Pod + +首先,通过`sealctl static-pod`命令生成LVScare的配置,并将其放入`/etc/kubernetes/manifests`目录。这样,Kubernetes集群中的所有节点都能获取到这份配置。 + +```bash +lvscare care --vs 10.103.97.12:6443 --rs 192.168.0.2:6443 --rs 192.168.0.3:6443 --rs 192.168.0.4:6443 --interval 5 --mode route +``` + +### 生成和调整静态Pod配置 + +在需要在每个节点上启动LVScare时,我们可以使用以下命令生成静态Pod的配置: + +```bash +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168 + +.0.3:6443 --print +``` + +当master节点有变化时,Sealos只需重新执行`sealctl static-pod`命令即可调整master节点,简化了维护静态Pod的逻辑。在集群join节点之前,我们需要调用`sealctl ipvs`(该命令直接调用LVScare sdk)手动启动IPVS规则,维护好IPVS集群。节点join成功后,Kubernetes的静态Pod就可以接管IPVS规则了。 + +## 使用LVScare的优势 + +### 高可用性 + +结合LVScare,Sealos可以实现Kubernetes集群master节点的高可用性。 + +### 健康检查机制 + +LVScare的健康检查机制可以及时发现并处理问题,防止单节点故障引发的更大问题。 + +### 无缝集成 + +作为Sealos中的静态Pod,LVScare可以与Kubernetes集群其他部分无缝集成。 + +### 简化运维 + +Sealos自动配置和管理LVScare,大大简化了Kubernetes集群的运维工作。 + +## LVScare使用示例 + +请注意,所有的real server需要在同一主机上监听,并设置为`route`模式。然后,你可以在前台运行LVScare。例如: + +```bash +docker run -p 8081:80 --name echoserver1 -d cilium/echoserver +docker run -p 8082:80 --name echoserver2 -d cilium/echoserver +docker run -p 8083:80 --name echoserver3 -d cilium/echoserver +lvscare care --vs 169.254.0.1:80 --rs 127.0.0.1:8081 --rs 127.0.0.1:8082 --rs 127.0.0.1:8083 --logger DEBG --health-schem http --health-path / +``` + +## 清理 + +最后,你可以使用以下命令进行清理: + +```bash +lvscare care --vs 169.254.0.1:80 --logger DEBG -C +``` + +结论:LVScare是一款基于IPVS的轻量级负载均衡和健康检查工具,它能与Sealos无缝集成,极大地提高了Kubernetes集群的可用性和性能。试一试,看看LVScare如何帮助你更好地管理你的Kubernetes集群! diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/_category_.json b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/_category_.json new file mode 100644 index 000000000000..a788e786e6c5 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 2 +} diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cert.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cert.md new file mode 100644 index 000000000000..77d295bad8a7 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cert.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 1 +keywords: [Kubernetes证书管理, sealctl cert命令, Kubernetes集群安全, TLS加密, Kubernetes证书生成] +description: 了解如何使用sealctl cert命令生成Kubernetes集群所需的证书,确保组件之间的安全通信和数据加密。 +--- + + +# cert 证书管理 + +`cert` 命令用于生成 Kubernetes 集群所需的证书文件。在 Kubernetes 集群中,证书用于确保组件之间的通信安全,例如 API server、kubelet 和 etcd 等。证书通过 TLS(Transport Layer Security)协议实现加密,以确保数据在传输过程中的保密性和完整性。 + +`sealctl cert` 命令可以根据提供的参数自动生成证书。这些参数包括节点 IP、节点名称、服务 CIDR、DNS 域以及可选的其他备用名称。通过生成并配置这些证书,您可以确保 Kubernetes 集群的安全通信。 + + + +``` +cert 命令用于生成 Kubernetes 证书。 + +参数: + --alt-names 备用名称,例如 sealos.io 或 10.103.97.2。可以包含多个备用名称。 + --node-name 节点名称,例如 master0。 + --service-cidr 服务网段,例如 10.103.97.2/24。 + --node-ip 节点的 IP 地址,例如 10.103.97.2。 + --dns-domain 集群 DNS 域,默认值为 cluster.local。 + --cert-path Kubernetes 证书文件路径,默认值为 /etc/kubernetes/pki。 + --cert-etcd-path Kubernetes etcd 证书文件路径,默认值为 /etc/kubernetes/pki/etcd。 + +示例: + sealctl cert --alt-names sealos.io --alt-names 10.103.97.2 \ + --node-name master0 --service-cidr 10.103.97.2/24 \ + --node-ip 10.103.97.2 --dns-domain cluster.local + +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cri.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cri.md new file mode 100644 index 000000000000..ba3dbec55204 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/cri.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 2 +keywords: [Kubernetes, CRI, 容器管理, sealctl, cgroup驱动] +description: 了解如何使用sealctl命令管理和检查Kubernetes集群中的CRI容器运行时环境,确保其正确配置和正常运行。 +--- + +# CRI 容器管理 + +`cri` 命令是用于管理和检查 Kubernetes 集群中的容器运行时(Container Runtime Interface,CRI)环境。容器运行时是负责运行容器的底层技术,如 Docker、containerd 或者 CRI-O 等。在 Kubernetes 中,容器运行时用于启动、停止和管理容器,以支持集群中的工作负载。 + +`sealctl cri` 命令提供了一组子命令,使您能够执行与容器运行时相关的各种操作,例如检查运行时是否是 Docker、是否正在运行,列出 Kubernetes 容器,删除容器,拉取镜像,检查镜像是否存在以及获取 CGroup 驱动信息等。 + +通过使用 `sealctl cri` 命令,您可以轻松地管理和检查 Kubernetes 集群中的容器运行时环境,确保其正确配置和正常运行。 + + + +```shell +sealctl cri [flags] +``` + + + +子命令: + +1. `socket`:检测 CRI 套接字。 + +```shell +sealctl cri socket +``` + +2. `cgroup-driver`:获取容器运行时的 cgroup 驱动。 + +```shell +sealctl cri cgroup-driver [--short] +``` + +- `--short`:仅打印结果。 + +全局参数: + +- `--socket-path`:CRI 套接字路径。 +- `--config`:CRI 配置文件。 + +示例: + +```shell +sealctl cri socket +sealctl cri cgroup-driver --short + +``` \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hostname.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hostname.md new file mode 100644 index 000000000000..d407e6f33b31 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hostname.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 3 +keywords: [sealctl, 主机名, 获取主机名, 操作系统, 命令行工具] +description: 使用sealctl命令行工具获取操作系统的主机名,无需传递任何参数,简单高效。 +--- + + +# hostname 获取主机名 + +获取操作系统的主机名: + +```shell +sealctl hostname +``` + +示例: + +```shell +sealctl hostname +``` + +执行此命令将返回操作系统的主机名。无需传递任何参数。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hosts.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hosts.md new file mode 100644 index 000000000000..97f1d8d112d8 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/hosts.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 4 +keywords: [hosts管理, sealctl命令, hosts文件, 域名解析, IP地址映射] +description: 了解如何使用sealctl命令管理操作系统的hosts文件,包括列出、添加和删除域名与IP地址映射的详细步骤。 +--- + +# hosts 管理 + +`hosts` 命令是用于管理操作系统的 hosts 文件。hosts 文件是一个用于解析域名到 IP 地址的文件,通常在本地系统中用于覆盖 DNS 解析。通过修改 hosts 文件,您可以为一个特定的域名分配一个自定义的 IP 地址,而不必依赖 DNS 服务器。 + +`sealctl hosts` 提供了以下三个子命令来实现 hosts 文件的管理: + +1. `list`:列出当前 hosts 文件中的所有条目。 +2. `add`:向 hosts 文件中添加一个新的域名与 IP 地址映射。 +3. `delete`:从 hosts 文件中删除一个指定的域名与 IP 地址映射。 + +通过这些子命令,您可以方便地查看、添加和删除 hosts 文件中的映射,从而更好地控制域名到 IP 地址的解析。 + +1. `sealctl hosts list`:列出当前 hosts 文件中的条目。 + + 示例: + + ```shell + sealctl hosts list + ``` + + + +2. `sealctl hosts add`:向 hosts 文件中添加一个新条目。 + + 参数: + + - `--ip`:IP 地址(必填) + - `--domain`:域名(必填) + + 示例: + + ```shell + sealctl hosts add --ip 192.168.1.100 --domain example.com + ``` + +3. `sealctl hosts delete`:从 hosts 文件中删除一个条目。 + + 参数: + + - `--domain`:要删除的域名(必填) + + 示例: + + ```shell + sealctl hosts delete --domain example.com + ``` + +注意:您可以在任何 `hosts` 子命令后面添加 `--path` 参数来指定 hosts 文件的路径。默认路径为 `/etc/hosts`(Linux 系统)。 + +示例: + +```shell +sealctl hosts list --path /custom/path/hosts +``` + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/ipvs.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/ipvs.md new file mode 100644 index 000000000000..7c906c3d44d8 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/ipvs.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 6 +keywords: [IPVS负载均衡, sealctl命令, 虚拟服务器, 真实服务器, 健康检查, 代理模式, 调度算法, 清理功能] +description: 了解如何使用sealctl ipvs命令创建和管理本地IPVS负载均衡,包括虚拟服务器和真实服务器的映射、健康检查、代理模式和调度算法配置。 +--- + +# ipvs 管理 + +`ipvs` 命令用于创建和管理本地的 IPVS 负载均衡。IPVS(IP Virtual Server)是 Linux 内核中的一个模块,它允许在内核空间实现高性能的负载均衡。`ipvs` 命令通过管理虚拟服务器和真实服务器的映射关系,实现对服务的负载均衡。 + +`sealctl ipvs` 支持以下功能: + +1. 创建和管理虚拟服务器 (virtual server) 和真实服务器 (real server) 的映射关系。 +2. 提供健康检查功能,定期检查真实服务器的健康状态,并根据需要对其进行上下线操作。 +3. 支持两种代理模式:`route` 和 `link`。 +4. 支持配置代理调度算法(如轮询、加权轮询等)。 +5. 支持一次性创建代理规则(`--run-once` 标志)或持续运行并管理代理规则。 +6. 支持清理功能:通过 `-C` 或 `--clean` 标志,可以清除现有的 IPVS 规则并退出。 + +通过 `sealctl ipvs` 命令,用户可以轻松地在本地创建和管理高性能的负载均衡服务。 + +**用法** + +```shell +sealctl ipvs [flags] +``` + +**选项** + +- `-C`, `--clean`: 清除现有规则,然后退出。 +- `--health-insecure-skip-verify`: 跳过不安全请求的验证(默认为 true)。 +- `--health-path string`: 用于探测的 URL 路径(默认为 "/healthz")。 +- `--health-req-body string`: 健康检查器发送的请求体。 +- `--health-req-headers stringToString`: HTTP 请求头(默认为 [])。 +- `--health-req-method string`: HTTP 请求方法(默认为 "GET")。 +- `--health-schem string`: 探测器的 HTTP 方案(默认为 "https")。 +- `--health-status ints`: 有效状态码。 +- `-h`, `--help`: ipvs 帮助。 +- `-i`, `--iface string`: 要创建的虚拟接口的名称,与 kube-proxy 的行为相同(默认为 "lvscare")。仅在 mode=link 时启用。 +- `--interval durationOrSecond`: 健康检查间隔(默认为 0s)。 +- `--ip ip`: 作为路由网关的目标 IP,与 route 模式一起使用。 +- `--logger string`: 日志级别:DEBG/INFO(默认为 "INFO")。 +- `--masqueradebit int`: IPTables masquerade 位。仅在 mode=link 时启用。 +- `--mode string`: 代理模式:route/link(默认为 "route")。 +- `--rs strings`: 真实服务器地址,例如 192.168.0.2:6443。 +- `--run-once`: 创建代理规则并退出。 +- `--scheduler string`: 代理调度器(默认为 "rr")。 +- `--vs string`: 虚拟服务器地址,例如 169.254.0.1:6443。 + +**全局选项** + +- `--debug`: 启用调试日志。 +- `--show-path`: 启用显示代码路径。 + +**使用文档** + +要使用 `sealctl ipvs` 命令,请按照以下步骤操作: + +1. 为命令提供必要的选项和参数。 +2. 执行命令,将创建或管理本地 IPVS 负载均衡。 + +**示例** + +创建代理规则并退出: + +```shell +sealctl ipvs --vs 169.254.0.1:6443 --rs 192.168.0.2:6443 --run-once +``` + +清除现有 IPVS 规则: + +```shell +sealctl ipvs --clean +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/registry.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/registry.md new file mode 100644 index 000000000000..bf46cc7441b4 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/registry.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 5 +keywords: [Sealos, sealctl, registry serve, Docker 镜像仓库, 文件系统模式, 内存模式, 镜像管理, Docker 分发, 镜像同步] +description: 了解如何使用Sealos的sealctl registry serve命令启动和管理Docker镜像仓库,支持文件系统和内存模式,轻松实现镜像同步和分发。 +--- + +# registry 镜像仓库 + +## Sealos:sealctl registry serve 命令详解与使用指南 + +在构建并管理 Docker 镜像仓库过程中,Sealos 提供了 `sealctl registry serve` 命令以方便用户进行相关操作。本文将详细介绍 `sealctl registry serve` 命令的使用方法和示例。 + +### 基本介绍 + +`sealctl registry serve` 命令的主要作用是启动一个 Docker 分发镜像仓库服务器,支持两种模式:`filesystem` 和 `inmem`。 + +1. **Filesystem 模式**:在此模式下,sealctl 将运行一个针对指定目录的 Docker 分发镜像仓库服务器。该模式下,镜像数据将存储在硬盘上。**该命令还用于sealos做增量镜像同步** + +2. **In-memory 模式**:在此模式下,sealctl 将运行一个内存中的 Docker 分发镜像仓库服务器。该模式下,镜像数据仅保存在内存中,进程退出后数据将丢失。 + +### 命令参数 + +`sealctl registry serve filesystem ` 命令支持以下参数: + +- `--disable-logging`: 禁用日志输出,默认为 false。 +- `--log-level`: 配置日志级别,默认为 'error'。 +- `-p, --port`: 服务器监听的端口,默认为随机未使用的端口。 + +### 使用示例 + +以下是一些 `sealctl registry serve` 命令的使用示例: + +#### 在文件系统中启动镜像仓库服务器 + +```bash +sealctl registry serve filesystem --port=5000 +``` + +以上命令将在端口5000上启动一个文件系统镜像仓库服务器。 + +#### 在内存中启动镜像仓库服务器 + +```bash +sealctl registry serve inmem +``` + +以上命令将启动一个内存镜像仓库服务器。该服务器在进程退出后,存储的数据将丢失。 + +通过 `sealctl registry serve` 命令,用户可以轻松地管理和操作 Docker 镜像仓库。无论是在开发环境,还是在生产环境中,它都是一个强大且易用的工具。 + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/sealctl.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/sealctl.md new file mode 100644 index 000000000000..509ce8a7e4ea --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/sealctl.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 0 +keywords: [Sealos, sealctl, 集群管理, 容器管理, 证书管理, IPVS负载均衡, 静态Pod, Kubernetes, 镜像仓库, CRI配置] +description: 了解如何使用Sealos的sealctl命令行工具进行集群管理、容器管理、证书管理、IPVS负载均衡、静态Pod部署等操作,全面掌控Kubernetes集群。 +--- + +# Sealctl 使用指南 + +Sealos 提供 sealctl 是使用 Sealos 与 集群节点进行操作的命令行工具。它包括以下几个子命令: + +1. `cert`:管理证书,用于生成、查看和更新TLS证书。 +2. `cri`:管理容器运行时接口(CRI)配置,例如Docker或containerd。 +3. `hostname`:查看或设置系统主机名。 +4. `hosts`:管理系统的hosts文件,用于定义静态主机名到IP地址映射。 +5. `ipvs`:管理IP虚拟服务器(IPVS)规则,用于负载均衡和代理。 +6. `registry`:管理镜像仓库,用于存储容器镜像仓库格式镜像以及镜像仓库管理。 +7. `static_pod`:管理静态Pod,可以创建静态Pod的配置。 +8. `token`:生成和管理访问令牌,用于授权访问Kubernetes集群。 + +通过这些子命令,您可以方便地管理和配置您的Sealos系统,实现对容器、镜像仓库、网络等各个方面的控制。 + + + + +# sealos 依赖命令 + +1. **添加Hosts** + + 在指定 IP 地址的节点上添加一个新的 hosts 记录。参数包括 IP 地址、主机名和域名。使用`sealctl hosts add `命令 + +2. **删除Hosts** + + 删除指定 IP 地址节点上的一个 hosts 记录。参数包括 IP 地址和域名。使用`sealctl hosts delete`命令 + +3. **hostname** + + 获取指定 IP 地址节点的主机名。 使用`sealctl hostname`命令 + +4. **IPVS负载均衡** + + 在指定 IP 地址的节点上配置 IPVS,实现负载均衡。参数包括节点 IP 地址、虚拟 IP 地址和主节点 IP 地址列表。 使用`sealctl ipvs`命令 + +5. **清空IPVS规则** + + 清除指定 IP 地址节点上的 IPVS 配置。参数包括节点 IP 地址和虚拟 IP 地址。 使用`sealctl ipvs`命令 + +6. **静态POD生成** + + 在指定 IP 地址的节点上部署一个静态 Pod(lvscare)。参数包括节点 IP 地址、虚拟 IP 地址、Pod 名称、镜像名称和主节点 IP 地址列表。使用`sealctl static-pod lvscare`命令 + +7. **处理集群交互认证的token** + + 为指定 IP 地址的节点生成一个 token。参数包括节点 IP 地址、配置文件和证书密钥。使用`sealctl token`命令 + +8. **获取节点的cgroup** + + 获取指定 IP 地址节点的cri CGroup 信息。 使用`sealctl cri cgroup`命令 + +9. **获取节点的cri-socket** + + 获取指定 IP 地址节点的 cri Socket 信息。 使用`sealctl cri socket`命令 + +10. **在节点生成https自签名证书** + + 为指定 IP 地址的节点生成证书。参数包括节点 IP 地址、备用名称列表、主机 IP 地址、主机名、服务 CIDR 和 DNS 域名。 使用`sealctl cert` 命令 + +11. **节点启动registry** + +​ 在指定节点启动regsitry,为进行增量镜像同步。使用`sealctl registry serve`命令 + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/static-pod.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/static-pod.md new file mode 100644 index 000000000000..2dc218faa21c --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/static-pod.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 7 +keywords: [static-pod, sealctl, lvscare, Kubernetes, 静态Pod配置, kubelet, IPVS, Kubernetes集群] +description: 了解如何使用sealctl命令生成Kubernetes静态Pod配置,特别是lvscare静态Pod,简化集群管理。 +--- + +# static-pod 配置 + +`static-pod` 命令用于生成静态 Pod,这些 Pod 是由 kubelet 直接管理的,而不是通过 API 服务器。静态 Pod 在某些场景下非常有用,比如设置和管理 Kubernetes 集群中的控制平面组件。 + +`sealctl static-pod` 命令提供了一种简便的方法,用于生成用于特定目的的静态 Pod 配置文件。目前,它主要支持生成 `lvscare` 静态 Pod,`lvscare` 是一种用于管理 IPVS 规则的工具。 + +使用 `sealctl static-pod lvscare`,您可以根据指定的参数(如 VIP、主节点地址、镜像名称等)生成 `lvscare` 静态 Pod YAML 文件。然后,该文件可以存储在 kubelet 的静态 Pod 路径下,kubelet 将自动创建和管理相应的 Pod。 + + + +**用法** + +```shell +sealctl static-pod lvscare [flags] +``` + +**选项** + +- `--vip`: 默认 VIP IP(默认为 "10.103.97.2:6443")。 +- `--name`: 生成 lvscare 静态 Pod 名称。 +- `--image`: 生成 lvscare 静态 Pod 镜像(默认为 `sealos.hub:5000/sealos/lvscare:latest`)。 +- `--masters`: 生成 master 地址列表。 +- `--print`: 是否打印 YAML。 + +**示例** + +生成 lvscare 静态 Pod 文件并打印 YAML: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 --print +``` + +如果没有使用 `--print` 选项,将直接生成配置文件到 `/etc/kubernetes/manifests` 并启用静态 Pod: + +```shell +sealctl static-pod lvscare --vip 10.103.97.2:6443 --name lvscare --image lvscare:latest --masters 192.168.0.2:6443,192.168.0.3:6443 +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md new file mode 100644 index 000000000000..b4bf0ec07e15 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/tar&&untar.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 9 +keywords: [Sealos, sealctl tar, sealctl untar, 文件压缩, 文件解压] +description: 了解如何使用Sealos的sealctl tar和sealctl untar命令进行文件和文件夹的压缩与解压,简化备份和迁移操作。 +--- + +# tar 与 untar 详解 + +Sealos 提供了 `sealctl tar` 和 `sealctl untar` 命令以便于用户进行文件或文件夹的压缩和解压。本文将详细介绍这两个命令的使用方法。 + +## sealctl tar 命令 + +`sealctl tar` 命令的主要作用是将指定路径的目录压缩成归档文件。注意,这将剥离父目录。 + +**命令参数:** + +- `--clear`:是否在压缩完成后删除源文件,默认为 false。 +- `--compression`:压缩算法,可用选项有 tar/gzip/zstd/disable,默认为 disable。 +- `-o, --output`:归档文件的路径。 + +**基本用法:** + +```bash +sealctl tar [flags] [options] +``` + +## sealctl untar 命令 + +`sealctl untar` 命令的主要作用是在指定路径 `src` 查找匹配 glob 模式的归档文件,并在 `dst` 路径进行解压。 + +**命令参数:** + +- `--clear`:是否在解压完成后删除源文件,默认为 false。 +- `-o, --output`:解压归档文件的路径。 + +**基本用法:** + +```bash +sealctl untar [flags] [options] +``` + +## 使用示例 + +以下是一些 `sealctl tar` 和 `sealctl untar` 命令的使用示例: + +**创建一个压缩文件:** + +```bash +sealctl tar --output=/path/to/archive.tar /path/to/source +``` + +以上命令将 `source` 目录压缩为 `archive.tar` 文件。 + +**解压一个压缩文件:** + +```bash +sealctl untar --output=/path/to/destination /path/to/archive.tar +``` + +以上命令将 `archive.tar` 文件解压到 `destination` 目录。 + +通过 `sealctl tar` 和 `sealctl untar` 命令,用户可以轻松地进行文件或文件夹的压缩和解压操作。这两个命令在文件管理中,尤其是在备份和迁移文件时,都是非常有用的工具。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/token.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/token.md new file mode 100644 index 000000000000..56e06c78fdbb --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealctl/token.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 8 +keywords: [sealctl token, Kubernetes 集群, token 管理, 节点身份验证, 配置文件, 证书密钥, 工作节点, 主节点, 安全性, 节点加入] +description: 了解如何使用 sealctl token 命令生成用于 Kubernetes 集群节点身份验证的 token,确保工作节点安全加入集群,简化管理过程。 +--- + +# token 管理 + +`sealctl token` 命令的主要目的是为了生成一个用于连接主节点(master)和工作节点(node)的 token。在 Kubernetes 集群中,当您想要将一个新的工作节点加入到集群时,通常需要提供一个 token 作为身份验证。这个 token 确保只有拥有正确 token 的工作节点才能加入到集群中。 + +`sealctl token` 命令通过接收配置文件(可选)和证书密钥(可选)作为参数,生成一个用于身份验证的 token。在默认情况下,如果不提供配置文件和证书密钥,命令会使用内置的默认设置来生成 token。 + +总之,`sealctl token` 命令用于生成一个用于身份验证的 token,允许工作节点安全地加入到 Kubernetes 集群中。使用这个命令可以简化节点加入集群的过程,确保集群的安全性。 + + +**用法** + +```shell +sealctl token [config] [certificateKey] +``` + +**参数** + +- `config`: 配置文件(可选)。 +- `certificateKey`: 证书密钥(可选)。 + +**示例** + +使用默认参数生成 token: + +```shell +sealctl token +``` + +使用自定义配置文件和证书密钥生成 token: + +```shell +sealctl token my-config my-certificate-key +``` + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/_category_.json b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/_category_.json new file mode 100644 index 000000000000..c9cb98244ebc --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/_category_.json @@ -0,0 +1,3 @@ +{ + "position" : 1 +} diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md new file mode 100644 index 000000000000..a92ca95f0b4a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 11 +keywords: [Sealos, cluster-image, Kubernetes集群, 应用镜像, Docker镜像, GitHub Action, 镜像构建, Helm chart, Kubernetes yaml, 镜像配置] +description: Sealos 提供 cluster-image 仓库,用于构建和发布 Kubernetes 集群的应用镜像,支持 Docker 和应用集群镜像构建,使用 GitHub Action。 +--- + +# 应用集群镜像使用指南 + +Sealos 提供了一个名为 [cluster-image](https://github.com/labring-actions/cluster-image) 的仓库,该仓库在 GitHub 上用于构建并发布 Kubernetes 集群的应用镜像。这些镜像可以通过提交代码到这个仓库来创建,并可以发布到 `docker.io/labring/` 作为官方的应用镜像。它支持构建 Docker 容器镜像以及应用的集群镜像。 + +## 镜像的类型 + +仓库支持三种类型的镜像构建: + +- **APP集群镜像**:主要是构建应用镜像,使用 GitHub Action,会同时支持 amd64 和 arm64 架构。 +- **配置集群镜像**:主要是构建配置镜像,使用 GitHub Action,没有容器镜像不区分架构,一般是一些脚本相关的配置或者覆盖默认的配置镜像。 +- **Docker镜像**:主要是构建容器镜像,使用 GitHub Action,会同时支持 amd64 和 arm64 架构。 + +## 镜像构建的工作流程 + +你可以直接在 GitHub 仓库中创建 Issue 来触发镜像的构建。这里提供了几个示例可以参考: + +- `/imagebuild_dockerimages helm v3.8.2 Key1=Value1,Key2=Value2` +- `/imagebuild_configs coredns v0.0.1` +- `/imagebuild_apps helm v3.8.2` + +每种类型的镜像构建命令的格式为 `/imagebuild_<类型> <应用名称> <版本> [Key=Value,...]`,其中 `<类型>` 是 `dockerimages`、`configs` 或 `apps`, `<应用名称>` 和 `<版本>` 分别代表应用的名称和版本,`[Key=Value,...]` 是可选的buildArg参数,仅用于 `dockerimages` 类型。 + +## 镜像配置的存放位置 + +你可以在 `applications/<应用名称>/<版本>/` 目录下放置你的配置文件,包括 Dockerfile、Kubefile 和 init.sh 等。init.sh 脚本通常用于下载一些依赖的二进制文件,如 helm、kubectl-minio 等。你可以选择使用 Dockerfile 或 Kubefile 来编写你的镜像构建逻辑。 + +## 镜像构建规则 + +对于每种类型的镜像,构建规则略有不同。通常,你需要在应用的目录下创建不同的子目录并放置不同类型的文件,然后 Sealos 会根据这些文件来构建镜像。具体的规则如下: + +1. `charts` 目录:放置一些集群镜像需要的 Helm chart,Kubernetes 会根据扫描的 chart 获取镜像并构建 + +出 registry 目录放到与 Kubefile 同级的目录。 +2. `manifests` 目录:直接放置一些 Kubernetes yaml 配置,Kubernetes 会扫描 manifests 目录所有的镜像并构建出 registry 目录放到与 Kubefile 同级的目录。 +3. `images/shim` 目录:主要存储一些额外的镜像列表并构建出 registry 目录放到与 Kubefile 同级的目录。 +4. 如果需要模板,在 `etc`、`charts`、`manifests` 放置一些以 `.tmpl` 结尾的文件可以被 `sealos run` 环境变量渲染后去掉 `.tmpl`,比如渲染之前是 `aa.yaml.tmpl`,渲染后为 `aa.yaml`。请注意文件名不要与现有的文件冲突。 +5. `registry` 必须放在与 Kubefile 同级的目录,否则无法拷贝到 master0 的私有仓库。制作镜像时也需要注意这一点。不要把 registry 存放到 chart 里,否则 helm 扫描可能会很慢,可能导致 OOM。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/add.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/add.md new file mode 100644 index 000000000000..7b808169a048 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/add.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 4 +keywords: [Sealos, 集群节点添加, Sealos add, 控制节点, 节点添加] +description: 了解如何使用Sealos add命令向集群中添加节点和控制节点,确保etcd正常选举,掌握基本用法和选项。 +--- + +# add 集群节点添加 + +`sealos add` 是 Sealos 命令行工具中的一个命令,主要用于向集群中添加节点。本指南将详细介绍其使用方法和选项。 + +**注意要保证控制节点的个数为奇数个以保证etcd可以正常选举** + +## 基本用法 + +### 添加节点 + +要向集群中添加节点,可以使用 `--nodes` 选项: + +```bash +sealos add --nodes x.x.x.x +``` + +在上述命令中,`x.x.x.x` 应替换为你想要添加的节点的 IP 地址。 + +### 添加控制节点 + +要向集群中添加控制节点,可以使用 `--masters` 选项: + +```bash +sealos add --masters x.x.x.x +``` + +### 同时添加控制节点和节点 + +如果你想同时向集群中添加控制节点和节点,可以同时使用 `--masters` 和 `--nodes` 选项: + +```bash +sealos add --masters x.x.x.x --nodes x.x.x.x +sealos add --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## 选项 + +`sealos add` 命令提供了以下选项: + +- `--cluster='default'`: 要执行加入操作的集群的名称。默认为 `default`。 + +- `--masters=''`: 要加入的主节点。 + +- `--nodes=''`: 要加入的节点。 + +每个选项后都可以跟随一个参数。 + +## 使用示例 + +以下是一个使用示例,该示例向集群中添加了 IP 地址为 `192.168.0.2` 的节点: + +```bash +sealos add --nodes 192.168.0.2 +``` + +以上就是 `sealos add` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md new file mode 100644 index 000000000000..cd0de789994b --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 1 +keywords: [sealos apply, Kubernetes 集群, 集群镜像, Clusterfile, sealos 命令] +description: 了解如何使用 sealos apply 命令在 Kubernetes 集群中运行集群镜像,详细介绍基本用法和选项设置。 +--- + +# apply 启动集群 + +`sealos apply` 是 Sealos 命令行工具中的一个重要命令,用于在 Kubernetes 集群中运行集群镜像。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +`sealos apply` 命令的基本用法如下: + +```shell +$ sealos apply -f Clusterfile +``` + +Clusterfile 内容: + +```yaml +apiVersion: apps.sealos.io/v1beta1 +kind: Cluster +metadata: + name: default +spec: + # 服务器 IP 地址列表和角色 + hosts: + - ips: + - 192.168.0.2:22 + - 192.168.0.3:22 + - 192.168.0.4:22 + roles: + - master + - amd64 + - ips: + - 192.168.0.5:22 + - 192.168.0.6:22 + - 192.168.0.7:22 + roles: + - node + - amd64 + image: + - labring/kubernetes:v1.25.0 + - labring/helm:v3.8.2 + - labring/calico:v3.24.1 + ssh: + passwd: xxx + pk: /root/.ssh/id_rsa + port: 22 + user: root +``` + +这条命令会根据指定的 `Clusterfile` 文件在 Kubernetes 集群中运行集群镜像。 + +## 选项 + +`sealos apply` 命令提供了多种选项,用于定制命令的行为: + +- `-f, --Clusterfile='Clusterfile'`: 指定要应用的集群文件。默认为 `Clusterfile`。 +- `--config-file=[]`: 指定自定义Config文件的路径,用于替换或者修改资源。 +- `--env=[]`: 设置在命令执行过程中要使用的环境变量。 +- `--set=[]`: 在命令行上设置值,一般是替换模板的值。 +- `--values=[]`: 指定要应用到 `Clusterfile` 的values文件,一般是用于模板方式。 + +每个选项后面都可以跟随一个或多个参数。多个参数之间用逗号分隔。 + +例如,你可以使用 `--set` 选项在命令行上设置一些值: + +```shell +sealos apply -f Clusterfile --set key1=value1,key2=value2 +``` + +这条命令会将 `key1` 和 `key2` 的值设置为 `value1` 和 `value2`,然后应用 `Clusterfile`。 + +同样,你也可以使用 `--values` 选项指定一个值文件: + +```shell +sealos apply -f Clusterfile --values values.yaml +``` + +这条命令会根据 `values.yaml` 文件中的值应用 `Clusterfile`。 + +**更多示例请参考[启动镜像](/developer-guide/lifecycle-management/operations/run-cluster/)** + +以上就是 `sealos apply` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md new file mode 100644 index 000000000000..9420f6d6149d --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 6 +keywords: [Sealos build命令, 构建OCI镜像, Dockerfile构建, 镜像构建选项, Sealos使用指南] +description: 了解如何使用Sealos的build命令构建OCI镜像,包括主要选项和示例,提升构建效率和便捷性。 +--- + +# build 构建镜像 + +Sealos 的 `build` 命令用于使用 Sealfiles、Kubefiles、Dockerfiles 或 Containerfiles 中的指令构建 OCI 镜像。这是 Sealos 构建集群镜像的基础命令。 + +如果没有指定任何参数,Sealos 将使用当前工作目录作为构建上下文,并查找指令文件。如果不存在 Sealfile、Kubefile、Dockerfile 或 Containerfile,则构建失败。 + +下面是一些主要的 `build` 选项: + +1. `--all-platforms`:尝试为所有基础镜像平台构建镜像。 +2. `--authfile`:认证文件的路径。 +3. `--build-arg`:向构建器提供的 `argument=value`。 +4. `--build-context`:向构建器提供额外构建上下文的 `argument=value`。 +5. `--creds`:访问 registry 使用的 `[username[:password]]`。 +6. `-D, --disable-compression`:默认不压缩图层。 +7. `--env`:为镜像设置环境变量。 +8. `-f, --file`:Dockerfile 的 `pathname 或 URL`。 +9. `--force-rm`:即使构建不成功,也始终在构建后删除中间容器。 +10. `--format`:构建的镜像的清单和元数据的 `format`。 +11. `--from`:用于替换 Containerfile 中第一条 FROM 指令的值的镜像名称。 +12. `--http-proxy`:传递 HTTP Proxy 环境变量。 +13. `--isolation`:使用的进程隔离 `type`。可以是 'oci' 或 'chroot'。 +14. `--max-pull-procs`:拉取时使用的最大 goroutine 数量。 +15. `--platform`:设置镜像的 OS/ARCH/VARIANT 为提供的值,而不是主机的当前操作系统和架构。 +16. `--pull`:从 registry 拉取镜像,如果新的或存储中不存在,则拉取,如果 false,只有在不存在时才拉取镜像,如果 always,即使命名的镜像存在于存储中,也拉取镜像,如果 never,只使用存储中可用的镜像。 +17. `-q, --quiet`:克制不宣布构建指令和镜像读/写进度。 +18. `--retry`:在执行 push/pull 失败时重试的次数。 +19. `--retry-delay`:在 push/pull 失败时重试的延迟。 +20. `--rm`:在成功构建后删除中间容器。 +21. `--save-image`:保存从特定目录解析的镜像,以 registry 格式存储。 +22. `--sign-by`:使用指定 `FINGERPRINT` 的 GPG 密钥签名镜像。 +23. `-t, --tag`:应用到构建镜像的标签 `name`。 + +24. `--target`:设置要构建的目标构建阶段。 +25. `--timestamp`:将创建的时间戳设置为指定的 epoch 秒,以允许确定性构建,默认为当前时间。 + +这些选项可以灵活地应对多种构建需求,包括针对特定平台的构建、环境变量设置、构建上下文管理、镜像签名等。使用 `--save-image` 选项,Sealos 可以自动识别镜像列表(包括从镜像列表、Helm charts、manifests 中解析出的镜像)并保存为 registry 格式。 + +进程隔离模式 `--isolation` 支持 'oci' 和 'chroot' 两个参数。如果本地支持 OCI,可以选择 'oci' 模式;如果不支持 OCI,应该使用 'chroot' 模式。 + +`--save-image` 是 Sealos 构建命令的一个选项,这个选项的作用是在构建过程中自动查找并保存需要的镜像。在 Sealos 中,构建一个镜像可能涉及到其他依赖镜像。这些依赖镜像可能来自镜像列表、Helm charts 或 集群 manifests。当使用 `--save-image` 选项时,Sealos 将根据构建上下文,自动解析这些依赖并将其保存为 Docker Registry 格式。 + +例如,以下是一个使用 `--save-image` 选项的示例: + +```bash +sealos build -t myapp:v1.0.0 -f Dockerfile . +``` + +在这个示例中,Sealos 将使用当前目录作为构建上下文,从 Dockerfile 文件中读取构建指令,并尝试构建出一个标记为 `myapp:v1.0.0` 的镜像。同时,Sealos 将解析 Dockerfile 文件中所有 `FROM` 指令引用的基础镜像,并将这些镜像保存下来。这些镜像将以 Docker Registry 的格式保存,可以被直接推送到 Docker Registry 中。 + +如果你的构建上下文中还包含了 Helm charts 或 集群manifests,Sealos 也会解析这些文件中引用的镜像,并将这些镜像一并保存。 + +总的来说,`--save-image` 选项为 Sealos 的构建过程提供了一种自动处理依赖镜像的方式,大大提高了构建镜像的便捷性和效率。 + +下面有一些详细的示例: + +- [基于镜像清单构建](/developer-guide/lifecycle-management/operations/build-image/build-image-image_list.md) +- [基于部署清单构建](/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md) +- [基于helm-charts构建](/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [基于二进制构建](/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md) +- [基于go-template构建](/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md) +- [基于exec和scp构建](/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md) + +通过 Sealos `build` 命令,可以基于多种指令文件构建 OCI 镜像,为sealos提供所需的镜像。这个过程包括处理 Dockerfile 或其他指令文件中的各种指令,如 `FROM`、`RUN`、`ADD` 等,以及处理镜像层次、镜像标签等。构建过程也包括拉取基础镜像、运行命令、保存结果等步骤。每一个步骤都可以通过上述的选项进行详细的控制和定制,以满足不同的构建需求。 + +以上就是 `sealos build` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/cert.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/cert.md new file mode 100644 index 000000000000..2cacf582ffc3 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/cert.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 3 +keywords: [sealos cert, 集群证书更新, API服务器证书, sealos命令行工具, Kubernetes证书] +description: 了解如何使用sealos cert命令更新Kubernetes集群的API服务器证书,添加域名和IP,确保集群安全。 +--- + +# cert 更新集群证书 + +`sealos cert` 是 Sealos 命令行工具中的一个命令,主要用于在集群中更新 API 服务器的证书。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +要在证书中添加域名或 IP,可以使用 `--alt-names` 选项: + +```bash +sealos cert --alt-names sealos.io,10.103.97.2,127.0.0.1,localhost +``` + +在上述命令中,`sealos.io,10.103.97.2,127.0.0.1,localhost` 应替换为你想要添加的域名和 IP 地址。 + +**注意**:在执行此操作之前,你最好先备份旧的证书。 + +执行 `sealos cert` 命令后,会更新集群 API 服务器的证书,你无需手动重启 API 服务器,sealos会自动帮你重启服务。 + +## 选项 + +`sealos cert` 命令提供了以下选项: + +- `--alt-names=''`: 在证书中添加域名或 IP,例如 `sealos.io` 或 `10.103.97.2`。 + +- `-c, --cluster='default'`: 要执行 exec 操作的集群的名称。默认为 `default`。 + +每个选项后都可以跟随一个参数。 + +## 校验证书 + +更新证书后,你可以使用以下命令进行校验: + +```bash +kubectl -n kube-system get cm kubeadm-config -o yaml +openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text +``` + +上述命令将获取 kube-system 命名空间中的 kubeadm-config 配置映射,并显示 apiserver.crt 证书的详细信息。 + +以上就是 `sealos cert` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md new file mode 100644 index 000000000000..8d0b4f0aeffc --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 0 +keywords: [Sealos命令, Kubernetes集群管理, 节点管理, 容器镜像, Sealos调试模式] +description: 了解Sealos命令,包括Kubernetes集群管理、节点管理、容器镜像操作等,开启调试模式获取详细系统运行信息。 +--- + +# Sealos 命令说明 + +## 集群管理命令 + +- `apply`:使用 Clusterfile 在 Kubernetes 集群内运行集群镜像。 +- `cert`:更新 Kubernetes API 服务器的证书。 +- `run`:轻松运行云原生应用。 +- `reset`:重置集群中的所有内容。 +- `status`:查看 Sealos集群 的状态。 + +## 节点管理命令 + +- `add`:将节点添加到集群中。 +- `delete`:从集群中删除节点。 + +## 远程操作命令 + +- `exec`:在指定节点上执行 shell 命令或脚本。 +- `scp`:将文件复制到指定节点的远程位置。 + +## 实验性命令 + +- `registry`:与镜像仓库相关的命令。 + +## 容器和镜像命令 + +- `build`:使用 Sealfile 或 Kubefile 中的指令构建镜像。 +- `create`:创建集群,但不运行 CMD,用于检查镜像。 +- `inspect`:检查容器或镜像的配置。 +- `images`:列出本地存储中的镜像。 +- `load`:从文件中加载镜像。 +- `login`:登录到容器仓库。 +- `logout`:登出容器仓库。 +- `manifest`:操作清单列表和镜像索引。 +- `merge`:合并多个镜像为一个。 +- `pull`:从指定位置拉取镜像。 +- `push`:将镜像推送到指定的目标。 +- `rmi`:从本地存储中删除一个或多个镜像。 +- `save`:将镜像保存到存档文件中。 +- `tag`:为本地镜像添加一个附加名称。 + +## 其他命令 + +- `completion`:为指定的 shell 生成自动补全脚本。 +- `docs`:生成 API 参考。 +- `env`:打印 Sealos 使用的所有环境信息。 +- `gen`:生成具有所有默认设置的 Clusterfile。 +- `version`:打印版本信息。 + +Sealos 的 `--debug` 参数是一个全局参数,用于开启调试模式,以便在出现问题时能更详细地了解系统的运行情况。 + +有关安装说明,请参见[下载 Sealos 命令行工具](/developer-guide/lifecycle-management/quick-start/install-cli.md); 如需安装 Kubernetes 集群,请参见[安装 Kubernetes 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md)。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/create.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/create.md new file mode 100644 index 000000000000..7cd27ee1441a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/create.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 6 +keywords: [Sealos create, 集群工作目录, Kubernetes, 镜像审查, Sealos 命令行] +description: 了解如何使用 Sealos create 命令创建集群工作目录,审查 Kubernetes 镜像,支持调试和测试,确保镜像内容符合预期。 +--- + +# create 创建工作目录 + +`sealos create` 是 Sealos 命令行工具中的一个命令,主要用于在不执行 CMD 的情况下创建集群工作目录,以便审查镜像。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +`sealos create` 命令用于创建集群工作目录,但不实际运行,主要用于调试或测试,它可以输出集群镜像的地址,你可以校验集群镜像内容是否与预期一致。 + +```bash +sealos create docker.io/labring/kubernetes:v1.24.0 +``` + +在上述命令中,`clustername` 代表你要创建的集群的名称。 + +## 选项 + +`sealos create` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要创建但不实际运行的集群的名称。默认为 `default`。 + +- `--platform='linux/arm64/v8'`: 将镜像的操作系统/架构/版本设置为提供的值,而不是主机的当前操作系统和架构(例如 `linux/arm`)。 + +- `--short=false`: 如果为真,只打印挂载路径。 + +- `-e, --env=[]`: 指定渲染模板文件时使用的环境变量。 + +每个选项后都可以跟随一个参数。 + +## 示例 + +例如,你可以使用以下命令创建一个名为 `mycluster` 的集群,但不实际运行它: + +```bash +sealos create -e registryPort=8443 docker.io/labring/kubernetes:v1.24.0 +``` + +此命令将创建一个镜像名称为 `docker.io/labring/kubernetes:v1.24.0` 的集群工作目录,并输出集群镜像的地址。`-e registryPort=8443` 选项指定了在渲染模板文件时使用的环境变量,其中 `registryPort` 被设置为 `8443`。请注意,这个示例中集群并没有被实际运行。 + +以上就是 `sealos create` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/delete.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/delete.md new file mode 100644 index 000000000000..3ed5b240f565 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/delete.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 4 +keywords: [sealos delete, 集群节点删除, sealos 命令, 删除控制节点, sealos 使用指南] +description: 了解如何使用 sealos delete 命令从集群中删除节点和控制节点,确保集群正常运行。详细指南和选项说明。 +--- + +# delete 集群节点删除 + +`sealos delete` 是 Sealos 命令行工具中的一个命令,主要用于从集群中移除节点。本指南将详细介绍其使用方法和选项。 + +**注意要保证控制节点的个数为奇数个以保证etcd可以正常选举** + +## 基本用法 + +### 删除节点 + +要从集群中删除节点,可以使用 `--nodes` 选项: + +```bash +sealos delete --nodes x.x.x.x +``` + +在上述命令中,`x.x.x.x` 应替换为你想要删除的节点的 IP 地址。如果不小心删除了错误的节点,可以使用 `sealos add` 命令恢复它: + +```bash +sealos add --nodes x.x.x.x +``` + +### 删除控制节点 + +要从集群中删除控制节点,可以使用 `--masters` 选项: + +```bash +sealos delete --masters x.x.x.x +``` + +请注意,如果指定了 `--masters` 参数,sealos 将删除你的控制节点。 + +### 删除控制节点和节点 + +如果你想同时删除控制节点和节点,可以同时使用 `--masters` 和 `--nodes` 选项: + +```bash +sealos delete --masters x.x.x.x --nodes x.x.x.x +sealos delete --masters x.x.x.x-x.x.x.y --nodes x.x.x.x-x.x.x.y +``` + +## 选项 + +`sealos delete` 命令提供了以下选项: + +- `--cluster='default'`: 执行删除操作应用的集群的名称。默认为 `default`。 + +- `--force=false`: 可以输入一个 `--force` 标志以强制删除节点。 + +- `--masters=''`: 要移除的控制节点。 + +- `--nodes=''`: 要移除的节点。 + +每个选项后都可以跟随一个参数。 + +## 使用示例 + +以下是一个使用示例,该示例删除了 IP 地址为 `192.168.0.2` 的节点: + +```bash +sealos delete --nodes 192.168.0.2 +``` + +以上就是 `sealos delete` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/env.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/env.md new file mode 100644 index 000000000000..288dcd906c71 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/env.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 7 +keywords: [sealos, 环境变量, sealos env, 命令行工具, sealos build] +description: 了解如何使用Sealos命令行工具查看和设置环境变量,掌握sealos env命令的基本用法和详细说明。 +--- + +# env 环境变量 + +`sealos env` 是 Sealos 命令行工具中的一个命令,用于展示目前sealos支持的环境变量以及当前的环境变量值。 + +## 基本用法 + +### 查看环境变量 + +要查看环境变量,可以使用 `sealos env` 命令: + +```bash +sealos env +``` + +### 查看环境变量以及说明 + +要查看环境变量以及说明,可以使用 `sealos env -v` 命令: + +```bash +sealos env -v +``` + + +## 如何设置环境变量 + +```shell +BUILDAH_LOG_LEVEL=debug sealos images +``` + +```shell +SEALOS_REGISTRY_SYNC_EXPERIMENTAL=true sealos build -t xxx . +``` + + +以上就是 `sealos env` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/exec.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/exec.md new file mode 100644 index 000000000000..28acfdacf089 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/exec.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 5 +keywords: [sealos exec, Sealos 命令行工具, 集群节点, Shell 命令, 执行脚本, 集群管理, Sealos 使用指南, Sealos 选项, Sealos 示例] +description: 了解如何使用 Sealos exec 命令在集群节点上执行 Shell 命令或脚本,掌握基本用法、选项和示例,提升集群管理效率。 +--- + +# exec 执行命令 + +`sealos exec` 是 Sealos 命令行工具中的一个命令,用于在指定的集群节点上执行 Shell 命令或脚本。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +基本的 `sealos exec` 命令格式如下: + +```bash +sealos exec "shell command or script" +``` + +在上述命令中,`shell command or script` 是你要在集群节点上执行的 Shell 命令或脚本。 + +## 选项 + +`sealos exec` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要在其上执行命令的集群的名称。默认为 `default`。 + +- `--ips=[]`: 在具有指定 IP 地址的节点上运行命令。 + +- `-r, --roles='':`: 在具有指定角色的节点上运行命令。目前支持 master,node,registry + +每个选项后都可以跟随一个或多个参数。 + +## 示例 + +例如,你可以使用以下命令在默认集群的所有节点上查看 `/etc/hosts` 文件的内容: + +```bash +sealos exec "cat /etc/hosts" +``` + +如果你想在名为 `my-cluster` 的集群的 `master` 和 `node` 角色的节点上查看 `/etc/hosts` 文件的内容,可以使用以下命令: + +```bash +sealos exec -c my-cluster -r master,node "cat /etc/hosts" +``` + +如果你只想在 IP 地址为 `172.16.1.38` 的节点上查看 `/etc/hosts` 文件的内容,可以使用以下命令: + +```bash +sealos exec -c my-cluster --ips 172.16.1.38 "cat /etc/hosts" +``` + +以上就是 `sealos exec` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md new file mode 100644 index 000000000000..c6e0ef997778 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md @@ -0,0 +1,51 @@ +--- +sidebar_position: 3 +keywords: [Sealos, Kubernetes集群, gen命令, Clusterfile, sealos apply] +description: 了解如何使用Sealos的gen命令生成Kubernetes集群配置文件,快速创建和管理集群,提升运维效率。 +--- + +# gen 生成集群配置 + +Sealos 的 `gen` 命令是用于生成 Kubernetes 集群的配置文件(Clusterfile),这个配置文件可以在之后通过 `sealos apply` 命令来应用。`gen` 命令可以帮助用户快速生成一个基本的配置文件,用户可以在此基础上根据自己的需求进行修改和调整。 + +下面是 `sealos gen` 命令的基本使用方法和一些常见的示例: + +1. 生成一个默认配置的单节点集群: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 + ``` + +注意:labring/helm 应当在 labring/calico 之前。 + +2. 生成一个包含多个镜像、指定了主节点和工作节点的集群: + + ```bash + sealos gen labring/kubernetes:v1.25.0 labring/helm:v3.8.2 labring/calico:v3.24.1 \ + --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' + ``` + +注意:labring/helm 应当在 labring/calico 之前。 + +3. 指定 SSH 端口,对于所有服务器使用相同的 SSH 端口: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' + ``` + + 对于使用不同 SSH 端口的服务器: + + ```bash + sealos gen labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3:23,192.168.0.4:24 \ + --nodes 192.168.0.5:25,192.168.0.6:25,192.168.0.7:27 --passwd 'xxx' + ``` + +在生成了 Clusterfile 之后,用户可以根据自己的需求来修改这个文件。添加或修改环境变量;修改集群cidr配置。完成修改后,用户就可以通过 `sealos apply` 命令来根据这个配置文件来创建或更新集群了。 + +示例说明: + +- [自定义配置安装](/developer-guide/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) + +以上就是 `sealos gen` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/images.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/images.md new file mode 100644 index 000000000000..8cb43762c512 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/images.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 6 +keywords: [Sealos, 镜像管理, Sealos images 命令, 镜像参数, 镜像过滤] +description: 了解如何使用Sealos的images命令查看和管理本地存储的镜像,包括显示所有镜像、特定镜像和使用多种参数进行过滤。 +--- + +# image 镜像列表 + +Sealos 的 `images` 命令主要用于查看本地存储的镜像。用户可以通过它来查看本地所有的镜像,或者筛选查看特定的镜像。该命令支持多种参数,可以帮助用户更方便的查看和管理镜像。 + +## 基本用法 + +基本的 `sealos images` 命令将显示所有非中间阶段的本地镜像,例如: + +```bash +sealos images +``` + +这将显示本地存储的所有最终阶段镜像。 + +## 示例 + +以下是 `sealos images` 命令的一些常见示例: + +1. 显示所有镜像,包括构建的中间镜像: + + ```bash + sealos images --all + ``` + +2. 显示特定镜像: + + ```bash + sealos images [imageName] + ``` + +3. 以指定的 Go 模板格式显示镜像: + + ```bash + sealos images --format '{{.ID}} {{.Name}} {{.Size}} {{.CreatedAtRaw}}' + ``` + +## 参数 + +以下是 `sealos images` 命令的一些常用参数: + +- `-a, --all`:显示所有镜像,包括构建过程中的中间镜像。 + +- `--digests`:显示镜像的摘要。 + +- `-f, --filter`:根据提供的条件过滤输出结果。 + +- `--format`:使用 Go 模板对镜像进行美化打印。 + +- `--history`:显示镜像的命名历史。 + +- `--json`:以 JSON 格式输出。 + +- `--no-trunc`:不截断输出。 + +- `-n, --noheading`:不打印列标题。 + +- `-q, --quiet`:只显示镜像 ID。 + +通过组合使用这些参数,用户可以轻松地获取和管理本地存储的镜像。例如,使用 `--all` 参数可以查看所有镜像,包括中间镜像;使用 `--filter` 参数可以根据特定条件过滤镜像;使用 `--json` 参数可以以 JSON 格式输出镜像信息,方便进行程序化处理等。 + +以上就是 `sealos images` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md new file mode 100644 index 000000000000..a59136929601 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/inspect.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 6 +keywords: [Sealos命令, inspect命令, 容器配置, 镜像配置, Go模板格式, 远程镜像仓库, 本地OCI归档, Docker归档文件] +description: 了解如何使用Sealos的inspect命令查看容器和镜像的详细配置信息,包括元数据、环境变量和启动命令等。 +--- + +# inspect 详细信息 + +Sealos 的 `inspect` 命令主要用于查看构建容器或已构建镜像的配置信息。该命令支持查看镜像或容器的详细信息,包括其元数据、环境变量、启动命令等。 + +## 基本用法 + +使用 `sealos inspect` 命令查看指定容器或镜像的配置信息。例如,查看指定容器的配置: + +```bash +sealos inspect containerID +``` + +或者查看指定镜像的配置: + +```bash +sealos inspect --type image imageWithTag +``` + +## 示例 + +以下是 `sealos inspect` 命令的一些常见示例: + +1. 查看容器配置: + + ```bash + sealos inspect containerID + ``` + +2. 查看镜像配置: + + ```bash + sealos inspect --type image imageWithTag + ``` + +3. 查看镜像ID的配置信息: + + ```bash + sealos inspect --type image @imageID # 或直接输入imageID, '@' 是可选的 + ``` + +4. 查看远程镜像仓库的配置信息: + + ```bash + sealos inspect --type image docker://alpine:latest + ``` + +5. 查看本地OCI归档文件中镜像的配置信息: + + ```bash + sealos inspect --type image oci-archive:/abs/path/of/oci/tarfile.tar + ``` + +6. 查看本地Docker归档文件中镜像的配置信息: + + ```bash + sealos inspect --type image docker-archive:/abs/path/of/docker/tarfile.tar + ``` + +7. 使用 Go 模板格式显示镜像环境变量: + + ```bash + sealos inspect --format '{{.OCIv1.Config.Env}}' alpine + ``` + +## 参数 + +以下是 `sealos inspect` 命令的一些常用参数: + +- `-f, --format`:使用 Go 模板格式显示输出结果。**模板结构代码[InspectOutput](https://github.com/labring/sealos/blob/f8a17787822714c5fdf21f2a75cc86fadb88adfa/pkg/buildah/inspect.go#L189)** + +- `-t, --type`:指定查看的类型,可以是容器(`container`)或镜像(`image`)。 + +根据你的需要,你可以结合使用这些参数,以获取特定的配置信息。例如,使用 `-t` 参数可以指定你想要查看的是容器的配置信息还是镜像的配置信息;使用 `-f` 参数,可以定义特定的输出格式,方便对输出结果进行处理或解析。 + +以上就是 `sealos inspect` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/load.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/load.md new file mode 100644 index 000000000000..b760b265119f --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/load.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 6 +keywords: [sealos load, 加载镜像, 存档文件, 无网络环境, 镜像导入] +description: 了解如何使用sealos load命令从存档文件中加载镜像,特别适用于无网络环境下的镜像导入。 +--- + +# load 加载镜像 + +`sealos load` 是一个用来从存档文件中加载镜像的命令。这对于需要从已有的存档文件中导入镜像非常有用,尤其是在没有网络连接的环境中。 + +## 用法: + +`sealos load [flags] [options]` + +## 参数: + +以下是 `sealos load` 命令的参数: + +- `-i, --input=''`: 从 tar 存档文件中加载镜像。 + +## 示例: + +- 从一个存档文件中加载镜像:`sealos load -i myimage.tar` + +注意,在使用 `sealos load` 命令时,你需要确保指定的存档文件存在,并且格式正确。如果你在导入镜像时遇到问题,你可能需要检查你的存档文件,以确保它们没有被损坏或格式化错误。 + +以上就是 `sealos load` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/login.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/login.md new file mode 100644 index 000000000000..14e778b0354a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/login.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 6 +keywords: [sealos login, 容器注册表登录, sealos命令, 镜像推送, 镜像拉取] +description: 了解如何使用sealos login命令登录容器注册表,进行镜像的拉取和推送,确保提供正确的用户名和密码。 +--- + +# login 登录仓库 + +`sealos login` 命令用于在指定服务器上登录容器注册表。登录注册表后,你可以拉取、推送镜像。 + +## 用法: + +`sealos login [flags] [options] registryName` + +## 参数: + +以下是 `sealos login` 命令的参数: + +- `--authfile=''`: 身份验证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 来覆盖。 + +- `--cert-dir=''`: 使用指定路径的证书来访问镜像仓库。 + +- `--get-login=true`: 返回注册表的当前登录用户。 + +- `-k, --kubeconfig=''`: 使用 kubeconfig 登录到 sealos 镜像仓库 hub.sealos.io。 + +- `-p, --password=''`: 注册表的密码。 + +- `--password-stdin=false`: 从标准输入获取密码。 + +- `-u, --username=''`: 注册表的用户名。 + +- `-v, --verbose=false`: 将更详细的信息写入标准输出。 + +## 示例: + +- 登录到 quay.io 注册表:`sealos login -u myusername -p mypassword quay.io ` + +注意,在使用 `sealos login` 命令时,你需要确保提供了正确的用户名和密码,否则登录过程可能会失败。如果你在登录过程中遇到问题,你可能需要检查你的用户名和密码,以确保它们没有输入错误或被遗忘。 + +以上就是 `sealos login` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/logout.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/logout.md new file mode 100644 index 000000000000..95e0102dec9a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/logout.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 6 +keywords: [sealos logout, 镜像仓库, 认证信息, 身份验证文件, quay.io] +description: 了解如何使用sealos logout命令移除本地缓存的镜像仓库账号和密码,包括参数和示例,帮助你轻松管理认证信息。 +--- + +# logout 登出仓库 + +`sealos logout` 命令用于在指定服务器上移除本地缓存的镜像仓库的账号和密码。 + +## 用法: + +`sealos logout [flags] [options] registryName` + +## 参数: + +以下是 `sealos logout` 命令的参数: + +- `--authfile=''`: 身份验证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 来覆盖。 + +- `-a, --all=false`: 删除所有的认证信息。 + + +## 示例: + +- 登出到 quay.io 镜像仓库:`sealos logout quay.io ` + +以上就是 `sealos logout` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md new file mode 100644 index 000000000000..0d638010a11d --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 6 +keywords: [Sealos, manifest命令, 多架构支持, Docker镜像, OCI镜像] +description: 了解Sealos的manifest命令,轻松创建、修改和推送多架构支持的Docker和OCI镜像清单,提升部署效率。 +--- + +# manifest 镜像清单 + +Sealos 的 `manifest` 命令用于创建、修改和推送 manifest 列表和镜像索引。这些功能主要用于处理镜像的多架构支持。在 Docker 和 OCI 镜像规范中,manifest 列表(也被称为 "fat manifest")或镜像索引允许一个镜像标签(如 `myimage:latest`)在多种硬件架构(如 amd64, arm64, ppc64le 等)上都能使用。 + +以下是一些主要的 `manifest` 子命令: + +1. `create`:创建新的 manifest 列表或镜像索引。例如:`sealos manifest create localhost/list` +2. `add`:将镜像添加到 manifest 列表或镜像索引中。例如:`sealos manifest add localhost/list localhost/image` +3. `annotate`:在 manifest 列表或镜像索引的条目中添加或更新信息。例如:`sealos manifest annotate --annotation A=B localhost/list localhost/image` +4. `inspect`:显示 manifest 列表或镜像索引的内容。例如:`sealos manifest inspect localhost/list` +5. `push`:将 manifest 列表或镜像索引推送到 registry。例如:`sealos manifest push localhost/list transport:destination` +6. `remove` 和 `rm`:从 manifest 列表或镜像索引中移除条目,或者完全删除 manifest 列表或镜像索引。例如:`sealos manifest remove localhost/list sha256:entryManifestDigest` 或 `sealos manifest rm localhost/list` + +通过 `sealos manifest` 命令,可以灵活地管理 manifest 列表或镜像索引,为多架构的 Docker 或 OCI 镜像提供支持。用户可以根据自己的需求,创建自定义的 manifest 列表,方便在不同的硬件架构上部署和运行 Docker 镜像。 + +用户如果想通过manifest命令构建多架构镜像,可以参考文档[构建支持多架构的集群镜像](/developer-guide/lifecycle-management/operations/build-image/build-multi-arch-image.md) diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/merge.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/merge.md new file mode 100644 index 000000000000..2f38c723d33a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/merge.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 6 +keywords: [Sealos merge, 合并镜像, Dockerfile, 镜像优化, 应用部署] +description: 了解如何使用Sealos的merge命令将多个镜像合并为一个,优化镜像大小,简化应用部署,节省存储空间。 +--- + +# merge 合并镜像 + +Sealos 的 `merge` 命令的主要作用是将多个镜像合并为一个。它通过读取各个输入镜像的 Dockerfile,将其中的命令和层次结构合并到一个新的镜像中。这个命令的运行逻辑很像 `build` 命令,许多参数也是相同的。 + +这个功能在多个镜像有共享层的情况下非常有用,因为它可以减少镜像的大小,节省存储空间。同时,由于合并后的镜像包含了多个镜像的全部功能,所以它可以帮助简化应用部署。 + +以下是 `sealos merge` 的基本使用示例: + +```bash +sealos merge -t new:0.1.0 kubernetes:v1.19.9 mysql:5.7.0 redis:6.0.0 +``` + +在这个示例中,`kubernetes:v1.19.9`、`mysql:5.7.0` 和 `redis:6.0.0` 这三个镜像被合并为一个新的镜像 `new:0.1.0`。 + +`sealos merge` 命令提供了丰富的选项来定制合并过程,例如 `--all-platforms` 用于尝试为所有基础镜像平台构建镜像,`--build-arg` 用于向构建器提供参数,`--no-cache` 用于禁用现有的缓存镜像,等等。 + +请注意,`sealos merge` 命令会根据各个输入镜像的 Dockerfile 来构建新的镜像,所以如果输入镜像的 Dockerfile 不兼容,或者有任何构建错误,那么这个命令可能会失败。在使用 `sealos merge` 命令时,请确保你了解每个输入镜像的 Dockerfile,并根据需要进行调整。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/pull.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/pull.md new file mode 100644 index 000000000000..1096a1ad86c6 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/pull.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +keywords: [sealos pull, 拉取镜像, 容器镜像仓库, 镜像下载, sealos命令] +description: 了解如何使用sealos pull命令从容器镜像仓库下载镜像并存储在本地,提高工作效率。获取详细参数和使用示例。 +--- + +# pull 拉取镜像 + +`sealos pull` 是一个非常有用的命令,它可以从容器镜像仓库下载镜像并将其存储在本地。用户可以通过镜像的标签(tag)或摘要(digest)来获取镜像。如果没有指定标签,那么会默认下载带有 'latest' 标签(如果存在)的镜像。 + +通过使用这个命令,用户可以方便地从远程仓库下载所需的镜像,极大地提高了工作效率。 + +## 用法: + +`sealos pull [flags] [options] imageName` + +## 参数: + +以下是 `sealos pull` 命令的参数: + +- `-a, --all-tags=false`: 下载仓库中所有带有标签的镜像。 + +- `--authfile=''`: 认证文件的路径。可以使用环境变量 REGISTRY_AUTH_FILE 进行覆盖。 + +- `--cert-dir=''`: 用于访问镜像仓库的证书的指定路径。 + +- `--creds=''`: 使用 `[username[:password]]` 访问镜像仓库。 + +- `--decryption-key=[]`: 解密镜像所需要的密钥。 + +- `--platform=[linux/arm64/v8]`: 选择镜像时,优先使用指定的 OS/ARCH,而不是当前操作系统和架构。 + +- `--policy='missing'`: 设置策略,可选的值包括 'missing', 'always', 'never'。 + +- `-q, --quiet=false`: 在拉取镜像时,不输出进度信息。 + +- `--remove-signatures=false`: 在拉取镜像时,不复制签名。 + +- `--retry=3`: 在拉取失败时的重试次数。 + +- `--retry-delay=2s`: 拉取失败时,重试之间的延迟。 + +## 示例: + +- 拉取一个镜像:`sealos pull my-image:latest` + +- 从 Docker 守护进程拉取一个镜像:`sealos pull docker-daemon:my-image:tag` + +- 从特定的仓库拉取一个镜像:`sealos pull myregistry/myrepository/my-image:tag` + +- 拉取多个镜像:`sealos pull imageID1 imageID2 imageID3` + +以上就是 `sealos push` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/push.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/push.md new file mode 100644 index 000000000000..0530fba9074c --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/push.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 6 +keywords: [Sealos push, 上传镜像, Docker 镜像, 镜像仓库, OCI 布局, Docker 注册表, 镜像传输, 镜像推送, 镜像管理] +description: Sealos push 命令指南,详细介绍如何将本地 Docker 镜像推送到远程镜像仓库,支持多种传输方式和可选参数,轻松管理镜像。 +--- + +# push 上传镜像 + +`sealos push` 是 Sealos 命令行工具中的一个命令,用于将镜像推送到指定的位置。这个命令在你需要将本地 Docker 镜像推送到远程镜像仓库的时候特别有用。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos push` 命令格式如下: + +```bash +sealos push IMAGE_ID DESTINATION +``` + +在上述命令中,`IMAGE_ID` 是你想要推送的镜像的 ID,而 `DESTINATION` 是你想要推送到的位置。 `DESTINATION` 使用 "transport:details" 格式,如果未指定,将复用源 IMAGE 作为 DESTINATION。 + +在 Sealos 中,传输方式定义了源镜像和目标镜像在复制过程中的格式和位置。以下是 Sealos 支持的各种传输方式: + +1. `containers-storage`: 此传输方式用于存储和管理在本地运行的容器。例如,使用 Podman 或 CRI-O 创建的容器的镜像。 + +2. `dir`: 这种传输方式将镜像存储在本地文件系统的一个目录中,该目录结构符合 OCI 布局。 + +3. `docker`: 这种传输方式用于与 Docker 注册表进行交互,如 Docker Hub 或任何其他兼容的私有注册表。 + +4. `docker-archive`: 此传输方式将镜像存储为一个本地的 Docker tar 文件(`.tar`),这是 Docker 的原生格式。 + +5. `docker-daemon`: 这种传输方式用于与本地 Docker 守护程序交互,可以从 Docker 守护程序中提取镜像,或者将镜像推送到 Docker 守护程序。 + +6. `oci`: 该传输方式将镜像存储在一个符合 OCI 布局的目录中,它是一种开放的容器镜像格式。 + +7. `oci-archive`: 这种传输方式将镜像存储为一个本地的 OCI tar 文件(`.tar`)。 + +8. `ostree`: 这种传输方式将镜像存储在 OSTree 存储库中,这是一种支持原子升级和回滚的文件系统。 + +9. `sif`: 这是 Singularity SIF 格式,主要用于高性能计算和数据密集型应用。 + +示例: + +- 将一个镜像推送到 Docker 注册表:`sealos push my-image:latest docker://my-registry.example.com/my-image:latest` + +- 将一个镜像从 Docker 守护程序导出:`sealos push docker-daemon:my-image:latest dir:/path/to/save/` + +- 将一个镜像推送到本地的容器存储:`sealos push my-image:latest containers-storage:my-new-image:latest` + +## 示例 + +例如,你可以使用以下命令将一个镜像推送到 `registry.example.com` 的仓库: + +```bash +sealos push my_image_id docker://registry.example.com/my_repository:my_tag +``` + +## 可选参数 + +- `--all`: 该参数用于推送清单列表引用的所有镜像。 + +- `--authfile`: 该参数用于指定身份验证文件的路径。 可以使用 REGISTRY_AUTH_FILE 环境变量进行覆盖。 + +- `--cert-dir`: 该参数用于指定访问注册表所需的证书的路径。 + +- `--compression-format`: 该参数用于指定要使用的压缩格式。 + +- `--compression-level`: 该参数用于指定要使用的压缩级别。 + +- `--cr-option` 参数是用于控制是否将镜像的自定义资源(Custom Resource,简称 CR)推送到目标镜像仓库的。 + + 具体来说,这个参数的可选值包括: + + - "yes": 将会把镜像以及其关联的 CR 都推送到目标镜像仓库。 + + - "no": 仅推送镜像,而不推送任何 CR。 + + - "only": 仅推送 CR,不推送镜像本身。 + + - "auto": 根据镜像和 CR 的实际状态自动决定是否推送。例如,如果 CR 有更改或者不存在于目标仓库,就会被推送。 + + 请注意,这个参数主要在处理包含自定义资源(如 Kubernetes CRD 对象)的镜像时使用,它能够让你更加灵活地控制镜像和 CR 的推送过程。 + +- `--creds`: 该参数用于访问注册表,使用 `[username[:password]]` 形式。 + +- `--digestfile`: 该参数在复制图像后,将结果图像的摘要写入文件。 + +- `-D`, `--disable-compression`: 该参数用于不压缩层。 + +- `--encrypt-layer`: 该参数用于指定要加密的层,0 索引层索引支持负索引(例如,0 是第一层,-1 是最后一层)。 如果未定义,则在指定 encryption-key 标志时将加密所有层。 + +- `--encryption-key`: 该参数用于指定加密图像所需的密钥,与加密协议一起使用(例如,jwe:/path/to/key.pem)。 + +- `-f`, `--format`: 该参数用于指定目标中要使用的清单类型(oci, v2s1, 或 v2s2)(默认是源的清单类型,带回退)。 + +- `-q`, `--quiet`: 该参数用于在推送图像时不输出进度信息。 + +- `--remove-signatures`: 该参数用于在推送图像时不复制签名。 + +- `--retry + +`: 该参数用于指定在推送/拉取失败时的重试次数。 + +- `--retry-delay`: 该参数用于指定在推送/拉取失败时重试之间的延迟。 + +- `--rm`: 该参数用于在推送成功后删除清单列表。 + +- `--sign-by`: 该参数用于使用指定的 `FINGERPRINT` 的 GPG 密钥签名图像。 + +以上就是 `sealos push` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/registry.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/registry.md new file mode 100644 index 000000000000..b390cfbbaf96 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/registry.md @@ -0,0 +1,234 @@ +--- +sidebar_position: 8 +keywords: [Sealos, registry 命令, Docker 镜像, 镜像仓库, 镜像同步, 镜像复制, 镜像管理, Docker 分发, 镜像保存, 镜像服务器] +description: 了解Sealos registry命令的使用指南,包括save、serve、passwd、sync和copy命令,帮助您高效管理Docker镜像仓库。 +--- + +# registry 镜像仓库命令 + +## Sealos:sealos registry save 命令详解与使用指南 + +`registry save` 命令用于将远程的 Docker 镜像拉取到本地并保存在指定的目录中。这对于在离线或者内网环境中部署容器镜像特别有用。 + +在执行 `registry save` 命令时,将自动获取 `sealos login` 认证信息进行仓库认证。 + +**使用说明** + +1. 使用context自动获取镜像 + + 使用默认方式拉取并保存镜像。这种模式会自动解析 `charts` 目录、`manifests` 目录和 `images` 目录以获取镜像列表。 + + **使用示例** + + ```shell + sealos registry save --registry-dir=/tmp/registry1 my-context + ``` + + +2. 指定镜像列表方式 + + 使用参数传入镜像列表 + + **使用示例** + + ```shell + sealos registry save --registry-dir=/tmp/registry2 --images=docker.io/library/busybox:latest + ``` + +**选项** + +以下选项适用于 `save` 命令及其子命令: + +- `--max-procs`: 拉取镜像时使用的最大并行进程数。 +- `--registry-dir`: 保存镜像的本地目录。 +- `--arch`: 镜像的目标架构,例如:`amd64`、`arm64` 等。 +- `--images`: 需要拉取并保存的镜像列表,以逗号分隔。例如:"my-image1:latest,my-image2:v1.0"。 + +## Sealos:sealos registry serve 命令详解与使用指南 + +在管理 Docker 镜像仓库过程中,Sealos 提供了 `sealos registry serve` 命令以方便用户进行相关操作。本文将详细介绍 `sealos registry serve` 命令的使用方法和示例。 + +### 基本介绍 + +`sealos registry serve` 命令的主要作用是启动一个 Docker 分发镜像仓库服务器,支持两种模式:`filesystem` 和 `inmem`。 + +1. **Filesystem 模式**:在此模式下,sealctl 将运行一个针对指定目录的 Docker 分发镜像仓库服务器。该模式下,镜像数据将存储在硬盘上。 + +2. **In-memory 模式**:在此模式下,sealctl 将运行一个内存中的 Docker 分发镜像仓库服务器。该模式下,镜像数据仅保存在内存中,进程退出后数据将丢失。 + +### 命令参数 + +`sealos registry serve filesystem ` 命令支持以下参数: + +- `--disable-logging`: 禁用日志输出,默认为 false。 +- `--log-level`: 配置日志级别,默认为 'error'。 +- `-p, --port`: 服务器监听的端口,默认为随机未使用的端口。 + +### 使用示例 + +以下是一些 `sealos registry serve` 命令的使用示例: + +#### 在文件系统中启动镜像仓库服务器 + +```bash +sealos registry serve filesystem --port=5000 +``` + +以上命令将在端口5000上启动一个文件系统镜像仓库服务器。 + +#### 在内存中启动镜像仓库服务器 + +```bash +sealos registry serve inmem +``` + +以上命令将启动一个内存镜像仓库服务器。该服务器在进程退出后,存储的数据将丢失。 + +通过 `sealctl registry serve` 命令,用户可以轻松地管理和操作 Docker 镜像仓库。无论是在开发环境,还是在生产环境中,它都是一个强大且易用的工具。 + + +## Sealos:sealos registry passwd 命令详解与使用指南 + +在管理 Docker 镜像仓库过程中,Sealos 提供了 `sealos registry passwd` 命令以方便用户对集群registry进行密码修改。它提供了一种简便的方法,帮助用户修改 registry 的密码。 + +### 基本用法 + +使用 `sealos registry passwd` 命令来修改registry的密码。 + +```bash +sealos registry passwd +``` + +### 参数 + +以下是 `sealos registry passwd` 命令的参数: + +- `-c, --cluster-name`:集群名称,默认为'default'。 + +- `-f, --cri-shim-file-path`:镜像 cri shim 文件路径,如果为空将不会更新镜像 cri shim 文件。默认路径为'/etc/image-cri-shim.yaml'。 + +- `-p, --htpasswd-path`:registry 密码文件路径。默认路径为'/etc/registry/registry_htpasswd'。 + +### 使用步骤 + +1. 执行 `sealos registry passwd` 命令,可以根据需要指定参数来进行配置。 + +2. 根据命令提示,输入新的密码。 + +3. 命令执行成功后,registry 的密码将被修改为新的密码。 + +### 演示说明 + +[![asciicast](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ.svg)](https://asciinema.org/a/Qu05jah4ZZmjMuFR4vHEKvBsQ) + +**在使用过程中,会让用户选择registry类型** + +- registry: 二进制启动,执行`systemctl restart registry`进行重启镜像仓库。 +- containerd: containerd启动,执行"nerdctl restart sealos-registry"进行重启镜像仓库。 +- docker: docker启动,执行"docker restart sealos-registry"进行重启镜像仓库。 + +### 注意事项 + +**修改 registry 密码后,修改Clusterfile中的registry密码** +修改 registry 密码后,所有使用该 registry 的节点和服务都需要更新配置,以使用新的密码进行身份验证。否则,它们将无法从该 registry 拉取或推送镜像。 + +如果你不确定如何更新节点和服务的配置,建议在修改 registry 密码之前,先查阅相关文档或者寻求专业的技术支持。 + +## Sealos:`sealos registry sync` 命令详解与使用指南 + +Sealos 的 `registry sync` 命令可帮助您在两个 registry 之间同步所有镜像。这不仅可以用于镜像的迁移,还可以备份您的镜像。 + +### 命令基本用法 + +执行 `sealos registry sync` 命令来进行镜像同步: + +```bash +sealos registry sync source dst +``` + +这里的 `source` 表示源 registry 的地址,而 `dst` 是目标 registry 的地址。 + +例如,您想将地址为 127.0.0.1:41669 的 registry 中的所有镜像同步到地址为 sealos.hub:5000 的 registry,您应执行以下命令: + +```bash +sealos registry sync 127.0.0.1:41669 sealos.hub:5000 +``` + +### 认证与权限 + +在执行 `sealos registry sync` 命令之前,请确保您具有访问源 registry 和目标 registry 的权限。可以使用`sealos login`对registry进行认证登录。 + +### 同步过程 + +请注意,镜像同步可能需要一些时间,这取决于镜像的数量和大小,以及网络的速度。在同步过程中,请保持网络的连通性,并确保在同步完成之前不要中断命令的执行。 + +重要的是,`sealos registry sync` 命令支持增量同步,已经存在于目标 registry 的镜像不会重新同步。 + +### 参数选项 + +`sealos registry sync` 命令还提供了一些参数选项,允许您更精细地控制同步过程: + +- `--override-arch ARCH`:使用指定的 `ARCH` 替代当前机器的架构来选择镜像。 + +- `--override-os OS`:使用指定的 `OS` 替代当前操作系统来选择镜像。 + +- `--override-variant VARIANT`:使用指定的 `VARIANT` 替代当前的架构变种来选择镜像。 + +- `-a` 或 `--all`:如果源镜像是一个列表,同步所有镜像。这对异构环境下特别有用,因为默认情况下,只会同步当前架构的镜像。 + +例如,如果您想同步所有架构的镜像,可以添加 `-a` 参数: + +```bash +sealos registry sync -a 127.0.0.1:41669 sealos.hub:5000 +``` + +以上就是 `sealos registry sync` 命令的详细说明与使用指南。希望这些信息能帮助您更好地理解和使用这个命令。如果您在使用过程中遇到任何问题,欢迎随时提问。 + +## Sealos:`sealos registry copy` 命令详解与使用指南 + +Sealos 的 `registry copy` 命令用于将指定镜像从一个 registry 复制到另一个 registry。这能帮助您在不同的 registry 之间进行镜像的迁移或备份。 + +### 命令基本用法 + +使用 `sealos registry copy` 命令来进行镜像的复制: + +```bash +sealos registry copy source-image dst +``` + +这里的 `source-image` 表示源镜像的全名(包括地址和镜像名),`dst` 是目标 registry 的地址。 + +例如,要将名为 `127.0.0.1:41669/my-image:tag` 的镜像复制到地址为 `sealos.hub:5000` 的 registry,您可以执行以下命令: + +```bash +sealos registry copy 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + +### 认证与权限 + +在执行 `sealos registry copy` 命令之前,请确保您具有访问源镜像和目标 registry 的权限。可以使用`sealos login`对registry进行认证登录。 + +### 复制过程 + +请注意,镜像复制可能需要一些时间,这取决于镜像的大小,以及网络的速度。在复制过程中,请保持网络的连通性,并确保在复制完成之前不要中断命令的执行。 + +### 参数选项 + +`sealos registry copy` 命令提供了一些参数选项,允许您更精细地控制复制过程: + +- `--override-arch ARCH`:使用指定的 `ARCH` 替代当前机器的架构来选择镜像。 + +- `--override-os OS`:使用指定的 `OS` 替代当前操作系统来选择镜像。 + +- `--override-variant VARIANT`:使用指定的 `VARIANT` 替代当前的架构变种来选择镜像。 + +- `-a` 或 `--all`:如果源镜像是一个列表,复制所有镜像。这对异构环境下特别有用,因为默认情况下,只会复制当前架构的镜像。 + +例如,如果您想复制所有架构的镜像,可以添加 `-a` 参数: + +```bash +sealos registry copy -a 127.0.0.1:41669/my-image:tag sealos.hub:5000 +``` + + +以上就是 `sealos registry` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/reset.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/reset.md new file mode 100644 index 000000000000..5687004e9623 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/reset.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 2 +keywords: [Sealos reset, 重置集群, Sealos 命令, 集群管理, Kubernetes 重置] +description: 了解如何使用 Sealos reset 命令重置集群,包括基本用法、示例和可选参数,确保集群管理更高效。 +--- + +# reset 重置集群 + +`sealos reset` 是 Sealos 命令行工具中的一个命令,用于重置整个集群。这个命令在你想要彻底清空集群数据或者重建集群的时候特别有用。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos reset` 命令格式如下: + +```bash +sealos reset --cluster cluster_name +``` + +在上述命令中,`cluster_name` 是你想要重置的集群的名称。 + +## 示例 + +例如,你可以使用以下命令重置名为 `mycluster` 的集群: + +```bash +sealos reset --cluster mycluster +``` + +## 可选参数 + +- `--force`: 该参数用于强制重置集群,即使集群重置操作未能成功完成。 + +```bash +sealos reset --cluster mycluster --force +``` + +- `--masters`: 该参数用于指定要重置的 master 节点。 + +```bash +sealos reset --cluster mycluster --masters master1 +``` + +- `--nodes`: 该参数用于指定要重置的工作节点。 + +```bash +sealos reset --cluster mycluster --nodes node1 node2 +``` + +- `-p`, `--passwd`: 该参数用于提供密码进行身份验证。 + +- `-i`, `--pk`: 该参数用于指定用于公钥认证的身份(私钥)读取的文件。 + +- `--pk-passwd`: 该参数用于解密 PEM 编码私钥的口令。 + +- `--port`: 该参数用于指定要连接的远程主机的端口。 + +- `-u`, `--user`: 该参数用于指定要作为身份验证的用户名。 + +```bash +sealos reset --cluster mycluster --user username --pk /root/.ssh/id_rsa --pk-passwd yourpassword +``` + +以上就是 `sealos reset` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md new file mode 100644 index 000000000000..4272a531630a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/rmi.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 6 +keywords: [sealos rmi, 删除本地镜像, sealos命令行工具, 镜像管理, sealos教程] +description: 了解如何使用Sealos rmi命令删除本地镜像,清理无用或过时的镜像,节省存储空间。阅读详细指南获取更多信息。 +--- + +# rmi 删除本地镜像 + +`sealos rmi` 是 Sealos 命令行工具中的一个命令,用于删除本地存储的一个或多个镜像。这个命令可以帮助你清理无用或者过时的镜像,节省存储空间。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos rmi` 命令格式如下: + +```bash +sealos rmi imageID +``` + +在上述命令中,`imageID` 是你想要删除的镜像的 ID。 + +## 示例 + +例如,你可以使用以下命令删除 ID 为 `imageID` 的镜像: + +```bash +sealos rmi imageID +``` + +如果你想要删除多个镜像,只需要在命令行中列出所有的镜像 ID,例如: + +```bash +sealos rmi imageID1 imageID2 imageID3 +``` + +## 可选参数 + +- `-a`, `--all`: 该参数用于删除所有镜像。使用此选项时,命令将不接受任何镜像 ID。 + +```bash +sealos rmi --all +``` + +- `-f`, `--force`: 该参数用于强制删除镜像,以及使用该镜像的任何容器。 + +```bash +sealos rmi --force imageID +``` + +- `-p`, `--prune`: 该参数用于修剪悬挂的镜像(没有标签且没有被任何容器引用的镜像)。 + +```bash +sealos rmi --prune +``` + +以上就是 `sealos rmi` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md new file mode 100644 index 000000000000..d5a44283aaad --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 1 +keywords: [Sealos, 集群镜像, Kubernetes, run命令, 多镜像执行, 集群初始化, 应用安装] +description: 了解如何使用Sealos的run命令进行集群初始化、应用安装和多镜像执行,掌握Kubernetes集群管理的最佳实践。 +--- + +# run 运行集群镜像 + +Sealos 的 `run` 命令是一个强大且灵活的工具,它支持集群初始化、应用安装、多镜像执行,单机集群等操作。下面是对于 `sealos run` 命令及其参数的详细解释和一些使用示例。 + +## 命令概览 + +``` +sealos run --masters [arg] --nodes [arg] [Options] +``` + +`` 参数是您想要在集群中运行的 Docker 镜像名称和版本。`--masters` 和 `--nodes` 是您想要运行这个镜像的 master 节点和 node 节点的 IP 列表。 + +### 选项解释 + +- `--cluster='default'`: 要运行操作的集群名称。 + +- `--cmd=[]`: 覆盖镜像中的 CMD 指令。 + +- `--config-file=[]`: 自定义配置文件的路径,用于替换资源。 + +- `-e, --env=[]`: 在命令执行期间设置的环境变量。 + +- `-f, --force=false`: 强制覆盖此集群中的应用。 + +- `--masters=''`: 要运行的 master 节点。 + +- `--nodes=''`: 要运行的 node 节点。 + +- `-p, --passwd=''`: 使用提供的密码进行认证。 + +- `-i, --pk='/root/.ssh/id_rsa'`: 选择从其中读取公钥认证身份的私钥文件。 + +- `--pk-passwd=''`: 解密 PEM 编码的私钥的密码。 + +- `--port=22`: 远程主机的连接端口。 + +- `-t, --transport='oci-archive'`: 从 tar 归档文件加载镜像传输。(可选值: oci-archive, docker-archive) + +- `-u, --user=''`: 认证的用户名。 + +## 示例 + +1. 创建集群到您的裸机服务器,指定 IP 列表: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +2. 多镜像运行: +``` +sealos run labring/kubernetes:v1.24.0 labring/helm:v3.11.3 calico:v3.24.6 \ + --masters 192.168.64.2,192.168.64.22,192.168.64.20 --nodes 192.168.64.21,192.168.64.19 +``` + +3. 指定服务器的 InfraSSH 端口: +``` +sealos run labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --port 24 --passwd 'xxx' +``` + +4. 自定义 VIP Kubernetes 集群: +``` +sealos run -e defaultVIP=10.103.97.2 labring/kubernetes:v1.24.0 --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +5. 创建单节点 Kubernetes 集群: +``` +sealos run labring/kubernetes:v1.24.0 +``` + +6. 使用自定义环境变量创建集群: +``` +sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.168.0.3,192.168.0.4 \ + --nodes 192.168.0.5,192.168.0.6,192.168.0.7 --passwd 'xxx' +``` + +这些示例展示了 `sealos run` 命令的强大和灵活性,可以根据您的需求进行定制和调整。 + +更多示例请参考 [运行集群](/developer-guide/lifecycle-management/operations/run-cluster.md)。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/save.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/save.md new file mode 100644 index 000000000000..8d2f50b38ce7 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/save.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 6 +keywords: [sealos save, 保存镜像, Sealos 命令行工具, 镜像备份, 镜像迁移] +description: 了解如何使用 Sealos save 命令保存镜像到归档文件中,轻松备份和迁移你的镜像,支持多种格式和参数。 +--- + +# save 保存镜像 + +`sealos save` 是 Sealos 命令行工具中的一个命令,用于将镜像保存到归档文件中。这个命令可以帮助你方便地备份和迁移你的镜像。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos save` 命令格式如下: + +```bash +sealos save -o outputFilename imageName +``` + +在上述命令中,`outputFilename` 是你想要保存的归档文件的名称,`imageName` 是你想要保存的镜像的名称。 + +## 示例 + +例如,你可以使用以下命令将名为 `labring/kubernetes:latest` 的镜像保存到一个名为 `kubernetes.tar` 的归档文件中: + +```bash +sealos save -o kubernetes.tar labring/kubernetes:v1.24.0 +``` + +## 可选参数 + +- `--format`: 这个参数用于指定保存镜像的传输方式。目前可用的选项有 `oci-archive` 和 `docker-archive`、`oci-dir`,`docker-dir`。默认值是 `oci-archive`。 +- `-m`: 这个参数可以同时保存多个镜像,但是仅限于`docker-archive`格式。 + +例如,你可以使用以下命令将名为 `labring/kubernetes:latest` 的镜像以 `docker-archive` 的方式保存到一个名为 `kubernetes.tar` 的归档文件中: + +```bash +sealos save -o kubernetes.tar --format docker-archive labring/kubernetes:v1.24.0 +sealos save -o kubernetes.tar -m --format docker-archive labring/kubernetes:v1.24.0 labring/helm:v3.5.0 +``` + +以上就是 `sealos save` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/scp.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/scp.md new file mode 100644 index 000000000000..0fce7a06951d --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/scp.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 5 +keywords: [sealos scp, 拷贝文件, 集群节点, 远程节点, 命令行工具] +description: 了解如何使用 Sealos scp 命令将文件复制到指定的集群节点,详细介绍基本用法和选项,助您轻松管理文件传输。 +--- + +# scp 拷贝文件 + +`sealos scp` 是 Sealos 命令行工具中的一个命令,用于将文件复制到指定的集群节点。本指南将详细介绍其使用方法和选项。 + +## 基本用法 + +基本的 `sealos scp` 命令格式如下: + +```bash +sealos scp "source file path" "destination file path" +``` + +在上述命令中,`source file path` 是你要复制的文件的本地路径,`destination file path` 是你要将文件复制到的远程节点路径。 + +## 选项 + +`sealos scp` 命令提供了以下选项: + +- `-c, --cluster='default'`: 要将文件复制到其上的集群的名称。默认为 `default`。 + +- `--ips=[]`: 将文件复制到具有指定 IP 地址的节点。 + +- `-r, --roles='':`: 将文件复制到具有指定角色的节点。 + +每个选项后都可以跟随一个或多个参数。 + +## 示例 + +例如,你可以使用以下命令将本地的 `/root/aa.txt` 文件复制到默认集群的所有节点的 `/root/dd.txt`: + +```bash +sealos scp "/root/aa.txt" "/root/dd.txt" +``` + +如果你想在名为 `my-cluster` 的集群的 `master` 和 `node` 角色的节点上复制文件,可以使用以下命令: + +```bash +sealos scp -c my-cluster -r master,node "/root/aa.txt" "/root/dd.txt" +``` + +如果你只想在 IP 地址为 `172.16.1.38` 的节点上复制文件,可以使用以下命令: + +```bash +sealos scp -c my-cluster --ips 172.16.1.38 "/root/aa.txt" "/root/dd.txt" +``` + +以上就是 `sealos scp` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/tag.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/tag.md new file mode 100644 index 000000000000..3ac7e03ce0ab --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/tag.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 6 +keywords: [Sealos tag, 镜像管理, 命令行工具, 添加镜像名称, Sealos 使用指南] +description: 了解如何使用 Sealos tag 命令为本地存储的镜像添加一个或多个附加名称,提升镜像管理效率。 +--- + +# tag 添加镜像名称 + +`sealos tag` 是 Sealos 命令行工具中的一个命令,用于给本地存储的镜像添加一个或多个附加名称。这个命令可以帮助你更好地管理你的镜像。本指南将详细介绍其使用方法。 + +## 基本用法 + +基本的 `sealos tag` 命令格式如下: + +```bash +sealos tag imageName newName +``` + +在上述命令中,`imageName` 是你要操作的镜像的名称,`newName` 是你想要添加的新标签。 + +## 示例 + +例如,你可以使用以下命令给名为 `imageName` 的镜像添加一个新的名称 `firstNewName`: + +```bash +sealos tag imageName firstNewName +``` + +你也可以一次添加多个名称,例如,添加 `firstNewName` 和 `SecondNewName` 两个名称: + +```bash +sealos tag imageName firstNewName SecondNewName +``` + +以上就是 `sealos tag` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md new file mode 100644 index 000000000000..656e6928ad9e --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 10 +keywords: [Sealos, Kubernetes集群镜像, rootfs类型, 容器运行时, Kubernetes镜像版本] +description: 了解Sealos官方提供的Kubernetes集群镜像,包括镜像名称、类型和版本,选择适合您的Kubernetes容器运行时解决方案。 +--- + +# Kubernetes 集群镜像说明文档 + +本文档主要介绍 Sealos 官方提供的 rootfs 类型集群镜像,包括镜像名称、镜像类型以及镜像版本等方面的说明。 + +## 镜像名称 + +Sealos 官方发布的集群镜像主要包括以下几种: + +1. kubernetes:使用 containerd 作为容器运行时(CRI)的 Kubernetes 镜像。 +2. kubernetes-docker:使用 docker 作为容器运行时(CRI)的 Kubernetes 镜像。 +3. kubernetes-crio:使用 crio 作为容器运行时(CRI)的 Kubernetes 镜像。 + +目前,Sealos 主要提供了 Kubernetes 相关的镜像,而其他类型的集群镜像,如 k3s、k0s 等,尚未提供。 + +## 镜像类型 + +根据容器运行时(CRI)的不同,Sealos 提供了不同类型的 Kubernetes 集群镜像: + +1. 使用 containerd 作为 CRI 的 Kubernetes 镜像。 +2. 使用 docker 作为 CRI 的 Kubernetes 镜像。 +3. 使用 crio 作为 CRI 的 Kubernetes 镜像。 + +用户可以根据自己的需求和偏好,选择合适的镜像类型。 + +## 镜像版本 + +Sealos 官方提供的集群镜像有多个版本可供选择,例如: + +### 1. 开发版(Development version) + +适用于想要尝试项目最新功能的用户。开发版可能包含尚未经过完整测试的新功能和改进,因此可能不够稳定。 + +示例:`v1.26(v1.26-amd64/v1.26-arm64)` + +### 2. 最新版(Latest version) + +通常比开发版更稳定,但可能不包含所有的新功能。这是推荐给大多数用户使用的版本。 + +示例:`v1.26.0(v1.26.0-amd64/v1.26.0-arm64)` + +### 3. 发布版(Release version,包含历史版本) + +包含了历史版本的发布版。发布版通常经过了严格的测试,被认为是稳定的。 + +示例:`v1.26.0-4.1.5(v1.26.0-4.1.5-amd64/v1.26.0-4.1.5-arm64)` `4.1.5`是sealos对应版本号 + +在选择镜像版本时,用户需要根据自己的需求和偏好来选择适合的版本。另外,Sealos 还提供了针对不同处理器架构的子版本,以满足用户在不同硬件平台上的需求。 + +## 总结 + +本文档对 Sealos 官方提供的 rootfs 类型集群镜像进行了说明,包括镜像名称、镜像类型以及镜像版本等方面。用户可以根据自己的需求和偏好,在不同的镜像类型和版本中进行选择,以便在 Kubernetes 集群中运行容器。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md new file mode 100644 index 000000000000..5948b3cd0817 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 0 +keywords: [Sealos, 云操作系统, Kubernetes管理, 集群镜像, 命令行工具] +description: 了解Sealos使用指南,掌握云操作系统管理Kubernetes集群的命令行工具和集群镜像的详细信息。 +--- + +# Sealos 使用指南 + +Sealos 是一个统一的云操作系统,用于管理云原生应用。它提供了一系列命令行工具来帮助用户管理 Kubernetes 集群,管理节点,远程操作,管理容器和镜像,以及其他一些功能。下面是这些命令的详细介绍: + +本章节主要介绍 Sealos 的使用指南以及集群镜像相关说明。 + +- Sealos使用指南: 有关每个命令的详细信息,包括所有受支持的参数和子命令, 请参阅 [sealos](/developer-guide/lifecycle-management/reference/sealos/commands.md) 参考文档。 +- 集群镜像: + - [Rootfs集群镜像](/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application集群镜像](/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md) + + diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/QA.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/QA.md new file mode 100644 index 000000000000..0bb258f2b87a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/QA.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 3 +keywords: [Sealos Cloud, 常见问题, 部署问题, 证书更新, 域名更换, 用户注册, 集群管理, Kubernetes] +description: 探索Sealos Cloud部署和使用中的常见问题解决方案,包括系统配置、证书更新、域名更换等关键操作指南,助您轻松管理Kubernetes集群。 +--- + +# 常见问题 + +在部署及使用 Sealos Cloud 过程中,您可能会遇到各种问题。为了更好地帮助您解决这些问题,我们对常见问题进行了总结,并提供了详细的答案和解决方法。 + +## 部署问题 + +下面总结了部署过程中可能遇到的问题及解决方法,假如您遇到了其他问题,请在 [Sealos 社区](https://forum.laf.run/)中联系我们。 + +### Q1:iptables / ip_forward 问题 + +**问题描述**:在部分操作系统中,iptables 或 IPv4 IP 转发默认未启用,例如旧版本的 Centos、RHEL 等。这可能导致部署过程中无法正常创建 +iptables 规则或转发数据包,从而导致集群无法正常启动。 + +**解决方法**:需要在每个节点上执行以下命令,以启用 iptables 和 IP 转发: + +```shell +$ modprobe br_netfilter +$ echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables +$ echo 1 > /proc/sys/net/ipv4/ip_forward +``` + +### Q2:系统内核问题 + +- **问题描述**:如果系统内核版本过低,可能导致集群无法正常启动。低版本内核也可能导致依赖 MongoDB 5.0 的应用无法正常运行。 +- **解决方法**:在部署前,请确保系统内核版本至少为 5.4 或更高。 + +### Q3:系统资源问题 + +- **问题描述**:系统资源紧张可能会导致部署过程中出现卡顿或停滞,当您等待过久时,请检查系统资源是否足够。 +- **解决方法**:使用命令 `kubectl describe nodes` 查看节点资源状态。一般情况下可以从 CPU、内存、存储等方向排查系统资源是否充足。 + +### Q4:网络问题 + +- **问题描述**: 在部署过程中,服务器的不当配置可能会引发多种网络问题,例如: + 1. http_proxy / https_proxy 环境变量配置; + 2. 服务器防火墙配置; + 3. 服务器路由配置; +- **解决方法**: 遇到网络问题时,请检查以上配置是否正确。 + +## 证书及域名相关问题 + +### 证书更新 + +在您使用 Sealos 过程中,证书是保障集群安全的重要组成部分。以下是详细的证书更新步骤,这些步骤可以帮助您在证书即将过期时顺利更新: + +1. **备份旧证书**: + + 在主节点 `master0` 上,您需要先备份当前使用的证书。这是一个防止更新过程中出现问题而导致证书丢失的重要步骤。使用以下命令进行备份: + + ```shell + $ kubectl get secret -n sealos-system wildcard-cert -o yaml > cert-backup.yaml + ``` + + 此命令会将名为 `wildcard-cert` 的证书以 YAML 格式保存到文件 `cert-backup.yaml` 中。 + +2. **保存新证书**: + + 将您已经准备好的新证书文件保存到 `master0` 节点上。确保新的证书文件(通常是 `.crt` 和 `.key` 文件)在节点上的某个位置。 + +3. **更新证书**: + + 使用以下脚本来更新证书。您需要替换脚本中的 `` 和 ``,以指向您的新证书文件和密钥文件的实际路径。 + + ```shell + #!/bin/bash + # 设置变量 + CRT_FILE= + KEY_FILE= + + # 将证书和密钥文件内容进行Base64编码 + CRT_BASE64=$(cat $CRT_FILE | base64 -w 0) + KEY_BASE64=$(cat $KEY_FILE | base64 -w 0) + + # 构建部分更新的JSON对象 + PATCH_JSON='{"data":{"tls.crt":"'$CRT_BASE64'","tls.key":"'$KEY_BASE64'"}}' + + # 使用kubectl patch命令更新Secret + kubectl patch secret wildcard-cert -n sealos-system -p $PATCH_JSON + ``` + + 这个脚本的主要作用是将新证书的内容编码为 Base64 格式,并使用 `kubectl patch` 命令更新 Kubernetes 集群中的相应 Secret + 对象。 + +### 域名更换 + +域名更换是一个更加复杂的过程,因为它通常涉及到集群内多个组件和服务的配置更改。目前,我们尚未在文档中提供域名更换的详细教程。不过,我们计划在未来推出 +Sealos 集群管理面板,该面板将提供更加简便的方法来替换集群域名和证书。 + +请注意,域名更换通常需要对集群的网络配置进行深入了解,并且可能涉及到 DNS +设置、服务发现等多个方面。因此,建议在执行此类操作时,确保您具备相应的技术知识或咨询专业人士的帮助。 + +### 用户注册开关 + +关闭用户注册: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: true/signUpEnabled: false/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` + +开启用户注册: + +```shell +kubectl get cm -n sealos desktop-frontend-config -o yaml | sed 's/signUpEnabled: false/signUpEnabled: true/g' | kubectl apply -f - +kubectl rollout restart deployment desktop-frontend -n sealos +``` diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-1.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-1.jpg new file mode 100644 index 000000000000..680d572141b3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-1.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-2.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-2.jpg new file mode 100644 index 000000000000..8b6a354702c0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/chrome-certificate-2.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-1.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-1.png new file mode 100644 index 000000000000..6e2c46692cf6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-2.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-2.jpg new file mode 100644 index 000000000000..a99aaeaf5a0a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-2.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-3.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-3.jpg new file mode 100644 index 000000000000..8a42e6c08c8e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/firefox-export-certificate-3.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-1.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-1.jpg new file mode 100644 index 000000000000..3a211ccd3e13 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-1.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-2.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-2.jpg new file mode 100644 index 000000000000..357d5880dbe4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-2.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-3.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-3.jpg new file mode 100644 index 000000000000..9c33c03204f5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/safari-export-certificate-3.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-cost-center.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-cost-center.jpg new file mode 100644 index 000000000000..e8e59ccb8709 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-cost-center.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-1.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-1.png new file mode 100644 index 000000000000..790d9cc7a3da Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-2.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-2.png new file mode 100644 index 000000000000..9f04a4668927 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-3.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-3.png new file mode 100644 index 000000000000..ea555c230e1d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-4.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-4.png new file mode 100644 index 000000000000..d409c55ba36d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-5.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-5.png new file mode 100644 index 000000000000..3cde5ae76850 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-6.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-6.png new file mode 100644 index 000000000000..4a6ef666fb1d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate.jpg new file mode 100644 index 000000000000..97dbcc274b2e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate1.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate1.png new file mode 100644 index 000000000000..3f1bfaebdc2a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate2.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate2.png new file mode 100644 index 000000000000..7c33e2693e9c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate3.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate3.png new file mode 100644 index 000000000000..609a653a4f15 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate4.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate4.png new file mode 100644 index 000000000000..6c6a20aadb78 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license-activate4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license.png new file mode 100644 index 000000000000..aa820a06c105 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/sealos-license.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/windows-trust-certificate.jpg b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/windows-trust-certificate.jpg new file mode 100644 index 000000000000..243d0d2d5583 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/images/windows-trust-certificate.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md new file mode 100644 index 000000000000..6da7063433f2 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md @@ -0,0 +1,434 @@ +--- +sidebar_position: 1 +toc_max_heading_level: 5 +keywords: [Sealos 集群部署, Kubernetes 安装, Sealos 私有云, Sealos Cloud, Sealos 证书] +description: 了解如何使用 Sealos 一键部署 Kubernetes 集群,支持多种安装方式,确保通信安全,适用于大规模集群和企业生产环境。 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Sealos 集群部署 + +export const Highlight = ({children, color}) => ( + + {children} + +); + +:::tip + +大规模集群以及企业生产环境强烈建议使用 [Sealos 私有云的企业版或者定制版](/self-hosting)。 + +::: + +## 准备工作 + +### 服务器 + +以下是一些基本的要求: + +- 每个集群节点应该有不同的主机名。 +- 所有节点的时间需要同步。 +- 建议使用干净的操作系统来创建集群。**不要自己装 Docker!** +- 支持大多数 Linux 发行版,例如:Ubuntu、Debian、CentOS、Rocky linux。 +- **系统内核版本在 5.4 及以上**。 +- **必须使用 root 用户安装!** + +推荐配置: + +推荐使用 Ubuntu 22.04 LTS 操作系统,内核版本在 5.4 及以上,配置如下: + +| 操作系统 | 内核版本 | CPU | 内存 | 存储 | Masters | Nodes | +|------------------|-------|-----|------|-------|---------|-------| +| Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | 奇数台 | 任意 | + +:::info注意 +Kubernetes 和 Sealos Cloud 的系统组件在每个 Master 节点上大约需要 2 核心(2c)和 2GB 内存(2g),在每个 Node 节点上则需要大约 +1 核心(1c)和 1GB 内存(1g),请确保集群中每个节点都有足够的计算资源以支持系统组件的运行。 +::: + +### 网络 + ++ 所有节点之间网络互通; ++ 需要在 Kubernetes 集群的**第一个 Master 节点**上执行脚本,目前**集群外的节点不支持集群安装**; ++ 所有节点之间可以互相通信。 + +### 域名 + ++ 需要一个域名用于访问 Sealos 及相关服务; ++ 如果您没有域名,可以使用 `nip.io` 提供的免费域名服务。 + +### 证书 + +Sealos 需要使用证书来保证通信安全,默认在您不提供证书的情况下我们会使用 [cert-manager](https://cert-manager.io/docs/) 来自动签发证书。 + +如果您能提供证书,证书需要解析下列域名 (假设您提供的域名为:cloud.example.io): + +- `*.cloud.example.io` +- `cloud.example.io` + +## 安装步骤 + +为了便于部署,我们提供了一个一键安装脚本。该脚本可以从零开始部署 Sealos 集群,也可以在已有的 Kubernetes 集群上部署 Sealos +集群(在已有集群上执行时请谨慎操作)。 + +:::info注意 + +该脚本只支持在 “使用 Sealos 安装的 Kubernetes 集群” 上部署 Sealos 集群,暂不支持其他方式部署的 Kubernetes。 + +关于如何使用 Sealos 部署 Kubernetes +集群,可以参考:[安装 Kubernetes 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md) +,支持 [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags) 中的几乎所有 Kubernetes 版本(**暂不支持 1.28 及以上版本 +**)。 + +::: + +如果您的机器还没有安装过 Kubernetes,**建议直接使用该脚本连同 Kubernetes 和 Sealos 集群一起安装。** + +根据您的域名情况,可以分为以下几种安装方式: + +### 1、无公网域名,也不想自定义域名 + +如果您没有公网域名,也不需要自定义域名,可以选择直接使用 `nip.io` 提供的免费域名服务。`nip.io` 是一个免费的通配符 DNS 服务,它可以将动态分配的 IP 地址映射到一个固定的子域名上,特别适合用于本地开发环境。具体的工作原理为: + +您可以使用任何 IP 地址作为 `nip.io` 子域名的一部分,而 `nip.io` 会将它解析回相应的 IP 地址。例如,如果你有一个内网 IP 地址 `192.168.1.10`,你可以使用 `192.168.1.10.nip.io` 作为域名,所有向这个域名发送的请求都会被解析到 `192.168.1.10` 这个 IP 地址上。这样就无需修改本地 hosts 文件,也不需要搭建内网 DNS 服务,直接通过这个域名就能访问内网服务了。 + +使用 nip.io 作为 Sealos 的域名非常简单,只需在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com +``` + +当需要你输入 Sealos Cloud 域名时,你需要输入这种格式的域名:`[ip].nip.io`,其中 [ip] 是你的 Master 节点 IP。 + +安装完成后,终端会输出 Sealos 访问域名以及用户名和密码,例如: + +```bash +Sealos cloud login info: +Cloud Version: latest +URL: https://10.214.210.102.nip.io +admin Username: admin +admin Password: sealos2023 +``` + +### 2、有公网域名,想公网访问 + +如果你有自己的公网域名,并且想通过公网访问 Sealos,那你就需要准备好公网受信任的 SSL/TLS 证书。你可以通过 acme.sh 等工具自动签发证书,也可以从域名提供商处下载免费证书或者购买商业证书。 + +:::info注意 +如果你的公网 IP 在国内,那么域名必须要备案! +::: + +准备好域名证书后,需要将证书放到第一个 Master 节点的某个目录中,例如 `/root/certs/`。 + +:::info注意 + +您还需要在域名服务商处添加一条该域名的 A 记录,地址解析到第一个 Master 节点的公网 IP 地址。同时还需要添加一条泛解析记录,将该域名的子域名也解析到第一个 Master 节点的公网 IP 地址。 + +例如 (假设你的域名是 `cloud.example.io`,假设你的 Master 节点公网地址是 `192.168.1.10`): + +```bash +cloud.example.io A 192.168.1.10 +*.cloud.example.io A 192.168.1.10 +``` + +::: + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= \ + --cert-path= \ + --key-path= +``` + ++ `` 需要替换成你自己的公网域名。 ++ `` 需要替换成你的证书位置,通常是 `.crt` 或 `.pem` 文件。例如:`/root/certs/example.crt`。 ++ `` 需要替换成你的私钥位置,通常是 `.key` 或 `.pem` 文件。例如:`/root/certs/example.key`。 + +### 3、有公网域名,想内网访问 + +如果您有自己的公网域名,但是只有内网 IP,或者只想在内网访问 Sealos,那您只需要在域名服务商处添加一条该域名的 A 记录,地址解析到第一个 Master 节点的内网 IP 地址。同时还需要添加一条泛解析记录,将该域名的子域名也解析到第一个 Master 节点的内网 IP 地址。 + +例如 (假设你的域名是 `cloud.example.io`,假设你的 Master 节点内网地址是 `192.168.1.10`): + +```bash +cloud.example.io A 192.168.1.10 +*.cloud.example.io A 192.168.1.10 +``` + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= +``` + +其中 `` 需要替换成你自己的公网域名。 + +安装过程中 Sealos 会使用 [cert-manager](https://cert-manager.io/docs/) 来自签名证书。 + +### 4、无公网域名,但想自定义域名 + +如果您没有公网域名,但是需要自定义域名,那么就需要在内网自建 DNS,然后将自定义域名解析到第一个 Master 节点的内网 IP 地址。 + +:::note + +假设您的第一个 Master 节点的内网 IP 地址为 `192.168.1.10`。假设您的域名为 `cloud.example.io`。 + +::: + +可以考虑使用 CoreDNS 来自建 DNS 服务,参考配置: + +```nginx +(global_cache) { + cache { + # [5, 60] + success 65536 3600 300 + # [1, 10] + denial 8192 600 60 + prefetch 1 60m 10% + } +} + +.:53 { + errors + health + ready + + import global_cache + + template IN A cloud.example.io { + answer "{{ .Name }} 60 IN A 192.168.1.10" + fallthrough + } + + forward . 223.5.5.5 + + log + loop + reload 6s +} +``` + +这样不管您访问 `cloud.example.io` 还是 `*.cloud.example.io` 都会解析到第一个 Master 节点的内网 IP 地址。 + +然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: + +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ + --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ + --proxy-prefix=https://mirror.ghproxy.com \ + --cloud-domain= +``` + +其中 `` 需要替换成你自己的自定义域名。 + +安装过程中 Sealos 会使用 [cert-manager](https://cert-manager.io/docs/) 来自签名证书。 + +## 信任自签名证书 + +如果您选择了上面提供的安装方式中的 1 或 3 或 4,那么您的证书默认是不受浏览器信任的,当你访问 Sealos Cloud 时,浏览器会提示下面的信息: + +![](images/chrome-certificate-1.jpg) + +即使点击继续访问,进入 Sealos Cloud 之后也无法正常显示 App 图标,无法打开 App。 + +![](images/chrome-certificate-2.jpg) + +我们需要导出自签名证书,并让系统信任自签名证书。步骤如下。 + +### 导出自签名证书 + +各个浏览器导出自签名证书的步骤略有不同。以下是在一些常用浏览器中导出自签名证书的步骤: + +#### Chrome (以及基于 Chromium 的浏览器如新版 Edge 和 Brave) + +1. 在浏览器地址栏左侧点击 “不安全” 字样。 +2. 点击 “证书无效”,这将打开一个证书信息窗口。 +3. 在打开的证书窗口中,切换到 “详细信息” 标签页。 +4. 在 “详细信息” 标签页中,找到并点击 “导出”。 +5. 选择一个文件名和保存位置,然后完成导出过程。 + +#### Firefox + +1. 点击页面中的 “高级”。 + + ![](images/firefox-export-certificate-1.png) + +2. 然后点击 “查看证书”。 + + ![](images/firefox-export-certificate-2.jpg) + +3. 在证书页面中点击 “PEM (证书)”。 + + ![](images/firefox-export-certificate-3.jpg) + +#### Safari + +1. 点击页面中的 “显示详细信息”。 + + ![](images/safari-export-certificate-1.jpg) + +2. 然后点击 “查看此证书”。 + + ![](images/safari-export-certificate-2.jpg) + +3. 在证书视图中,可以看到一个带有证书链的窗口。 + + ![](images/safari-export-certificate-3.jpg) + +4. 拖动红框圈出来的证书到桌面或文件夹中,证书就会被导出了。 + +### 信任自签名证书 + +#### macOS + +在 macOS 上信任自签名证书可以使用以下命令: + +```bash +# 假设证书保存在 ~/Downloads/10.214.210.102.nip.io.cer +$ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/10.214.210.102.nip.io.cer +``` + +您也可以通过以下步骤来操作: + +##### 1、添加证书到钥匙串 + +1. 启动**钥匙串访问** (Keychain Access) 应用程序,点击左侧 “系统钥匙串” 下方的 “系统” 类别。 +2. 然后打开 “访达”,进入证书保存位置,**双击证书文件**:这通常是一个 `.cer`,`.crt`,或 `.pem` 文件。 +3. 输入您的用户名和密码以允许修改。 +4. 证书现在应该已经被添加到您的钥匙串中。 + +##### 2、信任证书 + +1. 在钥匙串访问中,从 “系统” 类别的列表中找到刚刚添加的自签名证书,并双击它。 +2. 在打开的窗口中,展开 “信任” 部分。 +3. 在 “当使用此证书时” 选项旁边,有一个下拉菜单,默认设置可能是 “使用系统默认值”。要信任证书,请改为 “始终信任”。 +4. 关闭证书信息窗口,系统可能会提示您验证您的用户名和密码,以确认更改。 +5. 现在,证书已经被标记为受信任。 + +:::info注意 + +自签名证书不由第三方证书颁发机构 (CA) 颁发,因此,其他设备也不会信任该证书。如果您是在公司或组织内部使用自签名证书,可能需要在每个需要信任此证书的设备上手动进行上述步骤。 + +::: + +#### Windows + +1. 在根证书文件点鼠标右键,选择 “安装证书”。 +2. 选择 “当前用户” 或者 “本地计算机”,下一步 +3. “将所有的证书都放入下列存储”,“浏览”,“受信任的根证书颁发机构”,“确定”,下一步。 +4. 完成,“是”,确定。 + +![](images/windows-trust-certificate.jpg) + +#### Linux + +Linux 不同发行版更新根证书存储的命令不一样,用来保存私有证书的路径也不一样。需要先复制自签名 CA 证书到特定路径,再运行命令更新根证书存储。 + + + + + ```bash + # Debian/Ubuntu/Gentoo + # - 安装 + $ sudo cp root_ca.crt /usr/local/share/ca-certificates/root_ca.crt + # update-ca-certificates 会添加 /etc/ca-certificates.conf 配置文件中指定的证书 + # 另外所有 /usr/local/share/ca-certificates/*.crt 会被列为隐式信任 + $ sudo update-ca-certificates + + # - 删除 + $ sudo rm /usr/local/share/ca-certificates/root_ca.crt + $ sudo update-ca-certificates --fresh + ``` + + + + + ```bash + # CentOS/Fedora/RHEL + $ yum install ca-certificates + # 启用动态 CA 配置功能: + $ update-ca-trust force-enable + $ cp root_ca.crt /etc/pki/ca-trust/source/anchors/ + $ update-ca-trust + ``` + + + + + ```bash + # Alpine + $ apk update && apk add --no-cache ca-certificates + $ cp root_ca.crt /usr/local/share/ca-certificates/ + $ update-ca-certificates + ``` + + + + + ```bash + # OpenSUSE/SLES + $ cp root_ca.crt /etc/pki/trust/anchors/ + $ update-ca-certificates + ``` + + + + +## 激活集群 + +集群安装完成后,需要激活集群,步骤如下: + +1. 首先点击桌面的「许可证」打开许可证应用: + + ![](images/sealos-license-1.png) + +2. 然后点击出现的页面左侧的「激活/购买」: + + ![](images/sealos-license-2.png) + + 然后浏览器会跳转到 License 页面: + + ![](images/sealos-license-3.png) + +3. 如果你还没有在该页面创建过集群,就点击左上角-价格-获取,立即开始创建一个集群;如果你已经创建过集群了,只需要点击「我的集群」便会跳转到已有集群。 + +4. 新购买一个集群后,点进进入会看到新购买的集群显示-未激活: + + ![](images/sealos-license-4.png) + + 点击集群管理,在集群激活处输入一个集群id(自取,不能和他人重复),输入完成后点击激活即可成功激活集群。 + + ![](images/sealos-license-5.png) + + 5.成功激活集群后,集群管理的右侧会出现License管理选项,点击进入,在License列表下面导出License: + + ![](images/sealos-license-6.png) + +6.导出License后,回到 Sealos 集群的「许可证」应用界面,点击「上传 License 文件」: + + ![](images/sealos-license-activate4.png) + +选择刚刚下载的 License 文件进行上传,然后点击右下角的「激活 License」,便可激活 License。 + + ![](images/sealos-cost-center.jpg) diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/billing-system.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/billing-system.md new file mode 100644 index 000000000000..7d7599a76138 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/billing-system.md @@ -0,0 +1,173 @@ +--- +keywords: [计费系统, Sealos, 公有云, 账户管理, 资源计量, CockroachDB, MongoDB, 充值流程, 用户状态, 账单管理] +description: 了解Sealos计费系统的设计与实现,包括资源计量、账户管理、扣费与账单、暂停与恢复以及充值流程,确保用户轻松管理云服务费用。 +--- + +# 计费系统 + +# 概述 + +计费一直是公有云的服务痛点之一。Sealos +以简单,弹性为设计理念,自然也不想在计费系统上过多的增加用户的心智负担,因此计费系统采取按量计费的方式,通过尽可能的缩小计费品类,为用户带来更低的价格与更容易理解的方式。并通过完备详细的账单系统使用户能及时清晰的看到自己旗下各项类目的消费情况。 + +# 系统架构 + +## 系统组成 + +与其他系统的设计类似,Sealos 的计费系统由一系列的 CRD 与他们所对应的 Controller +为组成。按照计费的流程可以大致分为计量与计费系统两部分。计量系统主要负责计算用户各项资源具体用量,并交由计费系统,计费系统按照用量进行费用的计算与扣减,并根据用户余额执行停机/恢复等操作。同时计费系统还管理用户的账户信息,并处理用户充值流程。 + +## 持久化设计 + +由于 Sealos 存在多个可用区,因此无法使用一套数据源来完成所有功能。 + +计量系统存在数据及时性与复杂的结构,不适合使用持久化数据库存储,因此采用 MongoDB 存储在每个可用区中,并定时清楚。 + +而用户的账户,账单等关键数据则使用 CockroachDB 这种分布式关系型数据库,在不同的可用区之间分片存储并进行数据同步,并永久保存。 + +# 计量 + +## 设计 + +计量系统采用 Controller 执行定时任务的方式。具体工作流程为: + +- 定时(当前为1min)轮询所有用户资源 +- 把轮询的结果存入数据库 + +按照用户系统的设计,每个用户都有专属的命名空间,所以计量系统的是以命名空间为单位进行统计。 + +## 实现 + +计量系统代码位于 Sealos/controllers/resoucers.主要使用一个类 CRD 的 Controller 实现.即使用 Kuberbuilder 生成代码后,不使用其给出的 +Reconcile 方法,而是修改其生成的方法和main函数逻辑的方式实现。 + +main 函数的主要逻辑可以总结为:创建一个 Reconciler 对象,并调用 Reconciler 对象的 StartReconciler() 方法。 + +在 StartReconciler() 中,使用 ticker 创建了一个一个每分钟执行一次的定时任务。 + +该任务的执行逻辑大体如下: + +- 获取所有的命名空间 +- 对于每个命名空间,启动一个 goroutine 进行计费,使用 WaitGroup 同步。 +- 获取Pod资源:CPU,内存 +- 获取PVC资源 +- 获取数据库资源 +- 获取端口资源 +- 获取对象存储资源 +- 组装整体对象,调用数据库接口存入数据库 + +# 计费 + +## 设计 + +计费系统以计量系统为基础,采用定时任务与监听 CR 变化相结合的方式。 + +## 实现 + +### 账户 + +用户账户主要通过名为 account 的 CR 进行管理。代码位于 Sealos/account/account_controller + +当用户创建新的账户时,account controller会使用 syncAccount 为其创建新的 account,并完成分配配额等操作。 + +账户持久化的结构体如下: + +``` +type Account struct { + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;default:gen_random_uuid();primary_key"` + ActivityBonus int64 `gorm:"column:activityBonus;type:bigint;not null"` + EncryptBalance string `gorm:"column:encryptBalance;type:text;not null"` + EncryptDeductionBalance string `gorm:"column:encryptDeductionBalance;type:text;not null"` + CreatedAt time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"` + CreateRegionID string `gorm:"type:text;not null"` + Balance int64 + DeductionBalance int64 +} +``` + +其中,EncryptBalance 代表全部的历史充值金额,一定程度上反映了用户的信誉程度,EncryptDeductionBalance代表全部的历史扣费金额,均以加密字符串的密文形式存储。 + +Balance 为用户余额,使用明文存储。当余额与两个金额的差值出现不一致的时候,以更安全的差值为准。 + +## 扣费与账单 + +扣费主要由 Sealos/account/billing_controller 负责。这个 controller 没有对应的 CR,其修改的方式其主要是通过 uesr +crd.当前其主要逻辑为: + +- 以小时为单位,轮询 Mongodb 获取数据 +- 计算账单 +- 扣减用户费用,此处即为新增 EncryptDeductionBalance,同时更新 Balance + +扣费完成之后生成以小时为单位的账单数据,通过 billinginfoquery 和 billingrecordquery 两个 CRD 进行管理,提供给用户查询账单的各种接口。 + +## 暂停与恢复 + +### 状态转换 + +当 billing 扣费后,debt_controller 会检测用户的账户余额,并根据当前状态和余额的不同执行不同的操作。 + +用户一共有五个状态: + +- 正常期:账户余额大于等于0 +- 预警期:账户余额小于0时,且超时超过 WarningPeriodSeconds (default is 0 day) +- 临近删除期:账户余额小于0,且上次更新时间超过 ApproachingDeletionPeriodSeconds (default is 4 days +- 即刻删除期:账户余额小于0,且上次更新时间超过 ImmediateDeletePeriodSeconds (default is 3 days) +- 最终删除期:账户余额小于0,且上次更新时间超过 FinalDeletePeriodSeconds (default is 7 days) + +这些状态之间的转移方式大致如下: + +当用户处于正常期,且余额大于0,则不进行任何操作,若余额小于0,则进入预警期。 + +当用户处于预警期,且余额大于0,则返回进入正常期,若余额依然小于0,则判断当前时间,欠费额度与全部历史充值金额的关系,若上次更新时间小于临近删除时间, +且欠费小于历史充值金额的一半,则维持预警期不变,否则进入临近删除期,并向用户发送临近删除的通知。 + +当用户处于临近删除期,且余额大于0,则返回进入正常期,若余额依然小于0,则判断则判断当前时间,欠费额度与全部历史充值金额的关系,若上次更新时间小于临近删除时间, +且欠费小于历史充值金额,则维持临近删除期不变,否则进入即刻删除期,此时暂停用户的资源,并向用户发送即刻删除期的通知。 + +当用户处于即刻删除期,且余额大于0,则返回进入正常期,若余额依然小于0,则判断则判断当前时间,若上次更新时间小于临近删除时间, +则维持即刻删除期不变,否则进入最终删除期,并向用户发送最终删除期的通知。 + +当用户处于最终删除期,且余额大于0,则返回进入正常期。 + +### 实现 + +修改用户的状态通过 SuspendUserResource() 方法,这个方法本质上是通过修改 NameSpace 资源,然后触发 NameSpace CR 的controller +实现。 + +namespcace controller 位于 Sealos/controller/namespcae_controller 中,本质上,是通过一个管道执行一系列函数实现: + +``` +pipelines := []func(context.Context, string) error{ + //r.suspendKBCluster, + r.suspendOrphanPod, + r.limitResourceQuotaCreate, + r.deleteControlledPod, + //TODO how to suspend infra cr or delete infra cr + //r.suspendInfraResources, + r.suspendObjectStorage, + } +``` + +其中,suspendOrphanPod() 方法核心逻辑是使用自建的调度器调度 pod,工作流程如下: + +- 深拷贝原pod +- 修改原pod属性:将调度器修改为自定义的调度器 +- 重新创建pod + +通过这种方式可以保护部分 Pod 不被Kuberrnetes 自带的调度器删除,而可以只删除 Deployment 等资源。 + +limitResourceQuotaCreate() +方法通过为用户的命名空间添加新的,全部用量都为0的配额来限制用户创建新的资源,当用户创建新的命名空间时,会默认为其分配一定的配额,而命名空间中资源的使用必须符合所有的配额类型,所以此时用户就无法再创建任何全新的资源了。 + +deleteControlledPod() 是删除控制 Pod 的具体 Pod + +suspendObjectStorage()方法用于处理对象存储的相关操作 + +## 充值 + +充值功能通过 payment_controller 实现, + +具体的工作流程为: + +用户发起充值请求,前端调用 payment_controller 生成订单CR返回给前端,用户进行支付,同时 account_controller +轮询查询订单支付状态的接口,当判断到用户充值成功了之后,调用数据库接口为用户增加 EncryptBalance ,同时更新 Balance。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_dark.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_dark.png new file mode 100644 index 000000000000..403e9223d16c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_dark.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_light.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_light.png new file mode 100644 index 000000000000..500f67bbe589 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/application_light.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_dark.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_dark.png new file mode 100644 index 000000000000..6d14555ef4d6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_dark.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_light.png b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_light.png new file mode 100644 index 000000000000..17bf8cf5d6ae Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/architecture_light.png differ diff --git "a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" "b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" new file mode 100644 index 000000000000..44b16d80d808 Binary files /dev/null and "b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\224\250\346\210\267-1.png" differ diff --git "a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" "b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" new file mode 100644 index 000000000000..08124bf3e466 Binary files /dev/null and "b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/images/\347\233\221\346\216\247-1.png" differ diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/monitor-system.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/monitor-system.md new file mode 100644 index 000000000000..c3df044f8701 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/monitor-system.md @@ -0,0 +1,49 @@ +--- +keywords: [监控系统, 告警系统, VictoriaMetrics, Grafana, PrometheusAlert, Loki, 日志系统, 公有云服务, 系统设计, 高可用] +description: 了解如何使用VictoriaMetrics和Grafana构建高效的监控与告警系统,结合PrometheusAlert和Loki实现日志收集与展示,提升公有云服务的稳定性与可用性。 +--- + +# 监控与告警系统 + +# 概述 + +监控与告警系统是公有云服务中非常重要的一部分,本身 Sealos 的设计已经可以将运维人员的心智负担大大降低,通过搭建良好的监控与告警系统可以进一步提升系统的稳定性与可用性。 + +根据收集的指标来区分,整个系统可以分为两部分,一部分是面向系统实时 Metrics 的监控和告警,而另一部分主要包括日志系统的收集,聚合,展示。 + +# 监控 + +## 设计 + +监控系统整体采用 [ VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics),其相较于 Prometheus 的优势主要在于: + +- 同时支持推模型和拉模型,采集数据量更高,消耗内存更小 +- 原生支持高可用 +- 兼容 PromQL + +数据可视化工具使用 Grafana,在 Sealos 的每个可用区都分别部署一套 VictoriaMetrics+Grafana,分别监控。 + +# 告警 + +## 设计 + +告警系统设计思路主要是通过监控系统监控数据,当发现异常数据的时候,发送消息给消息处理中间层,这个中间层再把消息转发到相应的 +app 群聊。: + +![Application](./images/监控-1.png) + +## 实现 + +选择开源项目 [PrometheusAlert](https://github.com/feiyu563/PrometheusAlert) 作为转发系统,使用 Grafana 发出告警信息到 +PrometheusAlert ,在由其转发到飞书,微信等平台。 + +与监控系统一样,告警系统同样是每个可用区部署一套,分别进行告警。 + +# 日志 + +### 设计 + +日志系统采用 [Loki](https://github.com/grafana/loki) 实现。Loki 是一个水平可扩展,高可用性的日志聚合系统,对比起 ELK ,Loki +并不会存储大量的索引文件以提供全文检索的功能,因此比较轻量。 + +使用 Grafana,从 Loki 获取数据,完成日志的查询和展示。每个可用区部署一套。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md new file mode 100644 index 000000000000..4389f959b976 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-application.md @@ -0,0 +1,97 @@ +--- +keywords: [Sealos系统应用, Kubernetes API, CRD API, 应用管理, 费用中心, 定时任务, 数据库, KubePanel, 对象存储, 应用商店] +description: 了解Sealos系统应用的基本原理与实现,包括Kubernetes API、CRD API、应用管理、费用中心、定时任务、数据库、KubePanel等。 +--- + +# 系统应用 + +主要介绍 Sealos 当前各个系统应用的基本原理与实现。 + +## 系统桌面 + +组成:前端系统+Kubernetes API+Kubernetes CRD API+数据库API+其他API + +整个系统的入口,处理主界面上用户数据的展示与相关交互逻辑。 + +部署在桌面上的每个应用都单独存在并前后端分离部署。 + +## 应用管理 + +组成:前端系统+Kubernetes API + +用于管理用户自定义的应用,本质上是把用户在 GUI 上的命令映射成yml文件(主要是deployment,service等文件),交给Kubernetes执行。 + +## 费用中心 + +组成:前端系统 + Kubernetes CRD API + +前端页面为用户展示各种收费标准,调用 CRD API,CRD controller再调用后端一个单独的程序提供数据库查询接口,返回用户账单信息。 + +## 定时任务 + +组成:前端系统+Kubernetes API + +用户管理 Kubernetes 原生 CronJob 资源,逻辑与应用管理基本相同。 + +## 数据库 + +组成:前端系统+数据库 API+Kubernetes CRD API(Kube Blocks) + +数据库底层使用开源项目 Kube Blocks 实现,其使用了一系列的CRD来帮助用户完成数据库部署的过程。前端需要跟这些CRD的API交互。 + +用户系统的维护则在小强数据库中,使用API提供服务。 + +## KubePanel + +组成:前端系统+Kubernetes API + +用户对特定 Namespace 下的各项资源进行管理,直接调用 Kubernetes 提供的api即可完成。 + +## 对象存储 + +组成:前端系统+Kubernetes CRD API + +底层使用 minio,并使用两个crd维护关系,objectstorageuser_controller 用于维护存储账户与命名空间的关系,并处理限流等逻辑。 + +objectstoragebucket_controller 维护存储桶和存储账户之间的关系。 + +## 云开发 + +一个单独的应用链接,点击直接跳转到云开发应用。 + +## 文档中心 + +一个单独的应用链接,点击直接跳转到官方文档。 + +## 应用商店 + +组成:前端系统 + +应用的名称,种类等信息维护在 Github 的一个仓库中,前端直接拉取仓库中的数据并展示。 + +## 终端 + +组成:前端系统+Kubernetes CRD API + +具体实现可以概括为前端应用调用 API,然后 controller创建一套新的 pod/deployment/ingress等资源,并建立一个 +websocket返回给前端,后续使用这个 websocket进行通信。 + +## 工单 + +组成:前端系统+数据库API + +单独维护自己的用户系统,用户信息存储在单独的 Mongodb中,用户上传的文件存储到 minio中。 + +## fastgpt + +一个单独的应用链接,点击直接跳转到fastgpt应用。 + +## 邀请链接 + +组成:前端系统 + +展示邀请情况 + +## 私有云 + +一个单独的链接,点击直接跳转到一个问卷调查。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-architecture.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-architecture.md new file mode 100644 index 000000000000..5eeae528a45f --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/system-architecture.md @@ -0,0 +1,40 @@ +--- +keywords: [Sealos, Kubernetes云操作系统, 应用架构, 前后端分离, Kubernetes API] +description: Sealos 是基于 Kubernetes 的云操作系统,采用前后端分离架构,简化用户操作,提供专业高效的工具,满足不同需求。 +--- + +# 系统架构 + +## 设计哲学 + +Sealos 的目的是打造基于 Kubernetes 的云操作系统,在系统与普通用户的交互过程中尽可能多的向用户屏蔽掉 Kubernetes +的复杂性,使这部分功能尽可能的简单。同时对于具有专业知识的云计算领域人员,Sealos 又期望提供专业高效的工具使其完成工作。 + +基于这个目标,Sealos 提出了**"一切皆应用"** +的设计理念。把每个功能抽象成一个个独立又可以相互配合的应用,不同需求和目的的人员操作不同的应用完成不同的功能。尽可能的与用户在使用单机操作系统时的体验保持一致。 + +## 整体架构 + +Sealos 整体架构图如下: + +![Architecture](./images/architecture_light.png#gh-light-mode-only) + +![Architecture](./images/architecture_dark.png#gh-dark-mode-only) + +因此我们可以说,**Sealos = Kubernets + 一系列应用** + +## 应用架构 + +Sealos 上的应用采用前后端分离的架构,同时前端能够提供 `SSR` 能力,使得应用也可以单独对外提供服务,而不需要绑定在 Sealos 单 + +体上。 + +应用架构以及交互情况:(以App Launchpad 与 Terminal 应用与其交互为例子) + +![Application](./images/application_light.png#gh-light-mode-only)![Application](./images/application_dark.png#gh-dark-mode-only) + +每个应用都的前端系统会调用诸多不同的接口,调用 Kubernetes 原生 API 以实现一些简单的逻辑,调用 Kubernetes CRD API +实现复杂的逻辑,调用多种不同的数据库 API 持久化和获取数据,还会调用一些部署在集群内的特殊应用提供的 API 实现一些特殊的逻辑。 + +因此我们可以说,**Sealos = Kubernets + 一系列应用 = 一系列的 "前端系统 + Kubernetes API + Kubernetes CRD API + Database +API+ Service API"**。 diff --git a/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/user-system.md b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/user-system.md new file mode 100644 index 000000000000..9ea6266989a8 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/developer-guide/system-design/user-system.md @@ -0,0 +1,83 @@ +--- +keywords: [Kubernetes用户管理, Sealos用户系统, 命名空间隔离, CockroachDB, RBAC授权] +description: 了解Sealos如何通过Kubernetes用户管理系统实现高效隔离,使用CockroachDB持久化数据,并基于RBAC模式进行用户授权。 +--- + +# 用户系统 + +## 概述 + +Sealos 通过在公有云中,让多个用户共享一个 Kubernetes 集群的方式最大程度的调度了服务器的性能,而用户体系的设计就是为此服务,在满足隔离性要求的基础上,尽可能以简单,容易理解的实现满足需求。 + +## 用户设计 + +Kubernetes 本身不具备原生的用户管理系统,而 Sealos 使用名为 User 的 CR 来代表用户在不同可用区的权限等信息。而用户的姓名,手机号等信息则持久化在数据库中。 + +### 持久化设计 + +由于 Sealos 存在多个可用区,因此无法使用一套数据源来完成所有功能。 + +因此采用 CockroachDB 这种分布式关系型数据库存储在不同可用区之间保持一致的用户信息。 + +## 用户与命名空间 + +命名空间是 Kubernetes 中优秀的隔离资源机制,Sealos 基于命名空间打造了用户之间的隔离机制。 + +每当一个新的用户创建时,Sealos 会为其创建一个新的命名空间,称为个人空间,这个空间与用户绑定,用户拥有其最高的权限,当其用户注销的时候个人空间就会销毁。 + +Sealos 还提供了工作空间机制,每个用户可以创建自己的新工作空间,并可以邀请别人进入。 + +这样,就形成了用户与命名空间的多对多关系,即一个用户可以在多个命名空间中,一个命名空间内也可以有多个用户。 + +![Application](./images/用户-1.png) + +## 授权 + +Sealos 基于 Kubernetes 的 RBAC 模式实现用户授权。 + +当用户创建一个命名空间(包括新建用户时创建的个人空间)时,控制器会在这个新创建的命名空间下创建三个 Role 资源,对应三个角色。同时创建一个 +RoleBinding 资源将这个命名空间的 Owner 授予该用户。 + +同理,当用户拉其他人进入工作空间时,控制器会创建新的 RoleBinding 资源,将权限授予对应的用户。 + +## 认证 + +针对系统外部用户与内部 Pod,Kubernetes 提供了两种认证对象:Normal User 与 Service Account.而由于公有云上 + +用户的操作都在系统内部的 Pod 上执行,所以 Sealos 采用 Service Account 的方式实现用户认证。 + +当创建新用户时,系统会为其创建 Service Account,然后将 Service Account Token 导入进 Kubeconfig 中。 + +当用户访问各个应用程序时,系统会从用户的 Kubeconfig 中获取到 Token,然后挂载到启动的访问容器中,从而完成用户认证。 + +## 隔离 + +Sealos 除了在 Kubernetes 层面使用命名空间做了用户隔离,还在其他多方面实现了底层隔离。 + +如使用 OpenEBS 进行存储的块级别隔离,Firecracker 以及 Cloud Hypervisor 用于计算运行时的隔离,以及通过 Cilium +实现网络隔离等等。这些措施确保即使在共享环境中,每个租户的操作也不会影响到其他租户。 + +## 实现 + +用户系统主要代码放在 Sealos/controllers/user 部分,包含自定义资源的定义与控制器。整体采用 KubeBuilder 生成。 + +用户创建等操作相关的主要逻辑放在 user/controllers/user_controller 部分。其 Reconcile 方法的核心流程如下,由一系列函数组成: + +``` +pipelines := []func(ctx context.Context, user *userv1.User) context.Context{ + r.initStatus, + r.syncNamespace, #处理命名空间 + r.syncServiceAccount, #处理SA + r.syncServiceAccountSecrets, #处理SAS + r.syncKubeConfig, #处理kubeconfig + r.syncRole, #处理用户角色 + r.syncRoleBinding, #处理RoleBinding + r.syncFinalStatus, +} + +for _, fn := range pipelines { + ctx = fn(ctx, user) +} +``` + +而 operationrequest_controller 主要处理工作空间的相关逻辑。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/images/overview-1.png b/docs/archived/5.0/i18n/zh-Hans/images/overview-1.png new file mode 100644 index 000000000000..af1a5bfadae7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/images/overview-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/images/overview-2.jpeg b/docs/archived/5.0/i18n/zh-Hans/images/overview-2.jpeg new file mode 100644 index 000000000000..dc2a48c6c653 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/images/overview-2.jpeg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/images/sealos-desktop-zh.webp b/docs/archived/5.0/i18n/zh-Hans/images/sealos-desktop-zh.webp new file mode 100644 index 000000000000..b82a41f1c549 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/images/sealos-desktop-zh.webp differ diff --git a/docs/archived/5.0/i18n/zh-Hans/images/sealos-qr-code.jpg b/docs/archived/5.0/i18n/zh-Hans/images/sealos-qr-code.jpg new file mode 100644 index 000000000000..506286e82d7d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/images/sealos-qr-code.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/msa/privacy-policy.md b/docs/archived/5.0/i18n/zh-Hans/msa/privacy-policy.md new file mode 100644 index 000000000000..2cb3d8d95408 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/msa/privacy-policy.md @@ -0,0 +1,58 @@ +# Sealos 云服务隐私政策 + +我们非常重视您的隐私保护,在您使用本服务时,我们将按照以下政策收集、使用、披露和保护您的个人信息。请您仔细阅读并充分理解本隐私政策。 + +**信息收集** + +1. 在您注册或使用本服务时,我们可能收集您的姓名、电话号码、电子邮件地址、地址等个人信息。 +2. 在您使用本服务过程中产生的信息,如操作日志、访问IP地址、设备型号等。 +3. 我们可能会通过 Cookies 或其他技术收集和存储您访问本服务的相关信息,以便为您提供更好的用户体验。 + +**信息使用** + +1. 我们会根据法律法规规定以及与用户之间的约定来处理用户的个人信息。 +2. 我们可能会将收集到的信息用于改进服务质量、开发新产品或功能等目的。 +3. 我们可能会将收集到的信息用于向您推送与本服务相关的通知或广告。 + +**信息披露** + +1. 我们不会向任何第三方披露您的个人信息,除非: + + 1. 您事先同意; + + 2. 法律法规要求; + + 3. 为维护我们或其他用户的合法权益。 + +2. 我们可能与关联公司、合作伙伴分享您的个人信息,但我们会采取相应的保密措施,确保信息安全。 + +**信息保护** + +1. 我们采取各种安全措施,包括加密、访问控制等技术手段,以保护您的个人信息免受未经授权的访问、使用或泄露。 +2. 我们会定期对收集、存储和处理的个人信息进行安全评估,以确保个人信息安全。 +3. 在发生个人信息泄露等安全事件时,我们会立即启动应急预案,并在法律法规规定的范围内向您及时告知。 + +**用户权利** + +1. 您有权随时查阅、更正或删除您的个人信息。 +2. 您有权拒绝我们收集您的个人信息,但这可能导致您无法使用本服务的部分功能。 +3. 您有权要求我们停止处理您的个人信息,但这可能导致您无法继续使用本服务。 + +**隐私政策更新** + +1. 我们可能会对本隐私政策进行修改。如本隐私政策发生变更,我们将在本服务页面上发布修改后的隐私政策。如您继续使用本服务,则视为同意修改后的隐私政策。 +2. 我们鼓励您定期查阅本隐私政策,以了解我们如何保护您的个人信息。 + +**未成年人保护** + +我们非常重视对未成年人个人信息的保护,如您为未成年人,请在监护人指导下使用本服务,并请监护人帮助您在使用本服务过程中正确处理个人信息。 + +**跨境数据传输** + +由于我们的服务器可能位于不同国家或地区,您同意我们可能需要将您的个人信息传输至其他国家或地区,并在该等国家或地区存储和处理以向您提供服务。我们会采取适当措施确保跨境传输的数据仍然受到适当保护。 + +**联系我们** + +1. 如您对本隐私政策有任何疑问、建议或投诉,请通过以下方式与我们联系(微信): + [fangnux] +2. 我们将尽快回复并解决您提出的问题。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/msa/terms-of-service.md b/docs/archived/5.0/i18n/zh-Hans/msa/terms-of-service.md new file mode 100644 index 000000000000..d3970c9ea33a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/msa/terms-of-service.md @@ -0,0 +1,64 @@ +# Sealos 云服务服务协议 + +Sealos 云服务服务协议是您与珠海环界云计算有限公司(以下简称“我们”或“本公司”)之间就云服务(以下简称“本服务”)的使用等相关事项所订立的协议。请您仔细阅读并充分理解本协议各条款,特别是免除或者限制我们责任的条款、对您权益的限制条款、争议解决和法律适用条款等。如您不同意本协议任一内容,请勿注册或使用本服务。 + +**第1条 服务内容** + +1. 我们将向您提供存储、计算、网络传输等基于互联网的信息技术服务。 +2. 我们将为您提供相关技术支持和客户服务,帮助您更好地使用本服务。 +3. 您同意我们有权自行决定变更、中断、终止或暂停本服务的全部或部分功能,且无需对您承担任何责任。 + +**第2条 用户注册与账户管理** + +1. 您在使用本服务前需要注册一个账户。您保证在注册时提供的信息真实、准确、完整,并及时更新。 +2. 您应妥善保管账户名和密码,对由此产生的全部行为负责。如发现他人使用您的账户,应立即通知我们。 +3. 我们有权对您的账户进行审查,如发现您的账户存在异常情况,我们有权暂停或终止向您提供服务。 + +**第3条 使用规则** + +1. 您不得利用本服务从事任何违法活动或侵犯他人合法权益的行为,包括但不限于侵犯知识产权、泄露他人商业机密等。 +2. 您不得通过任何手段恶意注册账户,包括但不限于以牟利、炒作、套现等目的。 +3. 您不得利用本服务传播任何违法、有害、恶意软件等信息。 +4. 您应遵守相关法律法规及本协议的规定,对在本服务中发布的信息及使用本服务所产生的结果承担全部责任。 + +**第4条 费用及支付** + +1. 您同意支付与本服务相关的费用,具体费用标准以我们公布的价格为准。 +2. 我们可能会根据市场情况调整费用标准,并提前通知您。您如继续使用本服务,则视为同意调整后的费用标准。 +3. 如您未按时支付费用,我们有权暂停或终止向您提供本服务,并保留追讨欠款及要求赔偿的权利。 + +**第5条 服务免责与责任限制** + +1. 本服务按照现有技术和条件所能达到的水平提供。我们不能保证本服务无故障、安全完全可靠或满足您的所有需求。 +2. 如因不可抗力导致本服务发生故障或无法正常使用,我们不承担责任。 +3. 对于因您自身原因导致的数据丢失、损坏等情况,我们不承担责任。 + +**第6条 隐私政策** + +您同意接受并遵守我们制定的隐私政策。隐私政策为本协议的一部分,具体内容请参见下文“隐私政策”。 + +**第7条 知识产权** + +1. 我们对本服务及相关软件、技术、文档等拥有全部知识产权,除非经我们明确许可,您不得进行复制、分发、出租、反向工程等行为。 +2. 您在使用本服务过程中产生的数据和内容(包括但不限于文件、图片等)之知识产权归您所有,但为了向您提供服务,您同意授权我们在必要范围内对其进行存储、备份等操作。 + +**第8条 协议终止** + +1. 如您违反本协议规定,我们有权立即终止向您提供服务,并保留追究法律责任的权利。 +2. 如您决定停止使用本服务,可以申请注销账户,本协议自账户注销之日起终止。 + +**第9条 争议解决和法律适用** + +1. 本协议的签订、履行、解释和争议解决均适用中华人民共和国法律。 +2. 若您与我们发生争议,应先友好协商解决;协商不成的,任何一方均有权将争议提交至有管辖权的人民法院诉讼解决。 + +**第10条 网络安全** + +1. 如果利用平台服务提供违反国家法律的服务,您需要自行承担后果。 +2. 您对外提供的服务需要做好监管职责,如有用户在您的服务上从事违反国家法律的行为,您需自行承担后果。 +3. 我们如果发现您存在违法行为,将会对相关部门发起诉讼,并为有关部门提供证据。 + +**第11条 其他条款** + +1. 如本协议中部分条款因违反法律法规而被视为无效,不影响其他条款的效力。 +2. 本公司保留对本协议及隐私政策的最终解释权。如您对本协议或隐私政策有任何疑问,请联系我们。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos1.png new file mode 100644 index 000000000000..7313d36f5487 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos2.png new file mode 100644 index 000000000000..32f0fb67f319 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/imgs/sealos2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md new file mode 100644 index 000000000000..22ad21b787a5 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/ai-applications/install-fastgpt-on-desktop.md @@ -0,0 +1,20 @@ +--- +keywords: [FastGpt AI知识库, Sealos部署, 无服务器AI应用, 快速安装AI, 一键部署AI, 免费AI知识库, AI应用教程] +description: 了解如何快速安装FastGpt AI知识库,无需服务器、域名或特殊网络。通过Sealos一键部署,轻松搭建您的AI知识库系统,适合初学者和专业人士。 +--- + +# 快速安装 FastGpt AI 知识库 + +无需服务器、无需魔法、无需域名,点击即可部署 👇 + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt) + +由于需要部署数据库,部署完后需要等待 2~4 分钟才能正常访问。默认用了最低配置,首次访问时会有些慢。 + +![](./imgs/sealos1.png) + +## 运行 + +点击 sealos 提供的【外网地址】即可使用。登录用户名为: root,密码是刚设置的环境变量,上图中设置了: 1234 + +![](./imgs/sealos2.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/baas/install-fireboom.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/baas/install-fireboom.md new file mode 100644 index 000000000000..b555d3b885e8 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/baas/install-fireboom.md @@ -0,0 +1,109 @@ +--- +keywords: [Fireboom安装, 可视化API开发, Sealos部署, 声明式开发框架, 快速接口开发, 前端全栈, 应用管理, 控制面板配置] +description: 学习如何在Sealos上快速安装Fireboom,一个面向开发者的可视化API开发平台。本教程涵盖从创建应用到配置和访问Fireboom的全过程,助您提升开发效率。 +--- + +# 快速安装 Fireboom + +[Fireboom](https://fireboom.io) ⾯向开发者的可视化 API 开发平台,为开发者提供**声明式开发框架**、**可视化编辑套件**和**钩子机制 +**。 + +![fireboom introduction](../images/fireboom/fireboom-intro.jpeg) + +官网地址: https://fireboom.io + +**功能特性** + +- 分钟级交付: 将传统模式下 2天才能完成的接⼝缩短⾄ 2分钟 + +- 前端变全栈: 可视化开发,显著降低接⼝开发⻔槛,让前端搞定前后端秒变全栈 + +- 后端不搬砖: 声明式编程,避免复制粘贴,减少bug概率,让后端专注于业务开发 + +## 步骤 1:新建应用 + +- 在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/appsmith-1.png) + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/appsmith-2.png) + +## 步骤 2:应用部署 + +- 基础配置: + + - 应用名称(自定义):fireboom + + - 镜像名(默认最新版本):fireboomapi/fireboom_server:latest + + - CPU(推荐):2 Core ( 最低1 Core) + + - 内存(推荐):4 G (最低 512 M) + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/fireboom/2-1.png) + +- 网络配置: + + - 控制面板:9123 + - API端点:9991,开启外网 + +![](../images/fireboom/2-2.png) + +- 高级配置: + + - 命令设置: + - 参数设置: dev (dev表示开发模式,start表示生产模式) + + - 持久化存储: + + - 元数据:/fbserver/store + - 上传文件:/fbserver/upload + +![](../images/fireboom/2-3.png) + +## 步骤 3:部署应用 + +1, 点击「部署应用」开始部署应用。 + +![](../images/fireboom/3-1.png) + +2, 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +![](../images/fireboom/3-2.png) + +## 步骤 4:访问应用 + +- 访问控制面板:9123对应的域名 + +![](../images/fireboom/4-1.png) + +- 访问API端点:9991对应的域名 + +``` +status ok +``` + +## 步骤 5:配置Fireboom(可选) + +设置后,可方便使用swagger文档~ + +1, 查看API外网地址: 设置-> 系统 + +![](../images/fireboom/5-1.png) + +2, 修改为:API端点地址,9991对应的公网地址 + + - 静态值:选择静态值,设置为 **API端点** 公网域名 + - 环境变量:前往 环境变量 ,找到 FB_API_PUBLIC_URL 设置为 **API端点** 公网域名 + +![](../images/fireboom/5-2.png) + +3, 使用swagger文档测试API + +![](../images/fireboom/5-3.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-halo.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-halo.md new file mode 100644 index 000000000000..451246785105 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-halo.md @@ -0,0 +1,148 @@ +--- +sidebar_position: 2 +keywords: [Halo博客平台, Sealos部署, PostgreSQL数据库, 快速安装, 环境变量配置, 外网访问, 应用管理, 开源建站工具] +description: 本文详细介绍如何在Sealos上快速部署Halo博客平台,包括PostgreSQL数据库配置、应用部署步骤、环境变量设置等,助您轻松搭建个人博客系统。 +--- + +# 快速安装 Halo + +在 Sealos 上快速部署 Halo 博客平台 + +![](../images/halo_img-1.png) + +[Halo](https://github.com/halo-dev/halo) 是一款强大易用的开源建站工具,本文将介绍如何在 Sealos 上部署 +Halo,同时在 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) 可以帮助你轻松地管理和维护数据库,以满足不同的业务需求。 + +## 步骤 1:准备 PostgreSQL + +### 使用 Sealos 数据库安装 PostgreSQL + +参考 [Sealos 上部署 PostgreSQL](../../quick-start/install-db-with-database.md) ,一分钟即可成功安装和使用 PostgreSQL。 + +### 查看数据库配置: + +保存这里生成的配置中包含数据库的用户名密码以及数据库连接方式,用于下文 Halo 中配置: + +![](../images/halo_img-6.png) + +> 图中可以看到 PostgreSQL 连接配置为: +> +> Username: postgres +> +> Password: rcx7f47m +> +> Connection: postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432 + +## 步骤 2: 在 Sealos 中部署 Halo应用 + +### 打开 应用管理 + +![](../images/halo_img-7.png) + +![](../images/halo_img-8.png) + +### 填写配置 + +- 自定义应用名称 + +- 镜像名称设置为 `halohub/halo:2.5` + +- CPU 和存储应根据实际情况进行配置,memory 需要 `500Mi` 的内存才能启动 Halo,这里可以参考官方 + prepare:https://docs.halo.run/getting-started/prepare + +- 暴露端口应为 `8090`。同时,设置为外网访问将自动为应用配置一个出口域名用于外网访问,同时需要在 Halo 中进行配置该地址 + +- 环境变量包括数据库配置和其他相关配置。需要注意的是,你需要根据你在 [步骤 1](查看数据库配置:) 中设置的用户名、密码和 DNS + 来配置数据库连接信息。同时,你还需要为 Halo 配置超级管理员的用户名和密码。 + +配置 Halo 环境变量 + +```Bash +spring.sql.init.platform=postgresql +spring.r2dbc.url=r2dbc:pool:postgresql://postgres:rcx7f47m@dbname-postgresql.ns-sy32q9p9.svc:5432/halo +spring.r2dbc.username=postgres +spring.r2dbc.password=rcx7f47m +halo.external-url=tmtdvfjiyyfh.cloud.sealos.io +halo.security.initializer.superadminusername=root +halo.security.initializer.superadminpassword=sealos +``` + +环境变量配置详解: + +| 参数名 | 描述 | +|----------------------------------------------|--------------------------------| +| spring.r2dbc.url | 数据库连接地址,详细可查阅下方的 数据库链接格式 | +| spring.r2dbc.username | 数据库用户名 | +| spring.r2dbc.password | 数据库密码 | +| spring.sql.init.platform | 数据库平台名称,支持 postgresql、mysql、h2 | +| halo.external-url | 外部访问链接,如果需要在公网访问,需要配置为实际访问地址 | +| halo.security.initializer.superadminusername | 初始超级管理员用户名 | +| halo.security.initializer.superadminpassword | 初始超级管理员密码 | + +数据库的链接格式(这里我们使用 postgresql 的格式): + +| 链接方式 | 链接地址格式 | spring.sql.init.platform | +|-------------|------------------------------------------------------------------------------------|--------------------------| +| PostgreSQL | r2dbc:pool:postgresql://`{HOST}`:`{PORT}`/`{DATABASE}` | postgresql | +| MySQL | r2dbc:pool:mysql://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| MariaDB | r2dbc:pool:mariadb://`{HOST}`:`{PORT}`/`{DATABASE}` | mysql | +| H2 Database | r2dbc:h2:file:///`${halo.work-dir}`/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE | h2 | + +### 网络配置 + +![](../images/halo_img-9.png) + +### 环境变量配置 + +![](../images/halo_img-10.png) + +### 配置持久化存储卷 + +挂载 `/root/.halo2` 目录来持久化 halo 数据 : + +![](../images/halo_img-11.png) + +## 步骤 3: 使用外网访问 Halo + +成功启动应用后,即可通过外网访问地址访问 Halo 进行配置: + +![](../images/halo_img-12.png) + +![](../images/halo_img-13.png) + +### 初始化 Halo + +![](../images/halo_img-14.png) + +![](../images/halo_img-15.png) + +### First Halo + +![](../images/halo_img-16.png) + +![](../images/halo_img-17.png) + +![](../images/halo_img-18.png) + +## FAQ + +### 忘记密码 + +#### 进入 「应用管理」,点击日志查看 + +![](../images/halo_img-19.png) + +![](../images/halo_img-20.png) + +可以看到这里的日志中:用户名:`admin` 密码:`QTu2J2xgUQ3ngqUo` + +#### 或终端执行如下命令查看日志 + +![](../images/halo_img-21.png) + +``` +root@td3q8uc46:~# kubectl logs halo-0 | grep 'Generated random password:' | tail -1 +2023-05-30T13:32:16.942+08:00 INFO 7 --- [-controller-t-1] r.h.app.security.SuperAdminInitializer : === Generated random password: QTu2J2xgUQ3ngqUo for super administrator: admin === +``` + +![](../images/halo_img-22.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-wordpress.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-wordpress.md new file mode 100644 index 000000000000..749b1a8c1bad --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/blog-platform/install-wordpress.md @@ -0,0 +1,111 @@ +--- +sidebar_position: 1 +keywords: [WordPress安装, Sealos, MySQL数据库, 内容管理系统, 快速部署, 应用管理, 环境变量配置, 网站搭建, 博客平台, 外网访问] +description: 学习如何使用Sealos快速安装WordPress和MySQL数据库,轻松配置环境变量和网络设置,实现高效的网站和博客搭建。本教程提供详细步骤指导,助您迅速开启内容创作之旅。 +--- + +# 快速安装 WordPress + +[WordPress](https://github.com/WordPress/WordPress) 是一款功能强大、易用、开源的内容管理系统 (CMS), +可以用于在互联网上发布网站、新闻、博客等内容。它提供了便捷的内容编辑、发布和更新方法 , 同时也可以通过主题和插件进行高度自定义。WordPress +已经成为非常成功和普及的 CMS 系统 , 在全球有超过 3 千万个网站使用 WordPress 搭建。 + +> 使用 Sealos 安装 WordPress,可以大幅度减少安装时间,极速开启用户的内容输出之旅。 + +WordPress 需要搭配 MySQL 数据库一同安装使用。作为使用 Sealos 的最佳实践,建议使用**数据库**应用独立安装 MySQL +数据库,依托独立数据库,快速安装 WordPress。 + +## 新建 MySQL 实例 + +进入 [Sealos](https://cloud.sealos.io) 桌面环境,在「数据库」中,快速配置需要的数据库实例,等待数据库部署完成。 + +![](../images/wordpress_15.png) + +![](../images/wordpress_16.png) + +![](../images/wordpress_17.png) + +**注:此时的 MySQL 实例中并不存在用户自定义数据库,需要在终端中手动创建** + +## 新建数据库 + +通过**数据库**应用安装的 MySQL 实例时,并不会自动创建用户自定义数据库。在使用应用程序连接 MySQL 数据库时,您需要在 MySQL +实例中手动创建相应的数据库。 + +可以通过如下方式进入数据库的终端命令行 + +![](../images/wordpress_12.png) +![](../images/wordpress_13.png) + +也可以在终端中自行输入命令 + +`mysql -u username -p -h yourhost` + +![](../images/wordpress_14.png) + +数据库连接成功后,可以按照如下命令创建数据库 + +`CREATE DATABASE yourdb;` + +至此,您已经在 MySQL 实例中手动创建了一个用户自定义数据库。 + +## 应用管理 + +点击应用管理,进入安装界面。 + +![](../images/wordpress_1.png) + +## 基础配置 + +首先设置自定义的应用名称,选择需要安装的镜像。Sealos 既支持公有镜像源,也支持私有镜像源。本示例选用的官方镜像源,不指定镜像版本时,默认拉取 +latest 版本。 + +其次,为 WordPress 配置 CPU 与内存资源,WordPress 十分的轻量级,较少的资源就可以保证程序的正常运行。 + +![](../images/wordpress_8.png) + +## 网络配置 + +网络配置方面,打开外网访问,Sealos 会为用户自动分配出口域名用作外网访问,如果用户已经注册了自己的自定义域名,那么可以将自定义域名 +cname 到该出口域名,这样就可以使用该自定义域名访问用户安装的应用。 + +注:后续演示,使用的是 Sealos 随机生成的出口域名。 + +![](../images/wordpress_9.png) + +## 高级配置 + +在高级配置中,需要根据数据库的实际情况配置 WordPress 的环境变量。 + +高级配置中,需要为 WordPress 配置环境变量与本地存储。 + +WordPress 常见环境变量如下所示: + +```Plain +WORDPRESS_DB_HOST 数据库实例的ip地址 +WORDPRESS_DB_USER 连接数据库的用户 +WORDPRESS_DB_PASSWORD 连接库所需的用户密码 +WORDPRESS_DB_NAME 连接的数据库名称 +``` + +根据之前安装的 MySQL 示例,可以这样配置环境变量: + +```Plain +WORDPRESS_DB_HOST=mysql-host:3306 +WORDPRESS_DB_USER=root +WORDPRESS_DB_PASSWORD=root_passwd +WORDPRESS_DB_NAME=yourdb +``` + +WordPress 官方镜像的存储卷挂载地址为:`/var/www/html`,用户根据需求为应用分配存储空间。 + +这样,WordPress 也一并安装成功,点击详情,通过外网地址就可以成功访问。 + +![](../images/wordpress_10.png) + +点击该链接,如下页面,证明安装成功 + +![](../images/wordpress_11.png) + +**注:如部署失败 , 优先排查 WordPress 访问的用户自定义数据库是否存在于 MySQL 实例中** + diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/dial-testing-system/install-uptime-kuma.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/dial-testing-system/install-uptime-kuma.md new file mode 100644 index 000000000000..7d87627b81b0 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/dial-testing-system/install-uptime-kuma.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 1 +keywords: [Uptime Kuma, 服务器监控, Sealos, 应用部署, 开源监控工具, 实时监控, 服务器状态, 快速安装, 持久化数据, 外网访问] +description: 学习如何在Sealos上快速安装Uptime Kuma,一款开源的服务器监控工具。本教程涵盖从应用创建到配置和访问的全过程,帮助您轻松实现服务器状态的实时监控。 +--- + +# 快速安装 Uptime Kuma + +![](../images/uptimekuma_img-0.png) + +[Uptime Kuma](https://github.com/louislam/uptime-kuma) +是一款开源的、易于使用的服务器监控工具。它可以帮助你实时监控服务器的运行状态、响应时间以及其他关键指标,以确保你的服务器始终保持最佳状态。如果你想快速安装 +Uptime Kuma,可以按照以下步骤进行操作: + +### 步骤 1:首先进入 [Sealos](https://cloud.sealos.io) 桌面环境并打开「应用管理」 + +![](../images/uptimekuma_img-1.png) + +### 步骤 2: 新建应用 + +在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/uptimekuma_img-2.png) + +### 步骤 3: 设置启动参数 + +在启动参数中,按照以下方式进行设置: + +- 容器暴露端口指定为 3001。 +- 设置为外网访问即可通过给出的域名访问。 + +![](../images/uptimekuma_img-3.png) + +![](../images/uptimekuma_img-4.png) + +### 步骤 4: 设置存储卷 + +在高级配置中,添加存储卷挂载来持久化 Uptime Kuma 数据目录 `/app/data`。这样可以确保应用数据在容器重启后不会丢失。 + +![](../images/uptimekuma_img-5.png) + +### 步骤五:部署应用 + +点击「部署应用」后即可启动应用: + +![](../images/uptimekuma_img-6.png) + +### 步骤 6: 访问应用 + +一旦应用启动成功,你就可以使用应用的外网地址进行访问了。在浏览器中输入应用的域名,即可访问应用的主界面。 + +![](../images/uptimekuma_img-7.png) + +### 步骤 7: 创建管理员账户 + +为了保护 Uptime Kuma 的安全性,首次进入你需要创建一个管理员账户,填写管理员账户的基本信息,包括用户名、密码。填写完毕后,点击创建按钮进行注册。 + +![](../images/uptimekuma_img-8.png) + +### 步骤 8: 添加自定义监控项 + +![](../images/uptimekuma_img-9.png) + +![](../images/uptimekuma_img-10.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/gitea/install-gitea.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/gitea/install-gitea.md new file mode 100644 index 000000000000..e19b5e7a7e28 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/gitea/install-gitea.md @@ -0,0 +1,117 @@ +--- +keywords: [Gitea安装, Sealos部署, 自托管Git服务, 代码托管平台, MySQL配置, 应用管理, 容器化部署, 开发团队协作, CI/CD工具, 代码审查系统] +description: 学习如何在Sealos云平台上快速安装和配置Gitea,实现自托管的代码托管、团队协作和CI/CD服务。包含详细步骤和MySQL数据库设置指南。 +--- + +# 快速安装 Gitea + +喝杯茶吧!无痛自托管多合一软件开发服务,包括 Git 托管、代码审查、团队协作、包注册和 CI/CD。 + +## 步骤 1:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](../images/gitea-0.png) + +## 步骤 2:打开 「应用管理」 应用 + +![](../images/gitea-1.png) + +## 步骤 3:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/gitea-2.png) + +## 步骤 4:应用部署 + +- 基础配置: + + - 应用名称(自定义):gitea + - 镜像名(默认最新版本):gitea/gitea:latest-rootless + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + + ![](../images/gitea-3.png) + +- 网络配置: + + - 容器暴露端口:3000 + - 外网访问:开启 + + ![](../images/gitea-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 Gitea 的数据(推荐 1 G)。 + +![](../images/gitea-5.png) + +## 步骤 5:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/gitea-6.png) + +## 步骤6:配置数据库 + +- 通过 Sealos 为 Gitea 配置 MySQL 数据库 +- 进入 Database + +![](../images/gitea-9.png) + +- 新建数据库 + +![](../images/gitea-10.png) + +- 部署集群 + +- 基础配置: + + - 集群类型:mysql + - 数据库版本:ac-mysql-8.0.30 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +![](../images/gitea-11.png) + +- 部署成功后进入详情页面查看 MySQL 连接信息 + +![](../images/gitea-12.png) + +- 点击一键连接进入 MySQL 终端连接 + +![](../images/gitea-13.png) + +- 执行 + +```sql +CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +``` + +创建 Gitea 所需要的数据库 + +![](../images/gitea-14.png) + +## 步骤 7:访问应用 + +- 点击「App Launchpad」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/gitea-7.png) + +- 访问外网地址后进入到配置页面,根据刚刚创建的 MySQL 的详情页面进行配 + +![](../images/gitea-15.png) + +配置好后点击安装,等待一会便可进入到登陆页面,部署成功! + +![](../images/gitea-16.png) + +![](../images/gitea-17.png) + diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png new file mode 100644 index 000000000000..025a0c4e45a9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-10-52-H2ZWUZ.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png new file mode 100644 index 000000000000..1ca1cd0ed53a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-31-vrNHJU.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-1iudM0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-1iudM0.png new file mode 100644 index 000000000000..5ba9b3ad8784 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-1iudM0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png new file mode 100644 index 000000000000..ac8de099a1ae Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2022-04-10-14-32-ADfk8T.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg new file mode 100644 index 000000000000..1b1fe554ea55 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-54-EIVahX.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg new file mode 100644 index 000000000000..a773666c4ff2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-55-NDkuEg.jpg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-59-FxJE12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-59-FxJE12.png new file mode 100644 index 000000000000..fdc343a80edc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-11-59-FxJE12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-01-DKect7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-01-DKect7.png new file mode 100644 index 000000000000..b4d2db4792a7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-01-DKect7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-05-CWczxm.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-05-CWczxm.png new file mode 100644 index 000000000000..d512ad14195a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-05-CWczxm.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png new file mode 100644 index 000000000000..28a241ade20c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-06-lvv6ms.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png new file mode 100644 index 000000000000..aa8e415fb5b7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-07-s8W7iu.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-09-RslDGj.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-09-RslDGj.png new file mode 100644 index 000000000000..0935a645d999 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-09-RslDGj.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png new file mode 100644 index 000000000000..a446015a5c59 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-24-QHYKZt.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png new file mode 100644 index 000000000000..6996e718eb53 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-26-HYOaBJ.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png new file mode 100644 index 000000000000..c76f2e0535ae Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-28-ccnUOj.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png new file mode 100644 index 000000000000..ba4d166331c6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-29-z5E9gi.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg new file mode 100644 index 000000000000..7ca839b52b2f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-12-39-1jsF0t.jpeg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png new file mode 100644 index 000000000000..ac7134504f86 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-FwsbfW.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png new file mode 100644 index 000000000000..59460d087f22 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-Vs9ccy.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png new file mode 100644 index 000000000000..0fc1e2d152b9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-YFHPYc.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png new file mode 100644 index 000000000000..00e7aa10391a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-hdHfxP.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png new file mode 100644 index 000000000000..08ff96c0c126 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/2023-06-26-13-09-nwrxrv.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-3.png new file mode 100644 index 000000000000..01876eeff7a0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-4.png new file mode 100644 index 000000000000..fad0d90a71df Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-5.png new file mode 100644 index 000000000000..d6567da245ae Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-6.png new file mode 100644 index 000000000000..78b505e75298 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-7.png new file mode 100644 index 000000000000..9771645376a2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-8.png new file mode 100644 index 000000000000..ca7f7e62e60c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/appsmith-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-1.png new file mode 100644 index 000000000000..5fb4c0ec51f5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-2.png new file mode 100644 index 000000000000..714ad3891845 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-3.png new file mode 100644 index 000000000000..e0d5a1a605d4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/2-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-1.png new file mode 100644 index 000000000000..10d9ef1b793a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-2.png new file mode 100644 index 000000000000..eef62cdfc5f9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/3-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/4-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/4-1.png new file mode 100644 index 000000000000..880e86b32ce3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/4-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-1.png new file mode 100644 index 000000000000..b8f83e07b0d6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-2.png new file mode 100644 index 000000000000..b9b2404dc9ca Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-3.png new file mode 100644 index 000000000000..5e22398cdfcd Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/5-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-architecture.jpeg b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-architecture.jpeg new file mode 100644 index 000000000000..7f9660c847fa Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-architecture.jpeg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-intro.jpeg b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-intro.jpeg new file mode 100644 index 000000000000..848bd2b5d97d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/fireboom/fireboom-intro.jpeg differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-0.png new file mode 100644 index 000000000000..51d1b8564725 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-1.png new file mode 100644 index 000000000000..403270869164 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-10.png new file mode 100644 index 000000000000..ebcad36657b9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-11.png new file mode 100644 index 000000000000..7de1f375aa42 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-12.png new file mode 100644 index 000000000000..32fea7a571b3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-13.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-13.png new file mode 100644 index 000000000000..da74e2ae6af6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-14.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-14.png new file mode 100644 index 000000000000..70d6971d9906 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-15.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-15.png new file mode 100644 index 000000000000..cd370d79a7bf Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-16.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-16.png new file mode 100644 index 000000000000..1c2de6f7d4c2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-16.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-17.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-17.png new file mode 100644 index 000000000000..a41d1df04ff1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-17.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-3.png new file mode 100644 index 000000000000..7cbc77cebe6d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-4.png new file mode 100644 index 000000000000..7011b7769934 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-5.png new file mode 100644 index 000000000000..edadcc1ae00b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-6.png new file mode 100644 index 000000000000..4044b3367783 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-7.png new file mode 100644 index 000000000000..e2cd50bc191b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-8.png new file mode 100644 index 000000000000..9db3b3c06cf8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-9.png new file mode 100644 index 000000000000..e8dffac2625c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/gitea-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-1.png new file mode 100644 index 000000000000..51d7a08dd355 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-10.png new file mode 100644 index 000000000000..47ac9bd3b6ac Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-11.png new file mode 100644 index 000000000000..986e50a61e62 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-12.png new file mode 100644 index 000000000000..21e55e93ae98 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-13.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-13.png new file mode 100644 index 000000000000..7911c018cf43 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-14.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-14.png new file mode 100644 index 000000000000..c3d2cc3bde08 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-15.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-15.png new file mode 100644 index 000000000000..11197b975ece Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-16.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-16.png new file mode 100644 index 000000000000..d443d3d8dfe7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-16.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-17.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-17.png new file mode 100644 index 000000000000..1e88fe4f3c51 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-17.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-18.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-18.png new file mode 100644 index 000000000000..b93a0dcfc9f5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-18.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-19.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-19.png new file mode 100644 index 000000000000..17c863592f83 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-19.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-2.png new file mode 100644 index 000000000000..5a9d401f2631 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-20.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-20.png new file mode 100644 index 000000000000..9ed9ed3435f6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-20.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-21.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-21.png new file mode 100644 index 000000000000..6a1c4a896f58 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-21.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-22.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-22.png new file mode 100644 index 000000000000..d5e70681e57e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-22.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-3.png new file mode 100644 index 000000000000..29ba87c9cc57 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-4.png new file mode 100644 index 000000000000..0790b5ce2e43 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-5.png new file mode 100644 index 000000000000..f35eebc16cda Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-6.png new file mode 100644 index 000000000000..933d0efde05f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-7.png new file mode 100644 index 000000000000..93a5e88a6e85 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-8.png new file mode 100644 index 000000000000..9b41cf4dee07 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-9.png new file mode 100644 index 000000000000..609c7c0959c2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/halo_img-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-1.png new file mode 100644 index 000000000000..700c4e0d4d30 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-10.png new file mode 100644 index 000000000000..0aa0f2546ce3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-11.png new file mode 100644 index 000000000000..ef702aadb7e3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-2.png new file mode 100644 index 000000000000..bca16d416695 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-3.png new file mode 100644 index 000000000000..00edfb9382b2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-4.png new file mode 100644 index 000000000000..c947ad852ca0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-5.png new file mode 100644 index 000000000000..289789fd639f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-6.png new file mode 100644 index 000000000000..a58fbc16537b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-7.png new file mode 100644 index 000000000000..414ff44e067b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-8.png new file mode 100644 index 000000000000..dc7bb21bd4eb Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-9.png new file mode 100644 index 000000000000..0011841a7d14 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/pageplug-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/0.png new file mode 100644 index 000000000000..3b8766799280 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/1.png new file mode 100644 index 000000000000..8594460ab60b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/10.png new file mode 100644 index 000000000000..a8785255d657 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/11.png new file mode 100644 index 000000000000..4582e3ffe3f2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/12.png new file mode 100644 index 000000000000..1eca4f3c1aba Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/13.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/13.png new file mode 100644 index 000000000000..1c592128b98b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/14.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/14.png new file mode 100644 index 000000000000..d5ab998613d2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/15.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/15.png new file mode 100644 index 000000000000..d8ae3e93589d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/16.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/16.png new file mode 100644 index 000000000000..ab8def218f71 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/16.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/17.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/17.png new file mode 100644 index 000000000000..6dedc0883d31 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/17.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/2.png new file mode 100644 index 000000000000..0cbffe89e7bc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/3.png new file mode 100644 index 000000000000..d08b062b0a44 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/4.png new file mode 100644 index 000000000000..97b8d6a7c99e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/5.png new file mode 100644 index 000000000000..3568a104c8c7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/6.png new file mode 100644 index 000000000000..6a25d0523ce2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/7.png new file mode 100644 index 000000000000..05553d347357 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/8.png new file mode 100644 index 000000000000..50dbf2e31bc0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/9.png new file mode 100644 index 000000000000..5b117a1c33f6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/tailchat/9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-0.png new file mode 100644 index 000000000000..2281aaa41234 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-1.png new file mode 100644 index 000000000000..a19a6b255b39 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-10.png new file mode 100644 index 000000000000..6a7fb98ddb46 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-2.png new file mode 100644 index 000000000000..0cbffe89e7bc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-3.png new file mode 100644 index 000000000000..56f6c056be69 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-4.png new file mode 100644 index 000000000000..abda6fa2d152 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-5.png new file mode 100644 index 000000000000..8c9b87c88d9e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-6.png new file mode 100644 index 000000000000..8aa71909987b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-7.png new file mode 100644 index 000000000000..810d8e793df6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-8.png new file mode 100644 index 000000000000..1a657270d137 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-9.png new file mode 100644 index 000000000000..6aeebd4b6de1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/uptimekuma_img-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_1.png new file mode 100644 index 000000000000..d641378d4201 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_10.png new file mode 100644 index 000000000000..d1cd7993028d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_11.png new file mode 100644 index 000000000000..103bf474654c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_12.png new file mode 100644 index 000000000000..80c69d185806 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_13.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_13.png new file mode 100644 index 000000000000..250114b79ef9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_14.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_14.png new file mode 100644 index 000000000000..d180432b02c4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_15.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_15.png new file mode 100644 index 000000000000..877112038094 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_16.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_16.png new file mode 100644 index 000000000000..b490fade878c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_16.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_17.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_17.png new file mode 100644 index 000000000000..9d0bc0391604 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_17.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_2.png new file mode 100644 index 000000000000..83254bdb6cd1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_3.png new file mode 100644 index 000000000000..1b0a1f926bb8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_4.png new file mode 100644 index 000000000000..86ad0a605c7d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_5.png new file mode 100644 index 000000000000..1a9a1566ae1c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_6.png new file mode 100644 index 000000000000..52b0dc908d42 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_7.png new file mode 100644 index 000000000000..a18bfa6d1703 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_8.png new file mode 100644 index 000000000000..484684f79ae4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_9.png new file mode 100644 index 000000000000..24db5cadb900 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/images/wordpress_9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-appsmith.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-appsmith.md new file mode 100644 index 000000000000..528c030bd4e3 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-appsmith.md @@ -0,0 +1,71 @@ +--- +keywords: [Appsmith安装, 低代码平台, Sealos部署, 内部应用程序, 快速部署, 应用管理, 开源平台, CRUD应用, 管理面板, 自定义业务应用] +description: 学习如何在Sealos云平台上快速安装和部署Appsmith低代码平台。本教程提供详细步骤,帮助您轻松创建和管理内部应用程序、仪表板和工作流。 +--- + +# 快速安装 Appsmith + +[Appsmith](https://github.com/appsmithorg/appsmith) 是一个用于构建、部署和维护内部应用程序的开源平台。您可以构建任何东西,从简单的 +CRUD 应用程序、管理面板、仪表板到自定义业务应用程序和复杂的多步骤工作流。 + +## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/appsmith-1.png) + +## 步骤 2:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/appsmith-2.png) + +## 步骤 3:应用部署 + +- 基础配置: + + - 应用名称(自定义):appsmith + + - 镜像名(默认最新版本):appsmith/appsmith-ce + + - CPU(推荐):2 Core + + - 内存(推荐):4 G + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/appsmith-3.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](../images/appsmith-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 Appsmith 的数据(推荐 15 G)。 + +![](../images/appsmith-5.png) + +## 步骤 4:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/appsmith-6.png) + +## 步骤 5:访问应用 + +- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/appsmith-7.png) + +- 访问成功! + +![](../images/appsmith-8.png) + + diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-pageplug.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-pageplug.md new file mode 100644 index 000000000000..2278d1daedd6 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/low-code-platform/install-pageplug.md @@ -0,0 +1,78 @@ +--- +keywords: [PagePlug, 低代码平台, Sealos安装, 应用部署, 可视化开发, 前端框架, 开源项目, 快速部署, 容器化应用, 云原生开发] +description: 学习如何在Sealos云平台上快速安装和部署PagePlug低代码平台。本教程提供详细步骤,帮助您轻松搭建可视化开发环境,实现高效的前端应用开发。 +--- + +# 快速安装 PagePlug + +[PagePlug](https://github.com/cloudtogo/pageplug) 是 [Appsmith](https://github.com/appsmithorg/appsmith) 的中国化项目,基于 +Appsmith 做了整体性能优化及汉化,也集合了特色表单解决方案 Formily 组件、图表解决方案 Echarts +组件、低代码小程序开发等,是面向研发使用的一个开源的、声明式的、可视化的、符合开发者直觉的前端低代码框架。 + +## 步骤 1:在 [Sealos](https://cloud.sealos.io) 桌面环境中打开 「应用管理」 应用 + +![](../images/pageplug-1.png) + +## 步骤 2:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](../images/pageplug-2.png) + +## 步骤 3:应用部署 + +- 基础配置: + + - 应用名称(自定义):pageplug + + - 镜像名(默认最新版本):cloudtogouser/pageplug-ce + + - CPU(推荐):2 Core + + - 内存(推荐):4 G + +- 部署模式: + + - 实例数(自定义):1 + +![](../images/pageplug-3.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](../images/pageplug-4.png) + +- 高级配置: + + - 自定义本地存储,持久化 PagePlug 的数据。 + +![](../images/pageplug-5.png) + +## 步骤 4:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](../images/pageplug-6.png) + +## 步骤 5:访问应用 + +- 点击「详情」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 + +![](../images/pageplug-7.png) + +![](../images/pageplug-8.png) + +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](../images/pageplug-9.png) + +- 若访问出现 503 异常,等待一会重试即可。 + +![](../images/pageplug-10.png) + +- 访问成功! + +![](../images/pageplug-11.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/others/install-anki.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/others/install-anki.md new file mode 100644 index 000000000000..3662725e6cfa --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/others/install-anki.md @@ -0,0 +1,152 @@ +--- +keywords: [Anki同步服务器, Sealos部署, anki-sync-server-rs, 自定义同步, Anki客户端配置] +description: 本文详细介绍如何在Sealos上快速部署Anki自定义同步服务器,并指导如何在桌面端、安卓端和iOS端配置客户端,解决Anki同步速度慢的问题。 +--- + +# 快速安装 Anki 自定义同步服务器 + +Anki 是一个辅助记忆软件,其本质是一个卡片排序工具--**即依据使用者对卡片上的自定义内容进行主动测试、自我评判后,其内部算法根据评判结果更改每张卡片下次测试时间的排序工具。 +** + +所谓的卡片,专业说法叫 Flash Card(抽认卡或闪卡),是一小块纸片,分为正反两面,将问题和提示写在一面,将答案写在另一面。使用方法就是先看正面的问题与提示,在脑中回想答案,然后翻出反面进行对照验证。 + +闪卡的核心制作原则就是:**一个知识点一张卡**。因此非常适合用来学习英文,也可以用来记忆历史事件时间、公式等等。 + +Anki 的同步服务器在国外,还是一个个人项目,带宽很小,同步速度很慢,如果我们想在多个客户端之间同步学习进度和新增的知识点,那将非常痛苦。 + +为了解决这个问题,我们需要部署一个自定义的同步服务器,然后让客户端去使用这个同步服务器。 + +## Anki 同步服务器部署 + +自从 2023 年 2 月份,Anki 发布了 PC 端 2.1.57 版本以后,Anki 的 PC 端,安卓端,iOS 端用户都可以自定义同步服务器了,并且不再需要安装插件。从此 +Anki 小伙伴再也不用担心 Anki 同步的问题了,困扰 Anki 用户多年的同步问题终于得到彻底解决。 + +社区目前只有 [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs) 这个项目支持最新的 Anki +版本,其他的同步服务器项目基本上都失效了。这个项目是用 Rust 写的,追踪 [Anki 官方](https://github.com/ankitects/anki) +同步服务端的进度,它们都是基于sqlite c 作为数据存储后端。 + +下面我们将在 Sealos 中部署并配置 anki-sync-server-rs。 + +首先在浏览器地址栏输入网址 [https://cloud.sealos.io/](https://cloud.sealos.io/) 进入 Sealos 桌面。然后打开「应用管理」: + +![](../images/2023-06-26-11-54-EIVahX.jpg) + +点击「新建应用」: + +![](../images/2023-06-26-11-55-NDkuEg.jpg) + +依次输入应用名和镜像名,容器暴露端口是 `27701`,并开启外网访问: + +![](../images/2023-06-26-11-59-FxJE12.png) + +继续向下,展开「高级配置」,点击「编辑环境变量」: + +![](../images/2023-06-26-12-01-DKect7.png) + +将以下内容粘贴到环境变量输入框中: + +```bash +ANKISYNCD_USERNAME= +ANKISYNCD_PASSWORD= +``` + +请将 `` 替换为你自己的用户名,将 `` 替换为你自己的密码。 + +![](../images/2023-06-26-12-05-CWczxm.png) + +点击「新增存储卷」: + +![](../images/2023-06-26-12-06-lvv6ms.png) + +挂载路径填入 `/app`,然后点击确认: + +![](../images/2023-06-26-12-07-s8W7iu.png) + +最后点击右上角的部署应用即可。 + +部署完成后点击「详情」进入应用详情界面。 + +![](../images/2023-06-26-12-09-RslDGj.png) + +这里可以看到实例的运行状态,一定要等到状态是 running 才算是部署成功。如果一段时间以后状态还不是 running,可以点击「详情」查看故障原因: + +![](../images/2023-06-26-13-09-Vs9ccy.png) + +部署成功后,可以看到应用的运行情况,包括 CPU 占用、内存占用等。点击外网地址即可通过外网域名直接打开同步服务器的 Web 界面。 + +![](../images/2023-06-26-13-09-YFHPYc.png) + +如果出现以下的界面,则表示部署成功: + +![](../images/2023-06-26-13-09-FwsbfW.png) + +查看日志的方法也很简单,直接点击实例右侧的「三个点」,然后点击「日志」即可查看日志: + +![](../images/2023-06-26-13-09-hdHfxP.png) + +![](../images/2023-06-26-13-09-nwrxrv.png) + +## 客户端设置 + +### 桌面端 + +桌面客户端(macOS/Windows/Linux)配置方法如下: + +1. 先打开「首选项」 + + ![](../images/2023-06-26-12-24-QHYKZt.png) + +2. 点击「**网络**」,往下看,可以看到标有 `self-hosted sync server(自定义同步服务器)` 的方框,在里面填写您的服务端的地址: + + ![](../images/2023-06-26-12-26-HYOaBJ.png) + +3. 重启 Anki,然后点击「**同步**」: + + ![](../images/2023-06-26-12-28-ccnUOj.png) + +4. 这时候会弹出一个输入框让你输入用户名和密码,你需要将你之前设置的用户名和密码输入进去: + + ![](../images/2023-06-26-12-29-z5E9gi.png) + +5. 点击确认后,就会开始同步了。 + +### 安卓端 + +安卓端也是直接配置即可,我的 AnkiDroid 版本是 `2.15.6`。你可以通过「设置 -> 高级设置 -> 自定义同步服务器」找到配置页面。 + +![](../images/2022-04-10-14-31-vrNHJU.png) + +再填写用户名和密码: + +> 设置 -> 常用设置 -> AnkiWeb账户 + +这样就算配置完成了,所有的牌组都同步过来了。 + +![](../images/2022-04-10-14-32-ADfk8T.png) +![](../images/2022-04-10-14-32-1iudM0.png) + +官方的版本比较旧,如果你想使用更激进的社区版本,可以到这个页面下载最新的 Beta 版: + ++ [https://github.com/ankidroid/Anki-Android/releases](https://github.com/ankidroid/Anki-Android/releases) + +建议下载 **arm64-v8a** 版本。 + +安装完成后,可以通过「设置 -> 同步 -> 自定义同步服务器」找到配置页面: + +![](../images/2023-06-26-12-39-1jsF0t.jpeg) + +再填写用户名和密码: + +> 设置 -> 同步 -> AnkiWeb账户 + +### iOS 端 + +AnkiMobile 也已经支持和自建的同步服务器同步了。至少对于版本 Ankimobile 2.0.90(20090.2) 来说,似乎是可行的,这是一位 iOS +系统用户[在 Anki 论坛报告的](https://forums.ankiweb.net/t/ankimobile-self-sync-server-failure-the-one-bundled-in-version-2-1-60-qt6/27862)。 + +如果设置完成后发现不能同步可以参考下面的内容再试一次: + +> If you're using AnkiMobile and are unable to connect to a server on your local network, please go into the iOS +> settings, locate Anki near the bottom, and toggle "Allow Anki to access local network" off and then on again. + +上面的内容摘自 [ANki tutorial](https://docs.ankiweb.net/sync-server.html#client-setup) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Go Apps.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Go Apps.md new file mode 100644 index 000000000000..f3fa20260fbe --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Go Apps.md @@ -0,0 +1,156 @@ +--- +keywords: [Go程序安装, Docker镜像, Sealos部署, 容器化应用, 云原生开发, Go Web应用, Docker Hub, 应用管理, 快速部署, 外网访问] +description: 本教程详细介绍如何快速安装Go程序,包括编写Go代码、创建Docker镜像、推送到Docker Hub,以及在Sealos平台上部署和访问应用的完整流程。 +--- + +# 快速安装 Go 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Go 程序 + +```go +package main + +import ( + "fmt" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello,Wolrd!") + }) + + http.ListenAndServe(":8080", nil) +} +``` + +## 步骤2:创建Docker镜像 + +- 在与`main.go`相同的目录中,创建一个名为`Dockerfile`的文件,包含以下内容: + +``` +FROM golang:1.17 as builder + +WORKDIR /app +COPY . . + +RUN go mod init myapp +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /app/main /app/ + +EXPOSE 8080 +ENTRYPOINT ["/app/main"] +``` + +这个`Dockerfile`定义了一个多阶段构建过程。在第一阶段,我们使用官方的`golang`镜像作为基础镜像,然后编译Go应用程序。在第二阶段,我们使用轻量级的 +`alpine`镜像,将编译好的二进制文件复制到`/app`目录,并将8080端口暴露。 + +- 在`Dockerfile`所在的目录运行以下命令,为Go应用程序构建Docker镜像: + +``` +docker build -t your_image_name . +``` + +将`your_image_name`替换为您的镜像名称和标签。 + +## 步骤3:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名, + `your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag demo damager6666/demo:latest + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/demo:latest + ``` + +## 步骤4:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤5:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 6:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤7:应用部署 + +- 基础配置: + + - 应用名称(自定义):go-demo + - 镜像名(**步骤5**中推送的镜像):damager6666/demo:latest + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/java-example-5.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤8:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤9:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/hello +``` + +![](images/java-example-9.png) + +- 页面上显示 ”Hello,World! “ ,表示你的 Go 应用程序已经在 Sealos 上运行 + diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md new file mode 100644 index 000000000000..a88e636c53a3 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Java Apps.md @@ -0,0 +1,280 @@ +--- +keywords: [Java应用部署, Sealos云平台, Docker镜像, Spring Boot, 数据库配置, 应用管理, 快速安装] +description: 本教程详细介绍如何在Sealos云平台上快速部署Java应用,包括Spring Boot项目创建、Docker镜像构建、数据库配置及应用管理等步骤,助您轻松实现Java程序的云端部署。 +--- + +# 快速安装 Java 程序 + +首先,确保你已经安装了以下工具: + +- Docker +- Maven + +## 步骤1:编写你的 Java 程序 + +- 这里直接通过 [Spring Initializr](https://start.spring.io/) 生成生成一个基本的 Spring Boot 项目。 + +![](images/java-example-1.png) + +- 打开项目的 `pom.xml` 文件,在 `` 部分添加以下内容,以包含MySQL JDBC驱动: + + ``` + + mysql + mysql-connector-java + + ``` + +- 接下来,在 `src/main/resources` 目录下的 `application.properties` 文件中添加以下内容,以配置数据库连接: + + ``` + spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 + spring.datasource.username=root + spring.datasource.password=your_password + spring.jpa.hibernate.ddl-auto=update + ``` + +## 步骤2:创建实体类和存储类 + +在项目中创建一个新的Java类,如 `Person.java`,用于表示数据库中的实体。添加以下内容: + +```java +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + // Getters and setters + // ... +} +``` + +- 然后创建一个存储库接口,如 `PersonRepository.java`: + + ```java + import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.stereotype.Repository; + + @Repository + public interface PersonRepository extends JpaRepository { + } + ``` + +## 步骤3:创建 Web 控制器 + +- 在项目的 `src/main/java` 目录下,创建一个简单的 Web 控制器。例如,创建一个名为 `PersonController.java` 的文件,并添加以下内容: + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class PersonController { + @Autowired + private PersonRepository personRepository; + + @GetMapping("/getPersons") + public String getAllPersons() { + List persons = personRepository.findAll(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < persons.size(); i++) { + sb.append("id: " + persons.get(i).getId() + " name: " + persons.get(i).getName() + "/n"); + } + System.out.println(sb.toString()); + return sb.toString(); + } + + @PostMapping(value = "/addPerson") + public Person addPerson(@RequestBody Person person) { + return personRepository.save(person); + } + +} +``` + +## 步骤4:构建应用程序 + +- 使用maven构建项目,运行以下命令 + + ``` + mvn clean install + ``` + + ​ ![](images/java-example-2.png) + +## 步骤5:创建Docker镜像 + +- 在 Java 项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 + Dockerfile: + + ``` + # 使用官方的 OpenJDK 镜像作为基础镜像 + FROM openjdk:8-jre-slim + + # 设置工作目录 + WORKDIR /app + + # 复制构建好的 JAR 文件到镜像中 + COPY target/demo-0.0.1-SNAPSHOT.jar /app/demo-0.0.1-SNAPSHOT.jar + + # 暴露应用程序的端口 + EXPOSE 8080 + + # 设置启动命令 + CMD ["java", "-jar", demo-0.0.1-SNAPSHOT.jar"] + ``` + +- 在项目根目录下,运行以下命令构建 Docker 镜像: + +``` +docker build -t java-demo . +``` + +## 步骤6:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名, + `your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag java-demo damager6666/demo:v2 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/demo:v2 + ``` + +## 步骤7:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤8:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤9:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤10:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + +## 步骤11:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 12:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤13:应用部署 + +- 基础配置: + + - 应用名称(自定义):java-demo + - 镜像名:damager6666/java-demo:v2 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/java-example-5.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤14:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤15:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/java-example-13.png) + +- 页面上显示之间插入到数据库的数据 ,表示你的 Java 应用程序已经在 Sealos 上运行 diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md new file mode 100644 index 000000000000..8158c1068666 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Node.js Apps.md @@ -0,0 +1,230 @@ +--- +keywords: [Node.js应用部署, Docker镜像构建, Sealos云平台, 数据库配置, 快速安装教程, 容器化部署, Web应用开发, 云原生应用, Express框架, MySQL数据库] +description: 本教程详细介绍如何快速安装并部署Node.js应用到Sealos云平台。包括编写代码、构建Docker镜像、配置数据库和应用部署的完整步骤,助您轻松实现云原生应用开发。 +--- + +# 快速安装 Node.js 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Node.js 程序 + +- 在目录下创建一个名为 `app.js` 的文件,并添加以下代码: + + ```js + const express = require('express'); + const mysql = require('mysql'); + const app = express(); + + const db = mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }); + + db.connect((err) => { + if (err) throw err; + console.log('Connected to the database.'); + }); + + app.get('/', (req, res) => { + db.query('SELECT id, name FROM users', (err, results) => { + if (err) throw err; + res.send(JSON.stringify(results)); + }); + }); + + app.listen(8080, () => { + console.log('Server is running on port 8080'); + }); + + ``` + +- 在项目根目录下创建一个名为 `package.json` 的文件,并包含以下内容: + + ```json + { + "name": "nodejs-web-app-mysql", + "version": "1.0.0", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "^4.17.1", + "mysql": "^2.18.1" + } + } + ``` + +## 步骤2:创建 Docker 镜像 + +- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 + Dockerfile: + +```dockerfile +FROM node:16 + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "start"] +``` + +## 步骤3:构建 Docker 镜像 + +- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像: + + ``` + docker build -t nodejs-demo . + ``` + +## 步骤4:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名, + `your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag nodejs-demo damager6666/nodejs-demo:v1 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/nodejs-demo:v1 + ``` + +## 步骤5:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤6:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤7:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤8:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + +## 步骤9:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 10:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤11:应用部署 + +- 基础配置: + + - 应用名称(自定义):nodejs-demo + - 镜像名:damager6666/nodejs-demo:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/nodejs-example-0.png) + +- 网络配置: + + - 容器暴露端口:8080 + + - 外网访问:开启 + +![](images/java-example-6.png) + +## 步骤12:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/java-example-7.png) + +## 步骤13:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/java-example-8.png) + +- 在浏览器上输入 + +``` +https://tmgkflgdlstl.cloud.sealos.io/getPersons +``` + +![](images/nodejs-example-1.png) + +- 页面上显示之间插入到数据库的数据 ,表示你的 Node.js应用程序已经在 Sealos 上运行 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Python Apps.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Python Apps.md new file mode 100644 index 000000000000..7fb5385bf94a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Python Apps.md @@ -0,0 +1,250 @@ +--- +keywords: [Python程序安装, Docker镜像, Sealos部署, Flask应用, MySQL数据库, 容器化, 云原生应用, 快速部署, 应用管理, 外网访问] +description: 本教程详细介绍如何快速安装Python程序并部署到Sealos云平台。包括编写Flask应用、创建Docker镜像、配置MySQL数据库和使用Sealos进行应用管理与外网访问。 +--- + +# 快速安装 Python 程序 + +首先,确保你已经安装了以下工具: + +- Docker + +## 步骤1:编写你的 Python 程序 + +- 在目录下创建一个名为 `app.py` 的文件,并添加以下代码: + + ```python + from flask import Flask, render_template + import mysql.connector + + app = Flask(__name__) + + def get_data_from_database(): + cnx = mysql.connector.connect( + host="java-demo-db-mysql.ns-7otl3mb2.svc", + user="root", + password="l9h8f24b", + database="test_db" + ) + + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + data = cursor.fetchall() + cursor.close() + cnx.close() + + return data + + @app.route('/') + def index(): + data = get_data_from_database() + return render_template('index.html', data=data) + + if __name__ == '__main__': + app.run(host='0.0.0.0', debug=True) + ``` + +- 在项目目录下创建一个名为 `requirements.txt` 的文件,以列出应用程序所需的依赖。 + +``` +Flask +mysql-connector-python +``` + +- 在项目目录中创建一个名为 `templates` 的文件夹,并在其中创建一个名为 `index.html` 的文件。将以下内容添加到 `index.html`: + + ~~~html + + + + + Data from MySQL + + +

Data from MySQL

+ + + + + + + {% for row in data %} + + + + + + {% endfor %} +
Column1Column2Column3
{{ row[0] }}{{ row[1] }}{{ row[2] }}
+ + + ``` + + 请将 `Column1`、`Column2` 和 `Column3` 替换为您要显示的实际列名。 + ~~~ + +## 步骤2:创建 Docker 镜像 + +- 在项目的根目录下,创建一个名为 `Dockerfile` 的文件。这个文件将包含构建 Docker 镜像所需的指令。以下是一个简单的示例 + Dockerfile: + + ```dockerfile + FROM python:3.8 + + WORKDIR /app + + COPY requirements.txt . + + RUN pip install --no-cache-dir -r requirements.txt + + COPY . . + + EXPOSE 5000 + + CMD ["python", "app.py"] + ``` + +## 步骤3:构建 Docker 镜像 + +- 在包含 `Dockerfile` 的目录中运行以下命令来构建 Docker 镜像: + + ``` + docker build -t python-demo . + ``` + +## 步骤4:推送Docker镜像 + +- 将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + + - 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名, + `your-repo-name` 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag python-demo damager6666/python-demo:v1 + ``` + + - 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/python-demo:v1 + ``` + +## 步骤5:登陆 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤6:打开 「数据库」 应用 + +![](images/java-example-10.png) + +## 步骤7:创建数据库 + +- 点击新建数据库 + +![](images/java-example-11.png) + +- 基础配置: + - 集群类型:mysql + - 集群名称 : demo-db + - CPU(推荐):1 Core + - 内存(推荐):1 G + - 实例数:1 + - 存储容量: 1 Gi +- 点击 部署集群 + +## 步骤8:配置数据库 + +![](images/java-example-12.png) + +- 点击一键连接到数据库执行以下语句: + + - 创建 test_db 数据库 + + ```sql + create database test_db; + ``` + + - 创建 test 表 + + ```sql + CREATE TABLE `name_info` ( + `id` int(11) NOT NULL, + `name` varchar(12) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) + ); + ``` + + - 插入数据 + + ```sql + insert into test values (1,'Sealos'); + ``` + +## 步骤9:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 10:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤11:应用部署 + +- 基础配置: + + - 应用名称(自定义):python-demo + - 镜像名:damager6666/python-demo:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/python-example-0.png) + +- 网络配置: + + - 容器暴露端口:5000 + + +- 外网访问:开启 + +![](images/python-example-1.png) + +## 步骤12:部署应用 + +![](images/python-example-2.png) + +## 步骤13:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/python-example-3.png) + +- 在浏览器上输入外网地址,可以访问到刚刚存入到 MySQL 中的数据,表示程序已经成功 Sealos 上 + + ```http + https://mkqreaqfgwac.cloud.sealos.io/ + ``` + +![](images/python-example-4.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Vue Apps.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Vue Apps.md new file mode 100644 index 000000000000..7916c06f1411 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/Quick installation of Vue Apps.md @@ -0,0 +1,149 @@ +--- +keywords: [Vue应用部署, Docker镜像, Sealos云平台, 前端开发, 容器化, 快速部署, 云原生应用, DevOps实践, 微服务架构, 持续集成] +description: 本教程详细介绍如何快速安装Vue应用并部署到Sealos云平台。从Vue CLI创建到Docker镜像构建,再到Sealos上的一键部署,助您轻松实现云原生前端开发。 +--- + +# 快速安装 Vue 程序 + +首先,确保你已经安装了以下工具: + +- Docker +- Node.js +- npm + +## 步骤1:安装 Vue CLI + +```bash +npm install -g @vue/cli +``` + +## 步骤2:使用 Vue CLI 创建一个新项目 + +```bash +vue create my-vue-app +``` + +## 步骤3:使用 Vue CLI 构建项目的生产版本 + +```bash +cd my-vue-app +npm run build +``` + +## 步骤4:创建Docker镜像 + +- 在与my-vue-app目录中,创建一个名为`Dockerfile`的文件,包含以下内容: + +```dockerfile +# 使用 nginx 镜像作为基础镜像 +FROM nginx:1.21-alpine + +# 复制构建的前端文件到 nginx 容器中 +COPY dist/ /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] +``` + +- 在`Dockerfile`所在的目录运行以下命令,为 Vue 应用程序构建 Docker 镜像: + +```bash +docker build -t my-vue-app . +``` + +## 步骤5:推送Docker镜像 + +将创建的Docker镜像推送到Docker仓库,如Docker Hub或者私有仓库。**假设您已经登录到Docker仓库**,使用以下命令推送镜像 + +- 首先标记Docker镜像,在推送镜像之前,需要为其添加一个标签,以便 Docker 知道将其推送到哪个仓库。运行以下命令为镜像添加标签: + + ``` + docker tag your-image-name your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-image-name` 替换为你的本地镜像名称,`your-dockerhub-username` 替换为你的 Docker Hub 用户名,`your-repo-name` + 替换为你在 Docker Hub 上要创建的仓库名称,`your-tag` 替换为你为镜像设置的标签(例如:latest)。 + + 例如: + + ``` + docker tag my-vue-app damager6666/my-vue-app:v1 + ``` + +- 接下来推送Docker镜像,使用以下命令将镜像推送到 Docker Hub: + + ``` + docker push your-dockerhub-username/your-repo-name:your-tag + ``` + + 将 `your-dockerhub-username`、`your-repo-name` 和 `your-tag` 替换为实际的值。例如: + + ``` + docker push damager6666/my-vue-app:v1 + ``` + +## 步骤6:登录 Sealos + +- 进入 [Sealos](https://cloud.sealos.io/) 官网 + +![](images/java-example-0.png) + +## 步骤7:打开 「应用管理」 应用 + +![](images/java-example-3.png) + +## 步骤 8:新建应用 + +- 在 「应用管理」 中,点击「新建应用」来创建一个新的应用。 + +![](images/java-example-4.png) + +## 步骤9:应用部署 + +- 基础配置: + + - 应用名称(自定义):my-vue-app + - 镜像名(**步骤5**中推送的镜像):damager6666/my-vue-app:v1 + - CPU(推荐):1 Core + - 内存(推荐):1 G + +- 部署模式: + + - 实例数(自定义):1 + +![](images/vue-example-0.png) + +- 网络配置: + + - 容器暴露端口:80 + + - 外网访问:开启 + +![](images/vue-example-1.png) + +## 步骤10:部署应用 + +- 点击「部署应用」开始部署应用。 + +![](images/vue-example-2.png) + +## 步骤11:访问应用 + +- 点击「应用管理」查看,当应用的 STATUS 由 Pending 变为 Running,则表明该应用已成功启动。 +- 当 STATUS 为 Running,即可直接访问外网地址。 + +![](images/vue-example-3.png) + +- 在浏览器上输入 + +``` +https://mvpztqzczudy.cloud.sealos.io +``` + +![](images/vue-example-4.png) + +- 页面上显示 ”Hello,Vue.js! “ ,表示你的 Vue 应用程序已经在 Sealos 上运行 + diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-0.png new file mode 100644 index 000000000000..f856ec873a65 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-1.png new file mode 100644 index 000000000000..83b3f85e4441 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-10.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-10.png new file mode 100644 index 000000000000..7164ba6a31fc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-11.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-11.png new file mode 100644 index 000000000000..b1cf562ca8c0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-12.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-12.png new file mode 100644 index 000000000000..6eedee90f87c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-13.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-13.png new file mode 100644 index 000000000000..3548b505e15f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-2.png new file mode 100644 index 000000000000..179f4b85a5fb Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-3.png new file mode 100644 index 000000000000..07db46ce9e6f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-4.png new file mode 100644 index 000000000000..670338bb9b00 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-5.png new file mode 100644 index 000000000000..a79b84e285a8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-6.png new file mode 100644 index 000000000000..d5f28559a335 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-7.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-7.png new file mode 100644 index 000000000000..4a14b632edd4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-8.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-8.png new file mode 100644 index 000000000000..19a4f0f3276e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-9.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-9.png new file mode 100644 index 000000000000..b5582f255cd6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/java-example-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-0.png new file mode 100644 index 000000000000..629be8f5f144 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-1.png new file mode 100644 index 000000000000..83d4acd117f7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/nodejs-example-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-0.png new file mode 100644 index 000000000000..86d53a8b85c7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-1.png new file mode 100644 index 000000000000..2f03cf92b397 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-2.png new file mode 100644 index 000000000000..19dcda99374a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-3.png new file mode 100644 index 000000000000..54ceb5547f34 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-4.png new file mode 100644 index 000000000000..3ffbe07560ba Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/python-example-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-0.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-0.png new file mode 100644 index 000000000000..648276d14120 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-0.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-1.png new file mode 100644 index 000000000000..e47a1b27115d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-2.png new file mode 100644 index 000000000000..dab8d56621b8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-3.png new file mode 100644 index 000000000000..9ac654f24398 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-4.png new file mode 100644 index 000000000000..b00209521090 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/programming-languages/images/vue-example-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/social-communication/install-tailchat.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/social-communication/install-tailchat.md new file mode 100644 index 000000000000..f9d501d4c318 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/examples/social-communication/install-tailchat.md @@ -0,0 +1,183 @@ +--- +keywords: [Tailchat, Sealos, 开源IM, 快速部署, 微服务架构, 企业级应用, MongoDB, Redis, MinIO, 水平扩容] +description: 本文详细介绍如何在Sealos云平台上快速部署Tailchat开源IM应用,包括依赖服务的创建、主应用部署、服务预览及扩容等步骤,助您轻松搭建企业级即时通讯系统。 +--- + +# 快速安装 Tailchat + +[Tailchat](https://tailchat.msgbyte.com/) 是一款插件化易拓展的开源 IM 应用。可拓展架构赋予 Tailchat 无限可能性。 + +前端微内核架构 + 后端微服务架构 使得 Tailchat 能够驾驭任何定制化/私有化的场景 + +面向企业与私域用户打造,高度自由的群组管理与定制化的面板展示可以让私域主能够更好的展示自己的作品,管理用户,打造自己的品牌与圈子。 + +![](../images/tailchat/0.png) + +## 首先在 [Sealos](https://cloud.sealos.io) 桌面环境中打开「应用管理」 + +![](../images/tailchat/1.png) + +## 新建应用 + +![](../images/tailchat/2.png) + +### 创建依赖 + +`tailchat` 作为企业级的应用,最小依赖: `mongodb`, `redis`, `minio`. + +接下来让我们来一一创建。 + +#### mongodb + +为了方便起见我们固定一个实例,并且为数据库绑定本地存储。 + +使用的镜像是 `mongo:4` + +需要注意的是因为我没有给数据库设置密码,因此不要对外网提供网络服务。容器暴露端口填数据库默认服务端口 `27017` 即可 + +内容如下: + +![](../images/tailchat/3.png) + +点击部署应用提交部署 + +耐心等待一会,就可以看到应用已经启动起来了 + +![](../images/tailchat/4.png) + +> 需要注意的是初始分配的64m对于mongodb来说实在太小了,所以通过变更应用改为了128m。可以随时分配资源大小这也是sealos/k8s很方便的一点 + +#### minio + +接下来我们创建minio, minio是一个开源的对象存储服务。我们同样可以通过`sealos`的点点点来快速创建 + +使用的镜像是: `minio/minio` + +需要注意的是我们要进行一些调整: + +- 暴露端口: 9000 +- 运行命令改为: `minio` +- 命令参数改为: `server /data` +- 设置环境变量: + - MINIO_ROOT_USER: tailchat + - MINIO_ROOT_PASSWORD: com.msgbyte.tailchat +- 本地存储: `/data` + +最终结果如下: + +![](../images/tailchat/5.png) + +点击部署按钮同样看到服务已经正常启动起来了。 + +#### redis + +最后我们需要部署redis作为内容缓存与信息转发。 + +使用镜像: `redis:alpine` + +暴露端口: `6379` + +最终结果如下: + +![](../images/tailchat/6.png) + +### 创建 Tailchat 本体 + +此时Tailchat所需要的依赖均已部署完毕,如下: + +![](../images/tailchat/7.png) + +现在我们来部署 `Tailchat` 本体。 + +`Tailchat` 的本体会相对复杂一点,不过因为`sealos`纯UI操作也不会太过复杂。 + +- 使用镜像: `moonrailgun/tailchat` +- 暴露端口: `11000`(记得要打开外网访问) +- 配置环境变量如下: + ``` + SERVICEDIR=services,plugins + TRANSPORTER=redis://redis:6379 + REDIS_URL=redis://redis:6379 + MONGO_URL=mongodb://mongo/tailchat + MINIO_URL=minio:9000 + MINIO_USER=tailchat + MINIO_PASS=com.msgbyte.tailchat + ``` + +最终效果如下: + +![](../images/tailchat/8.png) + +耐心等待一段时间后可以看到`Tailchat` 服务已经启动起来了 + +![](../images/tailchat/9.png) + +## 预览服务 + +首先我们可以先检查一下`Tailchat`服务的可用性,可以通过外网地址提供的服务后面加上 `/health` 来检查服务可用性, 如: +`https://.cloud.sealos.io/health` + +当启动完毕后,Tailchat服务会返回如下内容: + +![](../images/tailchat/10.png) + +这段json字符串中包含了使用的镜像版本,节点名称,系统占用,微服务加载情况。 + +这里我们可以看到我的常见的服务, 如`user`/`chat.message`以及一些带有插件前缀的服务如`plugin.registry` +都已经正常启动起来了,说明我们的服务端是正常运行的。 + +现在我们可以直接访问我们的外网地址,可以看到经过短暂的加载后,页面正常打开自动跳转到了登录界面。 + +![](../images/tailchat/11.png) + +随便注册一个账号,可以看到我们可以正常进入Tailchat的主界面, 如下图: + +![](../images/tailchat/12.png) + +至此我们的服务已经成功在 sealos 中落地了。 + +## 扩容服务 + +当然,作为一个分布式架构的系统,`Tailchat` 天然是支持水平扩容的。而在 `sealos` 想要实现扩容也非常简单, +只需要通过变更操作修改实例数即可: + +![](../images/tailchat/13.png) + +![](../images/tailchat/14.png) + +![](../images/tailchat/15.png) + +此时当我们访问 `https://.cloud.sealos.io/health` 可以看到我们可以访问到不同的节点 + +![](../images/tailchat/16.png) + +## 添加 Tailchat 入口到桌面 + +打开 Terminal, 输入`vim app.yml`创建并编辑一个配置文件 + +输入以下内容,注意url要换成自己部署的网址 + +```yml +apiVersion: app.sealos.io/v1 +kind: App +metadata: + name: tailchat-app-entry +spec: + name: Tailchat + icon: + type: iframe + data: + url: + desc: + icon: https://tailchat.msgbyte.com/img/logo.svg + menuData: + displayType: normal +``` + +按`esc`退出编辑模式, 按`:wq`保存并退出vim + +输入`kubectl apply -f app.yml`启动配置。 + +完毕后刷新页面,此时我们可以看到我们的入口就出现在`sealos`的桌面上了 + +![](../images/tailchat/17.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-1.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-1.png new file mode 100644 index 000000000000..6fb723004f9d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-2.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-2.png new file mode 100644 index 000000000000..00f4cbbb2f1b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-3.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-3.png new file mode 100644 index 000000000000..23f6f8e3d56d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-4.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-4.png new file mode 100644 index 000000000000..9c4532f429d1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-5.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-5.png new file mode 100644 index 000000000000..3785a81c1d67 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-6.png b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-6.png new file mode 100644 index 000000000000..23d46dfb5add Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/quick-start/images/quick-start-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/quick-start/quick-start.md b/docs/archived/5.0/i18n/zh-Hans/quick-start/quick-start.md new file mode 100644 index 000000000000..783cc4db5621 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/quick-start/quick-start.md @@ -0,0 +1,34 @@ +--- +keywords: [Sealos快速部署, Flarum论坛, 开源论坛软件, 应用商店部署, 简洁论坛搭建] +description: 学习如何使用Sealos快速部署Flarum开源论坛。本教程提供详细步骤,从应用商店选择到成功访问论坛,助您轻松搭建简洁高效的社区平台。 +--- + +# 快速开始 + +使用 Sealos 快速部署 Flarum。 + +> Flarum 是一款非常简洁的开源论坛软件。它响应快速、简便易用,拥有打造一片成功的社区所需的所有功能。它也极其可扩展,允许达到终极的可定制性。 + +打开 Sealos 桌面,点击应用商店。 + +![](./images/quick-start-1.png) + +点击 Flarum 应用(左上角搜索栏可以快速查找应用)。 + +![](./images/quick-start-2.png) + +点击部署应用 + +![](./images/quick-start-3.png) + +等待所有组件的状态变为运行中,代表组件启动完毕(可以重新进入当前页面刷新状态)。 + +![](./images/quick-start-4.png) + +返回 Sealos 桌面,点击 Flarum 应用。 + +![](./images/quick-start-5.png) + +成功访问 Flarum。 + +![](./images/quick-start-6.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/sidebar.json b/docs/archived/5.0/i18n/zh-Hans/sidebar.json new file mode 100644 index 000000000000..8c63bb5e6a12 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/sidebar.json @@ -0,0 +1,142 @@ +{ + "version.label": { + "message": "5.0.0", + "description": "The label for version current" + }, + "sidebar.docSidebar.category.Examples": { + "message": "案例", + "description": "The label for category Examples in sidebar docSidebar" + }, + "sidebar.docSidebar.category.ai-applications": { + "message": "AI 应用", + "description": "The label for category ai-applications in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Quick Start": { + "message": "快速开始", + "description": "The label for category Quick Start in sidebar docSidebar" + }, + "sidebar.docSidebar.category.App Deployments": { + "message": "部署应用", + "description": "The label for category App Deployments in sidebar docSidebar" + }, + "sidebar.docSidebar.category.How-to Guides": { + "message": "使用指南", + "description": "The label for category How-to Guides in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Master Services Agreement": { + "message": "服务政策", + "description": "The label for category Master Services Agreement in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Self Hosting": { + "message": "私有化部署", + "description": "The label for the doc item Self Hosting in sidebar docSidebar, linking to the doc Community" + }, + "sidebar.docSidebar.category.User Guide": { + "message": "用户文档", + "description": "The label for category User Guide in sidebar docSidebar" + }, + "sidebar.docSidebar.doc.CronJob": { + "message": "定时任务", + "description": "The label for category CronJob in sidebar docSidebar" + }, + "sidebar.docSidebar.doc.App Store": { + "message": "应用商店", + "description": "The label for category App Store in sidebar docSidebar" + }, + "sidebar.docSidebar.category.App Launchpad": { + "message": "应用管理", + "description": "The label for category App Launchpad in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Database": { + "message": "数据库", + "description": "The label for category Database in sidebar docSidebar" + }, + "sidebar.docSidebar.category.System Function": { + "message": "系统功能", + "description": "The label for category System Function in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Developer Guide": { + "message": "开发者文档", + "description": "The label for category Developer Guide in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos": { + "message": "Sealos 私有云", + "description": "The label for category Sealos Self Hosting in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Kubernetes Lifecycle Management": { + "message": "K8s 集群生命周期管理", + "description": "The label for category Kubernetes Lifecycle Management in sidebar docSidebar" + }, + "sidebar.docSidebar.category.System Design": { + "message": "系统设计", + "description": "The label for category System Design in sidebar docSidebar" + }, + "sidebar.docSidebar.doc.Community": { + "message": "社区", + "description": "The label for the doc item Community in sidebar docSidebar, linking to the doc Community" + }, + "sidebar.docSidebar.category.Operations": { + "message": "操作说明", + "description": "The label for category Operations in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Advanced Guide": { + "message": "进阶指南", + "description": "The label for category Advanced Guide in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Reference": { + "message": "参考", + "description": "The label for category Reference in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealctl 使用指南": { + "message": "Sealctl 使用指南", + "description": "The label for category Sealctl 使用指南 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 使用指南": { + "message": "Sealos 使用指南", + "description": "The label for category Sealos 使用指南 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 集群镜像构建": { + "message": "Sealos 集群镜像构建", + "description": "The label for category Sealos 集群镜像构建 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 镜像仓库": { + "message": "Sealos 镜像仓库", + "description": "The label for category Sealos 镜像仓库 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 集群镜像运行": { + "message": "Sealos 集群镜像运行", + "description": "The label for category Sealos 集群镜像运行 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.Sealos 命令说明": { + "message": "Sealos 命令说明", + "description": "The label for category Sealos 命令说明 in sidebar docSidebar" + }, + "sidebar.docSidebar.category.baas": { + "message": "后端即服务", + "description": "The label for category baas in sidebar docSidebar" + }, + "sidebar.docSidebar.category.low-code-platform": { + "message": "低代码平台", + "description": "The label for category low-code-platform in sidebar docSidebar" + }, + "sidebar.docSidebar.category.others": { + "message": "其他", + "description": "The label for category others in sidebar docSidebar" + }, + "sidebar.docSidebar.category.dial-testing-system": { + "message": "拨测系统", + "description": "The label for category dial-testing-system in sidebar docSidebar" + }, + "sidebar.docSidebar.category.blog-platform": { + "message": "博客平台", + "description": "The label for category blog-platform in sidebar docSidebar" + }, + "sidebar.docSidebar.category.social-communication": { + "message": " IM 应用", + "description": "The label for category social-communication in sidebar docSidebar" + }, + "sidebar.docSidebar.category.programming-languages": { + "message": "常用语言案例", + "description": "The label for category programming-languages in sidebar docSidebar" + } +} \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/ai-proxy.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/ai-proxy.md new file mode 100644 index 000000000000..df3dd70f422c --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/ai-proxy.md @@ -0,0 +1,144 @@ +--- +title: AI Proxy +description: AI Proxy 是 Sealos 平台提供的统一 AI 模型调用服务,支持多平台 API Key 管理、统一计费和监控,让开发者轻松接入各类 AI 模型。 +keywords: ["AI Proxy", "Sealos", "AI模型调用", "API管理", "统一计费", "开发者工具"] +--- + +# AI Proxy 使用指南 + +## 简介 + +AI Proxy 是 [Sealos 平台](../../Intro.md)提供的一站式 AI 模型调用解决方案,让开发者能够在统一的平台中轻松调用和管理各类 AI 模型。无论是通义千问、文心一言还是其他 AI 模型,都可以通过统一的接口进行调用。 + +### 为什么选择 AI Proxy? + +- 🔑 **一键获取多平台密钥** - 无需分别注册各个 AI 平台 +- 💰 **统一计费更省心** - 告别多平台充值的烦恼 +- 📊 **集中管理更高效** - 一站式监控所有模型调用情况 + +## 功能特点 + +### 统一的 API 访问 + +- 提供标准化的 API 接口,支持多家主流 AI 模型厂商 +- 兼容 OpenAI API 格式,便于快速迁移和集成 +- 持续扩充支持的模型类型 + +### 简化的密钥管理 + +![密钥管理界面](images/ai-proxy-key-management.png) + +- 一键获取多平台 API Key +- 无需分别注册各个 AI 平台 +- 统一的密钥管理界面 + +### 集中化计费与监控 + +![计费系统界面](images/ai-proxy-billing.png) + +- 使用 Sealos 平台统一结算 +- 透明的按量计费模式 +- 详细的费用明细和账单查询 +- 详细的调用日志记录 + +## 快速开始 + +### 1. 获取 API Key + +1. 访问 [Sealos Cloud](https://hzh.sealos.run) +2. 打开 AI Proxy 应用 +3. 点击【新建】按钮创建新的 API Key +4. 复制生成的 API Endpoint 和 API Key + +### 2. API 调用示例 + +#### 使用 JavaScript 调用 + +```javascript +async function main() { + const apiKey = 'your-api-key' + const apiUrl = 'https://aiproxy.hzh.sealos.run/v1/chat/completions' + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: 'Doubao-lite-4k', + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: '你好,请介绍一下你自己。' } + ], + max_tokens: 2048, + temperature: 0.7, + }), + }) + + const data = await response.json() + console.log(data.choices[0].message.content) +} +``` + +### 3. 请求参数说明 + +| 参数 | 类型 | 说明 | 示例值 | +|------|------|------|--------| +| model | string | 要使用的模型名称 | 'Doubao-lite-4k' | +| messages | array | 对话消息列表 | [{"role": "user"}] | +| max_tokens | number | 最大生成的 token 数量 | 2048 | +| temperature | number | 生成文本的随机性,范围 0-1 | 0.7 | + +## 费用管理 + +### 查看费用明细 + +1. 在 AI Proxy 界面中点击【费用明细】 +2. 可查看各模型的调用次数和费用统计 +3. 支持按时间范围筛选费用记录 + +### 余额充值 +1. 进入 Sealos 费用中心 +2. 选择充值金额 +3. 完成支付后即可使用充值金额调用模型 + +## 调用日志 + +### 日志查看 + +- 支持查看详细的 API 调用记录 +- 包含调用时间、模型名称、输入输出 token 数量等信息 + +### 日志筛选 + +- 按时间范围筛选 +- 按模型类型筛选 +- 按 Token 筛选 + +## 最佳实践 + +### 开发建议 + +1. 合理设置 `max_tokens` 参数,避免生成过长的无效内容 +2. 根据场景调整 `temperature` 参数,对话场景建议使用较高的值 +3. 在生产环境中做好异常处理和重试机制 + +### 成本优化 + +1. 选择适合业务场景的模型 +2. 合理设置上下文长度,避免无效的 token 消耗 + +## 常见问题 + +### API 调用失败 + +- 检查 API Key 是否正确 +- 确认账户余额是否充足 +- 查看具体的错误信息进行排查 + +### 费用相关 + +- 费用按实际调用量计费 +- 不同模型的计费标准不同 +- 支持查看详细的计费规则 diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-billing.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-billing.png new file mode 100644 index 000000000000..9d163b444c85 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-billing.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-key-management.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-key-management.png new file mode 100644 index 000000000000..43c20d0366fd Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/ai-proxy/images/ai-proxy-key-management.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/add-domain.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/add-domain.md new file mode 100644 index 000000000000..880981cd1b5e --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/add-domain.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 2 +keywords: [自定义域名, Sealos, 应用部署, CNAME解析, 外网访问, 品牌体验, 域名绑定, 云平台] +description: 学习如何在Sealos云平台上为您的应用添加自定义域名,提升品牌形象。本指南涵盖部署时和部署后的域名设置流程,包括CNAME解析和外网访问配置。 +--- + +# 自定义域名 + +想让您的用户能够享受与您的品牌一致的专属体验?只需要在 [Sealos](https://cloud.sealos.io) 中为你的项目指定一个自定义域名。 + +## 部署时接入 + +如果您在部署应用时接入自定义域名,只需要在部署应用时打开「外网访问」,Sealos 会给您的应用自动分配一个外网域名。 + +![](./images/app-launchpad-domain.png) + +然后需要到您的域名服务商处,添加该域名的 `CNAME` 解析到上面分配的外网域名。以阿里云为例: + +![](./images/aliyun-cname.png) + +等待解析生效后即可回到 Sealos 中绑定自定义域名,直接点击右侧的「自定义域名」: + +![](./images/app-launchpad-domain2.png) + +在弹出的界面中输入您的自定义域名,然后点击确认即可。 + +![](./images/app-launchpad-domain3.png) + +最终点击右上角的「部署」开始部署应用,部署完成后点击外网地址即可通过自定义域名访问应用。 + +## 部署完成后接入 + +已经部署完成的应用可以在应用详情页面的右上角点击「变更」,然后参考之前的步骤接入自定义域名即可。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/app-launchpad.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/app-launchpad.md new file mode 100644 index 000000000000..c756679b1fd3 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/app-launchpad.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 0 +keywords: [应用管理, Sealos, 容器部署, 弹性伸缩, 持久化存储, 自定义域名, 多端口暴露, 应用监控] +description: Sealos 应用管理工具简化容器部署,支持私有镜像、资源定制、多副本、弹性伸缩等功能。快速部署、监控和优化您的应用,提高效率和性能。 +--- + +# 应用管理 + +**应用管理** 是 Sealos 内置的单镜像部署工具,主要用于简化和加速应用程序的部署过程,可以帮助您在 5 分钟内完成应用的部署和上线。 + +目前「应用管理」具备以下功能: + +- 支持使用私有镜像部署应用; +- 支持根据应用需求,自定义所需的 CPU 和内存资源; +- 支持多副本; +- 弹性伸缩 (HPA); +- 提供外网访问地址,便于公网访问; +- 允许用户为应用配置自定义域名,提高品牌识别度和用户体验; +- ConfigMap 配置文件; +- 应用数据的持久化存储,保障数据的安全性和持续性; +- 提供应用和 Pod 的实时监控,帮助用户及时发现并解决问题; +- 记录和管理应用日志,便于问题追踪和性能分析; +- 分析系统事件(Events),提供关键信息帮助优化应用性能; +- 一键进入容器终端,方便管理和调试; +- 支持将应用的多个端口暴露到外网。 + +## [快速开始](./use-app-launchpad.md) + +快速安装一些比较常见的应用。 + +## [更新应用](./update-app.md) + +应用部署完成后修改应用配置。 + +## [自定义域名](./add-domain.md) + +为应用接入自定义域名。 + +## [暴露多端口](./expose-multi-ports.md) + +将应用的多个端口暴露到外网中。 + +## [环境变量](./environment.md) + +通过环境变量为应用提供配置信息。 + +## [配置文件](./configmap.md) + +通过配置文件为应用提供配置信息。 + +## [弹性伸缩](./autoscale.md) + +通过弹性伸缩来根据负载自动调整应用的实例数量。 + +## [持久化存储](./persistent-volume.md) + +使用持久化存储来保障数据的持久化。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/autoscale.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/autoscale.md new file mode 100644 index 000000000000..264a85f5be35 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/autoscale.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 6 +keywords: [弹性伸缩, Horizontal Pod Autoscaler, HPA, Sealos, 应用管理, 自动调整, CPU使用率, 资源优化, 性能监控, 动态扩缩容] +description: 了解Sealos的弹性伸缩功能如何自动调整应用实例数量,优化资源利用。探索HPA工作原理,实现高效的应用性能管理和资源分配。 +--- + +# 弹性伸缩 + +「[应用管理](/guides/applaunchpad/applaunchpad.md)」可以在 Sealos 集群中自动调整应用的实例数量,以适应不同的负载条件。这个功能叫「 +**弹性伸缩**(Horizontal Pod Autoscaler,简称 HPA)」。 + +「弹性伸缩」通过监测特定的度量指标(如 CPU 使用率或内存使用率)来动态增加或减少应用的实例数量,以确保应用程序运行效率和资源利用的最优化。 + +> 注意:在弹性伸缩的上下文中,提到的“使用率”通常是指应用实例的平均使用率。例如,如果一个应用有两个实例,那么其 CPU 平均使用率将是这两个实例的 CPU 使用率的平均值。 + +弹性伸缩的工作原理: + +1. **监控**:持续监控应用的关键性能指标,如 CPU 使用率或内存使用量。 +2. **决策**:根据设定的目标(例如 CPU 使用率不超过 50%),系统会计算出为了达到这一目标所需增加或减少的应用实例数量。 +3. **调整**:基于计算结果,弹性伸缩会自动指示 Sealos 的控制器增加或减少应用实例的数量,以保持资源使用率在预定目标范围内。 + +假设我们为一个应用设置了如下规则:CPU 使用率不得超过 50%,实例数量可在 1 至 5 之间变动。在这种设定下,弹性伸缩将会: + +- 当应用的平均 CPU 使用率超过 50% 时,系统会增加实例数量,最多增至 5 个。 +- 当应用的平均 CPU 使用率低于 50% 时,系统会减少实例数量,但至少保持 1 个实例运行。 + +![](./images/autoscale.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/configmap.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/configmap.md new file mode 100644 index 000000000000..9c5fed13e8a0 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/configmap.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 5 +keywords: [Sealos配置文件, 应用程序配置管理, 容器配置注入, Nginx容器配置, 应用行为调整] +description: 探索Sealos应用程序中配置文件的重要性和优势。了解如何使用配置文件灵活管理复杂配置数据,并以Nginx容器为例说明配置文件的应用。 +--- + +# 配置文件 + +在 [Sealos](https://cloud.sealos.io) +应用程序中,配置文件的使用非常重要,尤其是当应用程序需要处理大量或复杂的配置信息时。与环境变量相比,配置文件可以提供一种更灵活、更可靠的配置管理方式。环境变量更适合用于传递少量、简单的配置数据。 + +配置文件的主要优势在于其能够存储和管理复杂的配置数据,这些配置数据可以包含配置文件、命令行参数、环境变量等信息。这些数据可以在应用容器启动时注入到容器中,从而允许你对应用程序的行为进行调整,而无需重新构建镜像。 + +下面以 Nginx 容器为例,说明如何使用配置文件来调整应用的行为: + +- **文件名**: 对应 Nginx 容器里的文件,可以参考镜像提供者给出的说明。 +- **文件值**: 文件对应的内容。如果内容过于复杂,建议在本地编辑完成后,再复制粘贴到相应位置。 +- **注意事项**:配置文件是挂载单个文件,而不是挂载目录。你需要明确指定要挂载的具体文件,而非仅仅指定一个目录路径。 + +![](./images/applaunchpad13.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/environment.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/environment.md new file mode 100644 index 000000000000..fb474a5710d8 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/environment.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 4 +keywords: [Sealos环境变量, 容器应用配置, 应用管理, 环境变量格式, 键值对设置, 应用可维护性, 应用可扩展性, 批量输入, 环境变量解析] +description: 探索Sealos中环境变量的重要性及其在容器应用配置中的应用。学习如何通过批量输入定义环境变量,提高应用的可维护性和可扩展性。 +--- + +# 环境变量 + +在 [Sealos](https://cloud.sealos.io) +中,环境变量扮演着至关重要的角色,它们是管理容器应用配置数据的关键工具。通过使用环境变量,可以为应用提供必要的配置信息,而无需直接修改应用的代码或其镜像,从而提高应用的可维护性和可扩展性。 + +在「[应用管理](/guides/applaunchpad/applaunchpad.md) +」界面,环境变量是通过批量输入的方式来定义的。用户可以通过按行输入的方式来定义多个环境变量,其中每个环境变量由键(key)和值(value)组成,两者之间可以使用等号(=)或冒号(: +)作为分隔符,「[应用管理](/guides/applaunchpad/applaunchpad.md)」会自动清除键(key)中的无效字符,以确保环境变量的正确性和有效性。 + +![](./images/applaunchpad12.png) + +**可正常解析的环境变量格式:** + +```bash +host=127.0.0.1 +port:3000 +name: sealos +- username=123 +- password:123 +# 这一行的注释会被忽略。因为不包含等号(=)或冒号(:)这两种标记字符。 +``` + +**不可解析的环境变量格式:** + +```bash +host=127.0.0.1 # 这一行会被解析,因为包含了等号(=)或冒号(:)这两种标记字符。这里的注释也会被解析,因为前面已经有等号(=)了。 +``` \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/expose-multi-ports.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/expose-multi-ports.md new file mode 100644 index 000000000000..b902918db63a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/expose-multi-ports.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 3 +keywords: [多端口暴露, Sealos应用管理, 网络配置, 公网访问, 多协议支持, 应用多功能, 兼容性, Prometheus监控, GRPC服务] +description: 了解如何在Sealos平台上轻松配置应用的多端口暴露,支持多协议、多功能和监控需求,并获得独立二级域名实现便捷的公网访问。 +--- + +# 暴露多端口 + +在复杂的应用环境中,经常需要应用服务同时暴露多个端口以满足不同的需求。这种需求可能出现在多种场景下: + ++ **多协议支持**:例如,一个应用可能同时支持 HTTP 和 HTTPS 协议,需要暴露 80 和 443 端口。 ++ **应用的多功能**:例如,一个应用可能有一个 Web 服务和一个 Admin 服务,它们分别监听不同的端口。 ++ **兼容性考虑**:为了与旧版本或其他服务兼容,可能需要同时暴露新旧两种接口的端口。 ++ **Prometheus 监控和应用服务**:如果你的应用既有业务端口,又有一个用于 Prometheus 监控的 `/metrics` 端口,你可能需要同时暴露两者。 ++ **GRPC 和 RESTful 服务并存**:如果你的应用同时提供 GRPC 和 RESTful 服务,你可能需要为每种服务类型暴露不同的端口。 + +[Sealos](https://cloud.sealos.io) 在使用「[应用管理](/guides/applaunchpad/applaunchpad.md) +」部署应用时,可以轻松地选择暴露多个端口。在部署应用的过程中,用户只需要点击「网络配置」选项,然后选择「添加端口」,即可实现多端口的配置。 + +![](./images/multi-ports1.png) + +此外,Sealos 平台还提供了端口的外网访问功能。一旦暴露到公网,每一个暴露的端口都会被分配一个独立的二级域名,使得用户可以更方便地进行远程访问和管理。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/11.png new file mode 100644 index 000000000000..a4ba01524001 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/aliyun-cname.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/aliyun-cname.png new file mode 100644 index 000000000000..54651df59314 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/aliyun-cname.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain.png new file mode 100644 index 000000000000..9e88bb76dc3d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain2.png new file mode 100644 index 000000000000..b5a9dd12dc40 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain3.png new file mode 100644 index 000000000000..5eb0377da0e8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/app-launchpad-domain3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad1.png new file mode 100644 index 000000000000..0e2d87f96122 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad10.png new file mode 100644 index 000000000000..37bbf7ab57d5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad11.png new file mode 100644 index 000000000000..ceeb631f3120 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad12.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad12.png new file mode 100644 index 000000000000..a0189b569ab8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad13.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad13.png new file mode 100644 index 000000000000..9317aa9f5c5d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad14.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad14.png new file mode 100644 index 000000000000..a7551be28826 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad2.png new file mode 100644 index 000000000000..016395059908 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad3.png new file mode 100644 index 000000000000..fd28e1f9fb6b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad4.png new file mode 100644 index 000000000000..82b4603f4ca3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad5.png new file mode 100644 index 000000000000..c4f8f7c33d95 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad6.png new file mode 100644 index 000000000000..6019b61ca10f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad7.png new file mode 100644 index 000000000000..73fcdcc39cf3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad8.png new file mode 100644 index 000000000000..227d5f1b8cff Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad9.png new file mode 100644 index 000000000000..77eb0919f867 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/applaunchpad9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/autoscale.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/autoscale.png new file mode 100644 index 000000000000..e8b4907448f6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/autoscale.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/multi-ports1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/multi-ports1.png new file mode 100644 index 000000000000..ab08b16299b9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/multi-ports1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/persistent-volume.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/persistent-volume.png new file mode 100644 index 000000000000..b904addd338a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/persistent-volume.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert1.png new file mode 100644 index 000000000000..373e56b2bee4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert2.png new file mode 100644 index 000000000000..b7ea59eed18a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert3.png new file mode 100644 index 000000000000..0fddf4574890 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert4.png new file mode 100644 index 000000000000..aa4fc627bc11 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/set-cert4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-1.png new file mode 100644 index 000000000000..53d586c7fc54 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-10.png new file mode 100644 index 000000000000..8e477e550a4d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-11.png new file mode 100644 index 000000000000..49ab8b95ae32 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-12.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-12.png new file mode 100644 index 000000000000..5f1da8a47b4b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-2.png new file mode 100644 index 000000000000..a9461aae6b7e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-3.png new file mode 100644 index 000000000000..2f2429e965d2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-4.png new file mode 100644 index 000000000000..355cd126eec3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-5.png new file mode 100644 index 000000000000..8948883dd408 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-6.png new file mode 100644 index 000000000000..834cb08f40d3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-7.png new file mode 100644 index 000000000000..7154f775b5d2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-8.png new file mode 100644 index 000000000000..2c57fd60b2e2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-9.png new file mode 100644 index 000000000000..b904addd338a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/images/use-app-launchpad-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/persistent-volume.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/persistent-volume.md new file mode 100644 index 000000000000..9b4394899781 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/persistent-volume.md @@ -0,0 +1,16 @@ +--- +sidebar_position: 7 +keywords: [持久化存储, Sealos, 容器数据, 数据持久性, 外挂存储, Nextcloud, 数据库, 文件存储系统] +description: 了解Sealos中的持久化存储如何解决容器数据丢失问题。学习使用外挂存储确保数据库、文件系统等关键数据的安全性,以Nextcloud为例展示实际应用。 +--- + +# 持久化存储 + +在 [Sealos](https://cloud.sealos.io) +中,容器可以轻松地被创建和销毁。虽然这为部署和管理应用带来了灵活性,但它也带来了数据持久性的挑战。如果应用在容器内部存储数据,那么当容器被销毁时,所有存储在容器内的数据也会丢失。 + +持久化存储可以确保数据存储在容器外部,即使在容器重启或重新部署的情况下也能保证数据不会丢失。**如果你的应用需要保留数据(例如数据库、文件存储系统或任何存储用户数据的服务),一定要使用持久化存储!** + +假设你要部署一个 Nextcloud,Nextcloud 容器的所有数据都存储在 `/var/www/html` 这个目录下,因此我们需要将该目录下的数据通过外挂存储进行持久化。 + +![](./images/persistent-volume.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/set-cert.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/set-cert.md new file mode 100644 index 000000000000..05aa054b0f7f --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/app-launchpad/set-cert.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 8 +keywords: [自定义域名证书, Sealos, 应用管理, cert-manager, Kubernetes, Ingress, TLS Secret, CNAME设置, 证书配置, 域名访问] +description: 学习如何在Sealos平台上为自定义域名设置证书,解决cert-manager签发失败问题。包括创建TLS Secret、修改Ingress配置等步骤,确保安全访问您的应用。 +--- + +# 设置自定义域名证书 + +在「应用管理」中成功设置自定义域名,但是无法访问域名并显示证书不安全。 这是因为 cert-manager 没有成功签发证书。为了解决这个问题,我们可以手动设置证书。 + +首先,确认在云厂商中成功设置 CNAME,并且下载域名对应的证书。 + +打开「应用管理」,设置自定义域名。 + +![](./images/set-cert1.png) + +打开「终端」,依次执行以下命令。 + +```bash +# 用证书文件信息创建 tls.crt(使用证书文件信息替换掉 xxxx) +cat > tls.crt < tls.key <的最大连接数 | +| max_wal_size | 设置WAL文件的最大大小 | +| min_wal_size | 设置WAL文件的最小大小 | +| max_worker_processes | 设置PostgreSQL可以启动
的最大后台进程数 | +| shared_buffers | 设置PostgreSQL可以启动
的最大后台进程数 | + +**常见的mysql参数**: + +| 参数名 | 描述 | +|-------------------------|----------------| +| innodb_buffer_pool_size | 设置InnoDB缓冲池的大小 | +| max_connections | 允许的最大并发连接数 | +| query_cache_size | 查询缓存的大小 | +| thread_cache_size | 线程缓存的大小 | +| max_allowed_packet | 最大数据包大小 | +| innodb_log_file_size | InnoDB日志文件的大小 | + +**常见的mongo参数**: + +| 参数名 | 描述 | +|-------------------------|----------| +| storage.dbPath | 数据文件存放路径 | +| storage.journal.enabled | 启用日志 | +| net.port | 服务器端口 | +| net.bindIp | 绑定的IP地址 | + +**常见的redis参数**: + +| 参数名 | 描述 | +|------------------|-----------| +| maxclients | 最大客户端连接数 | +| maxmemory | 最大内存使用量 | +| maxmemory-policy | 内存淘汰策略 | +| appendonly | AOF持久化开关 | +| appendfsync | AOF文件刷新频率 | + +4. **应用pg-config.yaml**: + +```bash +$ kubectl apply -f pg-config.yaml +``` + +![config_4](./imgs/config_4.png) + +5. **检查pg-config.yaml是否应用成功**: + +```bash +# OpsRequest中对应的状态为Succeed且pod对应的状态为Running则说明配置应用成功 +$ kubectl get OpsRequest +$ kubectl get pod +``` + +![config_5](./imgs/config_5.png) + +6. **进入数据库查看配置是否生效**: + +```bash +$ show max_connections; +``` + +![config_6](./imgs/config_6.png) +![config_7](./imgs/config_7.png) + diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_1.png new file mode 100644 index 000000000000..d8c8c62be3a6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_2.png new file mode 100644 index 000000000000..4bd234485bf3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_3.png new file mode 100644 index 000000000000..1f6d2ff3383b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_4.png new file mode 100644 index 000000000000..02f9a61edddf Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_5.png new file mode 100644 index 000000000000..412e0bbe7fc7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_6.png new file mode 100644 index 000000000000..5f49dd84f12e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_7.png new file mode 100644 index 000000000000..88f930a876f0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/config-docs/imgs/config_7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/database.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/database.md new file mode 100644 index 000000000000..6f898fe1803e --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/database.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 0 +keywords: [Sealos数据库, 数据库集群部署, PostgreSQL, MySQL, 数据库连接, 数据库备份, 数据库迁移, 数据库高可用, 可视化管理, 外网访问] +description: Sealos提供强大的数据库集群部署管理工具,支持多种数据库类型,具备弹性伸缩、备份、监控、迁移和高可用等功能,轻松实现数据库的快速部署和高效管理。 +--- + +# 数据库 + +数据库是一个应用的核心组成,Sealos 提供了一个数据库集群部署管理工具,可以帮助你快速部署数据库集群。目前支持 +MySQL、PostgreSQL、MongoDB、Redis、Kafka 等。目前数据库支持如下特性。 + ++ 弹性伸缩 ++ 支持多版本、多类型数据库 ++ 数据库连接:一键连接、外网访问 ++ 数据库备份:手动备份、自动备份 ++ 数据库监控:资源、状态、性能监控 ++ 数据库迁移:公网迁移、文件迁移 ++ 数据库高可用:支持多个 数据库 实例 ++ 可视化数据库管理:新建、删除、更新 、暂停、重启 数据库 + +## 快速开始 + +只需 6 步即可部署和访问一个 PostgreSQL 数据库。 + +1、打开 Sealos 桌面,点击数据库。 + +![start_1](./imgs/start_1.png) + +2、点击新建集群。 + +![start_2](./imgs/start_2.png) + +3、填写数据库名并选择对应的数据库和参数。 + +![start_3](./imgs/start_3.png) + +4、点击部署。 + +![start_4](./imgs/start_4.png) + +5、查看数据库详情信息。 + +![start_5](./imgs/start_5.png) + +6、数据库详情界面。 + +![start_6](./imgs/start_6.png) + +![start_7](./imgs/start_7.png) + +![start_8](./imgs/start_8.png) + +![start_9](./imgs/start_9.png) + +![start_10](./imgs/start_10.png) + +![start_11](./imgs/start_11.png) + +![start_12](./imgs/start_12.png) + +## 数据库连接 + +### 一键连接 + +1、进入数据库,点击连接。 + +![connect_1](./imgs/connect_1.png) + +2、在终端操作数据库。 + +![connect_2](./imgs/connect_2.png) + +### 外网访问 + +1、进入 数据库 ,开启外网访问。 + +![connect_3](./imgs/connect_3.png) + +2、确认开启。 + +![connect_4](./imgs/connect_4.png) + +3、复制数据库连接信息。 + +![connect_5](./imgs/connect_5.png) + +4、在数据库连接工具中连接数据库。 + +![connect_6](./imgs/connect_6.png) + +![connect_7](./imgs/connect_7.png) + +## 数据库备份 + +### 手动备份 + +1、进入数据库备份界面,点击备份。 + +![backup_1](./imgs/backup_1.png) + +2、填写备份信息,开始备份。 + +![backup_2](./imgs/backup_2.png) + +3、查看备份状态。 + +![backup_3](./imgs/backup_3.png) + +![backup_4](./imgs/backup_4.png) + +### 自动备份 + +1、进入数据库备份界面,点击备份。 + +![backup_5](./imgs/backup_5.png) + +2、开启自动备份,填写备份信息。 + +![backup_6](./imgs/backup_6.png) + +## 数据库迁移 + +以 MySQL 数据库为例,介绍 数据库迁移 的过程。 + +### 公网迁移 + +公网迁移涉及两个数据库:源数据库、目标数据库。源数据库是迁移的数据来源,目标数据库是迁移的数据目的地。以下介绍中以本地数据库做为源数据库进行演示。 + +1、进入目标数据库,连接目标数据库。 + +![migration_1](./imgs/migration_1.png) + +2、在终端界面中创建对应的 database(如果已经存在对应的 database,则跳过这一步)。 + +![migration_2](./imgs/migration_2.png) + +```bash +# 创建数据库sql语句,示例: +$ create database testmysql; +``` + +3、进入目标数据库公网迁移界面,点击迁移按钮。 + +![migration_3](./imgs/migration_3.png) + +4、查看迁移配置信息。 + +![migration_4](./imgs/migration_4.png) + +复制迁移配置信息。 + +```bash +# 设置配置信息sql语句,示例: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` + +5、在源数据库中执行设置配置信息(MySQL、Postgres 需要手动配置信息,Mongo 不需要进行配置)。 + +```bash +# 设置配置信息sql语句,示例: +$ set global binlog_format=ROW; +$ set binlog_row_image ='FULL'; +``` + +![migration_5](./imgs/migration_5.png) + +6、进入目标数据库,填写源数据库的迁移信息,要迁移哪些表,并填写要迁移到目标数据库中哪个 database。 + +![migration_6](./imgs/migration_6.png) + +如果需要持续从源数据库迁移数据,可开启高级配置中的持续迁移。 + +![migration_7](./imgs/migration_7.png) + +7、查看迁移任务信息。 + +![migration_8](./imgs/migration_8.png) + +8、进入目标数据库,连接目标数据库,检查迁移数据是否完整。 + +![migration_9](./imgs/migration_9.png) + +### 文件迁移 + +1、进入目标数据库,连接目标数据库。 + +![migration_10](./imgs/migration_10.png) + +2、在终端界面中创建对应的 database(如果已经存在对应的 database,则跳过这一步)。 + +```bash +# 创建数据库sql语句,示例: +$ create database testmysql; +``` + +![migration_11](./imgs/migration_11.png) + +3、上传迁移文件、填写数据库名,开始迁移。 + +![migration_12](./imgs/migration_12.png) + +4、文件迁移中,等待迁移结果。 + +![migration_13](./imgs/migration_13.png) + +![migration_14](./imgs/migration_14.png) + +5、连接数据库,检查迁移数据是否完整。 + +![migration_15](./imgs/migration_15.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_1.png new file mode 100644 index 000000000000..8c9e8c50867d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_2.png new file mode 100644 index 000000000000..02f239da516c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_3.png new file mode 100644 index 000000000000..e0d5dca8c2ef Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_4.png new file mode 100644 index 000000000000..58c37d7c5d2a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_5.png new file mode 100644 index 000000000000..8c9e8c50867d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_6.png new file mode 100644 index 000000000000..48b20cf2b8ca Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/backup_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_1.png new file mode 100644 index 000000000000..52c6879b4d29 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_2.png new file mode 100644 index 000000000000..76215ce1c9ab Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_3.png new file mode 100644 index 000000000000..0a205d4a3626 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_4.png new file mode 100644 index 000000000000..a1a4366d15ce Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_5.png new file mode 100644 index 000000000000..0b4a3b398f73 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_6.png new file mode 100644 index 000000000000..3125cbb7bf12 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_7.png new file mode 100644 index 000000000000..e8bce07c9cde Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/connect_7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_1.png new file mode 100644 index 000000000000..d5527ccf5116 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_10.png new file mode 100644 index 000000000000..d5527ccf5116 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_11.png new file mode 100644 index 000000000000..895255296854 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_12.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_12.png new file mode 100644 index 000000000000..12561fb4eba5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_13.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_13.png new file mode 100644 index 000000000000..074b4882b952 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_14.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_14.png new file mode 100644 index 000000000000..409e77e907a6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_15.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_15.png new file mode 100644 index 000000000000..54589726b673 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_2.png new file mode 100644 index 000000000000..895255296854 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_3.png new file mode 100644 index 000000000000..2bc1d55549e8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_4.png new file mode 100644 index 000000000000..9a49e93f1b62 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_5.png new file mode 100644 index 000000000000..e9b0d128758b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_6.png new file mode 100644 index 000000000000..271c5bafd8b5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_7.png new file mode 100644 index 000000000000..778411659966 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_8.png new file mode 100644 index 000000000000..3202d5031fb1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_9.png new file mode 100644 index 000000000000..54589726b673 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/migration_9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_1.png new file mode 100644 index 000000000000..1282c5c85d79 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_10.png new file mode 100644 index 000000000000..6e10d9cedd0e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_11.png new file mode 100644 index 000000000000..633705ae905c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_12.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_12.png new file mode 100644 index 000000000000..f08ae8c081df Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_2.png new file mode 100644 index 000000000000..016fe962470c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_3.png new file mode 100644 index 000000000000..f687bb3cda4f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_4.png new file mode 100644 index 000000000000..175419decad9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_5.png new file mode 100644 index 000000000000..da9edd9ca8cc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_6.png new file mode 100644 index 000000000000..e61a8d5befba Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_7.png new file mode 100644 index 000000000000..63aeada4ce95 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_8.png new file mode 100644 index 000000000000..8e2d489183a4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_9.png new file mode 100644 index 000000000000..cbb8d4a36ce9 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/database/imgs/start_9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/devbox.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/devbox.md new file mode 100644 index 000000000000..d4746167a676 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/devbox.md @@ -0,0 +1,35 @@ +# Devbox + +## 简介 + +> Sealos Devbox 是一个用于即时协作开发、无缝部署和严格环境隔离的平台,帮助你简化工作流程。 + +Sealos DevBox是一个一站式平台,集成了在线开发、测试和生产环境。它允许你一键创建环境和设置数据库依赖,让开发者在本地使用自己喜欢的IDE工作,同时简化了设置流程,还能自动部署应用,简直是开发者的得力助手。 + +![overview-1](./images/quick-start-1.png) + +### 主要特点和优势 + +#### 即时协作环境 + +Sealos Devbox 提供多种编程语言和框架的快速开发环境设置,支持团队立即开始协作,无论使用哪种技术栈。 + +#### 云开发环境 + +Sealos Devbox 提供统一云平台,消除了环境不一致的问题,让团队可以轻松共享代码、配置和测试数据,从而加速开发过程并提升效率。 + +#### 无头开发经验 + +Sealos Devbox 简化了开发流程,自动创建环境并与本地 IDE 无缝集成,为开发人员提供轻松的设置体验。 + +#### 简化持续交付 + +无需 Docker 或 Kubernetes 专业知识,Sealos Devbox 使团队能够轻松交付应用程序。开发人员只需指定版本,Sealos Devbox 负责处理构建和容器化等复杂任务。 + +#### 严格环境隔离 + +Sealos Devbox 提供隔离的开发环境,帮助团队避免依赖冲突。每个项目都可以拥有自己一致且可重现的工作空间,让开发人员可以专注于相关任务,而不必担心环境不一致。 + +#### 灵活的网络访问 + +Sealos Devbox 提供从内部网络和互联网访问应用程序的功能,并自动配置 TLS,确保安全和灵活的开发能力,让团队可以在全球任何地方工作。 diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/faq.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/faq.md new file mode 100644 index 000000000000..2fdb800ffb9e --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/faq.md @@ -0,0 +1,113 @@ +# FAQ + +## 1、Cursor 连接出现问题但是 VSCode 可以连接 + +Cursor 由于插件版本同步 VSCode 比较缓慢,比较落后的版本可能会导致连接出现问题。 + +解决措施:手动安装 Devbox 插件。在 Cursor 扩展市场里安装 Remote-SSH,注意版本暂时应该是 v0.113.x。(注意必须在 Cursor 里安装这个版本,在网页里下载 Remote-SSH 再导入到 Cursor 里大概率版本不对应导致不兼容。) + +1. 从 VSCode 插件市场下载 [Devbox](https://marketplace.visualstudio.com/items?itemName=labring.devbox-aio) 插件的 vsix 文件。 + +![devbox-1](./images/faq-1.png) + +2. 打开 Cursor 的扩展窗口。 + +3. 将下载的文件拖拽到扩展窗口中。 + +![devbox-2](./images/faq-2.png) + +4. 在 Cursor 里安装 Remote-SSH v0.113.x。 + +![devbox-3](./images/faq-3.png) + +## 2、Cursor 和 VSCode 都无法连接 + +首先明白 Devbox 插件的原理:即通过改动 ssh config 文件来添加远程环境信息,并通过 Remote-SSH 插件进行远程环境的连接。插件首先在 `~/.ssh/config` 写入下面这行代码(一些老版本可能写入的其他类似的内容): + +```bash +Include ~/.ssh/sealos/devbox_config +``` + +这行代码的作用是将 `~/.ssh/sealos/devbox_config` 这个文件的内容导入到当前文件。而 `devbox_config` 里则是正常的 SSH 配置内容,例如: + +```config +Host usw.sailos.io_ns-rqtny6y6_devbox1234 + HostName usw.sailos.io + User devbox + Port 40911 + IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox1234 + IdentitiesOnly yes + StrictHostKeyChecking no +``` + +所以如果出现问题,大概率是插件 BUG 读写文件出错,可以反馈给我们,或者自己尝试调整 SSH 文件。 + +## 3、一直卡在下载 vscode-server 过程,或者是不断重试 + +原因:因为某种操作(在这个过程中重启 Devbox 等)导致下载 cursor 假死,重新下载产生冲突。 + +解决措施: + +1. 进入Web 终端删除 `.cursor-server` 文件夹。 + 1. 点击 Devbox 网页列表项右边操作按钮里的“终端”。 + 2. 进入终端先进入用户目录,`cd ..`,然后通过 `ls -a ` 查看所有文件可以看到 `.cursor-server`。 + 3. 删除 `rm -rf .cursor-server`。 + 4. 重试连接即可。 +2. 如果是刚创建里面没有内容的话,可以直接删除该 Devbox 重建。 + +## 4、报如下错误 + +```bash +upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: delayed connect error: 111 +``` + +首先明白一下你此时的环境是开发环境,你现在连接的网址是测试网址,只用于开发环境,这个网址对应的是开发环境的端口。也就是说你必须运行开发环境,例如 `npm run dev` 让你的程序先运行起来,才能通过网址看到内容,否则就会报这个错误。 + +另一种可能的情况是等待一段时间就可以了,可能网络比较卡顿。 + +## 5、点击链接 Cursor,进入 cursor 界面报错 “Failed to fetch” + +尝试打开 Cursor 的扩展市场,如果扩展市场无法正常加载,报错 `Error while fetching extensions.Failed to fetch`,则是网络问题无法加载 Cursor 的插件市场。请参考上面的手动安装教程手动安装 Devbox 插件或者尝试更换您的网络环境。 + +## 6、本地 localhost 能打开项目但是公网地址打不开 + +代码里暴露地址由于网络原因必须从 `localhost` 改为 `0.0.0.0`。 + +## 7、在 Devbox 中可以正常运行程序,但是在发版之后,程序无法正常运行 + +在发版之前请确保能在终端中运行 `entrypoint.sh` (这是推荐的发版后的启动脚本),如果需要公网服务,也请同时检查公网地址是否联通。 + +```bash +./entrypoint.sh +``` + +如果遇到下面的问题: + +```bash +bash: ./entrypoint.sh: Permission denied +``` + +可以在终端中输入如下的命令,并再次运行 `entrypoint` 脚本,并确保结果与期望的相同。 + +```bash +sudo chmod +x entrypoint.sh +``` + +同时,建议先在 Devbox 中预先构建完项目代码后并测试,测试成功后再发版,这可以有效避免在发版后的启动时的错误和 oom 问题。 + +## 8、应用程序监听的端口为 80 时,应用程序启动失败 + +在使用 Devbox 时,本地编辑器(vscode、cursor 等) +默认的用户为 Devbox,监听 1-1023 端口是管理员用户的特权。 如果需要正常使用,请使用管理员账户运行你的程序,例如在 go 的 +Devbox 中,可以使用下面的命令: + +```bash +sudo go run main.go +``` + +或者变更你的端口。 + + +## 9、如何在 Devbox 中更换默认的用户为 root 用户 + +终端里运行`sudo su`即可切换到 root 用户。 diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-1.png new file mode 100644 index 000000000000..a04895dd2dff Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-2.png new file mode 100644 index 000000000000..699e988dfd91 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-3.png new file mode 100644 index 000000000000..66a94f7d1a7b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/faq-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-1.png new file mode 100644 index 000000000000..b270802c7093 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-10.png new file mode 100644 index 000000000000..48d338f8538c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-2.png new file mode 100644 index 000000000000..a9a50a7fb458 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-3.png new file mode 100644 index 000000000000..9b1a2b930b56 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-4.png new file mode 100644 index 000000000000..ab7e99b80956 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-5.png new file mode 100644 index 000000000000..83fb9f9d3534 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-6.png new file mode 100644 index 000000000000..15847a6d53bc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-7.png new file mode 100644 index 000000000000..a06126fbabdd Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-8.png new file mode 100644 index 000000000000..4b669720936a Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-9.png new file mode 100644 index 000000000000..91dddc32141e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/quick-start-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-1.png new file mode 100644 index 000000000000..a2b8ad66059c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-10.png new file mode 100644 index 000000000000..ec718d9dadc1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-11.png new file mode 100644 index 000000000000..1b6d1c13e696 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-2.png new file mode 100644 index 000000000000..d4a43cde1a91 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-3.png new file mode 100644 index 000000000000..71c14cc5ff98 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-4.png new file mode 100644 index 000000000000..9cbc4361c4d0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-5.png new file mode 100644 index 000000000000..06aa6e9d8665 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-6.png new file mode 100644 index 000000000000..adee170669f7 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-7.png new file mode 100644 index 000000000000..2e7cd1052c25 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-8.png new file mode 100644 index 000000000000..6a4e0077e6b8 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-9.png new file mode 100644 index 000000000000..9ef04d952612 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/images/use-jb-ide-9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/quick-start.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/quick-start.md new file mode 100644 index 000000000000..61dd38cdb327 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/quick-start.md @@ -0,0 +1,140 @@ +# 快速开始 + +> 本指南介绍如何使用 Sealos Devbox 创建、开发和部署 Next.js 应用。内容涵盖项目设置、使用 Cursor IDE 进行远程开发以及云端部署。 + +## 创建 Devbox 项目 + +1. 点击 Sealos 桌面的 Devbox,然后点击新建项目按钮,创建一个新项目。 + +2. 在运行环境部分,选择 Next.js 作为开发框架。然后设置项目的CPU和内存。 + +![quick-start-1](./images/quick-start-1.png) + +3. 设置网络配置: + +- 容器暴露端口设置为 3000,这是 Next.js 开发环境的默认端口。(如果需要额外的端口,请点击添加端口) +- 开启公网访问,这会自动生成一个随机的公网域名。(如果想使用自定义域名,请点击自定义域名) + +> 请确保容器暴露端口与 Next.js 应用配置的端口一致。如果你修改了 Next.js 配置中的端口,记得在此处同步更新。 + +![quick-start-2](./images/quick-start-2.png) + +4. 点击右上角的创建来创建项目。 + +## 连接 Cursor IDE + +创建项目后,你可以在 Devbox 项目列表中找到它。每个项目都有一个操作列,可以在操作列的下拉框中切换不同的 IDE。 + +![quick-start-3](./images/quick-start-3.png) + +1. 启动本地 Cursor IDE: + +- 选择操作列的下拉框中的 Cursor 来启动本地 Cursor IDE。 + +2. 本地 Cursor IDE 连接 Devbox: + +- Cursor IDE 会弹出窗口提示你安装 Devbox 插件,安装后即可通过 SSH 与 Devbox 连接。 + +> 你可以随时切换不同的 IDE(VSCode、VSCode Insiders、Cursor 或 Windsurf)。 + +## 开发 + +1. 本地 Cursor IDE 成功连接 Devbox 后,你就可以在 Cursor IDE 中直接编辑项目文件。 + +![quick-start-4](./images/quick-start-4.png) + +> 通过远程连接,你可以在 Devbox 运行时运行代码,确保开发与生产环境一致,并在任何地方、任何安装了 Cursor 的设备上访问项目,方便团队协作。 + +2. 调试 Next.js 应用: + +- 打开 Cursor IDE 终端。 +- 导航到项目目录。 +- 运行以下命令以开发模式启动 Next.js 服务: + +```bash +npm run dev +``` + +3. 访问正在运行的应用: + +- 打开 Sealos 桌面的 Devbox。 +- 找到你的项目并点击详情按钮。 +- 点击外网地址。 + +![quick-start-5](./images/quick-start-5.png) + +4. 成功打开你的 Next.js 应用。 + +![quick-start-6](./images/quick-start-6.png) + +## 发布 + +开发并测试 Next.js 应用后,你可以将其打包为 OCI 镜像(即容器镜像),这样可以方便地进行版本控制并准备部署。 + +1. 在 Cursor IDE 终端中,导航到项目目录并运行构建命令: + +```bash +npm run build +``` + +此命令在 `.next` 目录中生成可用于生产的 Next.js 应用版本。 + +2. 转到项目详情页面: + +- 打开 Sealos 桌面的 Devbox。 +- 找到你的项目并点击详情按钮。 + +3. 在详情页面,找到“版本历史”部分。 + +4. 点击“版本历史”右上角的“发布版本”按钮。 + +5. 在弹出的“发布版本”对话框中,提供以下信息: + +- 镜像名:预填为项目名。 +- 版本号:输入版本号(如:v1.0)。 +- 版本描述:简要描述版本内容(如:“初始版本”或“修复登录问题”)。 + +![quick-start-7](./images/quick-start-7.png) + +6. 填写完毕后,点击“发版”按钮。 + +7. 系统会处理发布,完成后,你会在“版本历史”中看到新版本的记录,包括版本号、状态、创建时间和描述。 + +![quick-start-8](./images/quick-start-8.png) + +通过这些步骤,你已成功创建 Next.js 应用的 OCI 镜像。此镜像可以用于部署或与团队共享,每次发布都会创建一个代码快照,方便版本管理和回滚。 + +> 每当有重大更改或里程碑时,记得发布新版本。这能帮助保持开发历史清晰,并使部署和协作更加顺畅。 + +## 部署 + +将 Next.js 应用发布为 OCI 镜像后,你可以将其部署到 Sealos Cloud 以供生产使用。操作步骤如下: + +1. 在项目详情页面中,找到“版本历史”部分。 + +2. 找到需要部署的版本,点击“操作”栏中的“上线”按钮。 + +3. 系统会将你重定向到 Sealos 的应用管理界面。 + +4. 在应用管理中,按照部署向导配置应用设置。通常包括: + +- 设置应用名称 +- 设置资源限制(CPU和内存) +- 设置环境变量 +- 设置卷或持久存储 + +![quick-start-9](./images/quick-start-9.png) + +5. 配置完毕后,点击右上角的“部署应用”按钮开始部署。 + +6. 部署完成后,进入应用详情页面。 + +7. 当状态变为“running”时,点击“公网地址”,即可在新标签页中打开你的 Next.js 应用。 + +![quick-start-10](./images/quick-start-10.png) + +通过这些步骤,你已成功将 Next.js 应用部署到 Sealos Cloud。现在,用户可以通过公网地址访问应用。 + +> 你可以随时通过 Devbox 创建新版本,并重复此过程更新应用。 + +此工作流程使你能够在云环境中开发和调试 Next.js 应用,同时使用本地 IDE。外部地址使你可以轻松地与团队或客户共享应用,任何地方都能访问。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/use-jetbrains-ide.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/use-jetbrains-ide.md new file mode 100644 index 000000000000..1a9f8fd2765f --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/devbox/use-jetbrains-ide.md @@ -0,0 +1,50 @@ +# 使用 JetBrains IDE 开发 + +> 本指南介绍如何使用 JetBrains IDE 中的 IntelliJ IDEA 来开发基础环境为 Java 的 Devbox。 + +## 前置准备 + +1. 需要提前下载好 [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/) 应用。 + +2. 启动一个 Devbox 项目。 + +3. 下载 Devbox 项目的私钥到本地。查看 Devbox 项目详情中的 SSH 配置(Username:devbox,Host:hzh.sealos.run,Port:30566)。 + +![use-jb-ide-2](./images/use-jb-ide-2.png) + +## 开始使用 + +1. 打开 Devbox,选择 JetBrains IDE 并点击。 + +![use-jb-ide-3](./images/use-jb-ide-3.png) + +2. 自动唤起本地的 JetBrains Gateway,点击 `New Connection`。 + +![use-jb-ide-4](./images/use-jb-ide-4.png) + +3. 打开 JetBrains Gateway,填写 Username、Host 和 Port,勾选 Specify private key,选择私钥的所在路径。点击 `Check Connection and Continue`,即可测试 SSH 连接。 + +![use-jb-ide-5](./images/use-jb-ide-5.png) + +4. IDE 版本选择 `IntelliJ IDEA 2024.3.1 Preview`(因为 Devbox 的运行环境是 Java,所以选择 IDEA,根据具体的语言选择不同 IDE),项目路径选择 `/home/devbox/project`。点击 Download IDE and Connect,即可下载 IDE 和连接。 + +![use-jb-ide-6](./images/use-jb-ide-6.png) + +需要等待 IDE 下载完毕。 + +![use-jb-ide-7](./images/use-jb-ide-7.png) + +5. 自动唤起本地的 IntelliJ IDEA,语言选择 Chinese 简体中文,打开项目文件,点击绿箭头来运行 Java 服务。 + +![use-jb-ide-8](./images/use-jb-ide-8.png) + +![use-jb-ide-9](./images/use-jb-ide-9.png) + +6. 打开 Devbox 项目的详情,点击公网地址,即可跳转访问 Java 服务。 + +![use-jb-ide-10](./images/use-jb-ide-10.png) + +7. 成功访问 Java 服务。 + +![use-jb-ide-11](./images/use-jb-ide-11.png) + diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-1.png new file mode 100644 index 000000000000..ae364749ff79 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-2.png new file mode 100644 index 000000000000..fd26d149699b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-3.png new file mode 100644 index 000000000000..bf6d3a713e1e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-4.png new file mode 100644 index 000000000000..1d051c97977c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-5.png new file mode 100644 index 000000000000..2c090d3eb5cc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-6.png new file mode 100644 index 000000000000..80f11b12eccf Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-7.png new file mode 100644 index 000000000000..f3446edd301c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/images/kubepanel-7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/kubepanel.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/kubepanel.md new file mode 100644 index 000000000000..a24bd8518e03 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/kubepanel/kubepanel.md @@ -0,0 +1,44 @@ +--- +keywords: [Kubepanel, Kubernetes IDE, 集群管理, 资源可视化, Sealos, 部署资源, Workload, Config, Network, Storage] +description: Kubepanel是一款用户友好的Kubernetes IDE,提供图形界面管理集群、实时监控和资源可视化。本文介绍如何使用Kubepanel查看和部署Kubernetes资源。 +--- + +# Kubepanel + +Kubepanel 是一个款 Kubernetes IDE(集成开发环境)。它提供了一个用户友好的图形界面来管理 Kubernetes +集群,具有实时监控和资源可视化管理的功能。它旨在使 Kubernetes 更加易于访问和管理,即使对于那些不太熟悉 Kubernetes +命令行工具的人也是如此。 + +## 快速开始 + +打开 Sealos 桌面,点击 Kubepanel。 + +![](images/kubepanel-1.png) + +### 查看资源 + +Kubepanel 内可以查看 Workload、Config、Network 和 Storage 资源。 + +![](images/kubepanel-2.png) + +### 部署资源 + +点击➕加号。 + +![](images/kubepanel-3.png) + +需要先选择一个模板,然后才可以编写资源 YAML。 + +![](images/kubepanel-4.png) + +这里选择了 Deployment 模板,点击 Create 创建 Deployment 资源。 + +![](images/kubepanel-5.png) + +创建成功后,可以在 Workload/Deployments 中看到该 Deployment 资源。 + +![](images/kubepanel-6.png) + +也支持对资源进行删除或者更新操作。 + +![](images/kubepanel-7.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/1.png new file mode 100644 index 000000000000..5b728e6ba75b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/10.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/10.png new file mode 100644 index 000000000000..ce6822c8da07 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/10.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/11.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/11.png new file mode 100644 index 000000000000..b303afd8c8f0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/11.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/12.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/12.png new file mode 100644 index 000000000000..c392cf2cad7e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/12.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/13.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/13.png new file mode 100644 index 000000000000..e3cc905cd04f Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/13.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/14.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/14.png new file mode 100644 index 000000000000..33ed05150a0b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/14.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/15.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/15.png new file mode 100644 index 000000000000..c1da8b158963 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/15.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/16.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/16.png new file mode 100644 index 000000000000..d696f4019148 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/16.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/17.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/17.png new file mode 100644 index 000000000000..e7a88fb904f4 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/17.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/2.png new file mode 100644 index 000000000000..b512a3ae81ed Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/3.png new file mode 100644 index 000000000000..b628c0ef664e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/4.png new file mode 100644 index 000000000000..51c5c50eb83d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/5.png new file mode 100644 index 000000000000..ff565585d890 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/6.png new file mode 100644 index 000000000000..47ae1c00d7e0 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/7.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/7.png new file mode 100644 index 000000000000..0d81b2eb3ef3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/7.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/8.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/8.png new file mode 100644 index 000000000000..eecf21479325 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/8.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/9.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/9.png new file mode 100644 index 000000000000..6ba91e30a63b Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/images/9.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/objectstorage.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/objectstorage.md new file mode 100644 index 000000000000..71327df3183a --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/objectstorage/objectstorage.md @@ -0,0 +1,209 @@ +--- +keywords: [对象存储, Sealos, 存储桶, 文件上传, SDK访问, 静态托管, 访问权限, Go Client, Java Client, 公共读写] +description: 探索Sealos对象存储服务:文件管理、权限控制、SDK集成和静态托管。轻松上传、下载文件,自定义访问权限,并使用Go和Java SDK实现高效存储操作。 +--- + +# 对象存储 + +对象存储是 Sealos 内置的对象存储服务,主要用于存储和管理非结构化数据。 + +目前对象存储具备以下功能: + +- 上传文件到存储桶; +- 从存储桶下载文件; +- 公开存储桶的访问权限; +- 使用 SDK 访问存储桶; +- 监控存储桶资源指标; +- 静态托管。 + +## 快速开始 + +打开 Sealos 桌面,点击对象存储。 + +![](./images/1.png) + +### 上传文件 + +点击创建存储桶。 + +![](./images/2.png) + +输入存储桶的名字,设置存储桶的权限。 + +存储桶权限说明: + +- private:私有桶,不开放访问; +- publicRead:共享存储桶,开放公共读功能; +- publicReadwrite:共享存储桶,开放公共读写功能。 + +![](./images/3.png) + +存储桶创建成功。 + +![](./images/4.png) + +上传文件。 + +![](./images/5.png) + +上传文件成功。 + +![](./images/6.png) + +## 公开存储桶的访问权限 + +点击编辑。 + +![](./images/7.png) + +设置存储桶权限为 publicRead,点击应用。 + +![](./images/8.png) + +复制文件链接。 + +![](./images/9.png) + +粘贴到浏览器地址栏访问文件。 + +![](./images/10.png) + +## 查看访问密钥配置 + +对象存储用户由唯一的 Access Key(用户名)和对应的 Secret Key(密码)组成。Internal 为对象存储的内部访问地址,External +为对象存储的外部访问地址。 + +![](./images/11.png) + +## 使用 SDK 访问存储桶 + +SDK 访问存储桶需要三个参数:AccessKey、SecretKey、Endpoint。参数都在访问密钥中,Internal 是内网地址 Endpoint,External 是外网地址 +Endpoint。如果需要使用 Region 参数,默认使用 us-east-1。 + +### Go Client SDK + +详细文档参考:https://min.io/docs/minio/linux/developers/go/API.html + +例子:使用 Go Client SDK 上传 style.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前 +K8s 集群内,可以将 Endpoint 改为内网地址 Internal。 + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" +) +import "github.com/minio/minio-go/v7" +import "github.com/minio/minio-go/v7/pkg/credentials" + +func main() { + endpoint := "objectstorageapi.xxx.xxx.xxx" + accessKey := "xxxxxxxx" + secretKey := "xxxxxxxxxxxxxxxx" + // init minio client + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + }) + if err != nil { + log.Fatalln(err) + } + // get local file + file, err := os.Open("./style.css") + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + fmt.Println(err) + return + } + // put object + uploadInfo, err := minioClient.PutObject(context.Background(), "sv3dd7u4-test", "style.css", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "text/css"}) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Successfully uploaded bytes: ", uploadInfo) +} +``` + +文件上传成功。 + +![](./images/12.png) + +### Java Client SDK + +详细文档参考:https://min.io/docs/minio/linux/developers/java/API.html + +例子:使用 Java Client SDK 上传 style1.css 文件到 sv3dd7u4-test 存储桶,将 Endpoint 设置为外网地址 External。如果服务部署在当前 +K8s 集群内,可以将 Endpoint 改为内网地址 Internal。 + +```xml + + + io.minio + minio + 8.5.9 + +``` + +```javascript +package org.example; + +import io.minio.MinioClient; +import io.minio.UploadObjectArgs; + +public class FileUploader { + public static void main(String[] args) throws Exception { + + MinioClient minioClient = + MinioClient.builder() + .endpoint("https://objectstorageapi.xxx.xxx.xxx") + .credentials("xxxxxxxx", "xxxxxxxxxxxxxxxx") + .build(); + + + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket("sv3dd7u4-test") + .object("style1.css") + .filename("src/main/java/org/example/style1.css") + .build()); + + System.out.println("Successfully uploaded bytes."); + } +} +``` + +文件上传成功。 + +![](./images/13.png) + +### 其他语言 SDK 略 + +详细文档参考:https://min.io/docs/minio/linux/developers/minio-drivers.html + +## 静态托管 + +创建一个权限为 publicRead/publicReadwrite 的存储桶,点击打开托管。 + +![](./images/14.png) + +点击地址跳转访问。 + +![](./images/15.png) + +点击自定义域名。 + +![](./images/16.png) + +跳转到应用管理的变更中,可以自定义域名。 + +![](./images/17.png) + diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/cost-center.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/cost-center.md new file mode 100644 index 000000000000..8e9e451d8959 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/cost-center.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 1 +keywords: [费用中心, Sealos, 账单概览, 资源分析, 计价标准, 开具发票, 成本管理, 云计算费用, 资源配额, 账单明细] +description: Sealos费用中心提供全面的成本管理功能,包括账单概览、资源分析、计价标准等模块,助您轻松掌控云计算支出,优化资源配置。 +--- + +# 费用中心 + +费用中心包含五个模块:账单概览、账单明细、资源分析、计价标准和开具发票。 + +账单概览和账单明细展示账户的总体收支情况和各应用的支出,资源分析显示各资源的支出占比,计价标准解释定价信息,而开具发票模块则支持为充值生成发票。 + +## 快速开始 + +打开 Sealos 桌面,点击费用中心。 + +![](./images/cost-center-1.png) + +### 首页 + +展示了所有可用区的成本趋势图和年度收支图。 + +![](./images/cost-center-2.png) + +### 账单概览和账单明细 + +支持快速查找账单,并支持通过设置时间范围、可用区、工作空间、应用类型和应用名称来筛选。 + +![](./images/cost-center-3.png) + +### 资源分析 + +展示了资源配额和成本分布图,并支持通过设置时间范围、可用区、工作空间、应用类型和应用名称来筛选。 + +![](./images/cost-center-4.png) + +### 计价标准 + +展示了资源定价,并支持通过设置可用区进行筛选。 + +![](./images/cost-center-5.png) + +### 开具发票 + +支持为充值订单申请开具发票。 + +![](./images/cost-center-6.png) \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-1.png new file mode 100644 index 000000000000..db317fd85113 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-2.png new file mode 100644 index 000000000000..a497e45e629c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-3.png new file mode 100644 index 000000000000..ef5cf7fe9811 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-4.png new file mode 100644 index 000000000000..e7c11f0ed0e5 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-5.png new file mode 100644 index 000000000000..cb165121c976 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-6.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-6.png new file mode 100644 index 000000000000..20755ecf55a1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/cost-center/images/cost-center-6.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/system-function.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/system-function.md new file mode 100644 index 000000000000..db067977bf20 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/system-function.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 0 +keywords: [Sealos系统功能, 费用中心, 工作空间, 工单系统, 团队协作, 账单管理, 发票开具, 问题反馈] +description: 探索Sealos系统功能:费用中心助您管理账单和发票,工作空间促进团队协作,工单系统便捷提交问题反馈。提升效率,优化协作体验。 +--- + +# 概述 + +Sealos 提供了费用中心、工作空间和工单等系统功能。 + +- 费用中心:查看账单、计价标准和开具发票; + +- 工作空间:通过不同的工作空间进行团队协作与隔离; + +- 工单:提交问题反馈。 \ No newline at end of file diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-1.png new file mode 100644 index 000000000000..95249c9951ff Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-2.png new file mode 100644 index 000000000000..6be4cf874207 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-3.png new file mode 100644 index 000000000000..225d5ccda85e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-4.png new file mode 100644 index 000000000000..6eb18871021c Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-5.png new file mode 100644 index 000000000000..23864bd434cc Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/images/order-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/work-order.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/work-order.md new file mode 100644 index 000000000000..d0ac480e2371 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/work-order/work-order.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 3 +keywords: [Sealos工单, 问题反馈, 客户支持, 在线帮助, 故障排除, 用户体验, 技术支持, 问题解决] +description: 了解如何使用Sealos工单系统快速提交问题、获取支持。从创建工单到与客服互动,轻松解决使用过程中遇到的各种问题,提升用户体验。 +--- + +# 工单 + +当您在使用 Sealos 的过程中,遇到任何问题,都可以使用工单提出您的疑问和反馈,我们会尽快为您处理。 + +## 快速开始 + +打开 Sealos 桌面,点击工单。 + +![](./images/order-1.png) + +点击新建工单。 + +![](./images/order-2.png) + +选择问题的类型,上传与问题相关的附件,描述问题的详细情况,最后点击提交工单。 + +![](./images/order-3.png) + +工单提交成后之后,会有🤖机器人客服回答问题,如果觉得机器人的回复无法解决问题,可以点击转人工客服。 + +![](./images/order-4.png) + +点击工单的详情,可以重新进入聊天界面,如果问题已经解决,可以点击关闭工单。 + +![](./images/order-5.png) + diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-1.png new file mode 100644 index 000000000000..b74972fbefa1 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-2.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-2.png new file mode 100644 index 000000000000..2363b30f01b3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-2.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-3.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-3.png new file mode 100644 index 000000000000..7f982cb070ca Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-3.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-4.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-4.png new file mode 100644 index 000000000000..acefec74b37e Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-4.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-5.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-5.png new file mode 100644 index 000000000000..2ca33dc27d11 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/images/workspace-5.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/workspace.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/workspace.md new file mode 100644 index 000000000000..ea3d8ddfb31e --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/system-function/workspace/workspace.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 2 +keywords: [工作空间, 多租户资源隔离, Sealos, 个人空间, 资源配额, 权限管理, 团队协作, 邀请用户, 角色权限] +description: 探索Sealos工作空间:多租户资源隔离机制,支持团队协作与资源分配。了解如何创建、切换工作空间,邀请用户并管理不同角色权限,提升协作效率。 +--- + +# 工作空间 + +工作空间是一种多租户资源隔离机制,它将一个集群划分为多个独立的工作空间,每个空间都有自己的资源配额和权限。这种机制支持团队协作与资源分配。 + +个人空间是工作空间的一种特殊形式,用户拥有一个与其用户名相同的个人空间。尽管个人空间的资源配额和权限与普通工作空间相同,但它无法被删除,也无法添加其他用户。 + +## 快速开始 + +### 创建工作空间 + +打开 Sealos 桌面,点击个人空间,点击管理工作空间。 + +![](./images/workspace-1.png) + +点击创建工作空间。 + +![](./images/workspace-2.png) + +### 切换工作空间 + +回到 Sealos 界面,点击个人空间,点击 hello 工作空间即可切换到 hello。 + +![](./images/workspace-3.png) + +### 邀请用户 + +在管理工作空间中,点击邀请成员,选择成员的身份权限,点击生成邀请链接,然后将链接发送给其他用户。 + +工作空间中的用户有三种不同的角色,角色和权限的说明如下: + +- 所有者(Owner):所有者拥有工作空间的所有权限,包括删除工作空间、查看/创建/修改工作空间中的所有资源、邀请用户以管理员/开发者身份加入工作空间等。 + +- 管理员(Manager):拥有工作空间的管理权限,包括查看/创建/修改工作空间中的所有资源、邀请用户以开发者身份加入工作空间等。 + +- 开发者(Developer):拥有工作空间的开发权限,包括查看工作空间中的资源状态。 + +![](./images/workspace-4.png) + +### 接受邀请 + +用户收到邀请链接,访问链接,点击接受邀请即可加入工作空间。 + +![](./images/workspace-5.png) diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal-1.png new file mode 100644 index 000000000000..f1fcc577c7f2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal.png new file mode 100644 index 000000000000..ac1a69448ca6 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/database-terminal.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal-1.png new file mode 100644 index 000000000000..a94f8582d11d Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal.png new file mode 100644 index 000000000000..44fad2374a07 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/nginx-terminal.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-1.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-1.png new file mode 100644 index 000000000000..78ce7eb3f242 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-1.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-pod.png b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-pod.png new file mode 100644 index 000000000000..bc47999cf4f3 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal-pod.png differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal.webp b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal.webp new file mode 100644 index 000000000000..151254f046c2 Binary files /dev/null and b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/images/terminal.webp differ diff --git a/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/terminal.md b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/terminal.md new file mode 100644 index 000000000000..7d5944fd0fc2 --- /dev/null +++ b/docs/archived/5.0/i18n/zh-Hans/user-guide/terminal/terminal.md @@ -0,0 +1,88 @@ +--- +关键词: [Sealos云操作系统, 终端应用, Kubernetes, 容器操作, 数据库连接, 资源管理, 故障排查, 云端开发] +描述: 探索Sealos云操作系统的强大终端应用:直接操作Kubernetes资源、连接应用容器、一键访问数据库。高效灵活的云端开发体验,助您轻松管理和调试云环境。 +--- + +# 终端 + +终端是 [Sealos](https://cloud.sealos.io) 云操作系统内置的应用,它是一款非常强大的应用,与单机操作系统的终端一样,允许用户与操作系统进行直接交互,用户可以通过键入命令来执行各种操作。 + +![](images/terminal.webp) + +终端的价值在于其高效,灵活和强大的特性。对于许多任务来说,使用终端比使用图形用户界面(GUI)更快更简洁。对于开发者而言,终端是必不可少的工具。使用终端,可以让用户深入理解和控制 Sealos 云操作系统。 + +**每一个用户的终端实际上都运行在 Kubernetes 的一个 Pod 中**,可以在终端中通过以下命令查看: + +![](images/terminal-pod.png) + +## 进入应用容器的终端 + +也可以直接通过终端 App 进入每个应用所在容器的终端。假设你在应用管理中部署了一个应用 Nginx,可以直接进入 Nginx +应用的详情页面,依次点击详情右侧的三个点,再点击「终端」,便进入了 Nginx 应用的终端。 + +![](images/nginx-terminal.png) + +![](images/nginx-terminal-1.png) + +## 终端一键直连数据库 + +还可以在终端中一键直连[数据库 App](../dbprovider/dbprovider.md) 中创建的数据库。 + +进入数据库详情页面,点击左侧的「一键连接」: + +![](images/database-terminal.png) + +就会直接跳转到终端 App 并连接数据库: + +![](images/database-terminal-1.png) + +## 终端直接操作 Kubernetes 资源 + +:::danger + +以下操作需要一定的 Kubernetes 和容器相关知识基础,否则不要轻易操作。 + +::: + +资深 Kubernetes 用户可以直接使用终端来操作 Kubernetes 资源,对于资深用户而言,终端的优势有很多: + ++ 可以批量操作和自动化 ++ 高度灵活性 ++ 可以进行深入的故障排查 + +以下是一些常见的终端中操作 Kubernetes 资源的示例: + +### 查看用户的 Pod 列表 + +```bash +kubectl get pods +``` + +### 创建和管理资源对象 + +```bash +# 使用YAML或JSON文件创建资源 +$ kubectl create -f + +# 使用YAML或JSON文件创建或更新资源 +$ kubectl apply -f + +# 删除指定类型和名称的资源 +$ kubectl delete + +# 查看指定 Deployment 的滚动更新状态 +$ kubectl rollout status deployment/ +``` + +### 故障排查 + +```bash +# 获取指定资源的详细信息,包括事件和状态 +$ kubectl describe + +# 获取指定 Pod 中指定容器的日志 +$ kubectl logs -c + +# 在指定 Pod 中打开一个终端,以便进行调试 +$ kubectl exec -it +``` \ No newline at end of file diff --git a/docs/archived/5.0/img/ai-applications/c++.png b/docs/archived/5.0/img/ai-applications/c++.png new file mode 100644 index 000000000000..f21cf1e1b2d5 Binary files /dev/null and b/docs/archived/5.0/img/ai-applications/c++.png differ diff --git a/docs/archived/5.0/img/ai-applications/python.png b/docs/archived/5.0/img/ai-applications/python.png new file mode 100644 index 000000000000..485d3f04b62f Binary files /dev/null and b/docs/archived/5.0/img/ai-applications/python.png differ diff --git a/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-2.png b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-2.png new file mode 100644 index 000000000000..4c1391260588 Binary files /dev/null and b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-2.png differ diff --git a/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-3.png b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-3.png new file mode 100644 index 000000000000..79208c3fc113 Binary files /dev/null and b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s-3.png differ diff --git a/docs/archived/5.0/img/ai-applications/sealos4-run-k8s.png b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s.png new file mode 100644 index 000000000000..4e2f8f750c25 Binary files /dev/null and b/docs/archived/5.0/img/ai-applications/sealos4-run-k8s.png differ diff --git a/docs/archived/5.0/img/app-launchpad-1-zh.jpg b/docs/archived/5.0/img/app-launchpad-1-zh.jpg new file mode 100644 index 000000000000..56c9a434ddcd Binary files /dev/null and b/docs/archived/5.0/img/app-launchpad-1-zh.jpg differ diff --git a/docs/archived/5.0/img/app-launchpad-1.jpg b/docs/archived/5.0/img/app-launchpad-1.jpg new file mode 100644 index 000000000000..c93e8368db96 Binary files /dev/null and b/docs/archived/5.0/img/app-launchpad-1.jpg differ diff --git a/docs/archived/5.0/img/app-launchpad-zh.png b/docs/archived/5.0/img/app-launchpad-zh.png new file mode 100644 index 000000000000..b9a1af059e16 Binary files /dev/null and b/docs/archived/5.0/img/app-launchpad-zh.png differ diff --git a/docs/archived/5.0/img/app-launchpad.png b/docs/archived/5.0/img/app-launchpad.png new file mode 100644 index 000000000000..e0afc48fffde Binary files /dev/null and b/docs/archived/5.0/img/app-launchpad.png differ diff --git a/docs/archived/5.0/img/bytebase.webp b/docs/archived/5.0/img/bytebase.webp new file mode 100644 index 000000000000..164aff68ff51 Binary files /dev/null and b/docs/archived/5.0/img/bytebase.webp differ diff --git a/docs/archived/5.0/img/database-zh.jpg b/docs/archived/5.0/img/database-zh.jpg new file mode 100644 index 000000000000..8fb5b1a33e23 Binary files /dev/null and b/docs/archived/5.0/img/database-zh.jpg differ diff --git a/docs/archived/5.0/img/database.jpg b/docs/archived/5.0/img/database.jpg new file mode 100644 index 000000000000..1394172749d4 Binary files /dev/null and b/docs/archived/5.0/img/database.jpg differ diff --git a/docs/archived/5.0/img/laf-zh.jpg b/docs/archived/5.0/img/laf-zh.jpg new file mode 100644 index 000000000000..457cda0050b0 Binary files /dev/null and b/docs/archived/5.0/img/laf-zh.jpg differ diff --git a/docs/archived/5.0/img/laf.jpg b/docs/archived/5.0/img/laf.jpg new file mode 100644 index 000000000000..85d7f1ccd0eb Binary files /dev/null and b/docs/archived/5.0/img/laf.jpg differ diff --git a/docs/archived/5.0/img/metering/metering-1.png b/docs/archived/5.0/img/metering/metering-1.png new file mode 100644 index 000000000000..4b9a6d1e58e0 Binary files /dev/null and b/docs/archived/5.0/img/metering/metering-1.png differ diff --git a/docs/archived/5.0/img/metering/metering-2.png b/docs/archived/5.0/img/metering/metering-2.png new file mode 100644 index 000000000000..380614649ac4 Binary files /dev/null and b/docs/archived/5.0/img/metering/metering-2.png differ diff --git a/docs/archived/5.0/img/metering/metering-3.png b/docs/archived/5.0/img/metering/metering-3.png new file mode 100644 index 000000000000..696eefab1fbb Binary files /dev/null and b/docs/archived/5.0/img/metering/metering-3.png differ diff --git a/docs/archived/5.0/img/metering/metering-4.png b/docs/archived/5.0/img/metering/metering-4.png new file mode 100644 index 000000000000..9acbce58235f Binary files /dev/null and b/docs/archived/5.0/img/metering/metering-4.png differ diff --git a/docs/archived/5.0/img/postgresql.webp b/docs/archived/5.0/img/postgresql.webp new file mode 100644 index 000000000000..5df4a51d09e7 Binary files /dev/null and b/docs/archived/5.0/img/postgresql.webp differ diff --git a/docs/archived/5.0/img/sealos-app-store-1.jpg b/docs/archived/5.0/img/sealos-app-store-1.jpg new file mode 100644 index 000000000000..9f4d148e4539 Binary files /dev/null and b/docs/archived/5.0/img/sealos-app-store-1.jpg differ diff --git a/docs/archived/5.0/img/sealos-app-store-2.jpg b/docs/archived/5.0/img/sealos-app-store-2.jpg new file mode 100644 index 000000000000..df5aeabc8bfb Binary files /dev/null and b/docs/archived/5.0/img/sealos-app-store-2.jpg differ diff --git a/docs/archived/5.0/img/sealos-desktop-zh.webp b/docs/archived/5.0/img/sealos-desktop-zh.webp new file mode 100644 index 000000000000..bead99528340 Binary files /dev/null and b/docs/archived/5.0/img/sealos-desktop-zh.webp differ diff --git a/docs/archived/5.0/img/sealos-desktop.webp b/docs/archived/5.0/img/sealos-desktop.webp new file mode 100644 index 000000000000..a35a02d3634c Binary files /dev/null and b/docs/archived/5.0/img/sealos-desktop.webp differ diff --git a/docs/archived/5.0/img/sealos-left-dark.png b/docs/archived/5.0/img/sealos-left-dark.png new file mode 100644 index 000000000000..4893d1b4bd23 Binary files /dev/null and b/docs/archived/5.0/img/sealos-left-dark.png differ diff --git a/docs/archived/5.0/img/sealos-left.png b/docs/archived/5.0/img/sealos-left.png new file mode 100644 index 000000000000..91e67b39b3f3 Binary files /dev/null and b/docs/archived/5.0/img/sealos-left.png differ diff --git a/docs/archived/5.0/img/sealos-qr-code-300.png b/docs/archived/5.0/img/sealos-qr-code-300.png new file mode 100644 index 000000000000..01c742215635 Binary files /dev/null and b/docs/archived/5.0/img/sealos-qr-code-300.png differ diff --git a/docs/archived/5.0/img/sealos-qr-code.jpg b/docs/archived/5.0/img/sealos-qr-code.jpg new file mode 100644 index 000000000000..a93bda5e93e6 Binary files /dev/null and b/docs/archived/5.0/img/sealos-qr-code.jpg differ diff --git a/docs/archived/5.0/img/templates-zh.jpg b/docs/archived/5.0/img/templates-zh.jpg new file mode 100644 index 000000000000..a4de6a217cf0 Binary files /dev/null and b/docs/archived/5.0/img/templates-zh.jpg differ diff --git a/docs/archived/5.0/img/templates.jpg b/docs/archived/5.0/img/templates.jpg new file mode 100644 index 000000000000..62bcc2efbd8b Binary files /dev/null and b/docs/archived/5.0/img/templates.jpg differ diff --git a/docs/archived/5.0/img/terminal-zh.webp b/docs/archived/5.0/img/terminal-zh.webp new file mode 100644 index 000000000000..8f3e086e9fed Binary files /dev/null and b/docs/archived/5.0/img/terminal-zh.webp differ diff --git a/docs/archived/5.0/img/terminal.webp b/docs/archived/5.0/img/terminal.webp new file mode 100644 index 000000000000..609543995962 Binary files /dev/null and b/docs/archived/5.0/img/terminal.webp differ diff --git a/docs/archived/5.0/sidebar-temp.json b/docs/archived/5.0/sidebar-temp.json new file mode 100644 index 000000000000..6af61bd4a5ae --- /dev/null +++ b/docs/archived/5.0/sidebar-temp.json @@ -0,0 +1,341 @@ +[ + "Intro", + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "quick-start" + } + ] + }, + { + "type": "category", + "label": "用户文档", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "系统功能", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/system-function" + } + ] + }, + { + "type": "category", + "label": "应用", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/application" + } + ] + }, + { + "type": "category", + "label": "数据库", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/database" + } + ] + }, + { + "type": "category", + "label": "对象存储", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/objectstorage" + } + ] + }, + { + "type": "category", + "label": "定时任务", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/cronjob" + } + ] + }, + { + "type": "category", + "label": "管理与配置", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/control" + } + ] + } + ] + }, + { + "type": "category", + "label": "开发者指南", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "系统设计", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/system-design" + } + ] + }, + { + "type": "category", + "label": "私有化部署", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "category", + "label": "部署Sealos", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/self-hosting/sealos" + } + ] + }, + { + "type": "category", + "label": "K8s 集群生命周期管理", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "category", + "label": "快速开始", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/self-hosting/lifecycle-management/quick-start" + } + ] + }, + { + "type": "category", + "label": "操作说明", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/self-hosting/lifecycle-management/operations" + } + ] + }, + { + "type": "category", + "label": "进阶指南", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/self-hosting/lifecycle-management/advanced-guide" + } + ] + }, + { + "type": "category", + "label": "参考", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/self-hosting/lifecycle-management/reference" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "category", + "label": "Developer Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "category", + "label": "Sealos", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/sealos" + } + ] + }, + { + "type": "category", + "label": "Lifecycle Management", + "link": { + "type": "doc", + "id": "self-hosting/lifecycle-management/lifecycle-management" + }, + "items": [ + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/quick-start" + } + ] + }, + { + "type": "category", + "label": "Operations", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/operations" + } + ] + }, + { + "type": "category", + "label": "Advanced Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/advanced-guide" + } + ] + }, + { + "type": "category", + "label": "Reference", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "self-hosting/lifecycle-management/reference" + } + ] + }, + "self-hosting/lifecycle-management/QA" + ] + } + ] + }, + { + "type": "category", + "label": "资源", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "隐私和服务协议", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": true, + "items": [ + { + "type": "autogenerated", + "dirName": "resources/privacy_and_service_policy" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/docs/archived/5.0/sidebar.json b/docs/archived/5.0/sidebar.json new file mode 100644 index 000000000000..994c44732f0a --- /dev/null +++ b/docs/archived/5.0/sidebar.json @@ -0,0 +1,214 @@ +{ + "docSidebar": [ + "Intro", + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "doc", + "id": "quick-start/quick-start" + }, + "items": [ + { + "type": "category", + "label": "Examples", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "quick-start/examples" + } + ] + } + ] + }, + { + "type": "category", + "label": "User Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "category", + "label": "System Function", + "link": { + "type": "doc", + "id": "user-guide/system-function/system-function" + }, + "items": [ + "user-guide/system-function/cost-center/cost-center", + "user-guide/system-function/workspace/workspace", + "user-guide/system-function/work-order/work-order" + ] + }, + "user-guide/ai-proxy/ai-proxy", + "user-guide/app-store/app-store", + { + "type": "category", + "label": "App Launchpad", + "link": { + "type": "doc", + "id": "user-guide/app-launchpad/app-launchpad" + }, + "items": [ + "user-guide/app-launchpad/use-app-launchpad", + "user-guide/app-launchpad/update-app", + "user-guide/app-launchpad/add-domain", + "user-guide/app-launchpad/expose-multi-ports", + "user-guide/app-launchpad/environment", + "user-guide/app-launchpad/configmap", + "user-guide/app-launchpad/autoscale", + "user-guide/app-launchpad/persistent-volume", + "user-guide/app-launchpad/set-cert" + ] + }, + { + "type": "category", + "label": "Database", + "link": { + "type": "doc", + "id": "user-guide/database/database" + }, + "items": [ + "user-guide/database/config-docs/config-docs" + ] + }, + "user-guide/objectstorage/objectstorage", + "user-guide/cronjob/cronjob", + "user-guide/terminal/terminal", + "user-guide/kubepanel/kubepanel", + { + "type": "category", + "label": "Devbox", + "link": { + "type": "doc", + "id": "user-guide/devbox/devbox" + }, + "items": [ + "user-guide/devbox/quick-start", + "user-guide/devbox/faq" + ] + } + ] + }, + { + "type": "category", + "label": "Developer Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "category", + "label": "Sealos", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/sealos" + } + ] + }, + { + "type": "category", + "label": "System Design", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/system-design" + } + ] + }, + { + "type": "category", + "label": "Kubernetes Lifecycle Management", + "link": { + "type": "doc", + "id": "developer-guide/lifecycle-management/lifecycle-management" + }, + "items": [ + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/lifecycle-management/quick-start" + } + ] + }, + { + "type": "category", + "label": "Operations", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/lifecycle-management/operations" + } + ] + }, + { + "type": "category", + "label": "Advanced Guide", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/lifecycle-management/advanced-guide" + } + ] + }, + { + "type": "category", + "label": "Reference", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "developer-guide/lifecycle-management/reference" + } + ] + }, + "developer-guide/lifecycle-management/QA" + ] + } + ] + }, + { + "type": "doc", + "id": "community", + "label": "Community" + }, + { + "type": "category", + "label": "Master Services Agreement", + "link": { + "type": "generated-index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "msa" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/archived/5.0/tmp.json b/docs/archived/5.0/tmp.json new file mode 100644 index 000000000000..32146ee90fe3 --- /dev/null +++ b/docs/archived/5.0/tmp.json @@ -0,0 +1,227 @@ +[ + "Intro", + { + "type": "category", + "label": "Quick Start", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "quick-start" + } + ] + }, + { + "type": "category", + "label": "User Guide", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "CronJob", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/cronjob" + } + ] + }, + { + "type": "category", + "label": "Object Storage", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/objectstorage" + } + ] + }, + { + "type": "category", + "label": "Application", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "App Launchpad", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/application/app-launchpad" + } + ] + }, + { + "type": "category", + "label": "App Store", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/application/app-store" + } + ] + } + ] + }, + { + "type": "category", + "label": "Control", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "Kubepanel", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/control/kubepanel" + } + ] + }, + { + "type": "category", + "label": "Terminal", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/control/terminal" + } + ] + } + ] + }, + { + "type": "category", + "label": "Database", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "database" + } + ] + }, + { + "type": "category", + "label": "System Function", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "category", + "label": "Introduce", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/system-function/introduce" + } + ] + }, + { + "type": "category", + "label": "Workspace", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/system-function/workspace" + } + ] + }, + { + "type": "category", + "label": "Cost Centor", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/system-function/cost-centor" + } + ] + }, + { + "type": "category", + "label": "Order", + "link": { + "type": "generated-index" + }, + "collapsible": true, + "collapsed": false, + "items": [ + { + "type": "autogenerated", + "dirName": "user-guide/system-function/order" + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/docs/design/system/user/user.md b/docs/design/system/user/user.md deleted file mode 100644 index a4f775fcbfe2..000000000000 --- a/docs/design/system/user/user.md +++ /dev/null @@ -1,69 +0,0 @@ -# 用户/用户组管理 - -作为一个云操作系统,用户管理是最基本的能力,sealos 用户管理吸取 linux 精髓,支持用户/用户组的一个多租户管理系统。 -同样 sealos 用户也可以对接 outh2 或者 ldap 这些外部系统, 不过需要超级管理员。 - -## 用户 CRD - -```shell script -apiVersion: sealos.io/v1 -data: - password: MWYyZDFlMmU2N2Rm -kind: User -metadata: - name: fanux - uid: cfee02d6-c137-11e5-8d73-42010af00002 -type: Opaque -``` - -## root 用户 - -超级管理员 root, 在集群安装时创建密码,拥有整个集群的管理权限。 - -```shell script -apiVersion: sealos.io/v1 -data: - password: MWYyZDFlMmU2N2Rm -kind: User -metadata: - name: root - uid: cfee02d6-c137-11e5-8d73-42010af00002 -type: Opaque -``` - -## 用户与用户组的关系 - -一个用户可以在多个组中, 一个组中也可有多个用户,是多对多关系 - -使用 UserGroupBinding 对象绑定二者 - -```shell script -kind: UserGroupBinding -apiVersion: sealos.io/v1 -metadata: - name: user-admin-test -subjects: -- kind: User - name: "fanux" # Name is case sensitive - apiGroup: sealos.io/v1 -roleRef: - kind: Group - name: admin # using admin role - apiGroup: sealos.io/v1 -``` - -## 用户与 namespace 关系 - -一个用户可以创建多个 namespace, 一个 namespace 也可以让多个用户或者用户组访问。 - -使用 UserNamespaceBinding 对象绑定二者 - -在用户被创建时会默认为该用户创建一个namespace, 如果用户不指定 ns 创建的所有 ns 都会在该 ns 中。 - -主意 namespace 的 quota, 以及 role 的处理. - -* 普通用户是否可以创建 namespace, 应该是可以的,但是需要对接计量系统,对其进行收费,sealos 完全按照公有云的需求去设计,企业不论大小,几遍私有云,也是多部门,场景更像公有云。 - -## 用户登录 - -登录时对用户的账户密码进行校验,管理员与普通用户拥有相同的用户界面,可以自由切换 namespace, 管理员可以切换任意的 namespace. \ No newline at end of file diff --git a/docs/images/arch.png b/docs/images/arch.png deleted file mode 100644 index 7d2667603396..000000000000 Binary files a/docs/images/arch.png and /dev/null differ diff --git a/docs/img/ai-applications/c++.png b/docs/img/ai-applications/c++.png new file mode 100644 index 000000000000..f21cf1e1b2d5 Binary files /dev/null and b/docs/img/ai-applications/c++.png differ diff --git a/docs/img/ai-applications/python.png b/docs/img/ai-applications/python.png new file mode 100644 index 000000000000..485d3f04b62f Binary files /dev/null and b/docs/img/ai-applications/python.png differ diff --git a/docs/img/ai-applications/sealos4-run-k8s-2.png b/docs/img/ai-applications/sealos4-run-k8s-2.png new file mode 100644 index 000000000000..4c1391260588 Binary files /dev/null and b/docs/img/ai-applications/sealos4-run-k8s-2.png differ diff --git a/docs/img/ai-applications/sealos4-run-k8s-3.png b/docs/img/ai-applications/sealos4-run-k8s-3.png new file mode 100644 index 000000000000..79208c3fc113 Binary files /dev/null and b/docs/img/ai-applications/sealos4-run-k8s-3.png differ diff --git a/docs/img/ai-applications/sealos4-run-k8s.png b/docs/img/ai-applications/sealos4-run-k8s.png new file mode 100644 index 000000000000..4e2f8f750c25 Binary files /dev/null and b/docs/img/ai-applications/sealos4-run-k8s.png differ diff --git a/docs/img/app-launchpad-1-zh.jpg b/docs/img/app-launchpad-1-zh.jpg new file mode 100644 index 000000000000..56c9a434ddcd Binary files /dev/null and b/docs/img/app-launchpad-1-zh.jpg differ diff --git a/docs/img/app-launchpad-1.jpg b/docs/img/app-launchpad-1.jpg new file mode 100644 index 000000000000..c93e8368db96 Binary files /dev/null and b/docs/img/app-launchpad-1.jpg differ diff --git a/docs/img/app-launchpad-zh.png b/docs/img/app-launchpad-zh.png new file mode 100644 index 000000000000..b9a1af059e16 Binary files /dev/null and b/docs/img/app-launchpad-zh.png differ diff --git a/docs/img/app-launchpad.png b/docs/img/app-launchpad.png new file mode 100644 index 000000000000..e0afc48fffde Binary files /dev/null and b/docs/img/app-launchpad.png differ diff --git a/docs/img/bytebase.webp b/docs/img/bytebase.webp new file mode 100644 index 000000000000..164aff68ff51 Binary files /dev/null and b/docs/img/bytebase.webp differ diff --git a/docs/img/database-zh.jpg b/docs/img/database-zh.jpg new file mode 100644 index 000000000000..8fb5b1a33e23 Binary files /dev/null and b/docs/img/database-zh.jpg differ diff --git a/docs/img/database.jpg b/docs/img/database.jpg new file mode 100644 index 000000000000..1394172749d4 Binary files /dev/null and b/docs/img/database.jpg differ diff --git a/docs/img/laf-zh.jpg b/docs/img/laf-zh.jpg new file mode 100644 index 000000000000..457cda0050b0 Binary files /dev/null and b/docs/img/laf-zh.jpg differ diff --git a/docs/img/laf.jpg b/docs/img/laf.jpg new file mode 100644 index 000000000000..85d7f1ccd0eb Binary files /dev/null and b/docs/img/laf.jpg differ diff --git a/docs/img/metering/metering-1.png b/docs/img/metering/metering-1.png new file mode 100644 index 000000000000..4b9a6d1e58e0 Binary files /dev/null and b/docs/img/metering/metering-1.png differ diff --git a/docs/img/metering/metering-2.png b/docs/img/metering/metering-2.png new file mode 100644 index 000000000000..380614649ac4 Binary files /dev/null and b/docs/img/metering/metering-2.png differ diff --git a/docs/img/metering/metering-3.png b/docs/img/metering/metering-3.png new file mode 100644 index 000000000000..696eefab1fbb Binary files /dev/null and b/docs/img/metering/metering-3.png differ diff --git a/docs/img/metering/metering-4.png b/docs/img/metering/metering-4.png new file mode 100644 index 000000000000..9acbce58235f Binary files /dev/null and b/docs/img/metering/metering-4.png differ diff --git a/docs/img/postgresql.webp b/docs/img/postgresql.webp new file mode 100644 index 000000000000..5df4a51d09e7 Binary files /dev/null and b/docs/img/postgresql.webp differ diff --git a/docs/img/sealos-app-store-1.jpg b/docs/img/sealos-app-store-1.jpg new file mode 100644 index 000000000000..9f4d148e4539 Binary files /dev/null and b/docs/img/sealos-app-store-1.jpg differ diff --git a/docs/img/sealos-app-store-2.jpg b/docs/img/sealos-app-store-2.jpg new file mode 100644 index 000000000000..df5aeabc8bfb Binary files /dev/null and b/docs/img/sealos-app-store-2.jpg differ diff --git a/docs/img/sealos-desktop-zh.webp b/docs/img/sealos-desktop-zh.webp new file mode 100644 index 000000000000..bead99528340 Binary files /dev/null and b/docs/img/sealos-desktop-zh.webp differ diff --git a/docs/img/sealos-desktop.webp b/docs/img/sealos-desktop.webp new file mode 100644 index 000000000000..a35a02d3634c Binary files /dev/null and b/docs/img/sealos-desktop.webp differ diff --git a/docs/img/sealos-left-dark.png b/docs/img/sealos-left-dark.png new file mode 100644 index 000000000000..4893d1b4bd23 Binary files /dev/null and b/docs/img/sealos-left-dark.png differ diff --git a/docs/img/sealos-left.png b/docs/img/sealos-left.png new file mode 100644 index 000000000000..91e67b39b3f3 Binary files /dev/null and b/docs/img/sealos-left.png differ diff --git a/docs/img/sealos-qr-code-300.png b/docs/img/sealos-qr-code-300.png new file mode 100644 index 000000000000..01c742215635 Binary files /dev/null and b/docs/img/sealos-qr-code-300.png differ diff --git a/docs/img/sealos-qr-code.jpg b/docs/img/sealos-qr-code.jpg new file mode 100644 index 000000000000..a93bda5e93e6 Binary files /dev/null and b/docs/img/sealos-qr-code.jpg differ diff --git a/docs/img/templates-zh.jpg b/docs/img/templates-zh.jpg new file mode 100644 index 000000000000..a4de6a217cf0 Binary files /dev/null and b/docs/img/templates-zh.jpg differ diff --git a/docs/img/templates.jpg b/docs/img/templates.jpg new file mode 100644 index 000000000000..62bcc2efbd8b Binary files /dev/null and b/docs/img/templates.jpg differ diff --git a/docs/img/terminal-zh.webp b/docs/img/terminal-zh.webp new file mode 100644 index 000000000000..8f3e086e9fed Binary files /dev/null and b/docs/img/terminal-zh.webp differ diff --git a/docs/img/terminal.webp b/docs/img/terminal.webp new file mode 100644 index 000000000000..609543995962 Binary files /dev/null and b/docs/img/terminal.webp differ diff --git a/fork/golang/expansion/expand.go b/fork/golang/expansion/expand.go deleted file mode 100644 index 6bf0ea8ce09f..000000000000 --- a/fork/golang/expansion/expand.go +++ /dev/null @@ -1,102 +0,0 @@ -package expansion - -import ( - "bytes" -) - -const ( - operator = '$' - referenceOpener = '(' - referenceCloser = ')' -) - -// syntaxWrap returns the input string wrapped by the expansion syntax. -func syntaxWrap(input string) string { - return string(operator) + string(referenceOpener) + input + string(referenceCloser) -} - -// MappingFuncFor returns a mapping function for use with Expand that -// implements the expansion semantics defined in the expansion spec; it -// returns the input string wrapped in the expansion syntax if no mapping -// for the input is found. -func MappingFuncFor(context ...map[string]string) func(string) string { - return func(input string) string { - for _, vars := range context { - val, ok := vars[input] - if ok { - return val - } - } - - return syntaxWrap(input) - } -} - -// Expand replaces variable references in the input string according to -// the expansion spec using the given mapping function to resolve the -// values of variables. -func Expand(input string, mapping func(string) string) string { - var buf bytes.Buffer - checkpoint := 0 - for cursor := 0; cursor < len(input); cursor++ { - if input[cursor] == operator && cursor+1 < len(input) { - // Copy the portion of the input string since the last - // checkpoint into the buffer - buf.WriteString(input[checkpoint:cursor]) - - // Attempt to read the variable name as defined by the - // syntax from the input string - read, isVar, advance := tryReadVariableName(input[cursor+1:]) - - if isVar { - // We were able to read a variable name correctly; - // apply the mapping to the variable name and copy the - // bytes into the buffer - buf.WriteString(mapping(read)) - } else { - // Not a variable name; copy the read bytes into the buffer - buf.WriteString(read) - } - - // Advance the cursor in the input string to account for - // bytes consumed to read the variable name expression - cursor += advance - - // Advance the checkpoint in the input string - checkpoint = cursor + 1 - } - } - - // Return the buffer and any remaining unwritten bytes in the - // input string. - return buf.String() + input[checkpoint:] -} - -// tryReadVariableName attempts to read a variable name from the input -// string and returns the content read from the input, whether that content -// represents a variable name to perform mapping on, and the number of bytes -// consumed in the input string. -// -// The input string is assumed not to contain the initial operator. -func tryReadVariableName(input string) (string, bool, int) { - switch input[0] { - case operator: - // Escaped operator; return it. - return input[0:1], false, 1 - case referenceOpener: - // Scan to expression closer - for i := 1; i < len(input); i++ { - if input[i] == referenceCloser { - return input[1:i], true, i + 1 - } - } - - // Incomplete reference; return it. - return string(operator) + string(referenceOpener), false, 1 - default: - // Not the beginning of an expression, ie, an operator - // that doesn't begin an expression. Return the operator - // and the first rune in the string. - return (string(operator) + string(input[0])), false, 1 - } -} diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 000000000000..b0819562b060 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,36 @@ +**/Dockerfile +**/.dockerignore +**/npm-debug.log +**/README.md +**/.next/ +**/.git/ +**/.env +**/.env.* +**/config.yaml +**/.vscode/ +**/.yalc/ +**/yalc.lock +**/.gitignore +**/providers/template/templates/ +**/providers/template/templates.json + +.git +**/.git + +node_modules +**/node_modules + +.next +**/.next + +dist +**/dist + +coverage +**/coverage + +*.log +**/*.log + +.DS_Store +**/.DS_Store diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000000..2f704e710aa2 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,14 @@ +node_modules/ +.next +.env*.local +next-env.d.ts +!.vscode/ +.vscode/tasks.json +.claude +.agents +skills-lock.json +.omx +AGENTS.md +.sisyphus +**/test-results/ +.codegraph/ diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit new file mode 100755 index 000000000000..2312dc587f61 --- /dev/null +++ b/frontend/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/frontend/.lintstagedrc b/frontend/.lintstagedrc new file mode 100644 index 000000000000..a5c31877e668 --- /dev/null +++ b/frontend/.lintstagedrc @@ -0,0 +1,4 @@ +{ + "**/*.{js,jsx,ts,tsx}": ["bash frontend/scripts/prettier-staged.sh"], + "**/*.{json,css,scss,md}": ["bash frontend/scripts/prettier-staged.sh"] +} diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 000000000000..0fa85002011b --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +git-branch-lockfiles=true \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000000..92bc26a4217b --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +20.20.2 \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000000..83c687439574 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,5 @@ +**/dist +**/.vscode +**/.DS_Store +**/node_modules +**/.next \ No newline at end of file diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js new file mode 100644 index 000000000000..b48f2c3caefb --- /dev/null +++ b/frontend/.prettierrc.js @@ -0,0 +1,20 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + quoteProps: 'as-needed', + jsxSingleQuote: false, + trailingComma: 'none', + bracketSpacing: true, + jsxBracketSameLine: false, + arrowParens: 'always', + rangeStart: 0, + rangeEnd: Infinity, + requirePragma: false, + insertPragma: false, + proseWrap: 'preserve', + htmlWhitespaceSensitivity: 'css', + endOfLine: 'lf' +} diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 000000000000..9dd916951bd7 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,31 @@ +{ + "i18n-ally.localesPaths": [ + "providers/**/public/locales", + "desktop/public/locales" + ], + "i18n-ally.enabledParsers": [ + "json", + "yaml", + "js", + "ts" + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.keepFulfilled": false, + "i18n-ally.sourceLanguage": "zh", + "i18n-ally.displayLanguage": "zh", + "i18n-ally.namespace": true, + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key", + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.formatOnSave": false, + "editor.formatOnPaste": false + }, + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000000..9c061e129ab6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,108 @@ +# Copyright © 2022 sealos. +# +# 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. +FROM node:20.20.2-alpine AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +WORKDIR /app + +# Install dependencies only when needed +FROM base AS deps + +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat && corepack enable && corepack prepare pnpm@8.9.0 --activate + + +# Install dependencies based on the preferred package manager root workspace +COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + [ -f pnpm-lock.yaml ] && pnpm fetch || \ + (echo "Lockfile not found." && exit 1) +COPY ./tsconfig.json ./tsconfig.json +COPY ./tsconfig.deps.json ./tsconfig.deps.json +COPY ./tsconfig.base.json ./tsconfig.base.json +COPY ./tsconfig.web.json ./tsconfig.web.json +COPY ./packages ./packages + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ +pnpm -r --offline --filter=./packages/* install \ +&& pnpm -r --filter=./packages/* run build + +FROM deps AS builder + +# COPY --from=deps /app/node_modules ./node_modules +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED=1 + +# COPY --from=deps /app/packages ./packages + +ARG name +ARG path + +COPY ${path} ${path} + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --frozen-lockfile --filter=$name install && \ + pnpm --filter=$name run build + +RUN if [ "$name" = "desktop" ]; then \ + mkdir -p desktop/.next/standalone/desktop/prisma/global && \ + mkdir -p desktop/.next/standalone/desktop/prisma/region && \ + cp -r ./desktop/prisma/global/migrations desktop/.next/standalone/desktop/prisma/global/migrations; \ + cp -r ./desktop/prisma/region/migrations desktop/.next/standalone/desktop/prisma/region/migrations; \ + if [ -d "./desktop/prisma/providers" ]; then \ + mkdir -p desktop/.next/standalone/desktop/prisma/providers && \ + cp -r ./desktop/prisma/providers/* desktop/.next/standalone/desktop/prisma/providers/; \ + fi; \ +fi +# Production image, copy all the files and run next +FROM base AS runner + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# OpenSSL: https://github.com/prisma/prisma/discussions/19341#discussioncomment-11521367 +RUN apk add curl openssl ca-certificates dumb-init --no-cache \ + && update-ca-certificates + +ARG name +ARG path + +# Install Git and OpenSSH client if $name is equal to template +RUN if [ "$name" = "template" ]; then \ + apk add --no-cache git openssh-client; \ +fi +RUN if [ "$name" = "desktop" ]; then \ + npm install -g prisma@5.10.2; \ +fi +USER nextjs + +# You only need to copy next.config.js if you are NOT using the default configuration +COPY --from=builder /app/$path/next.config.js ./$path/next.config.js +COPY --from=builder /app/$path/public ./$path/public +COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/$path/.next/static ./$path/.next/static + +EXPOSE 3000 + +ENV PORT=3000 + +ENV launchpath=./${path}/server.js + +ENTRYPOINT ["dumb-init", "sh", "-c", "node ${launchpath}"] diff --git a/frontend/Makefile b/frontend/Makefile new file mode 100644 index 000000000000..c5b48eea08d1 --- /dev/null +++ b/frontend/Makefile @@ -0,0 +1,67 @@ +DOCKER_USERNAME := $(DOCKER_USERNAME) +IMAGE_TAG := $(IMAGE_TAG) + +ifneq ($(strip $(DOCKER_USERNAME)),) + imageOwner := $(DOCKER_USERNAME)/ +else + imageOwner := +endif + +ifneq ($(strip $(IMAGE_TAG)),) + imageTag := $(IMAGE_TAG) +else + imageTag := dev +endif +# build image +buildTargets := \ + desktop \ + providers/costcenter \ + providers/dbprovider \ + providers/applaunchpad \ + providers/imagehub \ + providers/template \ + providers/license + +buildTargets-all := $(addprefix image-build-,$(buildTargets)) +pushTargets-all := $(addprefix image-push-,$(buildTargets)) +$(foreach target,$(buildTargets),$(eval .PHONY: image-build-$($(target)))) +$(foreach target,$(buildTargets),$(eval .PHONY: image-push-$($(target)))) +$(foreach target,$(buildTargets),$(eval .PHONY: dev-$($(target)))) +$(foreach target,$(buildTargets),$(eval .PHONY: build-$($(target)))) +.PHONY: all image-prebuild push-images + +all: image-prebuild $(buildTargets-all) + +push-images: image-prebuild $(pushTargets-all) +fetch-deps: pnpm-lock.yaml + pnpm fetch +build-packages: fetch-deps + pnpm -r --offline --filter=./packages/* install + pnpm -r --offline --filter=./packages/* build +build-providers/%: build-packages + pnpm -r --offline --filter=./providers/$* install + pnpm -r --offline --filter=./providers/$* build +build-%: build-packages + pnpm -r --offline --filter=$* install + pnpm -r --offline --filter=$* build +dev-providers/%: build-packages + pnpm -r --offline --filter=./providers/$* install + pnpm -r --offline --filter=./providers/$* dev +dev-%: build-packages + pnpm -r --offline --filter=$* install + pnpm -r --offline --filter=$* dev + +# prebuild-image-for -j +image-prebuild: pnpm-lock.yaml + docker build --platform=linux/amd64 --target deps . -t $(imageOwner)sealos-deps:dev + +image-build-providers/%: image-prebuild + docker build --platform=linux/amd64 -t $(imageOwner)sealos-$*:$(imageTag) --build-arg path=providers/$* --build-arg name=$* . +image-build-%: image-prebuild + docker build --platform=linux/amd64 -t $(imageOwner)sealos-$*:$(imageTag) --build-arg path=$* --build-arg name=$* . + +image-push-providers/%: image-build-providers/% + docker push $(imageOwner)sealos-$*:$(imageTag) +image-push-%: image-build-% + docker push $(imageOwner)sealos-$*:$(imageTag) +# Default target to run all builds. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000000..36d66253cb3c --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,108 @@ +# sealos frontend + +## Development setup + +- It's recommended using this directory (`/frontend`) as root for your workspace. +- We use `pnpm` for package management, you should have it installed before getting started. ([Tutorial](https://pnpm.io/installation)) +- Desktop app is under `desktop/` directory, sub apps are under `providers//`. + +### Install dependencies + +The post install script will run automatically, and local packages should be built. + +```sh +pnpm install +``` + +### Generate Prisma clients for desktop app + +```sh +cd desktop +pnpm gen:global && pnpm gen:region +``` + +### Prepare environment variables + +`.env.template` files are located at the workspace root directory for each app. Duplicate and rename them to `.env`, then fill the required configurations. + +`NEXT_PUBLIC_MOCK_USER` is the kubeconfig mocked for development, you can copy it from your Sealos desktop. (Only one line allowed, you should escape the line breaks) + +### Run apps for development + +You can either run apps in workspace root or in specific app's directory. + +```sh +# Run desktop app +pnpm dev-desktop + +# Run specific provider app +pnpm dev-app # applaunchpad +pnpm dev-db # dbprovider +pnpm dev-cost # costcenter +pnpm dev-template # template + +# or run dev script in app's package directory +cd desktop +pnpm dev +``` + +## how to add packages + +```bash +cd frontend +# add remote +pnpm --filter= -r add +# such as: +# pnpm --filter=providers/costcenter -r add lodash +# pnpm --filter=desktop -r add lodash +# add local +pnpm --filter= -r --offline add +# such as: +# pnpm --filter=providers/costcenter -r --offline add sealos-desktop-sdk +# pnpm --filter=desktop -r --offline add sealos-desktop-sdk +``` + +## how to build + +```bash +# sealos/frontend +make image-build- DOCKER_USERNAME= IMAGE_TAG= +# such as: +# make image-build-providers/costcenter +# make image-build-desktop +# make image-build-desktop IMAGE_TAG=test DOCKER_USERNAME=sealos +# multi jobs build +make -j +``` + +- you can use `make all DOCKER_USERNAME=` to build all apps for your account. +- you can user `make all IMAGE_TAG=` to build all apps and customize tag (default:dev) + +## how to publish image + +```bash +# sealos/frontend +make image-push- DOCKER_USERNAME= IMAGE_TAG= +# such as: +# make image-push-providers/costcenter +# make image-push-desktop + +# publish all +make push-images DOCKER_USERNAME= IMAGE_TAG= +``` + +## new App + +Refer to other apps to add some configuration. + +1. .github/workflows/frontends.yml +2. frontend/providers/app/deploy/manifests/appcr.yaml.tmpl +3. frontend/providers/app/deploy/manifests/deploy.yaml +4. frontend/providers/app/deploy/manifests/ingress.yaml.tmpl +5. makefile + +## multiple namespaces + +In order to support multiple namespaces, the method of obtaining 'namespace' in the +backend should be replaced with the method of obtaining it from 'kubeconfig' instead +of adding 'ns-' to 'user' for generation purpose. diff --git a/frontend/desktop/.dockerignore b/frontend/desktop/.dockerignore new file mode 100644 index 000000000000..acdf44118d10 --- /dev/null +++ b/frontend/desktop/.dockerignore @@ -0,0 +1,14 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git +.env.local +config.yaml +.yalc/ +yalc.lock + +prisma/global/generated/ +prisma/region/generated/ \ No newline at end of file diff --git a/frontend/desktop/.env.template b/frontend/desktop/.env.template new file mode 100644 index 000000000000..d561908ee798 --- /dev/null +++ b/frontend/desktop/.env.template @@ -0,0 +1,28 @@ +PUBLIC_URL=. +NEXT_PUBLIC_SERVICE=/service/ +SEALOS_CLOUD_DOMAIN="cloud.sealos.io" + +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= +# WECHAT_CLIENT_ID= +# WECHAT_CLIENT_SECRET= +# KUBECONFIG= +# MONGODB_URI= +# JWT_SECRET= +# ALI_ACCESS_KEY_ID= +# ALI_ACCESS_KEY_SECRET= +# ALI_TEMPLATE_CODE= +# ALI_SIGN_NAME= +# PRIVATE_PROTOCOL= +# SERVICE_PROTOCOL= +# CALLBACK_URL= +# PASSWORD_SALT= +# WECHAT_ENABLED= +# GITHUB_ENABLED= +# PASSWORD_ENABLED= +# SMS_ENABLED= + +# costcenter +STRIPE_ENABLED= +STRIPE_PUB= +SEALOS_CLOUD_DOMAIN= \ No newline at end of file diff --git a/frontend/desktop/.eslintrc.json b/frontend/desktop/.eslintrc.json new file mode 100644 index 000000000000..0e81f9b97c83 --- /dev/null +++ b/frontend/desktop/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} \ No newline at end of file diff --git a/frontend/desktop/.gitignore b/frontend/desktop/.gitignore new file mode 100644 index 000000000000..329a68a1d110 --- /dev/null +++ b/frontend/desktop/.gitignore @@ -0,0 +1,47 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/prisma/**/generated +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local +data/*.local +# vercel +.vercel + +# typescript +*.tsbuildinfo + +tests/* +!tests/.gitkeep + +.yalc/ +yalc.lock + +config.yaml +.env +.env.local +data/config.local.yaml +#/prisma/region/generated/ diff --git a/frontend/desktop/.prettierignore b/frontend/desktop/.prettierignore new file mode 100644 index 000000000000..ae2744a993c1 --- /dev/null +++ b/frontend/desktop/.prettierignore @@ -0,0 +1,18 @@ +*swp +*rpmnew +*swo +.idea +oss-config +ossutil64 +main.exe +dist +.vscode +scripts/release/Note.md +.ossutil_checkpoint +bin +.run +tools +coverage.out +tmp +**/.DS_Store +node_modules diff --git a/frontend/desktop/.prettierrc.js b/frontend/desktop/.prettierrc.js new file mode 100644 index 000000000000..3e3db788f64a --- /dev/null +++ b/frontend/desktop/.prettierrc.js @@ -0,0 +1,28 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + quoteProps: 'as-needed', + jsxSingleQuote: false, + trailingComma: 'none', + bracketSpacing: true, + jsxBracketSameLine: false, + arrowParens: 'always', + rangeStart: 0, + rangeEnd: Infinity, + requirePragma: false, + insertPragma: false, + proseWrap: 'preserve', + htmlWhitespaceSensitivity: 'css', + endOfLine: 'lf', + overrides: [ + { + files: 'config.local.yaml', + options: { + parser: 'yaml' + } + } + ] +} diff --git a/frontend/desktop/.vscode/settings.json b/frontend/desktop/.vscode/settings.json new file mode 100644 index 000000000000..60c94d412366 --- /dev/null +++ b/frontend/desktop/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "i18n-ally.localesPaths": [ + "public/locales" + ], + "i18n-ally.enabledParsers": [ + "json", + "yaml", + "js", + "ts" + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.keepFulfilled": false, + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "en", + "i18n-ally.namespace": true, + "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", + "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key", + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} \ No newline at end of file diff --git a/frontend/desktop/Makefile b/frontend/desktop/Makefile new file mode 100644 index 000000000000..803dae84c636 --- /dev/null +++ b/frontend/desktop/Makefile @@ -0,0 +1,29 @@ +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +.PHONY: build +build: ## Build desktop-frontend binary. + pnpm run build + +.PHONY: run +run: ## Run a dev service from host. + pnpm run start diff --git a/frontend/desktop/README.md b/frontend/desktop/README.md new file mode 100644 index 000000000000..f4dfd7c62cc7 --- /dev/null +++ b/frontend/desktop/README.md @@ -0,0 +1,272 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +```bash +pnpm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +### 目录说明 + +```c +. +├── Makefile +├── README.md +├── deploy +│ └── manifests +│ └── frontend.yaml +├── next-env.d.ts +├── next.config.js +├── package.json +├── pnpm-lock.yaml +├── public +│ ├── favicon.ico +│ ├── iconfont +│ ├── icons +│ ├── images +│ └── locales +├── src +│ ├── components +│ │ ├── account +│ │ ├── app_window +│ │ ├── background +│ │ ├── desktop_content +│ │ ├── floating_button +│ │ ├── LangSelect +│ │ ├── iconfont +│ │ ├── layout +│ │ ├── more_button +│ │ ├── notification +│ │ └── user_menu +│ ├── hooks +│ │ ├── useCopyData.ts +│ │ ├── useCustomToast.ts +│ │ ├── useRecharge.tsx +│ │ └── useScreen.ts +│ ├── pages +│ │ ├── _app.tsx +│ │ ├── _document.tsx +│ │ ├── api +│ │ │ ├── account +│ │ │ ├── auth +│ │ │ ├── desktop +│ │ │ ├── notification +│ │ │ └── price +│ │ ├── index.tsx +│ │ └── login +│ ├── services +│ │ ├── backend +│ │ │ ├── auth.ts +│ │ │ ├── oauth.ts +│ │ │ ├── kubernetes +│ │ │ │ ├── admin.ts +│ │ │ │ └── user.ts +│ │ │ └── response.ts +│ │ ├── enable.ts +│ │ └── request.ts +│ ├── stores +│ │ ├── app.ts +│ │ ├── desktop.ts +│ │ └── session.ts +│ ├── styles +│ │ ├── chakraTheme.ts +│ │ └── globals.scss +│ ├── types +│ │ ├── api.ts +│ │ ├── app.ts +│ │ ├── crd.ts +│ │ ├── index.ts +│ │ ├── payment.ts +│ │ ├── user.ts +│ │ └── session.ts +│ └── utils +│ ├── crypto.ts +│ ├── ProcessManager.ts +│ ├── delay.ts +│ ├── downloadFIle.ts +│ ├── format.ts +│ ├── i18n.ts +│ └── tools.ts +└── tsconfig.json +``` + +### 项目依赖的库 + +```json +{ + "name": "desktop", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@chakra-ui/react": "^2.5.5", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@kubernetes/client-node": "^0.18.1", + "@tanstack/react-query": "^4.29.3", + "axios": "^1.3.5", + "clsx": "^1.2.1", + "dayjs": "^1.11.7", + "eslint": "8.38.0", + "eslint-config-next": "13.3.0", + "framer-motion": "^10.12.3", + "i18next": "^22.4.14", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.2.0", + "immer": "^10.0.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "next": "13.3.0", + "next-pwa": "^5.6.0", + "nprogress": "^0.2.0", + "qrcode.react": "^3.1.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-draggable": "^4.4.5", + "react-i18next": "^12.2.0", + "sass": "^1.62.0", + "sealos-desktop-sdk": "^0.1.12", + "typescript": "5.0.4", + "zustand": "^4.3.7" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.14.194", + "@types/node": "18.15.11", + "@types/nprogress": "^0.2.0", + "@types/react": "18.0.37", + "@types/react-dom": "18.0.11" + } +} +``` + +### 代码阅读说明 + +1. src/pages/\_app.tsx // chakra-ui react-query +2. src/pages/index.ts +3. src/layout/index.tsx +4. src/components/desktop_content.tsx + +### 安装 App 数据流 + +- `src/pages/api/desktop/getInstalledApps.ts` 负责组装桌面可见 App 列表。共享 App 来自 `app-system` namespace 的 `app.sealos.io/v1 App`,workspace App 来自当前 workspace namespace。 +- 共享 App 保持 `displayType` 分组顺序 `normal -> more -> hidden`,组内按 `spec.position` 从小到大排序;没有 `position` 的旧数据按 `0` 处理,再按创建时间倒序和 key 稳定排序。 +- `spec.position` 由管理端共享 App 图标管理写入,集群 App CRD 需要先包含 `spec.position` schema;workspace App 管理不写这个字段。 + +### 测试环境 + +1. 需要设置环境变量`NODE_ENV=test` 或者 `$env:NODE_ENV="test"` +2. 先启动`pnpm dev`, 再启动`pnpm test:w` + +### 其它 + +1. 获取登录凭证: 由于 login 页面不是在 desktop 项目里,所以需要从线上 sealos 获取登录凭证到本地开发: 。复制 storage 里的 session 到 localhost 环境实现 mock 登录。 + +2. Chakra ui + +3. TanStack Query 用法: + +4. 使用 vscode 进行单步调试 + + 创建`.vscode/launch.json`文件,内容如下: + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run dev", + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "openExternally" + } + } + ] + } + ``` + + 然后即可点击 vscode 的调试按钮进行调试,同时增加断点 + +5. 环境变量说明 + +- 登录功能的开关, 部署时要用`true`配置想要使用的登录方式。 + + ``` + WECHAT_ENABLED=true + GITHUB_ENABLED=true + PASSWORD_ENABLED=true + SMS_ENABLED=true + RECHAGRE_ENABLED=true + ``` + +- 每个登陆要配置的变量 + + - wechat + + ``` + WECHAT_CLIENT_ID= + WECHAT_CLIENT_SECRET= + WECHAT_ENABLED="true" + ``` + + - github + + ``` + GITHUB_CLIENT_ID= + GITHUB_CLIENT_SECRET= + GITHUB_ENABLED="true" + ``` + + - password + + ``` + PASSWORD_SALT= + PASSWROD_ENABLED="true" + ``` + + - sms + + ``` + ALI_ACCESS_KEY_ID= + ALI_ACCESS_KEY_SECRET= + ALI_SIGN_NAME= + ALI_TEMPLATE_CODE= + SMS_ENABLED="true" + ``` + + - google + + ``` + GOOGLE_ENABLED="true" + GOOGLE_CLIENT_ID= + GOOGLE_CLIENT_SECRET= + ``` + + - support standard oauth2 + + ``` + OAUTH2_CLIENT_ID= + OAUTH2_CLIENT_SECRET= + OAUTH2_AUTH_URL= + OAUTH2_TOKEN_URL= + OAUTH2_USERINFO_URL= + ``` + + - number of teams and number of people in each team + + ``` + // default is '50' + TEAM_LIMIT="50" + TEAM_INVITE_LIMIT="50" + ``` diff --git a/frontend/desktop/data/config.json b/frontend/desktop/data/config.json new file mode 100644 index 000000000000..caf421288e49 --- /dev/null +++ b/frontend/desktop/data/config.json @@ -0,0 +1,3 @@ +{ + "scripts": [] +} diff --git a/frontend/desktop/data/config.yaml b/frontend/desktop/data/config.yaml new file mode 100644 index 000000000000..a849402e30c7 --- /dev/null +++ b/frontend/desktop/data/config.yaml @@ -0,0 +1,178 @@ +cloud: + domain: "127.0.0.1.nip.io" + port: "" + regionUID: "thisiaregionuid" + certSecretName: "wildcard-cert" + proxyDomain: "127.0.0.1.nip.io" + allowedOrigins: + - "https://applaunchpad.127.0.0.1.nip.io" + - "https://dbprovider.127.0.0.1.nip.io" + - "https://costcenter.127.0.0.1.nip.io" + - "https://cronjob.127.0.0.1.nip.io" + - "https://objectstorage.127.0.0.1.nip.io" + - "https://template.127.0.0.1.nip.io" + - "https://terminal.127.0.0.1.nip.io" +common: + enterpriseRealNameAuthEnabled: false + realNameAuthEnabled: false + realNameReward: 0 + realNameCallbackUrl: "https://cloud.example.org/api/account/faceIdRealNameAuthCallback" + guideEnabled: false + apiEnabled: false + rechargeEnabled: false + cfSiteKey: "" + licenseCheckEnabled: false + templateUrl: "https://template.127.0.0.1.nip.io" + objectstorageUrl: "https://objectstorage.127.0.0.1.nip.io" + applaunchpadUrl: "https://applaunchpad.127.0.0.1.nip.io" + dbproviderUrl: "https://dbprovider.127.0.0.1.nip.io" + trackingEnabled: false +database: + mongodbURI: "thisismongodburi" + globalCockroachdbURI: "thisisglobalcockroachdburi" + regionalCockroachdbURI: "thisisregionalcockroachdburi" +desktop: + layout: + version: "en" + title: "Sealos Cloud" + logo: "/logo.svg" + backgroundImage: "/images/bg-light.svg" + authTitle: + zh: "欢迎来到 Sealos" + en: "Welcome to Sealos" + authBackgroundImage: + zh: "/images/bg-light.svg" + en: "/images/bg-light.svg" + meta: + title: "Sealos Cloud" + description: "Sealos Cloud" + keywords: "Sealos Cloud" + scripts: [] + noscripts: [] + customerServiceURL: "" + discordInviteLink: "" + forcedLanguage: "en" + currencySymbol: "usd" + protocol: + serviceProtocol: + zh: "https://sealos.io/zh-Hans/docs/msa/terms-of-service" + en: "https://sealos.io/docs/msa/terms-of-service" + privateProtocol: + zh: "https://sealos.io/zh-Hans/docs/msa/privacy-policy" + en: "https://sealos.io/docs/msa/privacy-policy" + common: + githubStarEnabled: true + workorderEnabled: false + accountSettingEnabled: true + docsUrl: "https://sealos.run/docs/Intro/" + aiAssistantEnabled: false + bannerEnabled: false + subscriptionEnabled: false + guestModeEnabled: false + emailAlertEnabled: false + phoneAlertEnabled: false + announcementEnabled: false + kcRotationEnabled: true + communityEnabled: true + communityQRCodeImage: "" + communityLink: "" + gtmId: null + auth: + billingToken: "" + callbackURL: "https://127.0.0.1.nip.io/callback" + signUpEnabled: true + baiduToken: "" + bingAd: + tenant: "" + clientId: "" + clientSecret: "" + refreshToken: "" + developerToken: "" + customerId: 0 + customerAccountId: 0 + conversionName: "" + jwt: + internal: "thisisinternaljwt" + regional: "thisisregionaljwt" + global: "thisisglobaljwt" + marketingConsent: "" + billingUrl: "http://account-service.account-system.svc:2333" + workorderUrl: "" + cloudVitrualMachineUrl: "" + invite: + enabled: false + lafSecretKey: "" + lafBaseURL: "" + idp: + password: + enabled: true + salt: "thisispasswordsalt" + github: + enabled: false + proxyAddress: "" + clientID: "" + clientSecret: "" + wechat: + enabled: false + proxyAddress: "" + clientID: "" + clientSecret: "" + google: + enabled: false + proxyAddress: "" + clientID: "" + clientSecret: "" + oneTapOrigins: [] + oauth2: + enabled: false + pkce: false + callbackURL: "" + clientID: "" + proxyAddress: "" + clientSecret: "" + authURL: "" + tokenURL: "" + userInfoURL: "" + displayName: "" + sms: + enabled: false + ali: + enabled: false + endpoint: "" + templateCode: "" + signName: "" + accessKeyID: "" + accessKeySecret: "" + email: + enabled: false + host: "" + port: 0 + user: "" + password: "" + language: "" + captcha: + ali: + enabled: false + sceneId: "" + prefix: "" + endpoint: "" + accessKeyID: "" + accessKeySecret: "" + turnstile: + enabled: false + siteKey: "" + secretKey: "" + teamManagement: + maxTeamCount: 0 + maxTeamMemberCount: 0 +tracking: + websiteId: "" + hostUrl: "" +realNameOSS: + accessKey: "" + accessKeySecret: "" + endpoint: "" + ssl: false + port: 0 + realNameBucket: "" + enterpriseRealNameBucket: "" diff --git a/frontend/desktop/deploy/HELM_VALUES_GUIDE.md b/frontend/desktop/deploy/HELM_VALUES_GUIDE.md new file mode 100644 index 000000000000..7810b3af97bd --- /dev/null +++ b/frontend/desktop/deploy/HELM_VALUES_GUIDE.md @@ -0,0 +1,354 @@ +# Helm Values 自定义配置指南 + +本文档介绍如何通过 `HELM_OPTIONS` 环境变量传递 Helm `--set` 参数来自定义 Desktop Frontend 的所有配置项。 + +## 配置方式 + +### 方式 1: 通过 `HELM_OPTIONS` 使用 `--set` 参数 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=cloud.example.com --set desktopConfig.layoutTitle=\"My Cloud Platform\"" +``` + +### 方式 2: 使用 `--set-string` 参数(用于确保值被当作字符串处理) + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set-string desktopConfig.realNameReward=0" +``` + +### 方式 3: 结合环境变量使用 + +环境变量优先级高于 `HELM_OPTIONS` 中的 `--set` 参数: + +```bash +# 环境变量会覆盖 HELM_OPTIONS 中的值 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=from-helm.com" \ + -e CLOUD_DOMAIN=from-env.com # 这个值会生效 +``` + +## 配置项分类 + +### 1. 基础云配置 + +```yaml +desktopConfig: + cloudDomain: 'cloud.example.com' # 云域名 + cloudPort: '' # 云端口(默认 443) + regionUID: 'region-123' # 区域 UID + certSecretName: 'wildcard-cert' # TLS 证书 Secret 名称 +``` + +### 2. 数据库配置 + +```yaml +desktopConfig: + databaseMongodbURI: 'mongodb://user:pass@mongodb:27017' + databaseGlobalCockroachdbURI: 'postgres://user:pass@cockroachdb:26257' + databaseLocalCockroachdbURI: 'postgres://user:pass@cockroachdb-local:26257' +``` + +### 3. 认证配置 + +```yaml +desktopConfig: + passwordSalt: 'your-random-salt' # 密码哈希盐 + jwtInternal: 'your-jwt-internal-key' # 内部 JWT 密钥 + jwtRegional: 'your-jwt-regional-key' # 区域 JWT 密钥 + jwtGlobal: 'your-jwt-global-key' # 全局 JWT 密钥 + jwtMarketingConsent: 'your-marketing-consent-key' # Marketing consent JWT secret shared with Brain +``` + +### 4. 计费配置 + +```yaml +desktopConfig: + billingUrl: 'http://account-service.account-system.svc:2333' + billingToken: 'your-billing-token' +``` + +### 5. 货币和语言配置 + +```yaml +desktopConfig: + version: 'en' # UI 版本: "cn" 或 "en" + # forcedLanguage 和 currencySymbol 会根据 version 自动配置: + # - version: "cn" → forcedLanguage: "zh", currencySymbol: "shellCoin" + # - version: "en" → forcedLanguage: "en", currencySymbol: "usd" +``` + +### 6. Google Tag Manager + +```yaml +desktopConfig: + gtmId: 'GTM-XXXXXXXX' # GTM ID +``` + +### 6.1 Rybbit Analytics(自部署) + +```yaml +desktopConfig: + rybbitHost: 'https://rybbit.example.com' # Rybbit 服务地址,留空则关闭 + rybbitSiteId: '' # Rybbit 站点 ID +``` + +### 7. Discord 配置 + +```yaml +desktopConfig: + discordInviteLink: 'https://discord.gg/sealos' +``` + +### 8. 功能开关 + +```yaml +desktopConfig: + guideEnabled: false # 启用用户引导 + apiEnabled: false # 启用 API 访问 + rechargeEnabled: false # 启用充值功能 + enterpriseRealNameAuthEnabled: false # 启用企业实名认证 + trackingEnabled: false # 启用追踪/统计 + realNameAuthEnabled: false # 启用实名认证 + licenseCheckEnabled: false # 启用 License 检查 + passwordEnabled: true # 启用密码登录 +``` + +### 9. Common URLs 配置 + +```yaml +desktopConfig: + realNameReward: 0 + realNameCallbackUrl: 'https://cloud.example.org/api/account/callback' + cfSiteKey: '' +``` + +`templateUrl`, `applaunchpadUrl`, `dbproviderUrl`, and `objectstorageUrl` are generated automatically from `cloudDomain`. + +### 10. 桌面布局配置 + +```yaml +desktopConfig: + layoutTitle: 'Sealos Cloud' # 平台标题 + layoutLogo: '/logo.svg' # Logo 路径 + layoutBackgroundImage: '/images/bg-light.svg' # 背景图片 + customerServiceURL: '' # 客服 URL + layoutDocsUrl: 'https://sealos.run/docs/Intro/' # 文档 URL +``` + +### 11. Meta 标签配置 + +```yaml +desktopConfig: + metaTitle: 'Sealos Cloud' + metaDescription: 'Sealos Cloud' + metaKeywords: 'Sealos Cloud' +``` + +### 12. GitHub OAuth 配置 + +```yaml +desktopConfig: + githubEnabled: true + githubClientId: 'your-github-client-id' + githubClientSecret: 'your-github-client-secret' + githubProxyAddress: '' # 代理地址(可选) +``` + +### 13. 微信 OAuth 配置 + +```yaml +desktopConfig: + wechatEnabled: true + wechatClientId: 'your-wechat-app-id' + wechatClientSecret: 'your-wechat-app-secret' + wechatProxyAddress: '' # 代理地址(可选) +``` + +### 14. Google OAuth 配置 + +```yaml +desktopConfig: + googleEnabled: true + googleClientId: 'your-google-client-id.apps.googleusercontent.com' + googleClientSecret: 'your-google-client-secret' + googleProxyAddress: '' # 代理地址(可选) +``` + +### 15. 通用 OAuth2 配置 + +```yaml +desktopConfig: + oauth2Enabled: true + oauth2CallbackUrl: 'https://cloud.example.com/callback' + oauth2ClientId: 'your-oauth2-client-id' + oauth2ClientSecret: 'your-oauth2-client-secret' + oauth2AuthUrl: 'https://oauth2.example.com/oauth2/auth' + oauth2TokenUrl: 'https://oauth2.example.com/oauth2/token' + oauth2UserInfoUrl: 'https://oauth2.example.com/oauth2/userinfo' + oauth2ProxyAddress: '' # 代理地址(可选) +``` + +### 16. 验证码配置(Cloudflare Turnstile) + +```yaml +desktopConfig: + turnstileEnabled: true + turnstileSiteKey: 'your-turnstile-site-key' + turnstileSecretKey: 'your-turnstile-secret-key' +``` + +### 17. 阿里云验证码配置 + +```yaml +desktopConfig: + aliCaptchaEnabled: true + aliCaptchaEndpoint: 'https://captcha.aliyuncs.com' + aliCaptchaSceneId: 'your-scene-id' + aliCaptchaPrefix: 'your-prefix' + aliCaptchaAccessKeyID: 'your-access-key-id' + aliCaptchaAccessKeySecret: 'your-access-key-secret' +``` + +### 18. 短信配置(阿里云) + +```yaml +desktopConfig: + smsEnabled: true + smsAliEnabled: true + smsAliEndpoint: 'https://dysmsapi.aliyuncs.com' + smsAliTemplateCode: 'SMS_123456789' + smsAliSignName: 'YourSignName' + smsAliAccessKeyID: 'your-access-key-id' + smsAliAccessKeySecret: 'your-access-key-secret' +``` + +### 19. 邮件配置 + +```yaml +desktopConfig: + emailEnabled: true + emailHost: 'smtp.example.com' + emailPort: 587 + emailUser: 'noreply@example.com' + emailPassword: 'your-email-password' + emailLanguage: 'en' +``` + +### 20. 追踪配置(Umami) + +```yaml +desktopConfig: + trackingWebsiteId: 'your-website-id' + trackingHostUrl: 'https://umami.example.com' + trackingScriptUrl: 'https://umami.example.com/script.js' +``` + +### 21. 实名认证 OSS 配置 + +```yaml +desktopConfig: + realNameOSSAccessKey: 'your-oss-access-key' + realNameOSSAccessKeySecret: 'your-oss-secret-key' + realNameOSSEndpoint: 'oss-cn-hangzhou.aliyuncs.com' + realNameOSSSSL: true + realNameOSSPort: 443 + realNameOSSRealNameBucket: 'realname-bucket' + realNameOSSEnterpriseRealNameBucket: 'enterprise-realname-bucket' +``` + +### 22. 工作订单和云虚拟机配置 + +```yaml +desktopConfig: + workorderUrl: 'https://workorder.example.com' + cloudVirtualMachineUrl: 'https://cloudvirtualmachine.example.com' +``` + +### 23. 团队管理配置 + +```yaml +desktopConfig: + maxTeamCount: 10 # 最大团队数 + maxTeamMemberCount: 100 # 每个团队最大成员数 +``` + +## 使用示例 + +### 示例 1: 自定义域名和标题 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=mycloud.com --set desktopConfig.layoutTitle=\"My Cloud Platform\" --set desktopConfig.metaTitle=\"My Cloud Platform\" --set desktopConfig.metaDescription=\"Welcome to My Cloud Platform\"" +``` + +### 示例 2: 启用 GitHub 和 Google 登录 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-github-id --set desktopConfig.githubClientSecret=your-github-secret --set desktopConfig.googleEnabled=true --set desktopConfig.googleClientId=your-google-id --set desktopConfig.googleClientSecret=your-google-secret" +``` + +### 示例 3: 配置短信和邮件 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.smsEnabled=true --set desktopConfig.smsAliEnabled=true --set desktopConfig.smsAliEndpoint=https://dysmsapi.aliyuncs.com --set desktopConfig.smsAliAccessKeyID=your-key-id --set desktopConfig.smsAliAccessKeySecret=your-key-secret --set desktopConfig.emailEnabled=true --set desktopConfig.emailHost=smtp.example.com --set desktopConfig.emailPort=587 --set desktopConfig.emailUser=noreply@example.com --set desktopConfig.emailPassword=your-password" +``` + +### 示例 4: 完整生产环境配置 + +对于生产环境的多个配置,直接通过 `HELM_OPTIONS` 传递所有 Helm values: + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS=" + --set desktopConfig.cloudDomain=production.example.com + --set desktopConfig.regionUID=prod-region-001 + --set desktopConfig.version=cn + --set desktopConfig.githubEnabled=true + --set desktopConfig.githubClientId=prod-github-id + --set desktopConfig.githubClientSecret=prod-github-secret + --set desktopConfig.smsEnabled=true + --set desktopConfig.smsAliEnabled=true + --set desktopConfig.smsAliAccessKeyID=prod-sms-key-id + --set desktopConfig.smsAliAccessKeySecret=prod-sms-key-secret + --set desktopConfig.emailEnabled=true + --set desktopConfig.emailHost=smtp.production.example.com + --set desktopConfig.emailPort=587 + --set desktopConfig.emailUser=noreply@production.example.com + --set desktopConfig.emailPassword=prod-email-password + --set desktopConfig.trackingEnabled=true + --set desktopConfig.trackingWebsiteId=prod-website-id + " +``` + +**提示**: + +- 所有配置统一通过 `HELM_OPTIONS` 传递 +- 对于超长配置,建议创建部署脚本 + +## 注意事项 + +1. **敏感信息**: 建议将敏感配置(如密钥、密码)通过环境变量传递,或存储在 Kubernetes Secret 中 +2. **类型注意**: 布尔值使用 `true`/`false`,数字不需要引号,字符串建议使用引号 +3. **嵌套配置**: 使用 `.` 分隔嵌套层级,如 `desktopConfig.githubClientId` +4. **数组索引**: 对于数组配置,使用 `[]` 索引,如 `ingress.hosts[0].host` +5. **字符串包含特殊字符**: 使用 `--set-string` 或反斜杠转义引号(如 `\"`) +6. **引号转义**: 在 `HELM_OPTIONS` 中,字符串包含引号时需要转义(如 `--set desktopConfig.layoutTitle=\"My Cloud\"`) +7. **配置管理**: 对于复杂部署,建议使用脚本或配置管理工具来管理参数 + +## 验证配置 + +部署后可以查看生成的 ConfigMap 验证配置: + +```bash +kubectl get configmap sealos-desktop-config -n sealos -o yaml +``` + +或在 Pod 中查看实际配置: + +```bash +kubectl exec -n sealos deployment/sealos-desktop -- cat /app/data/config.yaml +``` diff --git a/frontend/desktop/deploy/HELM_VALUES_GUIDE_CN.md b/frontend/desktop/deploy/HELM_VALUES_GUIDE_CN.md new file mode 100644 index 000000000000..c2d3a6595e14 --- /dev/null +++ b/frontend/desktop/deploy/HELM_VALUES_GUIDE_CN.md @@ -0,0 +1,353 @@ +# Helm Values 自定义配置指南 + +本文档介绍如何通过 `HELM_OPTIONS` 环境变量传递 Helm `--set` 参数来自定义 Desktop Frontend 的所有配置项。 + +## 配置方式 + +### 方式 1: 通过 `HELM_OPTIONS` 使用 `--set` 参数 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=cloud.example.com --set desktopConfig.layoutTitle=\"我的云平台\"" +``` + +### 方式 2: 使用 `--set-string` 参数(用于确保值被当作字符串处理) + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set-string desktopConfig.realNameReward=0" +``` + +### 方式 3: 结合环境变量使用 + +环境变量优先级高于 `HELM_OPTIONS` 中的 `--set` 参数: + +```bash +# 环境变量会覆盖 HELM_OPTIONS 中的值 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=from-helm.com" \ + -e CLOUD_DOMAIN=from-env.com # 这个值会生效 +``` + +## 配置项分类 + +### 1. 基础云配置 + +```yaml +desktopConfig: + cloudDomain: 'cloud.example.com' # 云域名 + cloudPort: '' # 云端口(默认 443) + regionUID: 'region-123' # 区域 UID + certSecretName: 'wildcard-cert' # TLS 证书 Secret 名称 +``` + +### 2. 数据库配置 + +```yaml +desktopConfig: + databaseMongodbURI: 'mongodb://user:pass@mongodb:27017' + databaseGlobalCockroachdbURI: 'postgres://user:pass@cockroachdb:26257' + databaseLocalCockroachdbURI: 'postgres://user:pass@cockroachdb-local:26257' +``` + +### 3. 认证配置 + +```yaml +desktopConfig: + passwordSalt: 'your-random-salt' # 密码哈希盐 + jwtInternal: 'your-jwt-internal-key' # 内部 JWT 密钥 + jwtRegional: 'your-jwt-regional-key' # 区域 JWT 密钥 + jwtGlobal: 'your-jwt-global-key' # 全局 JWT 密钥 + jwtMarketingConsent: 'your-marketing-consent-key' # 与 Brain 共享的营销 consent JWT 密钥 +``` + +### 4. 计费配置 + +```yaml +desktopConfig: + billingUrl: 'http://account-service.account-system.svc:2333' + billingToken: 'your-billing-token' +``` + +### 5. 货币和语言配置 + +```yaml +desktopConfig: + version: 'en' # UI 版本: "cn" 或 "en" + # forcedLanguage 和 currencySymbol 会根据 version 自动配置: + # - version: "cn" → forcedLanguage: "zh", currencySymbol: "shellCoin" + # - version: "en" → forcedLanguage: "en", currencySymbol: "usd" +``` + +### 6. Google Tag Manager + +```yaml +desktopConfig: + gtmId: 'GTM-XXXXXXXX' # GTM ID +``` + +### 6.1 Rybbit Analytics(自部署) + +```yaml +desktopConfig: + rybbitHost: 'https://rybbit.example.com' # Rybbit 服务地址,留空则关闭 + rybbitSiteId: '' # Rybbit 站点 ID +``` + +### 7. Discord 配置(仅 USD 版本) + +```yaml +desktopConfig: + discordInviteLink: 'https://discord.gg/sealos' +``` + +### 8. 功能开关 + +```yaml +desktopConfig: + guideEnabled: false # 启用用户引导 + apiEnabled: false # 启用 API 访问 + rechargeEnabled: false # 启用充值功能 + enterpriseRealNameAuthEnabled: false # 启用企业实名认证 + trackingEnabled: false # 启用追踪/统计 + realNameAuthEnabled: false # 启用实名认证 + licenseCheckEnabled: false # 启用 License 检查 +``` + +### 9. Common URLs 配置 + +```yaml +desktopConfig: + realNameReward: 0 + realNameCallbackUrl: 'https://cloud.example.org/api/account/callback' + cfSiteKey: '' +``` + +`templateUrl`、`applaunchpadUrl`、`dbproviderUrl` 和 `objectstorageUrl` 会根据 `cloudDomain` 自动生成。 + +### 10. 桌面布局配置 + +```yaml +desktopConfig: + layoutTitle: 'Sealos Cloud' # 平台标题 + layoutLogo: '/logo.svg' # Logo 路径 + layoutBackgroundImage: '/images/bg-light.svg' # 背景图片 + customerServiceURL: '' # 客服 URL + layoutDocsUrl: 'https://sealos.run/docs/Intro/' # 文档 URL +``` + +### 11. Meta 标签配置 + +```yaml +desktopConfig: + metaTitle: 'Sealos Cloud' + metaDescription: 'Sealos Cloud' + metaKeywords: 'Sealos Cloud' +``` + +### 12. GitHub OAuth 配置 + +```yaml +desktopConfig: + githubEnabled: true + githubClientId: 'your-github-client-id' + githubClientSecret: 'your-github-client-secret' + githubProxyAddress: '' # 代理地址(可选) +``` + +### 13. 微信 OAuth 配置 + +```yaml +desktopConfig: + wechatEnabled: true + wechatClientId: 'your-wechat-app-id' + wechatClientSecret: 'your-wechat-app-secret' + wechatProxyAddress: '' # 代理地址(可选) +``` + +### 14. Google OAuth 配置 + +```yaml +desktopConfig: + googleEnabled: true + googleClientId: 'your-google-client-id.apps.googleusercontent.com' + googleClientSecret: 'your-google-client-secret' + googleProxyAddress: '' # 代理地址(可选) +``` + +### 15. 通用 OAuth2 配置 + +```yaml +desktopConfig: + oauth2Enabled: true + oauth2CallbackUrl: 'https://cloud.example.com/callback' + oauth2ClientId: 'your-oauth2-client-id' + oauth2ClientSecret: 'your-oauth2-client-secret' + oauth2AuthUrl: 'https://oauth2.example.com/oauth2/auth' + oauth2TokenUrl: 'https://oauth2.example.com/oauth2/token' + oauth2UserInfoUrl: 'https://oauth2.example.com/oauth2/userinfo' + oauth2ProxyAddress: '' # 代理地址(可选) +``` + +### 16. 验证码配置(Cloudflare Turnstile) + +```yaml +desktopConfig: + turnstileEnabled: true + turnstileSiteKey: 'your-turnstile-site-key' + turnstileSecretKey: 'your-turnstile-secret-key' +``` + +### 17. 阿里云验证码配置 + +```yaml +desktopConfig: + aliCaptchaEnabled: true + aliCaptchaEndpoint: 'https://captcha.aliyuncs.com' + aliCaptchaSceneId: 'your-scene-id' + aliCaptchaPrefix: 'your-prefix' + aliCaptchaAccessKeyID: 'your-access-key-id' + aliCaptchaAccessKeySecret: 'your-access-key-secret' +``` + +### 18. 短信配置(阿里云) + +```yaml +desktopConfig: + smsEnabled: true + smsAliEnabled: true + smsAliEndpoint: 'https://dysmsapi.aliyuncs.com' + smsAliTemplateCode: 'SMS_123456789' + smsAliSignName: 'YourSignName' + smsAliAccessKeyID: 'your-access-key-id' + smsAliAccessKeySecret: 'your-access-key-secret' +``` + +### 19. 邮件配置 + +```yaml +desktopConfig: + emailEnabled: true + emailHost: 'smtp.example.com' + emailPort: 587 + emailUser: 'noreply@example.com' + emailPassword: 'your-email-password' + emailLanguage: 'zh' +``` + +### 20. 追踪配置(Umami) + +```yaml +desktopConfig: + trackingWebsiteId: 'your-website-id' + trackingHostUrl: 'https://umami.example.com' + trackingScriptUrl: 'https://umami.example.com/script.js' +``` + +### 21. 实名认证 OSS 配置 + +```yaml +desktopConfig: + realNameOSSAccessKey: 'your-oss-access-key' + realNameOSSAccessKeySecret: 'your-oss-secret-key' + realNameOSSEndpoint: 'oss-cn-hangzhou.aliyuncs.com' + realNameOSSSSL: true + realNameOSSPort: 443 + realNameOSSRealNameBucket: 'realname-bucket' + realNameOSSEnterpriseRealNameBucket: 'enterprise-realname-bucket' +``` + +### 22. 工单和云虚拟机配置 + +```yaml +desktopConfig: + workorderUrl: 'https://workorder.example.com' + cloudVirtualMachineUrl: 'https://cloudvirtualmachine.example.com' +``` + +### 23. 团队管理配置 + +```yaml +desktopConfig: + maxTeamCount: 10 # 最大团队数 + maxTeamMemberCount: 100 # 每个团队最大成员数 +``` + +## 使用示例 + +### 示例 1: 自定义域名和标题 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=mycloud.com --set desktopConfig.layoutTitle=\"我的云平台\" --set desktopConfig.metaTitle=\"我的云平台\" --set desktopConfig.metaDescription=\"欢迎使用我的云平台\"" +``` + +### 示例 2: 启用 GitHub 和 Google 登录 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-github-id --set desktopConfig.githubClientSecret=your-github-secret --set desktopConfig.googleEnabled=true --set desktopConfig.googleClientId=your-google-id --set desktopConfig.googleClientSecret=your-google-secret" +``` + +### 示例 3: 配置短信和邮件 + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.smsEnabled=true --set desktopConfig.smsAliEnabled=true --set desktopConfig.smsAliEndpoint=https://dysmsapi.aliyuncs.com --set desktopConfig.smsAliAccessKeyID=your-key-id --set desktopConfig.smsAliAccessKeySecret=your-key-secret --set desktopConfig.emailEnabled=true --set desktopConfig.emailHost=smtp.example.com --set desktopConfig.emailPort=587 --set desktopConfig.emailUser=noreply@example.com --set desktopConfig.emailPassword=your-password" +``` + +### 示例 4: 完整生产环境配置 + +对于生产环境的多个配置,直接通过 `HELM_OPTIONS` 传递所有 Helm values: + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS=" + --set desktopConfig.cloudDomain=production.example.com + --set desktopConfig.regionUID=prod-region-001 + --set desktopConfig.version=cn + --set desktopConfig.githubEnabled=true + --set desktopConfig.githubClientId=prod-github-id + --set desktopConfig.githubClientSecret=prod-github-secret + --set desktopConfig.smsEnabled=true + --set desktopConfig.smsAliEnabled=true + --set desktopConfig.smsAliAccessKeyID=prod-sms-key-id + --set desktopConfig.smsAliAccessKeySecret=prod-sms-key-secret + --set desktopConfig.emailEnabled=true + --set desktopConfig.emailHost=smtp.production.example.com + --set desktopConfig.emailPort=587 + --set desktopConfig.emailUser=noreply@production.example.com + --set desktopConfig.emailPassword=prod-email-password + --set desktopConfig.trackingEnabled=true + --set desktopConfig.trackingWebsiteId=prod-website-id + " +``` + +**提示**: + +- 所有配置统一通过 `HELM_OPTIONS` 传递 +- 对于超长配置,建议创建部署脚本 + +## 注意事项 + +1. **敏感信息**: 建议将敏感配置(如密钥、密码)通过环境变量传递,或存储在 Kubernetes Secret 中 +2. **类型注意**: 布尔值使用 `true`/`false`,数字不需要引号,字符串建议使用引号 +3. **嵌套配置**: 使用 `.` 分隔嵌套层级,如 `desktopConfig.githubClientId` +4. **数组索引**: 对于数组配置,使用 `[]` 索引,如 `ingress.hosts[0].host` +5. **字符串包含特殊字符**: 使用 `--set-string` 或反斜杠转义引号(如 `\"`) +6. **引号转义**: 在 `HELM_OPTIONS` 中,字符串包含引号时需要转义(如 `--set desktopConfig.layoutTitle=\"我的云平台\"`) +7. **配置管理**: 对于复杂部署,建议使用脚本或配置管理工具来管理参数 + +## 验证配置 + +部署后可以查看生成的 ConfigMap 验证配置: + +```bash +kubectl get configmap sealos-desktop-config -n sealos -o yaml +``` + +或在 Pod 中查看实际配置: + +```bash +kubectl exec -n sealos deployment/sealos-desktop -- cat /app/data/config.yaml +``` diff --git a/frontend/desktop/deploy/Kubefile b/frontend/desktop/deploy/Kubefile new file mode 100644 index 000000000000..764dcc19f8ed --- /dev/null +++ b/frontend/desktop/deploy/Kubefile @@ -0,0 +1,9 @@ +FROM scratch + +USER 65532:65532 + +COPY registry registry +COPY charts charts +COPY desktop-frontend-entrypoint.sh desktop-frontend-entrypoint.sh + +CMD ["bash desktop-frontend-entrypoint.sh"] diff --git a/frontend/desktop/deploy/README.md b/frontend/desktop/deploy/README.md new file mode 100644 index 000000000000..68e2e0a1770a --- /dev/null +++ b/frontend/desktop/deploy/README.md @@ -0,0 +1,513 @@ +# Desktop Frontend Helm Chart + +Sealos Desktop Frontend deployment using Helm charts with auto-configuration support. + +## Quick Start + +### Basic Usage (Auto-Configuration) + +```bash +# Default deployment (auto-configured from sealos-system/sealos-config) +sealos run desktop-frontend:latest + +# With custom domain +sealos run desktop-frontend:latest -e CLOUD_DOMAIN=cloud.example.com + +# Chinese version with GitHub OAuth +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.version=cn --set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=xxx --set desktopConfig.githubClientSecret=yyy" + +# English version with Google OAuth and GTM +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=false --set desktopConfig.googleEnabled=true --set desktopConfig.googleClientId=xxx --set desktopConfig.googleClientSecret=yyy --set desktopConfig.gtmId=GTM-XXX" +``` + +## Values Files + +The Helm Chart uses two values files to manage configuration: + +### 1. values-default.yaml + +Contains default Helm Chart configurations that should not be modified. + +**Content**: + +- **Infrastructure configurations**: Image, service account, probes, ingress, scheduling, etc. +- **Auto-configured reference values**: cloudDomain, jwtInternal, databaseMongodbURI, etc. (automatically fetched from ConfigMap, values here are for reference only) + +**Modification**: ❌ Do not modify + +### 2. values-custom.yaml + +Contains user-customizable configurations. + +**Content**: + +- Resource limits and requests +- Feature flags +- OAuth providers (GitHub, Google, WeChat, etc.) +- SMS and email configuration +- Billing settings +- Custom URLs +- Layout customization + +**Modification**: ✅ Modify as needed + +**Note**: Auto-configured items in values-default.yaml (such as cloudDomain, jwtInternal, etc.) are automatically fetched by the entrypoint script from the `sealos-system/sealos-config` ConfigMap and will be overridden. To modify these values, edit the ConfigMap or use HELM_OPTIONS. + +For detailed documentation, see [VALUES_FILES_GUIDE.md](./VALUES_FILES_GUIDE.md). + +## Environment Variables + +### Google Tag Manager + +| Variable | Default | Description | +| -------- | ------- | --------------------- | +| `GTM_ID` | `""` | Google Tag Manager ID | + +### Feature Flags + +| Variable | Default | Description | +| ----------------------------------- | ------- | ------------------------------------------ | +| `GUIDE_ENABLED` | `false` | Enable user guide | +| `API_ENABLED` | `false` | Enable API access | +| `RECHARGE_ENABLED` | `false` | Enable recharge feature | +| `ENTERPRISE_REAL_NAME_AUTH_ENABLED` | `false` | Enable enterprise real-name authentication | +| `TRACKING_ENABLED` | `false` | Enable tracking/analytics | +| `REAL_NAME_AUTH_ENABLED` | `false` | Enable real-name authentication | +| `LICENSE_CHECK_ENABLED` | `false` | Enable license checking | + +### OAuth Providers + +#### GitHub OAuth + +| Variable | Default | Description | +| ---------------------- | ------- | -------------------------- | +| `GITHUB_ENABLED` | `false` | Enable GitHub OAuth | +| `GITHUB_CLIENT_ID` | `""` | GitHub OAuth client ID | +| `GITHUB_CLIENT_SECRET` | `""` | GitHub OAuth client secret | + +#### WeChat OAuth + +| Variable | Default | Description | +| ---------------------- | ------- | -------------------------- | +| `WECHAT_ENABLED` | `false` | Enable WeChat OAuth | +| `WECHAT_CLIENT_ID` | `""` | WeChat OAuth client ID | +| `WECHAT_CLIENT_SECRET` | `""` | WeChat OAuth client secret | + +#### Google OAuth + +| Variable | Default | Description | +| ---------------------- | ------- | -------------------------- | +| `GOOGLE_ENABLED` | `false` | Enable Google OAuth | +| `GOOGLE_CLIENT_ID` | `""` | Google OAuth client ID | +| `GOOGLE_CLIENT_SECRET` | `""` | Google OAuth client secret | + +#### Generic OAuth2 + +| Variable | Default | Description | +| ---------------------- | ------- | ------------------------ | +| `OAUTH2_ENABLED` | `false` | Enable generic OAuth2 | +| `OAUTH2_CALLBACK_URL` | `""` | OAuth2 callback URL | +| `OAUTH2_CLIENT_ID` | `""` | OAuth2 client ID | +| `OAUTH2_CLIENT_SECRET` | `""` | OAuth2 client secret | +| `OAUTH2_AUTH_URL` | `""` | OAuth2 authorization URL | +| `OAUTH2_TOKEN_URL` | `""` | OAuth2 token URL | +| `OAUTH2_USER_INFO_URL` | `""` | OAuth2 user info URL | + +### Captcha Configuration + +| Variable | Default | Description | +| ---------------------- | ------- | --------------------------- | +| `TURNSTILE_ENABLED` | `false` | Enable Cloudflare Turnstile | +| `TURNSTILE_SITE_KEY` | `""` | Turnstile site key | +| `TURNSTILE_SECRET_KEY` | `""` | Turnstile secret key | + +### Custom Override Variables + +| Variable | Default | Description | +| --------------------------------- | --------------- | ------------------------------------------------ | +| `CLOUD_DOMAIN` | `""` | Override cloud domain (auto from sealos-config) | +| `CLOUD_PORT` | `""` | Override cloud port (auto from sealos-config) | +| `CERT_SECRET_NAME` | `wildcard-cert` | TLS certificate secret name | +| `REGION_UID` | `""` | Override region UID (auto from sealos-config) | +| `DATABASE_MONGODB_URI` | `""` | Override MongoDB URI (auto from sealos-config) | +| `DATABASE_GLOBAL_COCKROACHDB_URI` | `""` | Override global DB URI (auto from sealos-config) | +| `DATABASE_LOCAL_COCKROACHDB_URI` | `""` | Override local DB URI (auto from sealos-config) | +| `PASSWORD_SALT` | `""` | Override password salt (auto from sealos-config) | +| `JWT_INTERNAL` | `""` | Override internal JWT (auto from sealos-config) | +| `JWT_REGIONAL` | `""` | Override regional JWT (auto from sealos-config) | +| `JWT_GLOBAL` | `""` | Override global JWT (auto from sealos-config) | + +## Auto-Configured Values + +The following values are **automatically retrieved** from the `sealos-system/sealos-config` ConfigMap and **do not need manual configuration** unless you want to override them: + +| ConfigMap Key | Target Value | Description | +| ------------------------------ | -------------------------------------------- | ---------------------- | +| `cloudDomain` | `desktopConfig.cloudDomain` | Cloud domain | +| `cloudPort` | `desktopConfig.cloudPort` | Cloud port | +| `jwtInternal` | `desktopConfig.jwtInternal` | Internal JWT secret | +| `jwtRegional` | `desktopConfig.jwtRegional` | Regional JWT secret | +| `jwtGlobal` | `desktopConfig.jwtGlobal` | Global JWT secret | +| `jwtMarketingConsent` | `desktopConfig.jwtMarketingConsent` | Marketing consent JWT secret | +| `regionUID` | `desktopConfig.regionUID` | Region UID | +| `databaseMongodbURI` | `desktopConfig.databaseMongodbURI` | MongoDB connection URI | +| `databaseGlobalCockroachdbURI` | `desktopConfig.databaseGlobalCockroachdbURI` | Global CockroachDB URI | +| `databaseLocalCockroachdbURI` | `desktopConfig.databaseLocalCockroachdbURI` | Local CockroachDB URI | +| `passwordSalt` | `desktopConfig.passwordSalt` | Password hash salt | + +## ConfigMap Structure + +The generated `sealos-desktop-config` ConfigMap contains the following structure: + +```yaml +cloud: + domain: 'cloud.example.com' + port: '' + regionUID: 'randomRegionUID' + certSecretName: 'wildcard-cert' + proxyDomain: 'cloud.example.com' + allowedOrigins: + - 'https://applaunchpad.cloud.example.com' + - 'https://dbprovider.cloud.example.com' + - 'https://costcenter.cloud.example.com' + - 'https://cronjob.cloud.example.com' + - 'https://objectstorage.cloud.example.com' + - 'https://template.cloud.example.com' + - 'https://terminal.cloud.example.com' + - 'https://kubepanel.cloud.example.com' + - 'https://license.cloud.example.com' + - 'https://devbox.cloud.example.com' + - 'https://aiproxy-web.cloud.example.com' + - 'https://aiproxy.cloud.example.com' + - 'https://sealaf-api.cloud.example.com' + - 'https://sealaf.cloud.example.com' + +common: + guideEnabled: false + apiEnabled: false + rechargeEnabled: false + enterpriseRealNameAuthEnabled: false + trackingEnabled: false + realNameAuthEnabled: false + passwordEnabled: true + realNameReward: 0 + realNameCallbackUrl: 'https://cloud.example.org/api/account/faceIdRealNameAuthCallback' + templateUrl: 'https://template.example.org' + applaunchpadUrl: 'https://applaunchpad.example.org' + dbproviderUrl: 'https://dbprovider.example.org' + objectstorageUrl: 'https://objectstorage.example.org' + cfSiteKey: '' + +database: + mongodbURI: 'mongodb://...' + globalCockroachdbURI: 'postgres://...' + regionalCockroachdbURI: 'postgres://...' + +desktop: + layout: + version: 'en' + title: 'Sealos Cloud' + logo: '/logo.svg' + backgroundImage: '/images/bg-light.svg' + forcedLanguage: 'en' # Auto-configured based on version: "cn"→"zh", "en"→"en" + customerServiceURL: '' + discordInviteLink: '' # Auto-configured: shown for "en", empty for "cn" + gtmId: null + currencySymbol: 'usd' # Auto-configured based on version: "cn"→"shellCoin", "en"→"usd" + meta: + title: 'Sealos Cloud' + description: 'Sealos Cloud' + keywords: 'Sealos Cloud' + scripts: [] + noscripts: [] + common: + githubStarEnabled: true + accountSettingEnabled: true + docsUrl: 'https://sealos.run/docs/Intro/' + aiAssistantEnabled: false + bannerEnabled: false + subscriptionEnabled: false + guestModeEnabled: false + emailAlertEnabled: false + phoneAlertEnabled: false + announcementEnabled: false + communityEnabled: true + communityQRCodeImage: '' + communityLink: '' + auth: + proxyAddress: '' + callbackURL: 'https://cloud.example.com/callback' + signUpEnabled: true + baiduToken: '' + hasBaiduToken: false + jwt: + internal: '...' + regional: '...' + global: '...' + idp: + password: + enabled: true + salt: '...' + github: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + wechat: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + google: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + oauth2: + enabled: false + callbackURL: '' + clientID: '' + proxyAddress: '' + clientSecret: '' + authURL: '' + tokenURL: '' + userInfoURL: '' + sms: + enabled: false + email: + enabled: false + captcha: + turnstile: + enabled: false + siteKey: '' + secretKey: '' + billingUrl: 'http://account-service.account-system.svc:2333' + billingToken: '' + teamManagement: + maxTeamCount: 0 + maxTeamMemberCount: 0 + +tracking: + websiteId: '' + hostUrl: '' + scriptUrl: '' + +realNameOSS: + accessKey: '' + accessKeySecret: '' + endpoint: '' + ssl: false + port: 0 + realNameBucket: '' + enterpriseRealNameBucket: '' +``` + +## AllowedOrigins Extension + +Desktop Frontend supports adding custom subdomains to the `allowedOrigins` list. The system automatically formats subdomains as `https://{subdomain}.{cloudDomain}` and adds them to the existing `allowedOrigins`. + +### Configuration + +Add `additionalAllowedOriginsPrefixes` in `values-custom.yaml`: + +```yaml +desktopConfig: + cloudDomain: 'cloud.example.com' + + # Add additional allowedOrigins subdomains + # Only provide the subdomain part, system will add https:// and .{cloudDomain} + additionalAllowedOriginsPrefixes: + - 'my-custom-app' + - 'another-service' +``` + +### Result + +The generated `allowedOrigins` will include: + +```yaml +allowedOrigins: + # Default origins... + - 'https://sealaf.cloud.example.com' + # Additional custom origins + - 'https://my-custom-app.cloud.example.com' + - 'https://another-service.cloud.example.com' +``` + +### Usage Examples + +**Via values file:** + +```yaml +additionalAllowedOriginsPrefixes: + - 'custom-app' + - 'analytics-service' +``` + +**Via Helm --set:** + +```bash +helm upgrade -i desktop-frontend ./charts/desktop-frontend \ + --set desktopConfig.additionalAllowedOriginsPrefixes[0]="my-app" \ + --set desktopConfig.additionalAllowedOriginsPrefixes[1]="another-app" +``` + +**Via environment variable:** + +```bash +export HELM_OPTIONS='--set desktopConfig.additionalAllowedOriginsPrefixes[0]="my-app"' +./desktop-frontend-entrypoint.sh +``` + +For more details, see [ALLOWED_ORIGINS_USAGE.md](./ALLOWED_ORIGINS_USAGE.md). + +## Helm Chart Values + +| Parameter | Description | Default | +| ---------------------------------------------------------- | ---------------------------------- | ------------------------------------------------ | +| `replicaCount` | Deployment replica count | `1` | +| `image` | Container image | `ghcr.io/labring/sealos-desktop-frontend:latest` | +| `imagePullPolicy` | Image pull policy | `IfNotPresent` | +| `fullnameOverride` | Override full resource names | `sealos-desktop` | +| `serviceAccount.create` | Create service account | `true` | +| `serviceAccount.name` | Service account name | `desktop-frontend` | +| `service.port` | Service port | `3000` | +| `ciliumNetworkPolicy.enabled` | Restrict inbound access to Desktop | `false` | +| `ciliumNetworkPolicy.trustedCallers[0].namespace` | Account Controller namespace | `account-system` | +| `ciliumNetworkPolicy.trustedCallers[0].serviceAccountName` | Account Controller service account | `account-controller-manager` | +| `ciliumNetworkPolicy.trustedCallers[1].namespace` | Costcenter namespace | `costcenter-frontend` | +| `ciliumNetworkPolicy.trustedCallers[1].serviceAccountName` | Costcenter service account | `default` | +| `resources.requests.cpu` | CPU request | `100m` | +| `resources.requests.memory` | Memory request | `128Mi` | +| `resources.limits.cpu` | CPU limit | `2000m` | +| `resources.limits.memory` | Memory limit | `2048Mi` | +| `ingress.enabled` | Enable ingress | `true` | +| `ingress.className` | Ingress class | `nginx` | +| `autoConfigEnabled` | Auto-config from sealos-config | `true` | + +The chart does not create an ingress policy by default. Unless another network +policy selects Desktop, all pods can connect to the service and callers that +bypass Higress can supply forwarding headers such as `X-Real-IP` themselves. +Deployments that use gateway-derived client IPs for security controls must +enable this policy or enforce equivalent traffic isolation. + +When `ciliumNetworkPolicy.enabled` is set to `true`, the cluster must provide the +`cilium.io/v2/CiliumNetworkPolicy` CRD or Helm stops the installation. The +policy allows the node-hosted gateway (`host` and `remote-node` Cilium +identities) and the namespace + service account pairs configured in +`ciliumNetworkPolicy.trustedCallers` to reach Desktop on TCP port 3000. Direct +traffic from other application pods is denied. + +Costcenter and Account Controller continue to call Desktop through its +ClusterIP and must be included in `trustedCallers`. If either namespace or +service account changes, update and verify the policy before rolling out the +caller. Other untrusted in-cluster callers must migrate to the public HTTPS +gateway. During a rollback, do not remove an allowlist entry while its caller +still uses the ClusterIP. + +## Troubleshooting + +### Check Deployment Status + +```bash +# Check helm release +helm status desktop-frontend -n sealos + +# Check pods +kubectl get pods -n sealos -l app.kubernetes.io/name=desktop-frontend + +# Check configmap +kubectl get configmap sealos-desktop-config -n sealos -o yaml + +# Check ingress +kubectl get ingress sealos-desktop -n sealos + +# Check logs +kubectl logs -n sealos -l app.kubernetes.io/name=desktop-frontend --tail=100 -f +``` + +### Common Issues + +**Issue**: Ingress returns 404 or 502 + +- **Solution**: Check if service exists and port is correct: `kubectl get svc sealos-desktop -n sealos` + +**Issue**: Pod CrashLoopBackOff + +- **Solution**: Check logs for database connection errors or missing environment variables + +**Issue**: OAuth callback fails + +- **Solution**: Verify `callbackURL` in config matches your OAuth app settings + +**Issue**: Existing resources prevent installation + +- **Solution**: The script automatically adopts existing resources by adding Helm labels + +## Advanced Usage + +### Using Helm Values for Custom Configuration + +All config.yaml settings can be customized via Helm `--set` parameters through the `HELM_OPTIONS` environment variable. See [HELM_VALUES_GUIDE.md](HELM_VALUES_GUIDE.md) for complete documentation. + +**Quick examples:** + +```bash +# UI customization via HELM_OPTIONS +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.layoutTitle=\"My Cloud Platform\" --set desktopConfig.metaTitle=\"My Cloud\"" + +# OAuth providers via HELM_OPTIONS +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-client-id --set desktopConfig.githubClientSecret=your-client-secret" + +# Features and communication +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.guideEnabled=true --set desktopConfig.rechargeEnabled=true --set desktopConfig.smsEnabled=true --set desktopConfig.emailEnabled=true --set desktopConfig.emailHost=smtp.example.com --set desktopConfig.emailPort=587" + +# Combine environment variables and HELM_OPTIONS +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.layoutTitle=\"My Cloud\"" \ + -e CLOUD_DOMAIN=override.example.com \ + -e GITHUB_ENABLED=true +``` + +**Common customization options:** + +- **UI customization**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL` +- **OAuth providers**: `githubEnabled`, `googleEnabled`, `wechatEnabled`, `oauth2Enabled` and their `*ClientId`, `*ClientSecret` +- **Features**: `guideEnabled`, `rechargeEnabled`, `trackingEnabled`, `apiEnabled`, `realNameAuthEnabled` +- **Communication**: `smsEnabled`, `emailEnabled`, `emailHost`, `emailPort`, `emailUser`, `emailPassword` +- **URLs**: `workorderUrl` and service URLs auto-generated from `cloudDomain` (`template`, `applaunchpad`, `dbprovider`, `objectstorage`) +- **Database**: `databaseMongodbURI`, `databaseGlobalCockroachdbURI`, `databaseLocalCockroachdbURI` +- **Team management**: `maxTeamCount`, `maxTeamMemberCount` + +For 60+ configurable parameters across 23 categories, see [HELM_VALUES_GUIDE.md](HELM_VALUES_GUIDE.md). + +### Disable Auto-Configuration + +```bash +sealos run desktop-frontend:latest \ + -e AUTO_CONFIG_ENABLED=false \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=cloud.example.com --set desktopConfig.databaseMongodbURI=mongodb://..." +``` + +### Custom Helm Options + +```bash +sealos run desktop-frontend:latest -e HELM_OPTIONS="--timeout 10m" +``` + +### Override Namespace + +```bash +sealos run desktop-frontend:latest -e RELEASE_NAMESPACE=my-namespace +``` + +## Build Image + +```bash +sealos build -t docker.io/labring/sealos-cloud-desktop:latest -f Kubefile . +``` diff --git a/frontend/desktop/deploy/README_CN.md b/frontend/desktop/deploy/README_CN.md new file mode 100644 index 000000000000..74055ca9c0c8 --- /dev/null +++ b/frontend/desktop/deploy/README_CN.md @@ -0,0 +1,606 @@ +# Desktop 前端 Helm Chart + +Sealos Desktop 前端使用 Helm Chart 部署,支持自动配置。 + +## 快速开始 + +### 基础用法(自动配置) + +```bash +# 默认部署(从 sealos-system/sealos-config 自动配置) +sealos run desktop-frontend:latest + +# 自定义域名 +sealos run desktop-frontend:latest -e CLOUD_DOMAIN=cloud.example.com + +# 中文版本 + GitHub 登录 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.version=cn --set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=xxx --set desktopConfig.githubClientSecret=yyy" + +# 英文版本 + Google 登录 + GTM +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.googleEnabled=true --set desktopConfig.googleClientId=xxx --set desktopConfig.googleClientSecret=yyy --set desktopConfig.gtmId=GTM-XXX" +``` + +## Values 文件说明 + +Helm Chart 使用两个 values 文件来管理配置: + +### 1. values-default.yaml + +包含 Helm Chart 的默认配置,不应修改。 + +**内容**: + +- **基础设施配置**: 镜像、服务账号、探针、Ingress、调度等 +- **自动配置参考值**: cloudDomain、jwtInternal、databaseMongodbURI 等(从 ConfigMap 自动获取,此处的值仅作为参考) + +**是否修改**: ❌ 不建议修改 + +### 2. values-custom.yaml + +包含需要用户手动配置的自定义选项。 + +**内容**: + +- 资源限制和请求 +- 功能开关 +- OAuth 提供商(GitHub、Google、微信等) +- 短信和邮件配置 +- 计费设置 +- 自定义 URL +- 布局定制 + +**是否修改**: ✅ 根据需要修改 + +**注意**: values-default.yaml 中的自动配置项(如 cloudDomain、jwtInternal 等)会被 entrypoint 脚本从 `sealos-system/sealos-config` ConfigMap 自动获取并覆盖。如需修改这些值,请修改 ConfigMap 或使用 HELM_OPTIONS。 + +详细文档请参考 [VALUES_FILES_GUIDE.md](./VALUES_FILES_GUIDE.md)。 + +## 环境变量 + +### Google Tag Manager + +| 变量 | 默认值 | 描述 | +| -------- | ------ | --------------------- | +| `GTM_ID` | `""` | Google Tag Manager ID | + +### 功能开关 + +| 变量 | 默认值 | 描述 | +| ----------------------------------- | ------- | ----------------- | +| `GUIDE_ENABLED` | `false` | 启用用户引导 | +| `API_ENABLED` | `false` | 启用 API 访问 | +| `RECHARGE_ENABLED` | `false` | 启用充值功能 | +| `ENTERPRISE_REAL_NAME_AUTH_ENABLED` | `false` | 启用企业实名认证 | +| `TRACKING_ENABLED` | `false` | 启用追踪/统计 | +| `REAL_NAME_AUTH_ENABLED` | `false` | 启用实名认证 | +| `LICENSE_CHECK_ENABLED` | `false` | 启用 License 检查 | + +### OAuth 提供商 + +#### GitHub OAuth + +| 变量 | 默认值 | 描述 | +| ---------------------- | ------- | ----------------------- | +| `GITHUB_ENABLED` | `false` | 启用 GitHub OAuth | +| `GITHUB_CLIENT_ID` | `""` | GitHub OAuth 客户端 ID | +| `GITHUB_CLIENT_SECRET` | `""` | GitHub OAuth 客户端密钥 | + +#### 微信 OAuth + +| 变量 | 默认值 | 描述 | +| ---------------------- | ------- | --------------------- | +| `WECHAT_ENABLED` | `false` | 启用微信 OAuth | +| `WECHAT_CLIENT_ID` | `""` | 微信 OAuth 客户端 ID | +| `WECHAT_CLIENT_SECRET` | `""` | 微信 OAuth 客户端密钥 | + +#### Google OAuth + +| 变量 | 默认值 | 描述 | +| ---------------------- | ------- | ----------------------- | +| `GOOGLE_ENABLED` | `false` | 启用 Google OAuth | +| `GOOGLE_CLIENT_ID` | `""` | Google OAuth 客户端 ID | +| `GOOGLE_CLIENT_SECRET` | `""` | Google OAuth 客户端密钥 | + +#### 通用 OAuth2 + +| 变量 | 默认值 | 描述 | +| ---------------------- | ------- | ------------------- | +| `OAUTH2_ENABLED` | `false` | 启用通用 OAuth2 | +| `OAUTH2_CALLBACK_URL` | `""` | OAuth2 回调 URL | +| `OAUTH2_CLIENT_ID` | `""` | OAuth2 客户端 ID | +| `OAUTH2_CLIENT_SECRET` | `""` | OAuth2 客户端密钥 | +| `OAUTH2_AUTH_URL` | `""` | OAuth2 授权 URL | +| `OAUTH2_TOKEN_URL` | `""` | OAuth2 令牌 URL | +| `OAUTH2_USER_INFO_URL` | `""` | OAuth2 用户信息 URL | + +### 验证码配置 + +| 变量 | 默认值 | 描述 | +| ---------------------- | ------- | ------------------------- | +| `TURNSTILE_ENABLED` | `false` | 启用 Cloudflare Turnstile | +| `TURNSTILE_SITE_KEY` | `""` | Turnstile 站点密钥 | +| `TURNSTILE_SECRET_KEY` | `""` | Turnstile 密钥 | + +### 自定义覆盖变量 + +| 变量 | 默认值 | 描述 | +| --------------------------------- | --------------- | ----------------------------------------------- | +| `CLOUD_DOMAIN` | `""` | 覆盖云域名(自动从 sealos-config 获取) | +| `CLOUD_PORT` | `""` | 覆盖云端口(自动从 sealos-config 获取) | +| `CERT_SECRET_NAME` | `wildcard-cert` | TLS 证书 Secret 名称 | +| `REGION_UID` | `""` | 覆盖区域 UID(自动从 sealos-config 获取) | +| `DATABASE_MONGODB_URI` | `""` | 覆盖 MongoDB URI(自动从 sealos-config 获取) | +| `DATABASE_GLOBAL_COCKROACHDB_URI` | `""` | 覆盖全局数据库 URI(自动从 sealos-config 获取) | +| `DATABASE_LOCAL_COCKROACHDB_URI` | `""` | 覆盖本地数据库 URI(自动从 sealos-config 获取) | +| `PASSWORD_SALT` | `""` | 覆盖密码盐(自动从 sealos-config 获取) | +| `JWT_INTERNAL` | `""` | 覆盖内部 JWT(自动从 sealos-config 获取) | +| `JWT_REGIONAL` | `""` | 覆盖区域 JWT(自动从 sealos-config 获取) | +| `JWT_GLOBAL` | `""` | 覆盖全局 JWT(自动从 sealos-config 获取) | + +## 自动配置的值 + +以下值会**自动从 `sealos-system/sealos-config` ConfigMap 获取**,**无需手动配置**(除非需要覆盖): + +| ConfigMap 键 | 目标值 | 描述 | +| ------------------------------ | -------------------------------------------- | -------------------- | +| `cloudDomain` | `desktopConfig.cloudDomain` | 云域名 | +| `cloudPort` | `desktopConfig.cloudPort` | 云端口 | +| `jwtInternal` | `desktopConfig.jwtInternal` | 内部 JWT 密钥 | +| `jwtRegional` | `desktopConfig.jwtRegional` | 区域 JWT 密钥 | +| `jwtGlobal` | `desktopConfig.jwtGlobal` | 全局 JWT 密钥 | +| `jwtMarketingConsent` | `desktopConfig.jwtMarketingConsent` | 营销 consent JWT 密钥 | +| `regionUID` | `desktopConfig.regionUID` | 区域 UID | +| `databaseMongodbURI` | `desktopConfig.databaseMongodbURI` | MongoDB 连接 URI | +| `databaseGlobalCockroachdbURI` | `desktopConfig.databaseGlobalCockroachdbURI` | 全局 CockroachDB URI | +| `databaseLocalCockroachdbURI` | `desktopConfig.databaseLocalCockroachdbURI` | 本地 CockroachDB URI | +| `passwordSalt` | `desktopConfig.passwordSalt` | 密码哈希盐 | + +## ConfigMap 结构 + +生成的 `sealos-desktop-config` ConfigMap 包含以下结构: + +```yaml +cloud: + domain: 'cloud.example.com' + port: '' + regionUID: 'randomRegionUID' + certSecretName: 'wildcard-cert' + proxyDomain: 'cloud.example.com' + allowedOrigins: + - 'https://applaunchpad.cloud.example.com' + - 'https://dbprovider.cloud.example.com' + - 'https://costcenter.cloud.example.com' + - 'https://cronjob.cloud.example.com' + - 'https://objectstorage.cloud.example.com' + - 'https://template.cloud.example.com' + - 'https://terminal.cloud.example.com' + - 'https://kubepanel.cloud.example.com' + - 'https://license.cloud.example.com' + - 'https://devbox.cloud.example.com' + - 'https://aiproxy-web.cloud.example.com' + - 'https://aiproxy.cloud.example.com' + - 'https://sealaf-api.cloud.example.com' + - 'https://sealaf.cloud.example.com' + +common: + guideEnabled: false + apiEnabled: false + rechargeEnabled: false + enterpriseRealNameAuthEnabled: false + trackingEnabled: false + realNameAuthEnabled: false + passwordEnabled: true + realNameReward: 0 + realNameCallbackUrl: 'https://cloud.example.org/api/account/faceIdRealNameAuthCallback' + templateUrl: 'https://template.example.org' + applaunchpadUrl: 'https://applaunchpad.example.org' + dbproviderUrl: 'https://dbprovider.example.org' + objectstorageUrl: 'https://objectstorage.example.org' + cfSiteKey: '' + +database: + mongodbURI: 'mongodb://...' + globalCockroachdbURI: 'postgres://...' + regionalCockroachdbURI: 'postgres://...' + +desktop: + layout: + version: 'en' + title: 'Sealos Cloud' + logo: '/logo.svg' + backgroundImage: '/images/bg-light.svg' + forcedLanguage: 'en' # 自动根据 version 配置: "cn"→"zh", "en"→"en" + customerServiceURL: '' + discordInviteLink: '' # 自动根据 version 配置: "en"时显示, "cn"时为空 + gtmId: null + currencySymbol: 'usd' # 自动根据 version 配置: "cn"→"shellCoin", "en"→"usd" + meta: + title: 'Sealos Cloud' + description: 'Sealos Cloud' + keywords: 'Sealos Cloud' + scripts: [] + noscripts: [] + common: + githubStarEnabled: true + accountSettingEnabled: true + docsUrl: 'https://sealos.run/docs/Intro/' + aiAssistantEnabled: false + bannerEnabled: false + subscriptionEnabled: false + guestModeEnabled: false + emailAlertEnabled: false + phoneAlertEnabled: false + announcementEnabled: false + communityEnabled: true + communityQRCodeImage: '' + communityLink: '' + auth: + proxyAddress: '' + callbackURL: 'https://cloud.example.com/callback' + signUpEnabled: true + baiduToken: '' + hasBaiduToken: false + jwt: + internal: '...' + regional: '...' + global: '...' + idp: + password: + enabled: true + salt: '...' + github: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + wechat: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + google: + enabled: false + proxyAddress: '' + clientID: '' + clientSecret: '' + oauth2: + enabled: false + callbackURL: '' + clientID: '' + proxyAddress: '' + clientSecret: '' + authURL: '' + tokenURL: '' + userInfoURL: '' + sms: + enabled: false + email: + enabled: false + captcha: + turnstile: + enabled: false + siteKey: '' + secretKey: '' + billingUrl: 'http://account-service.account-system.svc:2333' + billingToken: '' + teamManagement: + maxTeamCount: 0 + maxTeamMemberCount: 0 + +tracking: + websiteId: '' + hostUrl: '' + scriptUrl: '' + +realNameOSS: + accessKey: '' + accessKeySecret: '' + endpoint: '' + ssl: false + port: 0 + realNameBucket: '' + enterpriseRealNameBucket: '' +``` + +## AllowedOrigins 扩展配置 + +Desktop Frontend 支持在 `allowedOrigins` 列表中添加自定义的子域名。系统会自动将子域名格式化为 `https://{子域名}.{cloudDomain}` 并添加到现有的 `allowedOrigins` 中。 + +### 配置方式 + +在 `values-custom.yaml` 中添加 `additionalAllowedOriginsPrefixes`: + +```yaml +desktopConfig: + cloudDomain: 'cloud.example.com' + + # 添加额外的 allowedOrigins 子域名 + # 只需传入子域名部分,系统会自动添加 https:// 和 .{cloudDomain} + additionalAllowedOriginsPrefixes: + - 'my-custom-app' + - 'another-service' +``` + +### 生成的结果 + +生成的 `allowedOrigins` 将包含: + +```yaml +allowedOrigins: + # 默认的 origins... + - 'https://sealaf.cloud.example.com' + # 额外的自定义 origins + - 'https://my-custom-app.cloud.example.com' + - 'https://another-service.cloud.example.com' +``` + +### 使用示例 + +**通过 values 文件:** + +```yaml +additionalAllowedOriginsPrefixes: + - 'custom-app' + - 'analytics-service' +``` + +**通过 Helm --set 参数:** + +```bash +helm upgrade -i desktop-frontend ./charts/desktop-frontend \ + --set desktopConfig.additionalAllowedOriginsPrefixes[0]="my-app" \ + --set desktopConfig.additionalAllowedOriginsPrefixes[1]="another-app" +``` + +**通过环境变量:** + +```bash +export HELM_OPTIONS='--set desktopConfig.additionalAllowedOriginsPrefixes[0]="my-app"' +./desktop-frontend-entrypoint.sh +``` + +更多详情请参考 [ALLOWED_ORIGINS_USAGE.md](./ALLOWED_ORIGINS_USAGE.md)。 + +## Helm Chart 参数 + +| 参数 | 描述 | 默认值 | +| ---------------------------------------------------------- | --------------------------- | ------------------------------------------------ | +| `replicaCount` | 副本数 | `1` | +| `image` | 容器镜像 | `ghcr.io/labring/sealos-desktop-frontend:latest` | +| `imagePullPolicy` | 镜像拉取策略 | `IfNotPresent` | +| `fullnameOverride` | 覆盖完整资源名称 | `sealos-desktop` | +| `serviceAccount.create` | 创建服务账号 | `true` | +| `serviceAccount.name` | 服务账号名称 | `desktop-frontend` | +| `service.port` | 服务端口 | `3000` | +| `ciliumNetworkPolicy.enabled` | 限制 Desktop 的入站访问 | `false` | +| `ciliumNetworkPolicy.trustedCallers[0].namespace` | Account Controller 命名空间 | `account-system` | +| `ciliumNetworkPolicy.trustedCallers[0].serviceAccountName` | Account Controller 服务账号 | `account-controller-manager` | +| `ciliumNetworkPolicy.trustedCallers[1].namespace` | Costcenter 命名空间 | `costcenter-frontend` | +| `ciliumNetworkPolicy.trustedCallers[1].serviceAccountName` | Costcenter 服务账号 | `default` | +| `resources.requests.cpu` | CPU 请求 | `100m` | +| `resources.requests.memory` | 内存请求 | `128Mi` | +| `resources.limits.cpu` | CPU 限制 | `2000m` | +| `resources.limits.memory` | 内存限制 | `2048Mi` | +| `ingress.enabled` | 启用 Ingress | `true` | +| `ingress.className` | Ingress 类 | `nginx` | +| `autoConfigEnabled` | 从 sealos-config 自动配置 | `true` | + +Chart 默认不创建入站网络策略。如果没有其他网络策略选择 Desktop,则所有 Pod +都可以连接该服务;绕过 Higress 的调用方也可以自行设置 `X-Real-IP` 等转发头。 +使用网关提供的客户端 IP 实施安全控制时,必须启用此策略或提供等价的流量隔离。 + +将 `ciliumNetworkPolicy.enabled` 设置为 `true` 时,集群必须提供 +`cilium.io/v2/CiliumNetworkPolicy` CRD,否则 Helm 将终止安装。该策略允许 +节点上运行的网关(Cilium `host` 和 `remote-node` 身份)及 +`ciliumNetworkPolicy.trustedCallers` 中配置的 namespace + ServiceAccount 访问 Desktop +的 TCP 3000 端口,其他应用 Pod 不能直接访问。 + +Costcenter 和 Account Controller 保持通过 ClusterIP 调用 Desktop,必须加入 +`trustedCallers`。如果它们的 namespace 或 ServiceAccount 发生变化,应先更新并验证 +策略,再发布调用方;其他未受信任的集群内调用方必须迁移到公网 HTTPS 网关。 +回滚时不得先移除仍在使用 ClusterIP 的调用方白名单,否则对应功能会不可用。 + +## 故障排查 + +### 检查部署状态 + +```bash +# 检查 helm 发布 +helm status desktop-frontend -n sealos + +# 检查 pods +kubectl get pods -n sealos -l app.kubernetes.io/name=desktop-frontend + +# 检查 configmap +kubectl get configmap sealos-desktop-config -n sealos -o yaml + +# 检查 ingress +kubectl get ingress sealos-desktop -n sealos + +# 查看日志 +kubectl logs -n sealos -l app.kubernetes.io/name=desktop-frontend --tail=100 -f +``` + +### 常见问题 + +**问题**: Ingress 返回 404 或 502 + +- **解决**: 检查 service 是否存在且端口正确:`kubectl get svc sealos-desktop -n sealos` + +**问题**: Pod 处于 CrashLoopBackOff 状态 + +- **解决**: 查看日志,检查数据库连接错误或缺失的环境变量 + +**问题**: OAuth 回调失败 + +- **解决**: 验证配置中的 `callbackURL` 是否与 OAuth 应用设置匹配 + +**问题**: 现有资源阻止安装 + +- **解决**: 脚本会自动通过添加 Helm 标签来接纳现有资源 + +## 高级用法 + +### 使用 Helm Values 自定义配置 + +所有 config.yaml 设置都可以通过 `HELM_OPTIONS` 环境变量传递 Helm `--set` 参数来自定义。完整文档请参阅 [HELM_VALUES_GUIDE_CN.md](HELM_VALUES_GUIDE_CN.md)。 + +**快速示例:** + +```bash +# UI 自定义通过 HELM_OPTIONS +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.layoutTitle=\"我的云平台\" --set desktopConfig.metaTitle=\"我的云平台\"" + +# OAuth 提供商 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-client-id --set desktopConfig.githubClientSecret=your-client-secret" + +# 功能和通讯配置 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.guideEnabled=true --set desktopConfig.rechargeEnabled=true --set desktopConfig.smsEnabled=true --set desktopConfig.emailEnabled=true --set desktopConfig.emailHost=smtp.example.com --set desktopConfig.emailPort=587" + +# 结合环境变量和 HELM_OPTIONS +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.layoutTitle=\"我的云平台\"" \ + -e CLOUD_DOMAIN=override.example.com \ + -e GITHUB_ENABLED=true +``` + +**常用自定义选项:** + +- **UI 自定义**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL` +- **OAuth 提供商**: `githubEnabled`, `googleEnabled`, `wechatEnabled`, `oauth2Enabled` 及其对应的 `*ClientId`, `*ClientSecret` +- **功能开关**: `guideEnabled`, `rechargeEnabled`, `trackingEnabled`, `apiEnabled`, `realNameAuthEnabled` +- **通讯配置**: `smsEnabled`, `emailEnabled`, `emailHost`, `emailPort`, `emailUser`, `emailPassword` +- **URL 配置**: `workorderUrl`,以及基于 `cloudDomain` 自动生成的服务地址(`template`、`applaunchpad`、`dbprovider`、`objectstorage`) +- **数据库配置**: `databaseMongodbURI`, `databaseGlobalCockroachdbURI`, `databaseLocalCockroachdbURI` +- **团队管理**: `maxTeamCount`, `maxTeamMemberCount` + +查看 23 个分类共 60+ 可配置参数,请参阅 [HELM_VALUES_GUIDE_CN.md](HELM_VALUES_GUIDE_CN.md)。 + +### 禁用自动配置 + +```bash +sealos run desktop-frontend:latest \ + -e AUTO_CONFIG_ENABLED=false \ + -e CLOUD_DOMAIN=cloud.example.com \ + -e DATABASE_MONGODB_URI=mongodb://... +``` + +### 自定义 Helm 选项 + +```bash +sealos run desktop-frontend:latest -e HELM_OPTIONS="--timeout 10m --install" +``` + +### 覆盖命名空间 + +```bash +sealos run desktop-frontend:latest -e RELEASE_NAMESPACE=my-namespace +``` + +## 构建镜像 + +```bash +sealos build -t docker.io/labring/sealos-cloud-desktop:latest -f Kubefile . +``` + +## 常见问题 + +### 1. 如何配置中文/英文版本? + +**中文版本:** + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.version=cn" +``` + +**英文版本:** + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.version=en" +``` + +### 2. 如何启用第三方登录? + +**方式 1: 使用环境变量(推荐)** + +```bash +sealos run desktop-frontend:latest \ + -e GITHUB_ENABLED=true \ + -e GITHUB_CLIENT_ID=your-client-id \ + -e GITHUB_CLIENT_SECRET=your-client-secret +``` + +**方式 2: 使用 HELM_OPTIONS(推荐用于复杂配置)** + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-client-id --set desktopConfig.githubClientSecret=your-client-secret" +``` + +### 3. 如何更新配置? + +**方式 1: 使用环境变量(推荐用于少量配置)** + +```bash +sealos run desktop-frontend:latest -e CLOUD_DOMAIN=new.example.com +``` + +**方式 2: 使用 HELM_OPTIONS(推荐用于多个配置)** + +```bash +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=new.example.com --set desktopConfig.layoutTitle=\"New Title\" --set desktopConfig.guideEnabled=true" +``` + +**方式 3: 直接编辑 ConfigMap** + +```bash +kubectl edit configmap sealos-desktop-config -n sealos +kubectl rollout restart deployment sealos-desktop -n sealos +``` + +### 4. 配置文件在哪里? + +配置存储在 `sealos-desktop-config` ConfigMap 中,挂载到 Pod 的 `/app/data/config.yaml`。 + +查看配置: + +```bash +kubectl get configmap sealos-desktop-config -n sealos -o yaml +kubectl exec -n sealos deployment/sealos-desktop -- cat /app/data/config.yaml +``` + +### 5. 环境变量和 HELM_OPTIONS 的区别和优先级? + +**使用场景:** + +- **环境变量**: 少量配置、快速测试、常用配置项 +- **HELM_OPTIONS**: 多个配置、生产部署、访问所有 60+ 参数 + +**优先级从高到低:** + +1. 环境变量(如 `CLOUD_DOMAIN`) +2. HELM_OPTIONS 中的 `--set` 参数 +3. values.yaml 默认值 + +**示例:** + +```bash +# 环境变量会覆盖 HELM_OPTIONS 的值 +sealos run desktop-frontend:latest \ + -e HELM_OPTIONS="--set desktopConfig.cloudDomain=from-helm.com" \ + -e CLOUD_DOMAIN=from-env.com # 这个值会生效 +``` + +## 技术支持 + +- 文档: https://sealos.run/docs/ +- GitHub: https://github.com/labring/sealos +- 问题反馈: https://github.com/labring/sealos/issues diff --git a/frontend/desktop/deploy/charts/desktop-frontend/Chart.yaml b/frontend/desktop/deploy/charts/desktop-frontend/Chart.yaml new file mode 100644 index 000000000000..048adf26178b --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: desktop-frontend +description: Helm chart for sealos desktop frontend +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/frontend/desktop/deploy/charts/desktop-frontend/desktop-frontend-values.yaml b/frontend/desktop/deploy/charts/desktop-frontend/desktop-frontend-values.yaml new file mode 100644 index 000000000000..b590f7c6d033 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/desktop-frontend-values.yaml @@ -0,0 +1,196 @@ +# Custom values for desktop frontend helm chart. +# This file contains user-customizable configurations. +# +# Note: The following configurations are automatically fetched from sealos-system/sealos-config ConfigMap +# and do NOT need to be manually configured: +# - cloudDomain, cloudPort, regionUID +# - jwtInternal, jwtRegional, jwtGlobal, jwtMarketingConsent +# - passwordSalt +# - databaseMongodbURI, databaseGlobalCockroachdbURI, databaseLocalCockroachdbURI +# +# See values-auto.yaml for reference values and documentation. + +replicaCount: 1 + +# Prisma database backend used by the init container migrations. +# Supported values: cockroachdb, postgresql +# global: +# featureConfigs: +# database: +# type: cockroachdb + +resources: + limits: + cpu: 2000m + memory: 2048Mi + requests: + cpu: 100m + memory: 128Mi + +# Desktop frontend configuration (user-customizable) +desktopConfig: + proxyDomain: "" + + # Billing configuration + billingUrl: "http://account-service.account-system.svc:2333" + billingToken: "" + cloudVitrualMachineUrl: "" + + # Currency and language configuration + # Options: "cn" (Chinese) or "en" (English) + # forcedLanguage and currencySymbol will be auto-configured based on version: + # - version: "cn" → forcedLanguage: "zh", currencySymbol: "shellCoin" + # - version: "en" → forcedLanguage: "en", currencySymbol: "usd" + version: "en" + + # Google Tag Manager + gtmId: "" + + # Rybbit Analytics (self-hosted); empty disables tracking + rybbitHost: "" + rybbitSiteId: "" + + # Discord invite link (USD version only) + discordInviteLink: "" + + # Feature flags + guideEnabled: false + apiEnabled: false + rechargeEnabled: false + enterpriseRealNameAuthEnabled: false + trackingEnabled: false + realNameAuthEnabled: false + kcRotationEnabled: false + communityEnabled: true + communityQRCodeImage: "" + communityLink: "" + + # Common URLs (default values) + realNameReward: 0 + realNameCallbackUrl: "https://cloud.example.org/api/account/faceIdRealNameAuthCallback" + cfSiteKey: "" + + # Desktop layout configuration + layoutTitle: "Sealos Cloud" + layoutLogo: "/logo.svg" + layoutBackgroundImage: "/images/bg-light.svg" + customerServiceURL: "" + layoutDocsUseDocs: false + layoutDocsUrl: "https://sealos.run/docs/Intro/" + layoutGithubStarEnabled: true + layoutWorkorderEnabled: false + layoutAccountSettingEnabled: true + layoutAiAssistantEnabled: false + layoutBannerEnabled: false + layoutAnnouncementEnabled: false + + authTitle: + zh: "欢迎来到 Sealos" + en: "Welcome to Sealos" + authBackgroundImage: + zh: "/images/bg-light.svg" + en: "/images/bg-light.svg" + protocol: + serviceProtocol: + zh: https://sealos.io/zh-Hans/docs/msa/terms-of-service + en: https://sealos.io/docs/msa/terms-of-service + privateProtocol: + zh: https://sealos.io/zh-Hans/docs/msa/privacy-policy + en: https://sealos.io/docs/msa/privacy-policy + # Meta tags + metaTitle: "Sealos Cloud" + metaDescription: "Sealos Cloud" + metaKeywords: "Sealos Cloud" + + signUpEnabled: true + baiduToken: "" + bingAdTenant: "" + bingAdClientId: "" + bingAdClientSecret: "" + bingAdRefreshToken: "" + bingAdDeveloperToken: "" + bingAdCustomerId: 0 + bingAdCustomerAccountId: 0 + bingAdConversionName: "" + + passwordEnabled: true + # OAuth providers + githubEnabled: false + githubClientId: "" + githubClientSecret: "" + githubProxyAddress: "" + + wechatEnabled: false + wechatClientId: "" + wechatClientSecret: "" + wechatProxyAddress: "" + + googleEnabled: false + googleClientId: "" + googleClientSecret: "" + googleProxyAddress: "" + + oauth2Enabled: false + oauth2Pkce: false + oauth2CallbackUrl: "" + oauth2ClientId: "" + oauth2ClientSecret: "" + oauth2AuthUrl: "" + oauth2TokenUrl: "" + oauth2UserInfoUrl: "" + oauth2ProxyAddress: "" + oauth2DisplayName: "" + + # Captcha configuration + turnstileEnabled: false + turnstileSiteKey: "" + turnstileSecretKey: "" + + # SMS configuration (Aliyun) + smsEnabled: false + smsAliEnabled: false + smsAliEndpoint: "" + smsAliTemplateCode: "" + smsAliSignName: "" + smsAliAccessKeyID: "" + smsAliAccessKeySecret: "" + + # Email configuration + emailEnabled: false + emailHost: "" + emailPort: 0 + emailUser: "" + emailPassword: "" + emailLanguage: "" + + # Ali captcha configuration + aliCaptchaEnabled: false + aliCaptchaEndpoint: "" + aliCaptchaSceneId: "" + aliCaptchaPrefix: "" + aliCaptchaAccessKeyID: "" + aliCaptchaAccessKeySecret: "" + + inviteEnabled: false + inviteLafSecretKey: "" + inviteLafBaseURL: "" + + # Tracking configuration + trackingWebsiteId: "" + trackingHostUrl: "" + + # RealName OSS configuration + realNameOSSAccessKey: "" + realNameOSSAccessKeySecret: "" + realNameOSSEndpoint: "" + realNameOSSSSL: false + realNameOSSPort: 0 + realNameOSSRealNameBucket: "" + realNameOSSEnterpriseRealNameBucket: "" + + # Workorder and cloud virtual machine URLs + workorderUrl: "" + + # Team management + maxTeamCount: 0 + maxTeamMemberCount: 0 diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/NOTES.txt b/frontend/desktop/deploy/charts/desktop-frontend/templates/NOTES.txt new file mode 100644 index 000000000000..7ec2ecabaa2e --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/NOTES.txt @@ -0,0 +1,48 @@ +Thank you for installing {{ .Chart.Name }}! + +Your release is named {{ .Release.Name }}. + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} + +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "desktop.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "desktop.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "desktop.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} + +2. Check the deployment status: + + $ kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "desktop.name" . }} + +3. View logs: + + $ kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "desktop.name" . }} --tail=100 -f + +4. Get the configuration: + + $ kubectl get configmap {{ include "desktop.fullname" . }}-config -n {{ .Release.Namespace }} -o yaml + +For more information, visit the documentation: + https://sealos.run/docs/ diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/_helpers.tpl b/frontend/desktop/deploy/charts/desktop-frontend/templates/_helpers.tpl new file mode 100644 index 000000000000..31e50e2ce6e3 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "desktop.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "desktop.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "desktop.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "desktop.labels" -}} +helm.sh/chart: {{ include "desktop.chart" . }} +{{ include "desktop.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "desktop.selectorLabels" -}} +app.kubernetes.io/name: {{ include "desktop.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "desktop.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "desktop.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/cilium-network-policy.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/cilium-network-policy.yaml new file mode 100644 index 000000000000..31af2951d456 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/cilium-network-policy.yaml @@ -0,0 +1,36 @@ +{{- if .Values.ciliumNetworkPolicy.enabled }} +{{- if not (.Capabilities.APIVersions.Has "cilium.io/v2/CiliumNetworkPolicy") }} +{{- fail "ciliumNetworkPolicy.enabled requires the cilium.io/v2/CiliumNetworkPolicy CRD" }} +{{- end }} +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: {{ printf "%s-ingress" (include "desktop.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "desktop.labels" . | nindent 4 }} +spec: + endpointSelector: + matchLabels: + app: {{ include "desktop.name" . }} + ingress: + - fromEntities: + - host + - remote-node + toPorts: + - ports: + - port: "3000" + protocol: TCP + {{- range .Values.ciliumNetworkPolicy.trustedCallers }} + {{- if and .namespace .serviceAccountName }} + - fromEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: {{ .namespace | quote }} + io.cilium.k8s.policy.serviceaccount: {{ .serviceAccountName | quote }} + toPorts: + - ports: + - port: "3000" + protocol: TCP + {{- end }} + {{- end }} +{{- end }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/configmap.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/configmap.yaml new file mode 100644 index 000000000000..bf123444a10a --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/configmap.yaml @@ -0,0 +1,200 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "desktop.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "desktop.labels" . | nindent 4 }} +data: + config.yaml: | + cloud: + domain: "{{ .Values.desktopConfig.cloudDomain }}" + port: "{{ .Values.desktopConfig.cloudPort }}" + regionUID: "{{ .Values.desktopConfig.regionUID }}" + certSecretName: "{{ .Values.desktopConfig.certSecretName }}" + proxyDomain: "{{ .Values.desktopConfig.proxyDomain }}" + allowedOrigins: +{{- if .Values.desktopConfig.allowedAllOrigins }} + - "*" +{{- else }} +{{- $defaultAllowedOrigins := list "costcenter" "license" -}} +{{- $allowedOrigins := .Values.desktopConfig.allowedOrigins | default $defaultAllowedOrigins -}} +{{- range $origin := $allowedOrigins }} + - "https://{{ $origin }}.{{ $.Values.desktopConfig.cloudDomain }}" +{{- end }} +{{- end }} + + common: + guideEnabled: {{ .Values.desktopConfig.guideEnabled }} + apiEnabled: {{ .Values.desktopConfig.apiEnabled }} + rechargeEnabled: {{ .Values.desktopConfig.rechargeEnabled }} + enterpriseRealNameAuthEnabled: {{ .Values.desktopConfig.enterpriseRealNameAuthEnabled }} + trackingEnabled: {{ .Values.desktopConfig.trackingEnabled }} + realNameAuthEnabled: {{ .Values.desktopConfig.realNameAuthEnabled }} + realNameReward: {{ .Values.desktopConfig.realNameReward }} + realNameCallbackUrl: "{{ .Values.desktopConfig.realNameCallbackUrl }}" + templateUrl: "https://template.{{ .Values.desktopConfig.cloudDomain }}" + objectstorageUrl: "https://objectstorage.{{ .Values.desktopConfig.cloudDomain }}" + applaunchpadUrl: "https://applaunchpad.{{ .Values.desktopConfig.cloudDomain }}" + dbproviderUrl: "https://dbprovider.{{ .Values.desktopConfig.cloudDomain }}" + cfSiteKey: "{{ .Values.desktopConfig.cfSiteKey }}" + + database: + mongodbURI: "{{ .Values.desktopConfig.databaseMongodbURI }}" + globalCockroachdbURI: "{{ .Values.desktopConfig.databaseGlobalCockroachdbURI }}" + regionalCockroachdbURI: "{{ .Values.desktopConfig.databaseLocalCockroachdbURI }}" + + desktop: + layout: + version: "{{ .Values.desktopConfig.version }}" + title: "{{ .Values.desktopConfig.layoutTitle }}" + logo: "{{ .Values.desktopConfig.layoutLogo }}" + backgroundImage: "{{ .Values.desktopConfig.layoutBackgroundImage }}" + forcedLanguage: {{ if eq .Values.desktopConfig.version "cn" }}"zh"{{ else }}"en"{{ end }} + customerServiceURL: "{{ .Values.desktopConfig.customerServiceURL }}" + discordInviteLink: {{ if eq .Values.desktopConfig.version "en" }}"{{ .Values.desktopConfig.discordInviteLink }}"{{ else }}""{{ end }} + gtmId: {{ if .Values.desktopConfig.gtmId }}"{{ .Values.desktopConfig.gtmId }}"{{ else }}null{{ end }} + rybbitHost: {{ if .Values.desktopConfig.rybbitHost }}"{{ .Values.desktopConfig.rybbitHost }}"{{ else }}null{{ end }} + rybbitSiteId: {{ if .Values.desktopConfig.rybbitSiteId }}"{{ .Values.desktopConfig.rybbitSiteId }}"{{ else }}null{{ end }} + currencySymbol: {{ if eq .Values.desktopConfig.version "cn" }}"shellCoin"{{ else }}"usd"{{ end }} + authTitle: + zh: "{{.Values.desktopConfig.authTitle.zh}}" + en: "{{.Values.desktopConfig.authTitle.en}}" + authBackgroundImage: + zh: "{{.Values.desktopConfig.authBackgroundImage.zh}}" + en: "{{.Values.desktopConfig.authBackgroundImage.en}}" + protocol: + serviceProtocol: + zh: "{{.Values.desktopConfig.protocol.serviceProtocol.zh}}" + en: "{{.Values.desktopConfig.protocol.serviceProtocol.en}}" + privateProtocol: + zh: "{{.Values.desktopConfig.protocol.privateProtocol.zh}}" + en: "{{.Values.desktopConfig.protocol.privateProtocol.en}}" + meta: + title: "{{ .Values.desktopConfig.metaTitle }}" + description: "{{ .Values.desktopConfig.metaDescription }}" + keywords: "{{ .Values.desktopConfig.metaKeywords }}" + scripts: [] + noscripts: [] + common: + githubStarEnabled: {{ .Values.desktopConfig.layoutGithubStarEnabled }} + workorderEnabled: {{ .Values.desktopConfig.layoutWorkorderEnabled }} + accountSettingEnabled: {{ .Values.desktopConfig.layoutAccountSettingEnabled }} + {{- if .Values.desktopConfig.layoutDocsUseDocs }} + docsUrl: "https://docs.{{ .Values.desktopConfig.cloudDomain }}/docs/" + {{- else }} + docsUrl: "{{ .Values.desktopConfig.layoutDocsUrl }}" + {{- end }} + aiAssistantEnabled: false + bannerEnabled: false + subscriptionEnabled: false + guestModeEnabled: false + emailAlertEnabled: false + phoneAlertEnabled: false + announcementEnabled: false + kcRotationEnabled: {{ .Values.desktopConfig.kcRotationEnabled }} + communityEnabled: {{ .Values.desktopConfig.communityEnabled }} + communityQRCodeImage: {{ .Values.desktopConfig.communityQRCodeImage | quote }} + communityLink: {{ .Values.desktopConfig.communityLink | quote }} + auth: + callbackURL: "https://{{ .Values.desktopConfig.cloudDomain }}{{ if .Values.desktopConfig.cloudPort }}:{{ .Values.desktopConfig.cloudPort }}{{ end }}/callback" + signUpEnabled: {{ .Values.desktopConfig.signUpEnabled }} + baiduToken: "{{ .Values.desktopConfig.baiduToken }}" + bingAd: + tenant: "{{ .Values.desktopConfig.bingAdTenant }}" + clientId: "{{ .Values.desktopConfig.bingAdClientId }}" + clientSecret: "{{ .Values.desktopConfig.bingAdClientSecret }}" + refreshToken: "{{ .Values.desktopConfig.bingAdRefreshToken }}" + developerToken: "{{ .Values.desktopConfig.bingAdDeveloperToken }}" + customerId: {{ .Values.desktopConfig.bingAdCustomerId }} + customerAccountId: {{ .Values.desktopConfig.bingAdCustomerAccountId }} + conversionName: "{{ .Values.desktopConfig.bingAdConversionName }}" + jwt: + internal: "{{ .Values.desktopConfig.jwtInternal }}" + regional: "{{ .Values.desktopConfig.jwtRegional }}" + global: "{{ .Values.desktopConfig.jwtGlobal }}" + marketingConsent: "{{ .Values.desktopConfig.jwtMarketingConsent }}" + invite: + enabled: {{ .Values.desktopConfig.inviteEnabled }} + lafSecretKey: "{{ .Values.desktopConfig.inviteLafSecretKey }}" + lafBaseURL: "{{ .Values.desktopConfig.inviteLafBaseURL }}" + idp: + password: + enabled: {{.Values.desktopConfig.passwordEnabled }} + salt: "{{ .Values.desktopConfig.passwordSalt }}" + github: + enabled: {{ .Values.desktopConfig.githubEnabled }} + proxyAddress: "{{ .Values.desktopConfig.githubProxyAddress }}" + clientID: "{{ .Values.desktopConfig.githubClientId }}" + clientSecret: "{{ .Values.desktopConfig.githubClientSecret }}" + wechat: + enabled: {{ .Values.desktopConfig.wechatEnabled }} + proxyAddress: "{{ .Values.desktopConfig.wechatProxyAddress }}" + clientID: "{{ .Values.desktopConfig.wechatClientId }}" + clientSecret: "{{ .Values.desktopConfig.wechatClientSecret }}" + google: + enabled: {{ .Values.desktopConfig.googleEnabled }} + proxyAddress: "{{ .Values.desktopConfig.googleProxyAddress }}" + clientID: "{{ .Values.desktopConfig.googleClientId }}" + clientSecret: "{{ .Values.desktopConfig.googleClientSecret }}" + oneTapOrigins: +{{ toYaml .Values.desktopConfig.googleOneTapOrigins | indent 14 }} + oauth2: + enabled: {{ .Values.desktopConfig.oauth2Enabled }} + pkce: {{ .Values.desktopConfig.oauth2Pkce }} + callbackURL: "{{ .Values.desktopConfig.oauth2CallbackUrl }}" + clientID: "{{ .Values.desktopConfig.oauth2ClientId }}" + proxyAddress: "{{ .Values.desktopConfig.oauth2ProxyAddress }}" + clientSecret: "{{ .Values.desktopConfig.oauth2ClientSecret }}" + authURL: "{{ .Values.desktopConfig.oauth2AuthUrl }}" + tokenURL: "{{ .Values.desktopConfig.oauth2TokenUrl }}" + userInfoURL: "{{ .Values.desktopConfig.oauth2UserInfoUrl }}" + displayName: "{{ .Values.desktopConfig.oauth2DisplayName }}" + sms: + enabled: {{ .Values.desktopConfig.smsEnabled }} + ali: + enabled: {{ .Values.desktopConfig.smsAliEnabled }} + endpoint: "{{ .Values.desktopConfig.smsAliEndpoint }}" + templateCode: "{{ .Values.desktopConfig.smsAliTemplateCode }}" + signName: "{{ .Values.desktopConfig.smsAliSignName }}" + accessKeyID: "{{ .Values.desktopConfig.smsAliAccessKeyID }}" + accessKeySecret: "{{ .Values.desktopConfig.smsAliAccessKeySecret }}" + email: + enabled: {{ .Values.desktopConfig.emailEnabled }} + host: "{{ .Values.desktopConfig.emailHost }}" + port: {{ .Values.desktopConfig.emailPort }} + user: "{{ .Values.desktopConfig.emailUser }}" + password: "{{ .Values.desktopConfig.emailPassword }}" + language: "{{ .Values.desktopConfig.emailLanguage }}" + captcha: + turnstile: + enabled: {{ .Values.desktopConfig.turnstileEnabled }} + siteKey: "{{ .Values.desktopConfig.turnstileSiteKey }}" + secretKey: "{{ .Values.desktopConfig.turnstileSecretKey }}" + ali: + enabled: {{ .Values.desktopConfig.aliCaptchaEnabled }} + endpoint: "{{ .Values.desktopConfig.aliCaptchaEndpoint }}" + sceneId: "{{ .Values.desktopConfig.aliCaptchaSceneId }}" + prefix: "{{ .Values.desktopConfig.aliCaptchaPrefix }}" + accessKeyID: "{{ .Values.desktopConfig.aliCaptchaAccessKeyID }}" + accessKeySecret: "{{ .Values.desktopConfig.aliCaptchaAccessKeySecret }}" + billingUrl: "{{ .Values.desktopConfig.billingUrl }}" + billingToken: "{{ .Values.desktopConfig.billingToken }}" + workorderUrl: "{{ .Values.desktopConfig.workorderUrl }}" + cloudVitrualMachineUrl: "{{ .Values.desktopConfig.cloudVitrualMachineUrl }}" + + teamManagement: + maxTeamCount: {{ .Values.desktopConfig.maxTeamCount }} + maxTeamMemberCount: {{ .Values.desktopConfig.maxTeamMemberCount }} + + tracking: + websiteId: "{{ .Values.desktopConfig.trackingWebsiteId }}" + hostUrl: {{ if .Values.desktopConfig.trackingHostUrl }}"{{ .Values.desktopConfig.trackingHostUrl }}"{{ else if .Values.desktopConfig.trackingEnabled }}"https://umami.sealos.run"{{ else }}""{{ end }} + realNameOSS: + accessKey: "{{ .Values.desktopConfig.realNameOSSAccessKey }}" + accessKeySecret: "{{ .Values.desktopConfig.realNameOSSAccessKeySecret }}" + endpoint: "{{ .Values.desktopConfig.realNameOSSEndpoint }}" + ssl: {{ .Values.desktopConfig.realNameOSSSSL }} + port: {{ .Values.desktopConfig.realNameOSSPort }} + realNameBucket: "{{ .Values.desktopConfig.realNameOSSRealNameBucket }}" + enterpriseRealNameBucket: "{{ .Values.desktopConfig.realNameOSSEnterpriseRealNameBucket }}" diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/deployment.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/deployment.yaml new file mode 100644 index 000000000000..c241ec1b9353 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/deployment.yaml @@ -0,0 +1,112 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "desktop.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ include "desktop.name" . }} + {{- include "desktop.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ include "desktop.name" . }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + template: + metadata: + labels: + app: {{ include "desktop.name" . }} + {{- include "desktop.selectorLabels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- $globalPrismaSchema := "/app/desktop/prisma/global/schema.prisma" }} + {{- $regionPrismaSchema := "/app/desktop/prisma/region/schema.prisma" }} + {{- if eq .Values.global.featureConfigs.database.type "postgresql" }} + {{- $globalPrismaSchema = "/app/desktop/prisma/providers/postgresql/global/schema.prisma" }} + {{- $regionPrismaSchema = "/app/desktop/prisma/providers/postgresql/region/schema.prisma" }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "desktop.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: init-database + image: "{{ .Values.image }}" + env: + - name: PRISMA_DB_PROVIDER + value: "{{ .Values.global.featureConfigs.database.type }}" + command: ["/bin/sh", "-c"] + args: + - | + export REGION_DATABASE_URL=$(cat /app/data/config.yaml | awk '/regionalCockroachdbURI:/ {print $2}' | tr -d '"') + export GLOBAL_DATABASE_URL=$(cat /app/data/config.yaml | awk '/globalCockroachdbURI:/ {print $2}' | tr -d '"') + prisma migrate deploy --schema {{ $globalPrismaSchema }} + prisma migrate deploy --schema {{ $regionPrismaSchema }} + volumeMounts: + - mountPath: /app/data/config.yaml + name: desktop-frontend-volume + subPath: config.yaml + containers: + - name: {{ include "desktop.name" . }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + env: + - name: PRISMA_DB_PROVIDER + value: "{{ .Values.global.featureConfigs.database.type }}" + command: ["/bin/sh", "-c"] + args: + - | + REGION_DATABASE_URL=$(cat /app/data/config.yaml | awk '/regionalCockroachdbURI:/ {print $2}' | tr -d '"') \ + GLOBAL_DATABASE_URL=$(cat /app/data/config.yaml | awk '/globalCockroachdbURI:/ {print $2}' | tr -d '"') \ + node /app/desktop/server.js + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + image: "{{ .Values.image }}" + imagePullPolicy: {{ .Values.imagePullPolicy }} + volumeMounts: + - mountPath: /app/data/config.yaml + name: desktop-frontend-volume + subPath: config.yaml + resources: + {{- toYaml .Values.resources | nindent 12 }} + affinity: + {{- if .Values.affinity }} + {{- toYaml .Values.affinity | nindent 8 }} + {{- else }} + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "app" + operator: In + values: + - {{ include "desktop.name" . }} + topologyKey: "kubernetes.io/hostname" + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: desktop-frontend-volume + configMap: + name: {{ include "desktop.fullname" . }}-config diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/ingress.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/ingress.yaml new file mode 100644 index 000000000000..1df437c88e08 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "desktop.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "desktop.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "desktop.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} +{{- end }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/rbac.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/rbac.yaml new file mode 100644 index 000000000000..31bd371cbed1 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/rbac.yaml @@ -0,0 +1,150 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "desktop.fullname" . }}-manager-role +rules: + - apiGroups: + - user.sealos.io + resources: + - users + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - users/status + verbs: + - get + - apiGroups: + - user.sealos.io + resources: + - operationrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - operationrequests/status + verbs: + - get + - apiGroups: + - user.sealos.io + resources: + - deleterequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - user.sealos.io + resources: + - deleterequests/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "desktop.fullname" . }}-user-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "desktop.fullname" . }}-manager-role +subjects: + - kind: ServiceAccount + name: {{ include "desktop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "desktop.fullname" . }}-recharge-gift-cm-reader + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["recharge-gift"] + verbs: ["get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "desktop.fullname" . }}-recharge-gift-cm-reader-rolebinding + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "desktop.fullname" . }}-recharge-gift-cm-reader +subjects: + - kind: Group + name: system:serviceaccounts + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "desktop.fullname" . }}-account-editor-role +rules: + - apiGroups: ["account.sealos.io"] + resources: ["accounts"] + verbs: ["list", "get", "create", "update", "patch", "watch"] + - apiGroups: ["notification.sealos.io"] + resources: ["notifications"] + verbs: ["list", "get", "create", "update", "patch", "watch"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["patch"] + - apiGroups: ['license.sealos.io'] + resources: ['licenses'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "desktop.fullname" . }}-account-editor-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "desktop.fullname" . }}-account-editor-role +subjects: + - kind: ServiceAccount + name: {{ include "desktop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "desktop.fullname" . }}-app-reader-role +rules: + - apiGroups: ["app.sealos.io"] + resources: ["apps"] + verbs: ["list", "get", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "desktop.fullname" . }}-app-reader-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "desktop.fullname" . }}-app-reader-role +subjects: + - kind: ServiceAccount + name: {{ include "desktop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/service.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/service.yaml new file mode 100644 index 000000000000..3c050095d1bc --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "desktop.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "desktop.labels" . | nindent 4 }} +spec: + ports: + - port: {{ .Values.service.port }} + targetPort: 3000 + protocol: TCP + name: http + selector: + app: {{ include "desktop.name" . }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/templates/serviceaccount.yaml b/frontend/desktop/deploy/charts/desktop-frontend/templates/serviceaccount.yaml new file mode 100644 index 000000000000..5903393f4084 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "desktop.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "desktop.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/frontend/desktop/deploy/charts/desktop-frontend/values.yaml b/frontend/desktop/deploy/charts/desktop-frontend/values.yaml new file mode 100644 index 000000000000..c22ffc56ea81 --- /dev/null +++ b/frontend/desktop/deploy/charts/desktop-frontend/values.yaml @@ -0,0 +1,136 @@ +# Default values for desktop frontend helm chart. +global: + featureConfigs: + database: + # Database backend for Prisma migrations. + # Supported values: cockroachdb, postgresql + type: cockroachdb +image: ghcr.io/labring/sealos-desktop-frontend:latest +imagePullPolicy: IfNotPresent + +imagePullSecrets: [] +fullnameOverride: "sealos-desktop" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "desktop-frontend" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + runAsUser: 1001 + +securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + +service: + port: 3000 + +# Opt in to restricting Desktop ingress to node-hosted Higress gateways and +# explicitly trusted internal callers. When disabled, this chart creates no +# ingress policy; absent another policy, every pod can connect to Desktop. +ciliumNetworkPolicy: + enabled: false + trustedCallers: + - namespace: account-system + serviceAccountName: account-controller-manager + - namespace: costcenter-frontend + serviceAccountName: default + +# Auto configure from sealos-system configmap +autoConfigEnabled: true + +# ============================================================================ +# Auto-configured values (from sealos-system/sealos-config ConfigMap) +# ============================================================================ +# The following desktopConfig values are automatically fetched from the +# sealos-system/sealos-config ConfigMap by the entrypoint script and will +# override any values set here. These are provided as reference defaults. +# +# To override these auto-configured values, use HELM_OPTIONS or modify the +# sealos-config ConfigMap directly. +# ============================================================================ + +desktopConfig: + # Basic cloud configuration (auto-configured from sealos-config) + cloudDomain: "127.0.0.1.nip.io" # Auto-fetched from sealos-config.cloudDomain + cloudPort: "" # Auto-fetched from sealos-config.cloudPort + regionUID: "randomRegionUID" # Auto-fetched from sealos-config.regionUID + certSecretName: "wildcard-cert" # Fixed value + proxyDomain: "" # Auto-fetched from sealos-config.proxyDomain + + allowedAllOrigins: false + allowedOrigins: + - "costcenter" + - "license" + googleOneTapOrigins: [] + + # Database connections (auto-configured from sealos-config) + databaseMongodbURI: "" # Auto-fetched from sealos-config.databaseMongodbURI + databaseGlobalCockroachdbURI: "" # Auto-fetched from sealos-config.databaseGlobalCockroachdbURI + databaseLocalCockroachdbURI: "" # Auto-fetched from sealos-config.databaseLocalCockroachdbURI + + # Authentication secrets (auto-configured from sealos-config) + passwordSalt: "randomSalt" # Auto-fetched from sealos-config.passwordSalt + jwtInternal: "" # Auto-fetched from sealos-config.jwtInternal + jwtRegional: "" # Auto-fetched from sealos-config.jwtRegional + jwtGlobal: "" # Auto-fetched from sealos-config.jwtGlobal + jwtMarketingConsent: "" # Auto-fetched from sealos-config.jwtMarketingConsent + + # Render-safe defaults for nested layout fields. CI/sealos build may render + # the chart with only this base values file loaded. + authTitle: + zh: "欢迎来到 Sealos" + en: "Welcome to Sealos" + authBackgroundImage: + zh: "/images/bg-light.svg" + en: "/images/bg-light.svg" + protocol: + serviceProtocol: + zh: https://sealos.io/zh-Hans/docs/msa/terms-of-service + en: https://sealos.io/docs/msa/terms-of-service + privateProtocol: + zh: https://sealos.io/zh-Hans/docs/msa/privacy-policy + en: https://sealos.io/docs/msa/privacy-policy + +# End of auto-configured values +# ============================================================================ + +livenessProbe: + httpGet: + path: /api/platform/getAppConfig + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 20 + +readinessProbe: + httpGet: + path: /api/platform/getAppConfig + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 2 + successThreshold: 3 + failureThreshold: 3 + +# Ingress configuration +ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + # Will be auto-configured from desktopConfig.cloudDomain + hosts: [] + tls: [] + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/frontend/desktop/deploy/desktop-frontend-entrypoint.sh b/frontend/desktop/deploy/desktop-frontend-entrypoint.sh new file mode 100644 index 000000000000..4ae6a9b8e1f7 --- /dev/null +++ b/frontend/desktop/deploy/desktop-frontend-entrypoint.sh @@ -0,0 +1,161 @@ +#!/bin/bash +set -e + +# Default values +RELEASE_NAME=${RELEASE_NAME:-"desktop-frontend"} +RELEASE_NAMESPACE=${RELEASE_NAMESPACE:-"sealos"} +CHART_PATH=${CHART_PATH:-"./charts/desktop-frontend"} +AUTO_CONFIG_ENABLED=${AUTO_CONFIG_ENABLED:-"true"} + +# HELM_OPTS and HELM_OPTIONS support +# HELM_OPTS: Additional Helm options (e.g., --timeout, --install) +# HELM_OPTIONS: Helm --set parameters passed via environment variable +HELM_OPTS=${HELM_OPTS:-""} +HELM_OPTIONS=${HELM_OPTIONS:-""} + +# Build Helm command arguments +HELM_ARGS="" + +# Add HELM_OPTIONS if provided +if [ -n "$HELM_OPTIONS" ]; then + HELM_ARGS="$HELM_ARGS $HELM_OPTIONS" +fi + +# Add HELM_OPTS if provided +if [ -n "$HELM_OPTS" ]; then + HELM_ARGS="$HELM_ARGS $HELM_OPTS" +fi + +# Adopt existing resources for Helm +adopt_namespaced_resource() { + local kind="$1" + local name="$2" + if kubectl -n "${RELEASE_NAMESPACE}" get "${kind}" "${name}" >/dev/null 2>&1; then + echo "Adopting ${kind} ${name}..." + kubectl -n "${RELEASE_NAMESPACE}" label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl -n "${RELEASE_NAMESPACE}" annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +adopt_cluster_resource() { + local kind="$1" + local name="$2" + if kubectl get "${kind}" "${name}" >/dev/null 2>&1; then + kubectl label "${kind}" "${name}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate "${kind}" "${name}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +} + +# Get ConfigMap value +get_cm_value() { + local namespace="$1" + local name="$2" + local key="$3" + kubectl get configmap "${name}" -n "${namespace}" -o "jsonpath={.data.${key}}" 2>/dev/null || true +} + +# Auto configuration from sealos-system ConfigMap +if [ "$AUTO_CONFIG_ENABLED" = "true" ]; then + echo "Auto-configuring from sealos-system/sealos-config..." + + SEALOS_CLOUD_DOMAIN=$(get_cm_value sealos-system sealos-config cloudDomain) + SEALOS_CLOUD_PORT=$(get_cm_value sealos-system sealos-config cloudPort) + SEALOS_JWT_INTERNAL=$(get_cm_value sealos-system sealos-config jwtInternal) + SEALOS_JWT_REGIONAL=$(get_cm_value sealos-system sealos-config jwtRegional) + SEALOS_JWT_GLOBAL=$(get_cm_value sealos-system sealos-config jwtGlobal) + SEALOS_JWT_MARKETING_CONSENT=$(get_cm_value sealos-system sealos-config jwtMarketingConsent) + SEALOS_REGION_UID=$(get_cm_value sealos-system sealos-config regionUID) + SEALOS_DATABASE_MONGODB_URI=$(get_cm_value sealos-system sealos-config databaseMongodbURI) + SEALOS_DATABASE_GLOBAL_COCKROACHDB_URI=$(get_cm_value sealos-system sealos-config databaseGlobalCockroachdbURI) + SEALOS_DATABASE_LOCAL_COCKROACHDB_URI=$(get_cm_value sealos-system sealos-config databaseLocalCockroachdbURI) + SEALOS_PASSWORD_SALT=$(get_cm_value sealos-system sealos-config passwordSalt) + + # Build auto-configuration Helm args + if [ -n "$SEALOS_CLOUD_DOMAIN" ]; then + HELM_ARGS="$HELM_ARGS --set-string desktopConfig.cloudDomain=$SEALOS_CLOUD_DOMAIN" + HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].host=$SEALOS_CLOUD_DOMAIN" + HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].path=/" + HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].pathType=Prefix" + HELM_ARGS="$HELM_ARGS --set ingress.tls[0].hosts[0]=$SEALOS_CLOUD_DOMAIN" + HELM_ARGS="$HELM_ARGS --set ingress.tls[0].secretName=wildcard-cert" + fi + + [ -n "$SEALOS_CLOUD_PORT" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.cloudPort=$SEALOS_CLOUD_PORT" + [ -n "$SEALOS_JWT_INTERNAL" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.jwtInternal=$SEALOS_JWT_INTERNAL" + [ -n "$SEALOS_JWT_REGIONAL" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.jwtRegional=$SEALOS_JWT_REGIONAL" + [ -n "$SEALOS_JWT_GLOBAL" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.jwtGlobal=$SEALOS_JWT_GLOBAL" + [ -n "$SEALOS_JWT_MARKETING_CONSENT" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.jwtMarketingConsent=$SEALOS_JWT_MARKETING_CONSENT" + [ -n "$SEALOS_REGION_UID" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.regionUID=$SEALOS_REGION_UID" + [ -n "$SEALOS_DATABASE_MONGODB_URI" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.databaseMongodbURI=$SEALOS_DATABASE_MONGODB_URI" + [ -n "$SEALOS_DATABASE_GLOBAL_COCKROACHDB_URI" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.databaseGlobalCockroachdbURI=$SEALOS_DATABASE_GLOBAL_COCKROACHDB_URI" + [ -n "$SEALOS_DATABASE_LOCAL_COCKROACHDB_URI" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.databaseLocalCockroachdbURI=$SEALOS_DATABASE_LOCAL_COCKROACHDB_URI" + [ -n "$SEALOS_PASSWORD_SALT" ] && HELM_ARGS="$HELM_ARGS --set-string desktopConfig.passwordSalt=$SEALOS_PASSWORD_SALT" +fi + +# Adopt existing resources if fresh install +if ! helm status "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" >/dev/null 2>&1; then + echo "Fresh install detected, adopting existing resources..." + + kubectl get namespace "${RELEASE_NAMESPACE}" >/dev/null 2>&1 && \ + kubectl label namespace "${RELEASE_NAMESPACE}" app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl annotate namespace "${RELEASE_NAMESPACE}" meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + + adopt_namespaced_resource serviceaccount desktop-frontend + adopt_namespaced_resource configmap desktop-frontend-config + adopt_namespaced_resource service desktop-frontend + adopt_namespaced_resource deployment desktop-frontend + adopt_namespaced_resource ingress sealos-desktop + + # Rename old configmap for backward compatibility + if kubectl -n "${RELEASE_NAMESPACE}" get configmap desktop-frontend-config >/dev/null 2>&1; then + if ! kubectl -n "${RELEASE_NAMESPACE}" get configmap sealos-desktop-config >/dev/null 2>&1; then + echo "Renaming configmap desktop-frontend-config to sealos-desktop-config..." + kubectl -n "${RELEASE_NAMESPACE}" get configmap desktop-frontend-config -o yaml | \ + sed 's/name: desktop-frontend-config/name: sealos-desktop-config/g' | \ + kubectl apply -f - >/dev/null 2>&1 || true + fi + fi + + adopt_cluster_resource clusterrole desktop-frontend-manager-role + adopt_cluster_resource clusterrole desktop-frontend-account-editor-role + adopt_cluster_resource clusterrole desktop-frontend-app-reader-role + adopt_cluster_resource clusterrolebinding desktop-frontend-user-role-binding + adopt_cluster_resource clusterrolebinding desktop-frontend-account-editor-role-binding + adopt_cluster_resource clusterrolebinding desktop-frontend-app-reader-role-binding + + adopt_namespaced_resource role desktop-frontend-recharge-gift-cm-reader + adopt_namespaced_resource rolebinding desktop-frontend-recharge-gift-cm-reader-rolebinding +fi + +# Ensure ingress has proper Helm labels +if kubectl -n "${RELEASE_NAMESPACE}" get ingress sealos-desktop >/dev/null 2>&1; then + HAS_HELM_LABEL=$(kubectl -n "${RELEASE_NAMESPACE}" get ingress sealos-desktop -o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}' 2>/dev/null || echo "") + if [ "${HAS_HELM_LABEL}" != "Helm" ]; then + echo "Adding Helm labels to ingress sealos-desktop..." + kubectl -n "${RELEASE_NAMESPACE}" label ingress sealos-desktop app.kubernetes.io/managed-by=Helm --overwrite >/dev/null 2>&1 || true + kubectl -n "${RELEASE_NAMESPACE}" annotate ingress sealos-desktop meta.helm.sh/release-name="${RELEASE_NAME}" meta.helm.sh/release-namespace="${RELEASE_NAMESPACE}" --overwrite >/dev/null 2>&1 || true + fi +fi + +# Prepare values files +SERVICE_NAME="desktop-frontend" +USER_VALUES_PATH="/root/.sealos/cloud/values/core/desktop-values.yaml" + +# Copy user values template if not exists +if [ ! -f "${USER_VALUES_PATH}" ]; then + mkdir -p "$(dirname "${USER_VALUES_PATH}")" + cp "./charts/${SERVICE_NAME}/${SERVICE_NAME}-values.yaml" "${USER_VALUES_PATH}" +fi + +GLOBALS_FILE="/root/.sealos/cloud/values/global.yaml" +if [ -f "${GLOBALS_FILE}" ]; then + echo "Merging global values from ${GLOBALS_FILE} into user values..." + HELM_ARGS="$HELM_ARGS -f ${GLOBALS_FILE}" +fi + +# Deploy Helm chart +echo "Deploying Helm chart..." +helm upgrade -i "${RELEASE_NAME}" -n "${RELEASE_NAMESPACE}" --create-namespace "${CHART_PATH}" \ + -f "./charts/${SERVICE_NAME}/values.yaml" \ + -f "${USER_VALUES_PATH}" \ + ${HELM_ARGS} diff --git a/frontend/desktop/next-env.d.ts b/frontend/desktop/next-env.d.ts new file mode 100644 index 000000000000..a4a7b3f5cfa2 --- /dev/null +++ b/frontend/desktop/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/frontend/desktop/next-i18next.config.js b/frontend/desktop/next-i18next.config.js new file mode 100644 index 000000000000..1ef264034d34 --- /dev/null +++ b/frontend/desktop/next-i18next.config.js @@ -0,0 +1,12 @@ +/** + * @type {import('next-i18next').UserConfig} + */ + +module.exports = { + i18n: { + defaultLocale: 'en', + locales: ['en', 'zh'], + localeDetection: false + }, + reloadOnPrerender: process.env.NODE_ENV === 'development' +} diff --git a/frontend/desktop/next.config.js b/frontend/desktop/next.config.js new file mode 100644 index 000000000000..35256fb2c7e4 --- /dev/null +++ b/frontend/desktop/next.config.js @@ -0,0 +1,30 @@ +/** @type {import('next').NextConfig} */ +const path = require('path'); +const isProduction = process.env.NODE_ENV === 'production'; +const { i18n } = require('./next-i18next.config'); + +const nextConfig = { + i18n, + reactStrictMode: false, + async redirects() { + if (isProduction) { + return [ + { + source: '/api/dev/:slug', + destination: '/', + permanent: true + } + ]; + } else { + return []; + } + }, + swcMinify: isProduction, + output: 'standalone', + transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk', '@sealos/driver'], + experimental: { + outputFileTracingRoot: path.join(__dirname, '../') + } +}; + +module.exports = nextConfig; diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json new file mode 100644 index 000000000000..a39e61094656 --- /dev/null +++ b/frontend/desktop/package.json @@ -0,0 +1,123 @@ +{ + "name": "desktop", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "dotenv -e .env.local next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "vitest --project unit", + "test:watch": "vitest --project unit --watch", + "test:ci": "vitest run --project unit", + "gen:global": "prisma generate --schema ./prisma/global/schema.prisma", + "gen:region": "prisma generate --schema ./prisma/region/schema.prisma", + "test:components": "vitest --project components", + "test:components:ci": "vitest run --project components", + "gen:global:postgresql": "prisma generate --schema ./prisma/providers/postgresql/global/schema.prisma", + "gen:region:postgresql": "prisma generate --schema ./prisma/providers/postgresql/region/schema.prisma", + "gen:postgresql": "pnpm gen:global:postgresql && pnpm gen:region:postgresql", + "gen:all": "pnpm gen:global && pnpm gen:region && pnpm gen:global:postgresql && pnpm gen:region:postgresql", + "migrate:deploy:global:postgresql": "prisma migrate deploy --schema ./prisma/providers/postgresql/global/schema.prisma", + "migrate:deploy:region:postgresql": "prisma migrate deploy --schema ./prisma/providers/postgresql/region/schema.prisma", + "postinstall": "pnpm gen:all" + }, + "dependencies": { + "@alicloud/captcha20230305": "1.1.3", + "@alicloud/dysmsapi20170525": "^2.0.24", + "@alicloud/openapi-client": "^0.4.6", + "@alicloud/tea-typescript": "^1.8.0", + "@alicloud/tea-util": "^1.4.7", + "@chakra-ui/anatomy": "^2.2.1", + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/next-js": "^2.1.5", + "@chakra-ui/react": "^2.8.1", + "@chakra-ui/system": "^2.6.1", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@hookform/resolvers": "^3.9.0", + "@kubernetes/client-node": "^0.18.1", + "@marsidev/react-turnstile": "^1.1.0", + "@prisma/client": "^5.10.2", + "@sealos/driver": "workspace:^", + "@sealos/gtm": "workspace:^", + "@sealos/shadcn-ui": "workspace:^", + "@sealos/shared": "workspace:^", + "@sealos/ui": "workspace:^", + "@tailwindcss/postcss": "^4.1.10", + "@tanstack/react-query": "^4.35.3", + "@umami/node": "^0.4.0", + "axios": "^1.5.1", + "boring-avatars": "^1.11.2", + "clsx": "^1.2.1", + "cors": "^2.8.5", + "croner": "^8.0.2", + "dayjs": "^1.11.10", + "decimal.js": "^10.4.3", + "disposable-email-domains-js": "^1.11.0", + "dompurify": "^3.3.0", + "eslint": "8.38.0", + "eslint-config-next": "13.3.0", + "framer-motion": "^10.16.4", + "google-auth-library": "^10.9.0", + "i18next": "^23.11.5", + "immer": "^10.0.2", + "js-cookie": "^3.0.5", + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", + "lodash": "^4.17.21", + "lucide-react": "^0.488.0", + "minio": "^7.1.3", + "mongodb": "^5.9.0", + "nanoid": "^4.0.2", + "next": "14.2.35", + "next-i18next": "^15.3.0", + "nodemailer": "^6.9.13", + "nprogress": "^0.2.0", + "postcss": "^8.5.6", + "prisma": "^5.10.2", + "qrcode.react": "^3.1.0", + "randexp": "^0.5.3", + "react": "18.3.1", + "react-contexify": "^6.0.0", + "react-dom": "18.3.1", + "react-draggable": "^4.4.6", + "react-dropzone": "^14.2.3", + "react-hook-form": "^7.46.2", + "react-i18next": "^14.1.2", + "sass": "^1.68.0", + "sealos-desktop-sdk": "workspace:*", + "sharp": "^0.32.6", + "sonner": "^2.0.5", + "tailwindcss": "^4.1.12", + "tencentcloud-sdk-nodejs": "4.0.905", + "uuid": "^9.0.1", + "xml2js": "^0.6.2", + "zod": "^3.23.8", + "zustand": "^4.4.1" + }, + "devDependencies": { + "@playwright/test": "^1.58.1", + "@testing-library/react": "^14.0.0", + "@types/js-cookie": "^3.0.4", + "@types/js-yaml": "^4.0.6", + "@types/jsonwebtoken": "^9.0.3", + "@types/lodash": "^4.14.199", + "@types/minio": "^7.1.1", + "@types/node": "18.15.11", + "@types/nodemailer": "^6.4.15", + "@types/nprogress": "^0.2.1", + "@types/react": "18.3.27", + "@types/react-dom": "18.3.7", + "@types/umami-browser": "^2.3.2", + "@types/uuid": "^9.0.4", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/browser": "4.0.15", + "@vitest/browser-playwright": "4.0.15", + "dotenv-cli": "^7.3.0", + "prettier": "^2.8.8", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.0.12" + } +} diff --git a/frontend/desktop/postcss.config.js b/frontend/desktop/postcss.config.js new file mode 100644 index 000000000000..6b56317c54a9 --- /dev/null +++ b/frontend/desktop/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {} + } +}; diff --git a/frontend/desktop/prisma/global/migrations/20240229065500_/migration.sql b/frontend/desktop/prisma/global/migrations/20240229065500_/migration.sql new file mode 100644 index 000000000000..2bf31ebe141a --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240229065500_/migration.sql @@ -0,0 +1,154 @@ +-- CreateEnum +CREATE TYPE "ProviderType" AS ENUM ('PHONE', 'GITHUB', 'WECHAT', 'GOOGLE', 'PASSWORD'); + +-- CreateTable +CREATE TABLE "OauthProvider" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "providerType" "ProviderType" NOT NULL, + "providerId" STRING NOT NULL, + "password" STRING, + + CONSTRAINT "OauthProvider_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Region" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "displayName" STRING NOT NULL, + "location" STRING NOT NULL, + "domain" STRING NOT NULL, + "description" STRING, + + CONSTRAINT "Region_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Account" ( + "userUid" UUID NOT NULL DEFAULT gen_random_uuid(), + "activityBonus" INT8 NOT NULL, + "encryptBalance" STRING NOT NULL, + "encryptDeductionBalance" STRING NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "create_region_id" STRING NOT NULL, + "balance" INT8, + "deduction_balance" INT8, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "ErrorPaymentCreate" ( + "userUid" UUID NOT NULL, + "regionUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUserOwner" STRING NOT NULL, + "method" STRING NOT NULL, + "amount" INT8 NOT NULL, + "gift" INT8, + "trade_no" STRING NOT NULL, + "code_url" STRING, + "invoiced_at" BOOL DEFAULT false, + "remark" STRING, + "message" STRING NOT NULL, + "create_time" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "Payment" ( + "id" STRING NOT NULL, + "userUid" UUID NOT NULL, + "regionUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUserOwner" STRING NOT NULL, + "method" STRING NOT NULL, + "amount" INT8 NOT NULL, + "gift" INT8, + "trade_no" STRING NOT NULL, + "code_url" STRING, + "invoiced_at" BOOL DEFAULT false, + "remark" STRING, + "message" STRING NOT NULL, + + CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TransferAccountV1" ( + "regionUid" UUID NOT NULL, + "regionUserOwner" STRING NOT NULL, + "userUid" UUID NOT NULL DEFAULT gen_random_uuid(), + "activityBonus" INT8 NOT NULL, + "encryptBalance" STRING NOT NULL, + "encryptDeductionBalance" STRING NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "create_region_id" STRING NOT NULL, + "balance" INT8, + "deduction_balance" INT8, + + CONSTRAINT "TransferAccountV1_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "User" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "avatarUri" STRING NOT NULL, + "nickname" STRING NOT NULL, + "id" STRING NOT NULL, + "name" STRING NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "ErrorAccountCreate" ( + "userUid" UUID NOT NULL DEFAULT gen_random_uuid(), + "activityBonus" INT8 NOT NULL, + "encryptBalance" STRING NOT NULL, + "encryptDeductionBalance" STRING NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "create_region_id" STRING NOT NULL, + "balance" INT8, + "deduction_balance" INT8, + "userCr" STRING NOT NULL, + "error_time" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUid" UUID NOT NULL, + "regionUserOwner" STRING NOT NULL, + "message" STRING NOT NULL, + + CONSTRAINT "ErrorAccountCreate_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "NullUserRecord" ( + "crName" STRING NOT NULL, + "region_id" STRING NOT NULL +); + +-- CreateIndex +CREATE INDEX "OauthProvider_userUid_idx" ON "OauthProvider"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "OauthProvider_providerId_providerType_key" ON "OauthProvider"("providerId", "providerType"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorPaymentCreate_trade_no_key" ON "ErrorPaymentCreate"("trade_no"); + +-- CreateIndex +CREATE UNIQUE INDEX "Payment_trade_no_key" ON "Payment"("trade_no"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_name_key" ON "User"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorAccountCreate_userCr_key" ON "ErrorAccountCreate"("userCr"); + +-- CreateIndex +CREATE UNIQUE INDEX "NullUserRecord_crName_key" ON "NullUserRecord"("crName"); diff --git a/frontend/desktop/prisma/global/migrations/20240301085154_removetabe/migration.sql b/frontend/desktop/prisma/global/migrations/20240301085154_removetabe/migration.sql new file mode 100644 index 000000000000..65aa00b6d539 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240301085154_removetabe/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the `NullUserRecord` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `TransferAccountV1` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "NullUserRecord"; + +-- DropTable +DROP TABLE "TransferAccountV1"; diff --git a/frontend/desktop/prisma/global/migrations/20240604123813_add_invite_reward_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240604123813_add_invite_reward_table/migration.sql new file mode 100644 index 000000000000..ed43541b0b59 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240604123813_add_invite_reward_table/migration.sql @@ -0,0 +1,14 @@ +-- AlterEnum +ALTER TYPE "ProviderType" ADD VALUE 'OAUTH2'; + +-- CreateTable +CREATE TABLE "InviteReward" ( + "payment_id" STRING NOT NULL, + "userUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "payment_amount" INT8 NOT NULL, + "reward_amount" INT8 NOT NULL, + "inviteFrom" UUID NOT NULL, + + CONSTRAINT "InviteReward_pkey" PRIMARY KEY ("payment_id") +); diff --git a/frontend/desktop/prisma/global/migrations/20240604123814_add_transfer_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240604123814_add_transfer_table/migration.sql new file mode 100644 index 000000000000..3de68d4c9671 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240604123814_add_transfer_table/migration.sql @@ -0,0 +1,12 @@ +CREATE TABLE "Transfer" ( + uid uuid default gen_random_uuid () not null primary key, + "fromUserUid" uuid not null, + "toUserUid" uuid not null, + amount bigint not null, + remark text not null, + created_at timestamp + with + time zone default current_timestamp() not null +); + +COMMENT ON TABLE "Transfer" IS 'Calculates sum of squares of the independent variable.'; \ No newline at end of file diff --git a/frontend/desktop/prisma/global/migrations/20240604125758_add_transaction_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240604125758_add_transaction_table/migration.sql new file mode 100644 index 000000000000..ba69474d49cb --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240604125758_add_transaction_table/migration.sql @@ -0,0 +1,70 @@ +-- CreateEnum +CREATE TYPE "TransactionStatus" AS ENUM ('READY', 'RUNNING', 'FINISH', 'COMMITED', 'ERROR'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('MERGE_USER', 'DELETE_USER'); + +-- CreateTable +CREATE TABLE "CommitTransactionSet" ( + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "precommitTransactionUid" STRING NOT NULL +); + +-- CreateTable +CREATE TABLE "PrecommitTransaction" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "transactionType" "TransactionType" NOT NULL, + "infoUid" STRING NOT NULL, + "status" "TransactionStatus" NOT NULL, + + CONSTRAINT "PrecommitTransaction_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "TransactionDetail" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "status" "TransactionStatus" NOT NULL, + "regionUid" STRING NOT NULL, + "transactionUid" STRING NOT NULL, + + CONSTRAINT "TransactionDetail_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "MergeUserTransactionInfo" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "mergeUserUid" STRING NOT NULL, + "userUid" STRING NOT NULL, + + CONSTRAINT "MergeUserTransactionInfo_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "DeleteUserTransactionInfo" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" STRING NOT NULL, + + CONSTRAINT "DeleteUserTransactionInfo_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "CommitTransactionSet_precommitTransactionUid_key" ON "CommitTransactionSet"("precommitTransactionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "PrecommitTransaction_infoUid_transactionType_key" ON "PrecommitTransaction"("infoUid", "transactionType"); + +-- CreateIndex +CREATE INDEX "TransactionDetail_regionUid_idx" ON "TransactionDetail"("regionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "TransactionDetail_transactionUid_regionUid_key" ON "TransactionDetail"("transactionUid", "regionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "MergeUserTransactionInfo_mergeUserUid_key" ON "MergeUserTransactionInfo"("mergeUserUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "DeleteUserTransactionInfo_userUid_key" ON "DeleteUserTransactionInfo"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240605130547_add_index/migration.sql b/frontend/desktop/prisma/global/migrations/20240605130547_add_index/migration.sql new file mode 100644 index 000000000000..b76aa4fbd77a --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240605130547_add_index/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "MergeUserTransactionInfo_userUid_idx" ON "MergeUserTransactionInfo"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240611073728_add_delete_user_log/migration.sql b/frontend/desktop/prisma/global/migrations/20240611073728_add_delete_user_log/migration.sql new file mode 100644 index 000000000000..aeef8a52abd9 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240611073728_add_delete_user_log/migration.sql @@ -0,0 +1,7 @@ +-- CreateTable +CREATE TABLE "DeleteUserLog" ( + "userUid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DeleteUserLog_pkey" PRIMARY KEY ("userUid") +); diff --git a/frontend/desktop/prisma/global/migrations/20240612075113_add_audit_log/migration.sql b/frontend/desktop/prisma/global/migrations/20240612075113_add_audit_log/migration.sql new file mode 100644 index 000000000000..8c74f29d44e6 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240612075113_add_audit_log/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "AuditAction" AS ENUM ('UPDATE', 'DELETE', 'CREATE'); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "entityUid" STRING NOT NULL, + "entityName" STRING NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "action" "AuditAction" NOT NULL, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "AuditLogDetail" ( + "auditLogUid" STRING NOT NULL, + "key" STRING NOT NULL, + "preValue" STRING NOT NULL, + "newValue" STRING NOT NULL, + + CONSTRAINT "AuditLogDetail_pkey" PRIMARY KEY ("auditLogUid") +); diff --git a/frontend/desktop/prisma/global/migrations/20240614075113_add_account_transaction/migration.sql b/frontend/desktop/prisma/global/migrations/20240614075113_add_account_transaction/migration.sql new file mode 100644 index 000000000000..250822deb550 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240614075113_add_account_transaction/migration.sql @@ -0,0 +1,12 @@ +create table "AccountTransaction" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid (), + "type" TEXT NOT NULL, + "deduction_balance" INT8 NOT NULL, + "balance" INT8 NOT NULL, + "message" TEXT, + "created_at" TIMESTAMPTZ (3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ (3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "billing_id" UUID NOT NULL, + "userUid" UUID NOT NULL, + CONSTRAINT "AccountTransaction_pkey" PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/frontend/desktop/prisma/global/migrations/20240618115428_update_provider_type/migration.sql b/frontend/desktop/prisma/global/migrations/20240618115428_update_provider_type/migration.sql new file mode 100644 index 000000000000..35dff6e54414 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240618115428_update_provider_type/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `useruid` on the `AccountTransaction` table. All the data in the column will be lost. + - Added the required column `userUid` to the `AccountTransaction` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +ALTER TYPE "ProviderType" ADD VALUE 'EMAIL'; diff --git a/frontend/desktop/prisma/global/migrations/20240620070546_add_error_transaction_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240620070546_add_error_transaction_table/migration.sql new file mode 100644 index 000000000000..85fed8c4f9c3 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240620070546_add_error_transaction_table/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "ErrorPreCommitTransaction" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "transactionUid" STRING NOT NULL, + "reason" STRING, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ErrorPreCommitTransaction_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorPreCommitTransaction_transactionUid_key" ON "ErrorPreCommitTransaction"("transactionUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240620075123_update_uid_type/migration.sql b/frontend/desktop/prisma/global/migrations/20240620075123_update_uid_type/migration.sql new file mode 100644 index 000000000000..484873463852 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240620075123_update_uid_type/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - Changed the type of `precommitTransactionUid` on the `CommitTransactionSet` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `transactionUid` on the `ErrorPreCommitTransaction` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "CommitTransactionSet" DROP COLUMN "precommitTransactionUid"; +ALTER TABLE "CommitTransactionSet" ADD COLUMN "precommitTransactionUid" UUID NOT NULL; + +-- AlterTable +ALTER TABLE "ErrorPreCommitTransaction" DROP COLUMN "transactionUid"; +ALTER TABLE "ErrorPreCommitTransaction" ADD COLUMN "transactionUid" UUID NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "CommitTransactionSet_precommitTransactionUid_key" ON "CommitTransactionSet"("precommitTransactionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorPreCommitTransaction_transactionUid_key" ON "ErrorPreCommitTransaction"("transactionUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240710112344_update_eventlog/migration.sql b/frontend/desktop/prisma/global/migrations/20240710112344_update_eventlog/migration.sql new file mode 100644 index 000000000000..472e95a57ac5 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240710112344_update_eventlog/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "EventLog" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "mainId" STRING NOT NULL, + "eventName" STRING NOT NULL, + "data" STRING NOT NULL, + + CONSTRAINT "EventLog_pkey" PRIMARY KEY ("uid") +); diff --git a/frontend/desktop/prisma/global/migrations/20240731100240_add_user_realname_info/migration.sql b/frontend/desktop/prisma/global/migrations/20240731100240_add_user_realname_info/migration.sql new file mode 100644 index 000000000000..0433b6908bed --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240731100240_add_user_realname_info/migration.sql @@ -0,0 +1,46 @@ +-- CreateTable +CREATE TABLE "UserRealNameInfo" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "realName" STRING, + "idCard" STRING, + "phone" STRING, + "isVerified" BOOL NOT NULL DEFAULT false, + "idVerifyFailedTimes" INT4 NOT NULL DEFAULT 0, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "UserRealNameInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RestrictedUser" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "restrictedLevel" INT4 NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "RestrictedUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RealNameAuthProvider" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "backend" STRING NOT NULL, + "authType" STRING NOT NULL, + "maxFailedTimes" INT4 NOT NULL, + "config" JSONB, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "RealNameAuthProvider_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserRealNameInfo_userUid_key" ON "UserRealNameInfo"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "RestrictedUser_userUid_key" ON "RestrictedUser"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240807090531_add_user_sem_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240807090531_add_user_sem_table/migration.sql new file mode 100644 index 000000000000..2cfd235cf03a --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240807090531_add_user_sem_table/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "UserSemChannel" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "channel" STRING NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "UserSemChannel_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSemChannel_userUid_key" ON "UserSemChannel"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/20240829074904_add_giftcode/migration.sql b/frontend/desktop/prisma/global/migrations/20240829074904_add_giftcode/migration.sql new file mode 100644 index 000000000000..bfbbfd1ed5f7 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240829074904_add_giftcode/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "GiftCode" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "code" STRING NOT NULL, + "creditAmount" INT8 NOT NULL DEFAULT 0, + "used" BOOL NOT NULL DEFAULT false, + "usedBy" UUID, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiredAt" TIMESTAMP(3), + "comment" STRING, + + CONSTRAINT "GiftCode_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GiftCode_code_key" ON "GiftCode"("code"); diff --git a/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql b/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql new file mode 100644 index 000000000000..7f8f30917a88 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "UserStatus" AS ENUM ('NORMAL_USER', 'LOCK_USER', 'DELETE_USER'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "status" "UserStatus" NOT NULL DEFAULT 'NORMAL_USER'; diff --git a/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql new file mode 100644 index 000000000000..41b6425f77af --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql @@ -0,0 +1,41 @@ +-- CreateEnum +CREATE TYPE "TaskType" AS ENUM ('LAUNCHPAD', 'COSTCENTER', 'DATABASE', 'DESKTOP', 'APPSTORE'); + +-- CreateEnum +CREATE TYPE "TaskStatus" AS ENUM ('NOT_COMPLETED', 'COMPLETED'); + +-- CreateTable +CREATE TABLE "Task" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "title" STRING NOT NULL, + "description" STRING NOT NULL, + "reward" INT8 NOT NULL, + "order" INT4 NOT NULL, + "isActive" BOOL NOT NULL DEFAULT true, + "isNewUserTask" BOOL NOT NULL DEFAULT false, + "taskType" "TaskType" NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserTask" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "taskId" UUID NOT NULL, + "status" "TaskStatus" NOT NULL, + "rewardStatus" "TaskStatus" NOT NULL, + "completedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserTask_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "UserTask_taskId_idx" ON "UserTask"("taskId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserTask_userUid_taskId_key" ON "UserTask"("userUid", "taskId"); diff --git a/frontend/desktop/prisma/global/migrations/20240928050904_add_enterprise_real_name_info_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240928050904_add_enterprise_real_name_info_table/migration.sql new file mode 100644 index 000000000000..4d5e5d8b47d5 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240928050904_add_enterprise_real_name_info_table/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "EnterpriseRealNameInfo" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "enterpriseName" STRING, + "enterpriseQualification" STRING, + "legalRepresentativePhone" STRING, + "isVerified" BOOL NOT NULL DEFAULT false, + "verificationStatus" STRING, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + "supportingMaterials" JSONB, + + CONSTRAINT "EnterpriseRealNameInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "EnterpriseRealNameInfo_userUid_key" ON "EnterpriseRealNameInfo"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/20241113062124_change_account_transaction_structure/migration.sql b/frontend/desktop/prisma/global/migrations/20241113062124_change_account_transaction_structure/migration.sql new file mode 100644 index 000000000000..21385ee9b40a --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20241113062124_change_account_transaction_structure/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "AccountTransaction" ADD COLUMN "balance_before" INT8; +ALTER TABLE "AccountTransaction" ADD COLUMN "deduction_balance_before" INT8; diff --git a/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql b/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql new file mode 100644 index 000000000000..948a17bf8a5c --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20241210040952_update_user_tasktype/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "TaskType" ADD VALUE 'CRONJOB'; +ALTER TYPE "TaskType" ADD VALUE 'DEVBOX'; +ALTER TYPE "TaskType" ADD VALUE 'CONTACT'; +ALTER TYPE "TaskType" ADD VALUE 'REAL_NAME_AUTH'; diff --git a/frontend/desktop/prisma/global/migrations/20250512031158_add_plan/migration.sql b/frontend/desktop/prisma/global/migrations/20250512031158_add_plan/migration.sql new file mode 100644 index 000000000000..e8c313cb0a36 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20250512031158_add_plan/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "user_info" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_uid" UUID NOT NULL, + "sign_up_region_uid" UUID NOT NULL, + "is_inited" BOOL NOT NULL DEFAULT false, + "config" JSONB, + + CONSTRAINT "user_info_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "workspace_usage" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_uid" UUID NOT NULL, + "workspace_uid" UUID NOT NULL, + "region_uid" UUID NOT NULL, + "seat" INT4 NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "workspace_usage_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_info_user_uid_key" ON "user_info"("user_uid"); + +-- CreateIndex +CREATE INDEX "workspace_usage_user_uid_idx" ON "workspace_usage"("user_uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "workspace_usage_region_uid_user_uid_workspace_uid_key" ON "workspace_usage"("region_uid", "user_uid", "workspace_uid"); diff --git a/frontend/desktop/prisma/global/migrations/20250514025456_add_account_updated_at_field/migration.sql b/frontend/desktop/prisma/global/migrations/20250514025456_add_account_updated_at_field/migration.sql new file mode 100644 index 000000000000..671cbcb9307d --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20250514025456_add_account_updated_at_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/frontend/desktop/prisma/global/migrations/20250814030426_gif_code0814/migration.sql b/frontend/desktop/prisma/global/migrations/20250814030426_gif_code0814/migration.sql new file mode 100644 index 000000000000..ae4cbb2a96fe --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20250814030426_gif_code0814/migration.sql @@ -0,0 +1,37 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('EXTERNAL_USER', 'INTERNAL_EMPLOYEE'); + +-- CreateEnum +CREATE TYPE "ProductSeries" AS ENUM ('SEALOS', 'FASTGPT', 'LAF_SEALAF', 'AI_PROXY'); + +-- CreateEnum +CREATE TYPE "RechargeCodeType" AS ENUM ('TEST_RECHARGE', 'COMPENSATION_RECHARGE', 'ACTIVITY_RECHARGE', 'CORPORATE_RECHARGE'); + +-- CreateTable +CREATE TABLE "UserAccountType" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "userType" "UserType" NOT NULL DEFAULT 'EXTERNAL_USER', + "productSeries" "ProductSeries"[], + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserAccountType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GiftCodeCreation" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "giftCodeId" UUID NOT NULL, + "createdByUserUid" UUID NOT NULL, + "rechargeType" "RechargeCodeType" NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GiftCodeCreation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAccountType_userUid_key" ON "UserAccountType"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "GiftCodeCreation_giftCodeId_key" ON "GiftCodeCreation"("giftCodeId"); diff --git a/frontend/desktop/prisma/global/migrations/20251210063458_add_last_activity_time/migration.sql b/frontend/desktop/prisma/global/migrations/20251210063458_add_last_activity_time/migration.sql new file mode 100644 index 000000000000..9b2479c38e83 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20251210063458_add_last_activity_time/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "lastActivityTime" TIMESTAMPTZ(3); diff --git a/frontend/desktop/prisma/global/migrations/20260304103000_add_oauth2_idp_models/migration.sql b/frontend/desktop/prisma/global/migrations/20260304103000_add_oauth2_idp_models/migration.sql new file mode 100644 index 000000000000..92b4204cd6b1 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20260304103000_add_oauth2_idp_models/migration.sql @@ -0,0 +1,76 @@ +-- CreateEnum +CREATE TYPE "OAuthClientType" AS ENUM ('PUBLIC', 'CONFIDENTIAL'); + +-- CreateEnum +CREATE TYPE "DeviceGrantStatus" AS ENUM ('PENDING', 'APPROVED', 'DENIED', 'CONSUMED'); + +-- CreateTable +CREATE TABLE "OAuthClient" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "clientId" STRING NOT NULL, + "clientType" "OAuthClientType" NOT NULL DEFAULT 'PUBLIC', + "userUid" UUID, + "clientSecretHash" STRING, + "allowedGrantTypes" STRING[] NOT NULL DEFAULT ARRAY[]::STRING[], + "name" STRING NOT NULL, + "logoUrl" STRING, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OAuthClient_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OAuthDeviceGrant" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "clientId" STRING NOT NULL, + "deviceCodeHash" STRING NOT NULL, + "userCodeHash" STRING NOT NULL, + "userUid" UUID, + "status" "DeviceGrantStatus" NOT NULL DEFAULT 'PENDING', + "expiresAt" TIMESTAMP(3) NOT NULL, + "lastPollAt" TIMESTAMP(3), + "pollCount" INT4 NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OAuthDeviceGrant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OAuthUserConsent" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "clientId" STRING NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OAuthUserConsent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "OAuthClient_clientId_key" ON "OAuthClient"("clientId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OAuthDeviceGrant_deviceCodeHash_key" ON "OAuthDeviceGrant"("deviceCodeHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "OAuthDeviceGrant_userCodeHash_key" ON "OAuthDeviceGrant"("userCodeHash"); + +-- CreateIndex +CREATE INDEX "idx_device_grants_client_status_exp" ON "OAuthDeviceGrant"("clientId", "status", "expiresAt"); + +-- CreateIndex +CREATE INDEX "idx_device_grants_user_status_exp" ON "OAuthDeviceGrant"("userUid", "status", "expiresAt"); + +-- CreateIndex +CREATE INDEX "idx_device_grants_expires" ON "OAuthDeviceGrant"("expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "uq_user_consents_user_client" ON "OAuthUserConsent"("userUid", "clientId"); + +-- CreateIndex +CREATE INDEX "idx_user_consents_client" ON "OAuthUserConsent"("clientId"); + +-- CreateIndex +CREATE INDEX "idx_user_consents_user" ON "OAuthUserConsent"("userUid"); diff --git a/frontend/desktop/prisma/global/migrations/migration_lock.toml b/frontend/desktop/prisma/global/migrations/migration_lock.toml new file mode 100644 index 000000000000..7106c8a84885 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "cockroachdb" \ No newline at end of file diff --git a/frontend/desktop/prisma/global/schema.prisma b/frontend/desktop/prisma/global/schema.prisma new file mode 100644 index 000000000000..fd952cba5c57 --- /dev/null +++ b/frontend/desktop/prisma/global/schema.prisma @@ -0,0 +1,535 @@ +generator globalClient { + provider = "prisma-client-js" + output = "./generated/client" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "cockroachdb" + url = env("GLOBAL_DATABASE_URL") + relationMode = "prisma" +} + +model OauthProvider { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + providerType ProviderType + providerId String + password String? + user User @relation(fields: [userUid], references: [uid]) + + @@unique([providerId, providerType]) + @@index([userUid]) +} + +enum OAuthClientType { + PUBLIC + CONFIDENTIAL +} + +enum DeviceGrantStatus { + PENDING + APPROVED + DENIED + CONSUMED +} + +model OAuthClient { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + clientId String @unique + clientType OAuthClientType @default(PUBLIC) + userUid String? @db.Uuid + user User? @relation(fields: [userUid], references: [uid], onDelete: SetNull) + clientSecretHash String? + allowedGrantTypes String[] @default([]) + name String + logoUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + deviceGrants OAuthDeviceGrant[] + consents OAuthUserConsent[] + + @@map("OAuthClient") +} + +model OAuthDeviceGrant { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + clientId String + client OAuthClient @relation(fields: [clientId], references: [clientId], onDelete: Cascade) + deviceCodeHash String @unique + userCodeHash String @unique + userUid String? @db.Uuid + user User? @relation(fields: [userUid], references: [uid], onDelete: SetNull) + status DeviceGrantStatus @default(PENDING) + expiresAt DateTime + lastPollAt DateTime? + pollCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([clientId, status, expiresAt], map: "idx_device_grants_client_status_exp") + @@index([userUid, status, expiresAt], map: "idx_device_grants_user_status_exp") + @@index([expiresAt], map: "idx_device_grants_expires") + @@map("OAuthDeviceGrant") +} + +model OAuthUserConsent { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @db.Uuid + user User @relation(fields: [userUid], references: [uid], onDelete: Cascade) + clientId String + client OAuthClient @relation(fields: [clientId], references: [clientId], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userUid, clientId], map: "uq_user_consents_user_client") + @@index([clientId], map: "idx_user_consents_client") + @@index([userUid], map: "idx_user_consents_user") + @@map("OAuthUserConsent") +} + +model Region { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + displayName String + location String + domain String + description String? + txDetail TransactionDetail[] + WorkspaceUsage WorkspaceUsage[] +} + +model Account { + userUid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + activityBonus BigInt + encryptBalance String + encryptDeductionBalance String + created_at DateTime @default(now()) @db.Timestamptz(3) + create_region_id String + updated_at DateTime @default(now()) @db.Timestamptz(3) + balance BigInt? + deduction_balance BigInt? +} + +model AccountTransaction { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + type String + userUid String @db.Uuid + deduction_balance BigInt + deduction_balance_before BigInt? + balance BigInt + balance_before BigInt? + message String? + created_at DateTime @default(now()) @db.Timestamptz(3) + updated_at DateTime @default(now()) @db.Timestamptz(3) + billing_id String @db.Uuid +} + +model UserInfo { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @map("user_uid") @db.Uuid + signUpRegionUid String @map("sign_up_region_uid") @db.Uuid + isInited Boolean @default(false) @map("is_inited") + // verify_email Boolean @default(false) + config Json? + user User @relation(fields: [userUid], references: [uid]) + + @@map("user_info") +} + +model WorkspaceUsage { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @map("user_uid") @db.Uuid + workspaceUid String @map("workspace_uid") @db.Uuid + regionUid String @map("region_uid") @db.Uuid + seat Int + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + region Region @relation(fields: [regionUid], references: [uid]) + user User @relation(fields: [userUid], references: [uid]) + + @@unique([regionUid, userUid, workspaceUid]) + @@index([userUid]) + @@map("workspace_usage") +} + +model ErrorPaymentCreate { + userUid String @db.Uuid + regionUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + regionUserOwner String + method String + amount BigInt + gift BigInt? + trade_no String @unique + code_url String? + invoiced_at Boolean? @default(false) + remark String? + message String + create_time DateTime @default(now()) @db.Timestamptz(3) +} + +model Payment { + id String @id + userUid String @db.Uuid + regionUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + regionUserOwner String + method String + amount BigInt + gift BigInt? + trade_no String @unique + code_url String? + invoiced_at Boolean? @default(false) + remark String? + message String +} + +model User { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + lastActivityTime DateTime? @db.Timestamptz(3) + avatarUri String + nickname String + id String @unique + name String @unique + oauthProvider OauthProvider[] + oauthClients OAuthClient[] + oauthDeviceGrants OAuthDeviceGrant[] + oauthUserConsents OAuthUserConsent[] + status UserStatus @default(NORMAL_USER) + oldMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("oldUser") + newMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("newUser") + DeleteUserTransactionInfo DeleteUserTransactionInfo? + deleteUserLog DeleteUserLog? + userTasks UserTask[] + userInfo UserInfo? + WorkspaceUsage WorkspaceUsage[] +} + +model Transfer { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + fromUserUid String @db.Uuid + toUserUid String @db.Uuid + amount BigInt + remark String + created_at DateTime @default(now()) @db.Timestamptz(6) +} + +model ErrorAccountCreate { + userUid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + activityBonus BigInt + encryptBalance String + encryptDeductionBalance String + created_at DateTime @default(now()) @db.Timestamptz(3) + create_region_id String + balance BigInt? + deduction_balance BigInt? + userCr String @unique + error_time DateTime @default(now()) @db.Timestamptz(3) + regionUid String @db.Uuid + regionUserOwner String + message String +} + +model CommitTransactionSet { + createdAt DateTime @default(now()) @db.Timestamptz(3) + precommitTransactionUid String @unique @db.Uuid + precommitTransaction PrecommitTransaction @relation(fields: [precommitTransactionUid], references: [uid]) +} + +model PrecommitTransaction { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + transactionType TransactionType + infoUid String + status TransactionStatus + transactionDetail TransactionDetail[] + commitTransactionSet CommitTransactionSet? + errorPreCommitTransaction ErrorPreCommitTransaction? + + @@unique([infoUid, transactionType]) +} + +model ErrorPreCommitTransaction { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + transactionUid String @unique @db.Uuid + reason String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + precommitTransaction PrecommitTransaction @relation(fields: [transactionUid], references: [uid]) +} + +model TransactionDetail { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + status TransactionStatus + regionUid String + transactionUid String + region Region @relation(fields: [regionUid], references: [uid]) + precommitTransaction PrecommitTransaction @relation(fields: [transactionUid], references: [uid]) + + @@unique([transactionUid, regionUid]) + @@index([regionUid]) +} + +model MergeUserTransactionInfo { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + mergeUserUid String @unique + userUid String + mergeUser User? @relation("oldUser", fields: [mergeUserUid], references: [uid]) + user User? @relation("newUser", fields: [userUid], references: [uid]) + + @@index([userUid]) +} + +model DeleteUserTransactionInfo { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique + user User? @relation(fields: [userUid], references: [uid]) +} + +model DeleteUserLog { + userUid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + user User @relation(fields: [userUid], references: [uid]) +} + +model AuditLog { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + entityUid String + entityName String + createdAt DateTime @default(now()) @db.Timestamptz(3) + action AuditAction + auditLogDetail AuditLogDetail[] +} + +model AuditLogDetail { + auditLogUid String @id + key String + preValue String + newValue String + auditLog AuditLog @relation(fields: [auditLogUid], references: [uid]) +} + +model EventLog { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + mainId String + eventName String + // json + data String +} + +model InviteReward { + payment_id String @id + userUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + payment_amount BigInt + reward_amount BigInt + inviteFrom String @db.Uuid +} + +model UserRealNameInfo { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @db.Uuid + realName String? + idCard String? + phone String? + isVerified Boolean @default(false) + idVerifyFailedTimes Int @default(0) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("UserRealNameInfo") +} + +model EnterpriseRealNameInfo { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @db.Uuid + enterpriseName String? + enterpriseQualification String? + legalRepresentativePhone String? + isVerified Boolean @default(false) + verificationStatus String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + supportingMaterials Json? + + @@map("EnterpriseRealNameInfo") +} + +model RestrictedUser { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @db.Uuid + restrictedLevel Int + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("RestrictedUser") +} + +model RealNameAuthProvider { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + backend String + authType String + maxFailedTimes Int + config Json? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@map("RealNameAuthProvider") +} + +model UserSemChannel { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @db.Uuid + channel String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("UserSemChannel") +} + +model GiftCode { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + code String @unique + creditAmount BigInt @default(0) + used Boolean @default(false) + usedBy String? @db.Uuid + usedAt DateTime? + createdAt DateTime @default(now()) + expiredAt DateTime? + comment String? +} + +model UserAccountType { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @unique @db.Uuid + userType UserType @default(EXTERNAL_USER) + productSeries ProductSeries[] + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@map("UserAccountType") +} + +model GiftCodeCreation { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + giftCodeId String @unique @db.Uuid // 添加 @unique + createdByUserUid String @db.Uuid + rechargeType RechargeCodeType + createdAt DateTime @default(now()) @db.Timestamptz(3) + + @@map("GiftCodeCreation") +} + +enum UserType { + EXTERNAL_USER + INTERNAL_EMPLOYEE +} + +enum ProductSeries { + SEALOS + FASTGPT + LAF_SEALAF + AI_PROXY +} + +enum RechargeCodeType { + TEST_RECHARGE + COMPENSATION_RECHARGE + ACTIVITY_RECHARGE + CORPORATE_RECHARGE +} + +enum ProviderType { + PHONE + GITHUB + WECHAT + GOOGLE + PASSWORD + OAUTH2 + EMAIL +} + +enum TransactionStatus { + READY + RUNNING + FINISH + COMMITED + ERROR +} + +enum TransactionType { + MERGE_USER + DELETE_USER +} + +enum AuditAction { + UPDATE + DELETE + CREATE +} + +enum UserStatus { + NORMAL_USER + LOCK_USER + DELETE_USER +} + +model Task { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String + description String + reward BigInt + order Int + isActive Boolean @default(true) + isNewUserTask Boolean @default(false) + taskType TaskType + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userTasks UserTask[] +} + +model UserTask { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @db.Uuid + taskId String @db.Uuid + status TaskStatus + rewardStatus TaskStatus + completedAt DateTime + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + user User @relation(fields: [userUid], references: [uid]) + task Task @relation(fields: [taskId], references: [id]) + + @@unique([userUid, taskId]) + @@index([taskId]) +} + +enum TaskType { + LAUNCHPAD + COSTCENTER + DATABASE + DESKTOP + APPSTORE + CRONJOB + DEVBOX + CONTACT + REAL_NAME_AUTH +} + +enum TaskStatus { + NOT_COMPLETED + COMPLETED +} diff --git a/frontend/desktop/prisma/providers/postgresql/global/migrations/20260411072233_init/migration.sql b/frontend/desktop/prisma/providers/postgresql/global/migrations/20260411072233_init/migration.sql new file mode 100644 index 000000000000..932f10aa42b6 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/global/migrations/20260411072233_init/migration.sql @@ -0,0 +1,518 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('EXTERNAL_USER', 'INTERNAL_EMPLOYEE'); + +-- CreateEnum +CREATE TYPE "ProductSeries" AS ENUM ('SEALOS', 'FASTGPT', 'LAF_SEALAF', 'AI_PROXY'); + +-- CreateEnum +CREATE TYPE "RechargeCodeType" AS ENUM ('TEST_RECHARGE', 'COMPENSATION_RECHARGE', 'ACTIVITY_RECHARGE', 'CORPORATE_RECHARGE'); + +-- CreateEnum +CREATE TYPE "ProviderType" AS ENUM ('PHONE', 'GITHUB', 'WECHAT', 'GOOGLE', 'PASSWORD', 'OAUTH2', 'EMAIL'); + +-- CreateEnum +CREATE TYPE "TransactionStatus" AS ENUM ('READY', 'RUNNING', 'FINISH', 'COMMITED', 'ERROR'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('MERGE_USER', 'DELETE_USER'); + +-- CreateEnum +CREATE TYPE "AuditAction" AS ENUM ('UPDATE', 'DELETE', 'CREATE'); + +-- CreateEnum +CREATE TYPE "UserStatus" AS ENUM ('NORMAL_USER', 'LOCK_USER', 'DELETE_USER'); + +-- CreateEnum +CREATE TYPE "TaskType" AS ENUM ('LAUNCHPAD', 'COSTCENTER', 'DATABASE', 'DESKTOP', 'APPSTORE', 'CRONJOB', 'DEVBOX', 'CONTACT', 'REAL_NAME_AUTH'); + +-- CreateEnum +CREATE TYPE "TaskStatus" AS ENUM ('NOT_COMPLETED', 'COMPLETED'); + +-- CreateTable +CREATE TABLE "OauthProvider" ( + "uid" UUID NOT NULL, + "userUid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "providerType" "ProviderType" NOT NULL, + "providerId" TEXT NOT NULL, + "password" TEXT, + + CONSTRAINT "OauthProvider_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Region" ( + "uid" UUID NOT NULL, + "displayName" TEXT NOT NULL, + "location" TEXT NOT NULL, + "domain" TEXT NOT NULL, + "description" TEXT, + + CONSTRAINT "Region_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Account" ( + "userUid" UUID NOT NULL, + "activityBonus" BIGINT NOT NULL, + "encryptBalance" TEXT NOT NULL, + "encryptDeductionBalance" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "create_region_id" TEXT NOT NULL, + "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "balance" BIGINT, + "deduction_balance" BIGINT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "AccountTransaction" ( + "id" UUID NOT NULL, + "type" TEXT NOT NULL, + "userUid" UUID NOT NULL, + "deduction_balance" BIGINT NOT NULL, + "deduction_balance_before" BIGINT, + "balance" BIGINT NOT NULL, + "balance_before" BIGINT, + "message" TEXT, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "billing_id" UUID NOT NULL, + + CONSTRAINT "AccountTransaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_info" ( + "id" UUID NOT NULL, + "user_uid" UUID NOT NULL, + "sign_up_region_uid" UUID NOT NULL, + "is_inited" BOOLEAN NOT NULL DEFAULT false, + "config" JSONB, + + CONSTRAINT "user_info_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "workspace_usage" ( + "id" UUID NOT NULL, + "user_uid" UUID NOT NULL, + "workspace_uid" UUID NOT NULL, + "region_uid" UUID NOT NULL, + "seat" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "workspace_usage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ErrorPaymentCreate" ( + "userUid" UUID NOT NULL, + "regionUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUserOwner" TEXT NOT NULL, + "method" TEXT NOT NULL, + "amount" BIGINT NOT NULL, + "gift" BIGINT, + "trade_no" TEXT NOT NULL, + "code_url" TEXT, + "invoiced_at" BOOLEAN DEFAULT false, + "remark" TEXT, + "message" TEXT NOT NULL, + "create_time" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "Payment" ( + "id" TEXT NOT NULL, + "userUid" UUID NOT NULL, + "regionUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUserOwner" TEXT NOT NULL, + "method" TEXT NOT NULL, + "amount" BIGINT NOT NULL, + "gift" BIGINT, + "trade_no" TEXT NOT NULL, + "code_url" TEXT, + "invoiced_at" BOOLEAN DEFAULT false, + "remark" TEXT, + "message" TEXT NOT NULL, + + CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "uid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "avatarUri" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "status" "UserStatus" NOT NULL DEFAULT 'NORMAL_USER', + + CONSTRAINT "User_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "Transfer" ( + "uid" UUID NOT NULL, + "fromUserUid" UUID NOT NULL, + "toUserUid" UUID NOT NULL, + "amount" BIGINT NOT NULL, + "remark" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Transfer_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "ErrorAccountCreate" ( + "userUid" UUID NOT NULL, + "activityBonus" BIGINT NOT NULL, + "encryptBalance" TEXT NOT NULL, + "encryptDeductionBalance" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "create_region_id" TEXT NOT NULL, + "balance" BIGINT, + "deduction_balance" BIGINT, + "userCr" TEXT NOT NULL, + "error_time" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "regionUid" UUID NOT NULL, + "regionUserOwner" TEXT NOT NULL, + "message" TEXT NOT NULL, + + CONSTRAINT "ErrorAccountCreate_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "CommitTransactionSet" ( + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "precommitTransactionUid" UUID NOT NULL +); + +-- CreateTable +CREATE TABLE "PrecommitTransaction" ( + "uid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "transactionType" "TransactionType" NOT NULL, + "infoUid" TEXT NOT NULL, + "status" "TransactionStatus" NOT NULL, + + CONSTRAINT "PrecommitTransaction_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "ErrorPreCommitTransaction" ( + "uid" UUID NOT NULL, + "transactionUid" UUID NOT NULL, + "reason" TEXT, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ErrorPreCommitTransaction_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "TransactionDetail" ( + "uid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "status" "TransactionStatus" NOT NULL, + "regionUid" TEXT NOT NULL, + "transactionUid" TEXT NOT NULL, + + CONSTRAINT "TransactionDetail_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "MergeUserTransactionInfo" ( + "uid" UUID NOT NULL, + "mergeUserUid" TEXT NOT NULL, + "userUid" TEXT NOT NULL, + + CONSTRAINT "MergeUserTransactionInfo_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "DeleteUserTransactionInfo" ( + "uid" UUID NOT NULL, + "userUid" TEXT NOT NULL, + + CONSTRAINT "DeleteUserTransactionInfo_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "DeleteUserLog" ( + "userUid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DeleteUserLog_pkey" PRIMARY KEY ("userUid") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "uid" UUID NOT NULL, + "entityUid" TEXT NOT NULL, + "entityName" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "action" "AuditAction" NOT NULL, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "AuditLogDetail" ( + "auditLogUid" TEXT NOT NULL, + "key" TEXT NOT NULL, + "preValue" TEXT NOT NULL, + "newValue" TEXT NOT NULL, + + CONSTRAINT "AuditLogDetail_pkey" PRIMARY KEY ("auditLogUid") +); + +-- CreateTable +CREATE TABLE "EventLog" ( + "uid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "mainId" TEXT NOT NULL, + "eventName" TEXT NOT NULL, + "data" TEXT NOT NULL, + + CONSTRAINT "EventLog_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "InviteReward" ( + "payment_id" TEXT NOT NULL, + "userUid" UUID NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "payment_amount" BIGINT NOT NULL, + "reward_amount" BIGINT NOT NULL, + "inviteFrom" UUID NOT NULL, + + CONSTRAINT "InviteReward_pkey" PRIMARY KEY ("payment_id") +); + +-- CreateTable +CREATE TABLE "UserRealNameInfo" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "realName" TEXT, + "idCard" TEXT, + "phone" TEXT, + "isVerified" BOOLEAN NOT NULL DEFAULT false, + "idVerifyFailedTimes" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "UserRealNameInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EnterpriseRealNameInfo" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "enterpriseName" TEXT, + "enterpriseQualification" TEXT, + "legalRepresentativePhone" TEXT, + "isVerified" BOOLEAN NOT NULL DEFAULT false, + "verificationStatus" TEXT, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + "supportingMaterials" JSONB, + + CONSTRAINT "EnterpriseRealNameInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RestrictedUser" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "restrictedLevel" INTEGER NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "RestrictedUser_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RealNameAuthProvider" ( + "id" UUID NOT NULL, + "backend" TEXT NOT NULL, + "authType" TEXT NOT NULL, + "maxFailedTimes" INTEGER NOT NULL, + "config" JSONB, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "RealNameAuthProvider_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserSemChannel" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "channel" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "additionalInfo" JSONB, + + CONSTRAINT "UserSemChannel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GiftCode" ( + "id" UUID NOT NULL, + "code" TEXT NOT NULL, + "creditAmount" BIGINT NOT NULL DEFAULT 0, + "used" BOOLEAN NOT NULL DEFAULT false, + "usedBy" UUID, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiredAt" TIMESTAMP(3), + "comment" TEXT, + + CONSTRAINT "GiftCode_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserAccountType" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "userType" "UserType" NOT NULL DEFAULT 'EXTERNAL_USER', + "productSeries" "ProductSeries"[], + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserAccountType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GiftCodeCreation" ( + "id" UUID NOT NULL, + "giftCodeId" UUID NOT NULL, + "createdByUserUid" UUID NOT NULL, + "rechargeType" "RechargeCodeType" NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GiftCodeCreation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Task" ( + "id" UUID NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "reward" BIGINT NOT NULL, + "order" INTEGER NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "isNewUserTask" BOOLEAN NOT NULL DEFAULT false, + "taskType" "TaskType" NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserTask" ( + "id" UUID NOT NULL, + "userUid" UUID NOT NULL, + "taskId" UUID NOT NULL, + "status" "TaskStatus" NOT NULL, + "rewardStatus" "TaskStatus" NOT NULL, + "completedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserTask_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "OauthProvider_userUid_idx" ON "OauthProvider"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "OauthProvider_providerId_providerType_key" ON "OauthProvider"("providerId", "providerType"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_info_user_uid_key" ON "user_info"("user_uid"); + +-- CreateIndex +CREATE INDEX "workspace_usage_user_uid_idx" ON "workspace_usage"("user_uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "workspace_usage_region_uid_user_uid_workspace_uid_key" ON "workspace_usage"("region_uid", "user_uid", "workspace_uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorPaymentCreate_trade_no_key" ON "ErrorPaymentCreate"("trade_no"); + +-- CreateIndex +CREATE UNIQUE INDEX "Payment_trade_no_key" ON "Payment"("trade_no"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_name_key" ON "User"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorAccountCreate_userCr_key" ON "ErrorAccountCreate"("userCr"); + +-- CreateIndex +CREATE UNIQUE INDEX "CommitTransactionSet_precommitTransactionUid_key" ON "CommitTransactionSet"("precommitTransactionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "PrecommitTransaction_infoUid_transactionType_key" ON "PrecommitTransaction"("infoUid", "transactionType"); + +-- CreateIndex +CREATE UNIQUE INDEX "ErrorPreCommitTransaction_transactionUid_key" ON "ErrorPreCommitTransaction"("transactionUid"); + +-- CreateIndex +CREATE INDEX "TransactionDetail_regionUid_idx" ON "TransactionDetail"("regionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "TransactionDetail_transactionUid_regionUid_key" ON "TransactionDetail"("transactionUid", "regionUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "MergeUserTransactionInfo_mergeUserUid_key" ON "MergeUserTransactionInfo"("mergeUserUid"); + +-- CreateIndex +CREATE INDEX "MergeUserTransactionInfo_userUid_idx" ON "MergeUserTransactionInfo"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "DeleteUserTransactionInfo_userUid_key" ON "DeleteUserTransactionInfo"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserRealNameInfo_userUid_key" ON "UserRealNameInfo"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "EnterpriseRealNameInfo_userUid_key" ON "EnterpriseRealNameInfo"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "RestrictedUser_userUid_key" ON "RestrictedUser"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSemChannel_userUid_key" ON "UserSemChannel"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "GiftCode_code_key" ON "GiftCode"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAccountType_userUid_key" ON "UserAccountType"("userUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "GiftCodeCreation_giftCodeId_key" ON "GiftCodeCreation"("giftCodeId"); + +-- CreateIndex +CREATE INDEX "UserTask_taskId_idx" ON "UserTask"("taskId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserTask_userUid_taskId_key" ON "UserTask"("userUid", "taskId"); diff --git a/frontend/desktop/prisma/providers/postgresql/global/migrations/migration_lock.toml b/frontend/desktop/prisma/providers/postgresql/global/migrations/migration_lock.toml new file mode 100644 index 000000000000..fbffa92c2bb7 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/global/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/frontend/desktop/prisma/providers/postgresql/global/schema.prisma b/frontend/desktop/prisma/providers/postgresql/global/schema.prisma new file mode 100644 index 000000000000..d8074d33c396 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/global/schema.prisma @@ -0,0 +1,463 @@ +generator globalClient { + provider = "prisma-client-js" + output = "./generated/client" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + url = env("GLOBAL_DATABASE_URL") + relationMode = "prisma" +} + +model OauthProvider { + uid String @id @default(uuid()) @db.Uuid + userUid String @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + providerType ProviderType + providerId String + password String? + user User @relation(fields: [userUid], references: [uid]) + + @@unique([providerId, providerType]) + @@index([userUid]) +} + +model Region { + uid String @id @default(uuid()) @db.Uuid + displayName String + location String + domain String + description String? + txDetail TransactionDetail[] + WorkspaceUsage WorkspaceUsage[] +} + +model Account { + userUid String @id @default(uuid()) @db.Uuid + activityBonus BigInt + encryptBalance String + encryptDeductionBalance String + created_at DateTime @default(now()) @db.Timestamptz(3) + create_region_id String + updated_at DateTime @default(now()) @db.Timestamptz(3) + balance BigInt? + deduction_balance BigInt? +} + +model AccountTransaction { + id String @id @default(uuid()) @db.Uuid + type String + userUid String @db.Uuid + deduction_balance BigInt + deduction_balance_before BigInt? + balance BigInt + balance_before BigInt? + message String? + created_at DateTime @default(now()) @db.Timestamptz(3) + updated_at DateTime @default(now()) @db.Timestamptz(3) + billing_id String @db.Uuid +} + +model UserInfo { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @map("user_uid") @db.Uuid + signUpRegionUid String @map("sign_up_region_uid") @db.Uuid + isInited Boolean @default(false) @map("is_inited") + // verify_email Boolean @default(false) + config Json? + user User @relation(fields: [userUid], references: [uid]) + + @@map("user_info") +} + +model WorkspaceUsage { + id String @id @default(uuid()) @db.Uuid + userUid String @map("user_uid") @db.Uuid + workspaceUid String @map("workspace_uid") @db.Uuid + regionUid String @map("region_uid") @db.Uuid + seat Int + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + region Region @relation(fields: [regionUid], references: [uid]) + user User @relation(fields: [userUid], references: [uid]) + + @@unique([regionUid, userUid, workspaceUid]) + @@index([userUid]) + @@map("workspace_usage") +} + +model ErrorPaymentCreate { + userUid String @db.Uuid + regionUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + regionUserOwner String + method String + amount BigInt + gift BigInt? + trade_no String @unique + code_url String? + invoiced_at Boolean? @default(false) + remark String? + message String + create_time DateTime @default(now()) @db.Timestamptz(3) +} + +model Payment { + id String @id + userUid String @db.Uuid + regionUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + regionUserOwner String + method String + amount BigInt + gift BigInt? + trade_no String @unique + code_url String? + invoiced_at Boolean? @default(false) + remark String? + message String +} + +model User { + uid String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + avatarUri String + nickname String + id String @unique + name String @unique + oauthProvider OauthProvider[] + status UserStatus @default(NORMAL_USER) + oldMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("oldUser") + newMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("newUser") + DeleteUserTransactionInfo DeleteUserTransactionInfo? + deleteUserLog DeleteUserLog? + userTasks UserTask[] + userInfo UserInfo? + WorkspaceUsage WorkspaceUsage[] +} + +model Transfer { + uid String @id @default(uuid()) @db.Uuid + fromUserUid String @db.Uuid + toUserUid String @db.Uuid + amount BigInt + remark String + created_at DateTime @default(now()) @db.Timestamptz(6) +} + +model ErrorAccountCreate { + userUid String @id @default(uuid()) @db.Uuid + activityBonus BigInt + encryptBalance String + encryptDeductionBalance String + created_at DateTime @default(now()) @db.Timestamptz(3) + create_region_id String + balance BigInt? + deduction_balance BigInt? + userCr String @unique + error_time DateTime @default(now()) @db.Timestamptz(3) + regionUid String @db.Uuid + regionUserOwner String + message String +} + +model CommitTransactionSet { + createdAt DateTime @default(now()) @db.Timestamptz(3) + precommitTransactionUid String @unique @db.Uuid + precommitTransaction PrecommitTransaction @relation(fields: [precommitTransactionUid], references: [uid]) +} + +model PrecommitTransaction { + uid String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + transactionType TransactionType + infoUid String + status TransactionStatus + transactionDetail TransactionDetail[] + commitTransactionSet CommitTransactionSet? + errorPreCommitTransaction ErrorPreCommitTransaction? + + @@unique([infoUid, transactionType]) +} + +model ErrorPreCommitTransaction { + uid String @id @default(uuid()) @db.Uuid + transactionUid String @unique @db.Uuid + reason String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + precommitTransaction PrecommitTransaction @relation(fields: [transactionUid], references: [uid]) +} + +model TransactionDetail { + uid String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + status TransactionStatus + regionUid String + transactionUid String + region Region @relation(fields: [regionUid], references: [uid]) + precommitTransaction PrecommitTransaction @relation(fields: [transactionUid], references: [uid]) + + @@unique([transactionUid, regionUid]) + @@index([regionUid]) +} + +model MergeUserTransactionInfo { + uid String @id @default(uuid()) @db.Uuid + mergeUserUid String @unique + userUid String + mergeUser User? @relation("oldUser", fields: [mergeUserUid], references: [uid]) + user User? @relation("newUser", fields: [userUid], references: [uid]) + + @@index([userUid]) +} + +model DeleteUserTransactionInfo { + uid String @id @default(uuid()) @db.Uuid + userUid String @unique + user User? @relation(fields: [userUid], references: [uid]) +} + +model DeleteUserLog { + userUid String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + user User @relation(fields: [userUid], references: [uid]) +} + +model AuditLog { + uid String @id @default(uuid()) @db.Uuid + entityUid String + entityName String + createdAt DateTime @default(now()) @db.Timestamptz(3) + action AuditAction + auditLogDetail AuditLogDetail[] +} + +model AuditLogDetail { + auditLogUid String @id + key String + preValue String + newValue String + auditLog AuditLog @relation(fields: [auditLogUid], references: [uid]) +} + +model EventLog { + uid String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + mainId String + eventName String + // json + data String +} + +model InviteReward { + payment_id String @id + userUid String @db.Uuid + created_at DateTime @default(now()) @db.Timestamptz(3) + payment_amount BigInt + reward_amount BigInt + inviteFrom String @db.Uuid +} + +model UserRealNameInfo { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @db.Uuid + realName String? + idCard String? + phone String? + isVerified Boolean @default(false) + idVerifyFailedTimes Int @default(0) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("UserRealNameInfo") +} + +model EnterpriseRealNameInfo { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @db.Uuid + enterpriseName String? + enterpriseQualification String? + legalRepresentativePhone String? + isVerified Boolean @default(false) + verificationStatus String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + supportingMaterials Json? + + @@map("EnterpriseRealNameInfo") +} + +model RestrictedUser { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @db.Uuid + restrictedLevel Int + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("RestrictedUser") +} + +model RealNameAuthProvider { + id String @id @default(uuid()) @db.Uuid + backend String + authType String + maxFailedTimes Int + config Json? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@map("RealNameAuthProvider") +} + +model UserSemChannel { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @db.Uuid + channel String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + additionalInfo Json? + + @@map("UserSemChannel") +} + +model GiftCode { + id String @id @default(uuid()) @db.Uuid + code String @unique + creditAmount BigInt @default(0) + used Boolean @default(false) + usedBy String? @db.Uuid + usedAt DateTime? + createdAt DateTime @default(now()) + expiredAt DateTime? + comment String? +} + + +model UserAccountType { + id String @id @default(uuid()) @db.Uuid + userUid String @unique @db.Uuid + userType UserType @default(EXTERNAL_USER) + productSeries ProductSeries[] + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + @@map("UserAccountType") +} + +model GiftCodeCreation { + id String @id @default(uuid()) @db.Uuid + giftCodeId String @unique @db.Uuid // 添加 @unique + createdByUserUid String @db.Uuid + rechargeType RechargeCodeType + createdAt DateTime @default(now()) @db.Timestamptz(3) + @@map("GiftCodeCreation") +} + +enum UserType { + EXTERNAL_USER + INTERNAL_EMPLOYEE +} + +enum ProductSeries { + SEALOS + FASTGPT + LAF_SEALAF + AI_PROXY +} + +enum RechargeCodeType { + TEST_RECHARGE + COMPENSATION_RECHARGE + ACTIVITY_RECHARGE + CORPORATE_RECHARGE +} + +enum ProviderType { + PHONE + GITHUB + WECHAT + GOOGLE + PASSWORD + OAUTH2 + EMAIL +} + +enum TransactionStatus { + READY + RUNNING + FINISH + COMMITED + ERROR +} + +enum TransactionType { + MERGE_USER + DELETE_USER +} + +enum AuditAction { + UPDATE + DELETE + CREATE +} + +enum UserStatus { + NORMAL_USER + LOCK_USER + DELETE_USER +} + +model Task { + id String @id @default(uuid()) @db.Uuid + title String + description String + reward BigInt + order Int + isActive Boolean @default(true) + isNewUserTask Boolean @default(false) + taskType TaskType + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userTasks UserTask[] +} + +model UserTask { + id String @id @default(uuid()) @db.Uuid + userUid String @db.Uuid + taskId String @db.Uuid + status TaskStatus + rewardStatus TaskStatus + completedAt DateTime + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + user User @relation(fields: [userUid], references: [uid]) + task Task @relation(fields: [taskId], references: [id]) + + @@unique([userUid, taskId]) + @@index([taskId]) +} + +enum TaskType { + LAUNCHPAD + COSTCENTER + DATABASE + DESKTOP + APPSTORE + CRONJOB + DEVBOX + CONTACT + REAL_NAME_AUTH +} + +enum TaskStatus { + NOT_COMPLETED + COMPLETED +} diff --git a/frontend/desktop/prisma/providers/postgresql/region/migrations/20260411072156_init/migration.sql b/frontend/desktop/prisma/providers/postgresql/region/migrations/20260411072156_init/migration.sql new file mode 100644 index 000000000000..1c324ec2d463 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/region/migrations/20260411072156_init/migration.sql @@ -0,0 +1,138 @@ +-- CreateEnum +CREATE TYPE "JoinStatus" AS ENUM ('INVITED', 'IN_WORKSPACE', 'NOT_IN_WORKSPACE'); + +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('MANAGER', 'DEVELOPER', 'OWNER'); + +-- CreateTable +CREATE TABLE "Workspace" ( + "uid" UUID NOT NULL, + "id" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Workspace_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserCr" ( + "uid" UUID NOT NULL, + "crName" TEXT NOT NULL, + "userUid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserCr_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserWorkspace" ( + "uid" UUID NOT NULL, + "alias" TEXT, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "workspaceUid" UUID NOT NULL, + "userCrUid" UUID NOT NULL, + "handlerUid" UUID, + "role" "Role" NOT NULL DEFAULT 'DEVELOPER', + "status" "JoinStatus" NOT NULL, + "isPrivate" BOOLEAN NOT NULL, + "joinAt" TIMESTAMPTZ(3), + + CONSTRAINT "UserWorkspace_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "VerificationCodes" ( + "uid" UUID NOT NULL, + "userUid" UUID, + "scenario" TEXT NOT NULL, + "providerType" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "code" TEXT NOT NULL, + "flowToken" TEXT, + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + "metadata" JSONB NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "VerificationCodes_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "OAuthVerifications" ( + "uid" UUID NOT NULL, + "provider" TEXT NOT NULL, + "scenario" TEXT NOT NULL, + "providerId" TEXT, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "OAuthVerifications_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "WorkspaceInvitations" ( + "uid" UUID NOT NULL, + "workspaceUid" UUID NOT NULL, + "inviterUid" UUID NOT NULL, + "inviterCrUid" UUID NOT NULL, + "role" "Role" NOT NULL, + "invitationCode" TEXT NOT NULL, + "expiresAt" TIMESTAMPTZ(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "WorkspaceInvitations_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Workspace_id_key" ON "Workspace"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCr_crName_key" ON "UserCr"("crName"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCr_userUid_key" ON "UserCr"("userUid"); + +-- CreateIndex +CREATE INDEX "UserWorkspace_userCrUid_idx" ON "UserWorkspace"("userCrUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserWorkspace_workspaceUid_userCrUid_key" ON "UserWorkspace"("workspaceUid", "userCrUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationCodes_flowToken_key" ON "VerificationCodes"("flowToken"); + +-- CreateIndex +CREATE INDEX "VerificationCodes_expiresAt_idx" ON "VerificationCodes"("expiresAt"); + +-- CreateIndex +CREATE INDEX "VerificationCodes_scenario_code_idx" ON "VerificationCodes"("scenario", "code"); + +-- CreateIndex +CREATE INDEX "VerificationCodes_scenario_providerType_providerId_expiresA_idx" ON "VerificationCodes"("scenario", "providerType", "providerId", "expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationCodes_scenario_providerType_providerId_key" ON "VerificationCodes"("scenario", "providerType", "providerId"); + +-- CreateIndex +CREATE INDEX "OAuthVerifications_provider_providerId_idx" ON "OAuthVerifications"("provider", "providerId"); + +-- CreateIndex +CREATE INDEX "OAuthVerifications_expiresAt_idx" ON "OAuthVerifications"("expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "OAuthVerifications_provider_code_key" ON "OAuthVerifications"("provider", "code"); + +-- CreateIndex +CREATE UNIQUE INDEX "WorkspaceInvitations_invitationCode_key" ON "WorkspaceInvitations"("invitationCode"); + +-- CreateIndex +CREATE INDEX "WorkspaceInvitations_expiresAt_idx" ON "WorkspaceInvitations"("expiresAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "WorkspaceInvitations_workspaceUid_inviterUid_inviterCrUid_r_key" ON "WorkspaceInvitations"("workspaceUid", "inviterUid", "inviterCrUid", "role"); diff --git a/frontend/desktop/prisma/providers/postgresql/region/migrations/migration_lock.toml b/frontend/desktop/prisma/providers/postgresql/region/migrations/migration_lock.toml new file mode 100644 index 000000000000..fbffa92c2bb7 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/region/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/frontend/desktop/prisma/providers/postgresql/region/schema.prisma b/frontend/desktop/prisma/providers/postgresql/region/schema.prisma new file mode 100644 index 000000000000..85bbdd01f635 --- /dev/null +++ b/frontend/desktop/prisma/providers/postgresql/region/schema.prisma @@ -0,0 +1,109 @@ +generator regionClient { + provider = "prisma-client-js" + output = "./generated/client" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + url = env("REGION_DATABASE_URL") + relationMode = "prisma" +} + +model Workspace { + uid String @id @default(uuid()) @db.Uuid + id String @unique + displayName String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userWorkspace UserWorkspace[] +} + +model UserCr { + uid String @id @default(uuid()) @db.Uuid + crName String @unique + userUid String @unique @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userWorkspace UserWorkspace[] +} + +model UserWorkspace { + uid String @id @default(uuid()) @db.Uuid + alias String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + workspaceUid String @db.Uuid + userCrUid String @db.Uuid + handlerUid String? @db.Uuid + role Role @default(DEVELOPER) + status JoinStatus + isPrivate Boolean + joinAt DateTime? @db.Timestamptz(3) + userCr UserCr @relation(fields: [userCrUid], references: [uid]) + workspace Workspace @relation(fields: [workspaceUid], references: [uid]) + + @@unique([workspaceUid, userCrUid]) + @@index([userCrUid]) +} + +model VerificationCodes { + uid String @id @default(uuid()) @db.Uuid + userUid String? @db.Uuid + scenario String + providerType String + providerId String + code String + flowToken String? @unique + expiresAt DateTime @db.Timestamptz(3) + metadata Json + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@unique([scenario, providerType, providerId]) + @@index([expiresAt]) + @@index([scenario, code]) + @@index([scenario, providerType, providerId, expiresAt]) +} + +model OAuthVerifications { + uid String @id @default(uuid()) @db.Uuid + provider String + scenario String + providerId String? + code String + expiresAt DateTime @db.Timestamptz(3) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@unique([provider, code]) + @@index([provider, providerId]) + @@index([expiresAt]) +} + +model WorkspaceInvitations { + uid String @id @default(uuid()) @db.Uuid + workspaceUid String @db.Uuid + inviterUid String @db.Uuid + inviterCrUid String @db.Uuid + role Role + invitationCode String @unique + expiresAt DateTime @db.Timestamptz(3) + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + @@unique([workspaceUid, inviterUid, inviterCrUid, role]) + @@index([expiresAt]) +} + +enum JoinStatus { + INVITED + IN_WORKSPACE + NOT_IN_WORKSPACE +} + +enum Role { + MANAGER + DEVELOPER + OWNER +} diff --git a/frontend/desktop/prisma/region/migrations/20240204080217_init/migration.sql b/frontend/desktop/prisma/region/migrations/20240204080217_init/migration.sql new file mode 100644 index 000000000000..5fc288a52362 --- /dev/null +++ b/frontend/desktop/prisma/region/migrations/20240204080217_init/migration.sql @@ -0,0 +1,57 @@ +-- CreateEnum +CREATE TYPE "JoinStatus" AS ENUM ('INVITED', 'IN_WORKSPACE', 'NOT_IN_WORKSPACE'); + +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('MANAGER', 'DEVELOPER', 'OWNER'); + +-- CreateTable +CREATE TABLE "Workspace" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "id" STRING NOT NULL, + "displayName" STRING NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Workspace_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserCr" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "crName" STRING NOT NULL, + "userUid" UUID NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + CONSTRAINT "UserCr_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserWorkspace" ( + "uid" UUID NOT NULL DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + "workspaceUid" UUID NOT NULL, + "userCrUid" UUID NOT NULL, + "handlerUid" UUID, + "role" "Role" NOT NULL DEFAULT 'DEVELOPER', + "status" "JoinStatus" NOT NULL, + "isPrivate" BOOL NOT NULL, + "joinAt" TIMESTAMPTZ(3), + + CONSTRAINT "UserWorkspace_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCr_crName_key" ON "UserCr"("crName"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCr_userUid_key" ON "UserCr"("userUid"); + +-- CreateIndex +CREATE INDEX "UserWorkspace_userCrUid_idx" ON "UserWorkspace"("userCrUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserWorkspace_workspaceUid_userCrUid_key" ON "UserWorkspace"("workspaceUid", "userCrUid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Workspace_id_key" ON "Workspace"("id"); \ No newline at end of file diff --git a/frontend/desktop/prisma/region/migrations/20251022070057_add_user_alias_in_workspaces/migration.sql b/frontend/desktop/prisma/region/migrations/20251022070057_add_user_alias_in_workspaces/migration.sql new file mode 100644 index 000000000000..2eac90baa4c3 --- /dev/null +++ b/frontend/desktop/prisma/region/migrations/20251022070057_add_user_alias_in_workspaces/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserWorkspace" ADD COLUMN "alias" STRING; diff --git a/frontend/desktop/prisma/region/migrations/migration_lock.toml b/frontend/desktop/prisma/region/migrations/migration_lock.toml new file mode 100644 index 000000000000..7106c8a84885 --- /dev/null +++ b/frontend/desktop/prisma/region/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "cockroachdb" \ No newline at end of file diff --git a/frontend/desktop/prisma/region/schema.prisma b/frontend/desktop/prisma/region/schema.prisma new file mode 100644 index 000000000000..82803c1066b9 --- /dev/null +++ b/frontend/desktop/prisma/region/schema.prisma @@ -0,0 +1,60 @@ +generator regionClient { + provider = "prisma-client-js" + output = "./generated/client" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "cockroachdb" + url = env("REGION_DATABASE_URL") + relationMode = "prisma" +} + +model Workspace { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id String @unique + displayName String + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userWorkspace UserWorkspace[] +} + +model UserCr { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + crName String @unique + userUid String @unique @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userWorkspace UserWorkspace[] +} + +model UserWorkspace { + uid String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + alias String? + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + workspaceUid String @db.Uuid + userCrUid String @db.Uuid + handlerUid String? @db.Uuid + role Role @default(DEVELOPER) + status JoinStatus + isPrivate Boolean + joinAt DateTime? @db.Timestamptz(3) + userCr UserCr @relation(fields: [userCrUid], references: [uid]) + workspace Workspace @relation(fields: [workspaceUid], references: [uid]) + + @@unique([workspaceUid, userCrUid]) + @@index([userCrUid]) +} + +enum JoinStatus { + INVITED + IN_WORKSPACE + NOT_IN_WORKSPACE +} + +enum Role { + MANAGER + DEVELOPER + OWNER +} diff --git a/frontend/desktop/public/favicon.ico b/frontend/desktop/public/favicon.ico new file mode 100644 index 000000000000..aaf5a412ae67 Binary files /dev/null and b/frontend/desktop/public/favicon.ico differ diff --git a/frontend/desktop/public/iconfont/iconfont.js b/frontend/desktop/public/iconfont/iconfont.js new file mode 100644 index 000000000000..67e0464caafd --- /dev/null +++ b/frontend/desktop/public/iconfont/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_3859970='',function(o){var a=(a=document.getElementsByTagName("script"))[a.length-1],t=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,i,h,c,e,m=function(a,t){t.parentNode.insertBefore(a,t)};if(t&&!o.__iconfont__svg__cssinject__){o.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,t=document.createElement("div");t.innerHTML=o._iconfont_svg_string_3859970,(t=t.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",t=t,(a=document.body).firstChild?m(t,a.firstChild):a.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),l()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(h=l,c=o.document,e=!1,s(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,d())})}function d(){e||(e=!0,h())}function s(){try{c.documentElement.doScroll("left")}catch(a){return void setTimeout(s,50)}d()}}(window); \ No newline at end of file diff --git a/frontend/desktop/public/icons/app_launchpad.svg b/frontend/desktop/public/icons/app_launchpad.svg new file mode 100644 index 000000000000..1558e2cb3916 --- /dev/null +++ b/frontend/desktop/public/icons/app_launchpad.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/icons/apps.svg b/frontend/desktop/public/icons/apps.svg new file mode 100644 index 000000000000..354bb75cf2b1 --- /dev/null +++ b/frontend/desktop/public/icons/apps.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/desktop/public/icons/close.png b/frontend/desktop/public/icons/close.png new file mode 100644 index 000000000000..0b548cbcdb56 Binary files /dev/null and b/frontend/desktop/public/icons/close.png differ diff --git a/frontend/desktop/public/icons/close_white.svg b/frontend/desktop/public/icons/close_white.svg new file mode 100644 index 000000000000..af44633954f0 --- /dev/null +++ b/frontend/desktop/public/icons/close_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/icons/database.svg b/frontend/desktop/public/icons/database.svg new file mode 100644 index 000000000000..5a5370a328fa --- /dev/null +++ b/frontend/desktop/public/icons/database.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/icons/devbox.svg b/frontend/desktop/public/icons/devbox.svg new file mode 100644 index 000000000000..eb7ae604424f --- /dev/null +++ b/frontend/desktop/public/icons/devbox.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/icons/driverStar.svg b/frontend/desktop/public/icons/driverStar.svg new file mode 100644 index 000000000000..506fa8bea942 --- /dev/null +++ b/frontend/desktop/public/icons/driverStar.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/desktop/public/icons/empty.svg b/frontend/desktop/public/icons/empty.svg new file mode 100644 index 000000000000..f213a801161d --- /dev/null +++ b/frontend/desktop/public/icons/empty.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/desktop/public/icons/favicon-16x16.png b/frontend/desktop/public/icons/favicon-16x16.png new file mode 100644 index 000000000000..20f0b5b00761 Binary files /dev/null and b/frontend/desktop/public/icons/favicon-16x16.png differ diff --git a/frontend/desktop/public/icons/favicon-32x32.png b/frontend/desktop/public/icons/favicon-32x32.png new file mode 100644 index 000000000000..0684464ad8a4 Binary files /dev/null and b/frontend/desktop/public/icons/favicon-32x32.png differ diff --git a/frontend/desktop/public/icons/home.svg b/frontend/desktop/public/icons/home.svg new file mode 100644 index 000000000000..40e60d227c1d --- /dev/null +++ b/frontend/desktop/public/icons/home.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/icons/icon-512x512.png b/frontend/desktop/public/icons/icon-512x512.png new file mode 100644 index 000000000000..1ca76f5363ee Binary files /dev/null and b/frontend/desktop/public/icons/icon-512x512.png differ diff --git a/frontend/desktop/public/icons/inviter.svg b/frontend/desktop/public/icons/inviter.svg new file mode 100644 index 000000000000..f7c0fee038e5 --- /dev/null +++ b/frontend/desktop/public/icons/inviter.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/desktop/public/icons/license.svg b/frontend/desktop/public/icons/license.svg new file mode 100644 index 000000000000..9893206ace49 --- /dev/null +++ b/frontend/desktop/public/icons/license.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/maximize.png b/frontend/desktop/public/icons/maximize.png new file mode 100644 index 000000000000..356086e41e2c Binary files /dev/null and b/frontend/desktop/public/icons/maximize.png differ diff --git a/frontend/desktop/public/icons/maxmin.png b/frontend/desktop/public/icons/maxmin.png new file mode 100644 index 000000000000..f756664f2327 Binary files /dev/null and b/frontend/desktop/public/icons/maxmin.png differ diff --git a/frontend/desktop/public/icons/minimize.png b/frontend/desktop/public/icons/minimize.png new file mode 100644 index 000000000000..329e19612bfc Binary files /dev/null and b/frontend/desktop/public/icons/minimize.png differ diff --git a/frontend/desktop/public/icons/my-home.svg b/frontend/desktop/public/icons/my-home.svg new file mode 100644 index 000000000000..40e60d227c1d --- /dev/null +++ b/frontend/desktop/public/icons/my-home.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/icons/pay_wechat.svg b/frontend/desktop/public/icons/pay_wechat.svg new file mode 100644 index 000000000000..ecab7b8e6a87 --- /dev/null +++ b/frontend/desktop/public/icons/pay_wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/icons/shell_coin.svg b/frontend/desktop/public/icons/shell_coin.svg new file mode 100644 index 000000000000..759c769a0a04 --- /dev/null +++ b/frontend/desktop/public/icons/shell_coin.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/stripe.svg b/frontend/desktop/public/icons/stripe.svg new file mode 100644 index 000000000000..219cbed9b966 --- /dev/null +++ b/frontend/desktop/public/icons/stripe.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/token.svg b/frontend/desktop/public/icons/token.svg new file mode 100644 index 000000000000..b0dd76f35a8c --- /dev/null +++ b/frontend/desktop/public/icons/token.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/desktop/public/icons/warning.svg b/frontend/desktop/public/icons/warning.svg new file mode 100644 index 000000000000..767b5b2ae609 --- /dev/null +++ b/frontend/desktop/public/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/Vector.svg b/frontend/desktop/public/images/Vector.svg new file mode 100644 index 000000000000..b86710dd65da --- /dev/null +++ b/frontend/desktop/public/images/Vector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/adminer.svg b/frontend/desktop/public/images/adminer.svg new file mode 100644 index 000000000000..8d98579f79c8 --- /dev/null +++ b/frontend/desktop/public/images/adminer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/allVector.svg b/frontend/desktop/public/images/allVector.svg new file mode 100644 index 000000000000..664c071cb7bf --- /dev/null +++ b/frontend/desktop/public/images/allVector.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/images/ant-design_safety-outlined.svg b/frontend/desktop/public/images/ant-design_safety-outlined.svg new file mode 100644 index 000000000000..cc2c56a18884 --- /dev/null +++ b/frontend/desktop/public/images/ant-design_safety-outlined.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/app_launchpad.svg b/frontend/desktop/public/images/app_launchpad.svg new file mode 100644 index 000000000000..327da443eaa9 --- /dev/null +++ b/frontend/desktop/public/images/app_launchpad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/bg-blue.svg b/frontend/desktop/public/images/bg-blue.svg new file mode 100644 index 000000000000..a2bcf99ee2e1 --- /dev/null +++ b/frontend/desktop/public/images/bg-blue.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/bg-dark.svg b/frontend/desktop/public/images/bg-dark.svg new file mode 100644 index 000000000000..2f76d9361d6a --- /dev/null +++ b/frontend/desktop/public/images/bg-dark.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/bg-light.svg b/frontend/desktop/public/images/bg-light.svg new file mode 100644 index 000000000000..c3040d491274 --- /dev/null +++ b/frontend/desktop/public/images/bg-light.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/bg.svg b/frontend/desktop/public/images/bg.svg new file mode 100644 index 000000000000..8b3de7be43fa --- /dev/null +++ b/frontend/desktop/public/images/bg.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/boxgrid.svg b/frontend/desktop/public/images/boxgrid.svg new file mode 100644 index 000000000000..93ad84f31e80 --- /dev/null +++ b/frontend/desktop/public/images/boxgrid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/cloud_providers/alibaba_cloud.svg b/frontend/desktop/public/images/cloud_providers/alibaba_cloud.svg new file mode 100644 index 000000000000..dd83754e7f2b --- /dev/null +++ b/frontend/desktop/public/images/cloud_providers/alibaba_cloud.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/desktop/public/images/cloud_providers/google_cloud.svg b/frontend/desktop/public/images/cloud_providers/google_cloud.svg new file mode 100644 index 000000000000..ef410340d346 --- /dev/null +++ b/frontend/desktop/public/images/cloud_providers/google_cloud.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/cloud_providers/tencent_cloud.svg b/frontend/desktop/public/images/cloud_providers/tencent_cloud.svg new file mode 100644 index 000000000000..467f5b2d091b --- /dev/null +++ b/frontend/desktop/public/images/cloud_providers/tencent_cloud.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/cloud_providers/volcano_engine.svg b/frontend/desktop/public/images/cloud_providers/volcano_engine.svg new file mode 100644 index 000000000000..c5547763b33d --- /dev/null +++ b/frontend/desktop/public/images/cloud_providers/volcano_engine.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/desktop/public/images/cost_center.svg b/frontend/desktop/public/images/cost_center.svg new file mode 100644 index 000000000000..3dc3db42a687 --- /dev/null +++ b/frontend/desktop/public/images/cost_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/customer-service-qr.png b/frontend/desktop/public/images/customer-service-qr.png new file mode 100644 index 000000000000..1dc5bd824dff Binary files /dev/null and b/frontend/desktop/public/images/customer-service-qr.png differ diff --git a/frontend/desktop/public/images/database.svg b/frontend/desktop/public/images/database.svg new file mode 100644 index 000000000000..bd8e2e4c9fed --- /dev/null +++ b/frontend/desktop/public/images/database.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/default-user.svg b/frontend/desktop/public/images/default-user.svg new file mode 100644 index 000000000000..a3ce9deb795e --- /dev/null +++ b/frontend/desktop/public/images/default-user.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/desktop/public/images/docs.svg b/frontend/desktop/public/images/docs.svg new file mode 100644 index 000000000000..02d4de2a16a2 --- /dev/null +++ b/frontend/desktop/public/images/docs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/driver-bg.png b/frontend/desktop/public/images/driver-bg.png new file mode 100644 index 000000000000..492aa89f3178 Binary files /dev/null and b/frontend/desktop/public/images/driver-bg.png differ diff --git a/frontend/desktop/public/images/hexgrid.svg b/frontend/desktop/public/images/hexgrid.svg new file mode 100644 index 000000000000..a3b227f5204a --- /dev/null +++ b/frontend/desktop/public/images/hexgrid.svg @@ -0,0 +1 @@ + diff --git a/frontend/desktop/public/images/kubernetes.svg b/frontend/desktop/public/images/kubernetes.svg new file mode 100644 index 000000000000..cf60cebfe90f --- /dev/null +++ b/frontend/desktop/public/images/kubernetes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/language.svg b/frontend/desktop/public/images/language.svg new file mode 100644 index 000000000000..bb5820f349dc --- /dev/null +++ b/frontend/desktop/public/images/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/lock.svg b/frontend/desktop/public/images/lock.svg new file mode 100644 index 000000000000..67c66a181017 --- /dev/null +++ b/frontend/desktop/public/images/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/material-symbols_expand-more-rounded.svg b/frontend/desktop/public/images/material-symbols_expand-more-rounded.svg new file mode 100644 index 000000000000..350795ab9d2e --- /dev/null +++ b/frontend/desktop/public/images/material-symbols_expand-more-rounded.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/images/material-symbols_update.svg b/frontend/desktop/public/images/material-symbols_update.svg new file mode 100644 index 000000000000..5678bcb3e2d7 --- /dev/null +++ b/frontend/desktop/public/images/material-symbols_update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/no-notification.svg b/frontend/desktop/public/images/no-notification.svg new file mode 100644 index 000000000000..33406ee7d203 --- /dev/null +++ b/frontend/desktop/public/images/no-notification.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/onboarding/app-launchpad-1.png b/frontend/desktop/public/images/onboarding/app-launchpad-1.png new file mode 100644 index 000000000000..bbdbf1c1ab09 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/app-launchpad-1.png differ diff --git a/frontend/desktop/public/images/onboarding/app-launchpad-2.png b/frontend/desktop/public/images/onboarding/app-launchpad-2.png new file mode 100644 index 000000000000..a80c07fdc8e3 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/app-launchpad-2.png differ diff --git a/frontend/desktop/public/images/onboarding/app-launchpad-3.png b/frontend/desktop/public/images/onboarding/app-launchpad-3.png new file mode 100644 index 000000000000..cf68f0631fde Binary files /dev/null and b/frontend/desktop/public/images/onboarding/app-launchpad-3.png differ diff --git a/frontend/desktop/public/images/onboarding/appstore-1.png b/frontend/desktop/public/images/onboarding/appstore-1.png new file mode 100644 index 000000000000..03fff1e0a703 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/appstore-1.png differ diff --git a/frontend/desktop/public/images/onboarding/appstore-2.png b/frontend/desktop/public/images/onboarding/appstore-2.png new file mode 100644 index 000000000000..05cd3adc95a8 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/appstore-2.png differ diff --git a/frontend/desktop/public/images/onboarding/bg.png b/frontend/desktop/public/images/onboarding/bg.png new file mode 100644 index 000000000000..34ea90d64a22 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/bg.png differ diff --git a/frontend/desktop/public/images/onboarding/database-1.png b/frontend/desktop/public/images/onboarding/database-1.png new file mode 100644 index 000000000000..6c817c1b92b1 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/database-1.png differ diff --git a/frontend/desktop/public/images/onboarding/database-2.png b/frontend/desktop/public/images/onboarding/database-2.png new file mode 100644 index 000000000000..2bf4ab42e584 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/database-2.png differ diff --git a/frontend/desktop/public/images/onboarding/database-3.png b/frontend/desktop/public/images/onboarding/database-3.png new file mode 100644 index 000000000000..12b8f9cb8884 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/database-3.png differ diff --git a/frontend/desktop/public/images/onboarding/devbox-1.png b/frontend/desktop/public/images/onboarding/devbox-1.png new file mode 100644 index 000000000000..6834b9dd7962 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/devbox-1.png differ diff --git a/frontend/desktop/public/images/onboarding/devbox-2.png b/frontend/desktop/public/images/onboarding/devbox-2.png new file mode 100644 index 000000000000..0e220c24874c Binary files /dev/null and b/frontend/desktop/public/images/onboarding/devbox-2.png differ diff --git a/frontend/desktop/public/images/onboarding/devbox-3.png b/frontend/desktop/public/images/onboarding/devbox-3.png new file mode 100644 index 000000000000..6c3350ca6b21 Binary files /dev/null and b/frontend/desktop/public/images/onboarding/devbox-3.png differ diff --git a/frontend/desktop/public/images/person.svg b/frontend/desktop/public/images/person.svg new file mode 100644 index 000000000000..33a049c4b0fb --- /dev/null +++ b/frontend/desktop/public/images/person.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/pgadmin.svg b/frontend/desktop/public/images/pgadmin.svg new file mode 100644 index 000000000000..f1fa779945af --- /dev/null +++ b/frontend/desktop/public/images/pgadmin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/sealos-box.svg b/frontend/desktop/public/images/sealos-box.svg new file mode 100644 index 000000000000..c8d362291e31 --- /dev/null +++ b/frontend/desktop/public/images/sealos-box.svg @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/frontend/desktop/public/images/sealos-title.png b/frontend/desktop/public/images/sealos-title.png new file mode 100644 index 000000000000..c2e7789a8d34 Binary files /dev/null and b/frontend/desktop/public/images/sealos-title.png differ diff --git a/frontend/desktop/public/images/sealos.svg b/frontend/desktop/public/images/sealos.svg new file mode 100644 index 000000000000..70e73265686f --- /dev/null +++ b/frontend/desktop/public/images/sealos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/images/signin_bg.png b/frontend/desktop/public/images/signin_bg.png new file mode 100644 index 000000000000..14275a113963 Binary files /dev/null and b/frontend/desktop/public/images/signin_bg.png differ diff --git a/frontend/desktop/public/images/signin_bg_zh.png b/frontend/desktop/public/images/signin_bg_zh.png new file mode 100644 index 000000000000..113a145478c6 Binary files /dev/null and b/frontend/desktop/public/images/signin_bg_zh.png differ diff --git a/frontend/desktop/public/images/uil_info-circle.svg b/frontend/desktop/public/images/uil_info-circle.svg new file mode 100644 index 000000000000..5ce5d337439a --- /dev/null +++ b/frontend/desktop/public/images/uil_info-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/applist.json b/frontend/desktop/public/locales/en/applist.json new file mode 100644 index 000000000000..ec1eb9924380 --- /dev/null +++ b/frontend/desktop/public/locales/en/applist.json @@ -0,0 +1,12 @@ +{ + "terminal": "terminal", + "job": "job", + "other": "other", + "object-storage": "object-storage", + "cloud-vm": "cloud-vm", + "db": "database", + "app": "app", + "app-store": "app-store", + "db-backup": "db-backup", + "app_type": "app type" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/cloudProviders.json b/frontend/desktop/public/locales/en/cloudProviders.json new file mode 100644 index 000000000000..d3d952959d6f --- /dev/null +++ b/frontend/desktop/public/locales/en/cloudProviders.json @@ -0,0 +1,11 @@ +{ + "volcano_engine": "Volcano Engine", + "alibaba_cloud": "Alibaba Cloud", + "tencent_cloud": "Tencent Cloud", + "google_cloud": "Google Cloud", + "provider": "Provider", + "beijing": "Beijing", + "singapore": "Singapore", + "guangzhou": "Guangzhou", + "hangzhou": "Hangzhou" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json new file mode 100644 index 000000000000..40e0e7462b6a --- /dev/null +++ b/frontend/desktop/public/locales/en/common.json @@ -0,0 +1,494 @@ +{ + "abdication": "Abdicate", + "accept": "Accept", + "accept_invitation": "Accept Invitation", + "access": "Access", + "account_bank_required": "Please enter the correct bank name of the enterprise", + "account_number": "Bank Account", + "account_number_required": "Please enter the correct recipient bank account of the enterprise", + "account_settings": "Account Settings", + "added": "Added", + "agree_policy": "I have read and agree to the", + "alarm_pod": "Unhealthy Pods: {{count}}", + "notification_alert": "Alert", + "alerts": "Alerts", + "all_apps": "All Apps", + "already_sent_code": "Already sent code", + "amount_forecast": "Estimated cost based on recent usage", + "and": "and", + "app_info": "App Info", + "app_launchpad": "App Launchpad", + "application_desktop": "Application desktop", + "application_desktop_tips": "Installed application portal", + "apps_running_alert": "It is detected that the following applications are currently running. Please confirm whether to stop the project from running to avoid consuming the remaining balance.", + "apps_running_app_applaunchpad": "App Launchpad", + "apps_running_app_database": "Database", + "apps_running_app_devbox": "DevBox", + "apps_running_desc_devbox_one": "{{count}} DevBox is running.", + "apps_running_desc_devbox_other": "{{count}} DevBoxes are running.", + "apps_running_do_not_show_again": "Don't show this prompt again.", + "apps_running_prompt_desc_applaunchpad_one": "{{count}} application is running.", + "apps_running_prompt_desc_applaunchpad_other": "{{count}} applications are running.", + "apps_running_prompt_desc_devbox_one": "{{count}} database service is running.", + "apps_running_prompt_desc_devbox_other": "{{count}} database services are running.", + "apps_running_running_badge": "Running", + "apps_running_shutdown": "Go to Shutdown", + "apps_running_title": "Confirm closing the page?", + "attachment": "appendix", + "avatar": "Avatar", + "balance_popover": { + "balance": "Balance", + "cancelled_notice": "Your subscription is cancelled. Resources will be deleted after {{date}}. Please back up your data or renew to avoid loss.", + "check_costcenter_tip": "Check more details in Cost Center", + "expired_at": "Expired At:", + "expires_on": "Expires on:", + "payg_plan": "PAYG", + "plan_suffix": "Plan", + "renew_subscription_button": "Renew subscription", + "subscription_status": { + "expired": "Expired", + "limited_trial": "Limited Trial" + }, + "top_up": "Top Up", + "trial_expiry_tip_one": "Your trial will expire in {{count}} day.", + "trial_expiry_tip_other": "Your trial will expire in {{count}} days.", + "trial_expiry_upgrade_tip_one": "Your trial will expire in {{count}} day. Upgrade to keep your services online.", + "trial_expiry_upgrade_tip_other": "Your trial will expire in {{count}} days. Upgrade to keep your services online.", + "upgrade_button": "Upgrade Plan", + "upgrade_tip": "To upgrade your plan, you can visit the Cost Center.", + "can_not_manage_payments": "Please contact the namespace owner to update or cancel the subscription." + }, + "balance_text": "Balance", + "bank_name": "Bank", + "banner": { + "button": "check the details", + "date": ", Valid April 22 - April 28.", + "description": "Up to a bonus of ", + "title": "First charge discount, limited time only." + }, + "billing": "Billing", + "bind": "Link", + "bind_failed": "Binding failed", + "bind_success": "Binding successful", + "provider_conflict_error": "This phone number is already bound and does not meet the account merge conditions. Please try a different phone number!", + "complete_binding": "Complete Binding", + "bindemail": "Bind Email", + "bindphone": "Bind Phone", + "bonus": "Bonus", + "bound": "Bound", + "business_license": "business license", + "cancel": "Cancel", + "go_back": "Go Back", + "captcha_init_failed": "Failed to initialize CAPTCHA", + "change": "Change", + "change_binding": "Change Binding", + "changeemail": "Modify Email", + "changepassword": "Change Password", + "changephone": "Change Phone", + "charge": "Charge", + "click_anywhere_to_continue": "Click on any blank space to continue", + "click_on_any_shadow_to_skip": "Click on any shadow to skip", + "click_to_upload_file": "Click to upload file", + "completed": "Finish", + "community": { + "menu": { + "title": "Community", + "badge": "Featured", + "description": "Updates • Events • Stories" + }, + "modal": { + "title": "Join the Sealos Community", + "description": "Help shape the future of Sealos.", + "button": "Join Us", + "qr_alt": "Sealos community QR code" + } + }, + "completed_the_deployment_of_an_nginx_for_the_first_time": "Completed a deployment of nginx for the first time", + "confirm": "Confirm", + "confirm_again": "confirm again", + "copy": "Copy", + "confirmnewpassword": "Confirm New Password", + "contact_info": "Phone Number", + "contact_info_must_be_numeric": "It must be a numeric string", + "contact_info_required": "Please enter the correct contact information", + "core": "Core", + "cost_center": "Cost Center", + "create_team": "Create Workspace", + "create_workspace": "Create Workspace", + "created_time": "Created Time", + "currentpassword": "Current Password", + "currentpasswordrequired": "Current password cannot be left empty", + "database": "Database", + "day": "Day", + "default_team": "Personal Workspace", + "delete_account": "Delete account", + "delete_account_button": "Delete", + "delete_account_caution": "Once a resource is deleted, it cannot be recovered. \nPlease ensure you backup any important data before continuing.", + "delete_account_force_button": "Delete Directly", + "delete_account_remain_resources": "The following resources in your account have not been deleted. Please manually delete all the resources listed below.", + "delete_account_tips": "Permanently delete this account and all its content.", + "delete_account_title": "Delete Account", + "deleteaccounttitle": "Sealos will permanently delete this account.", + "deletemyaccount": "DeleteMyAccount", + "deploy_an_application": "Let’s deploy an application~", + "description": "Description", + "detail": "Detail", + "dissolve_team": "Delete Workspace", + "dissovle_tips": "Deleting the workspace will all delete all resources. Ensure you have a copy of any important data. Are you sure you want to continue", + "doc": "Docs", + "email": "Email", + "emailchangesuccess": "Email modified successfully", + "enter": "Enter", + "enter_confirm": "Please enter {{value}} to confirm", + "enter_phone_number": "Please enter phone number", + "enter_verify_code": "Please enter verification code", + "enterpriseKey": "Enterprise Name (Bank Account Name)", + "enterpriseKeyPlaceholder": "Enter the name of your enterprise", + "enterprise_auth_tips": "We will deposit a small amount into your enterprise bank account.\nConfirm the amount to complete verification. ", + "enterprise_key": "Unified Social Credit Code", + "enterprise_key_required": "Please enter the correct Unified Social Credit Code (USCC) of the enterprise", + "enterprise_keyname": "Enterprise Name (Bank Account Name) ", + "enterprise_keyname_placeholder": "Enter the name of your enterprise", + "enterprise_name": "Company name", + "enterprise_name_required": "Please enter the correct enterprise name", + "enterprise_realname_cancel_failed": "Cancellation failed.", + "enterprise_realname_cancel_success": "Cancellation successful.", + "enterprise_realname_payment_failed": "Verify that the payment amount failed to be transferred.", + "enterprise_realname_payment_success": "Verify that the amount has been transferred to your company's bank account.", + "enterprise_realname_verify_failed": "Enterprise real-name verification failed.", + "enterprise_realname_verify_success": "Enterprise real-name verification successful.", + "enterprise_verification": "Enterprise", + "expected_to_use_next_month": "Usage for the next 30 days", + "expected_used": "Estimated Runaway", + "face_recognition_failed": "Personal real name failed", + "face_recognition_success": "Personal real-name success", + "failed_to_generate_invitation_link": "Failed to generate invitation link", + "failed_to_get_qr_code": "Failed to obtain real name QR code", + "failed_to_refresh_qr_code": "The QR code failed to refresh", + "flow": " Network", + "force_delete_keywords": "No resources can be recovered after account deletion.", + "force_delete_tips": "There are still active resources in your account. Once deleted, these resources will be unrecoverable. Please ensure you have have a backup of any important data.", + "from": "From", + "generate_invitation_link": "Generate invitation link", + "get_code": "Verification", + "get_code_failed": "Get code failed", + "get_code_too_frequent": "Code request too frequent", + "get_verification_amount": "Request", + "gift_amount": "Reward {{amount}} balance.", + "github": "Github", + "google": "Google", + "guide": "Guide", + "guide_applaunchpad": "Quickly deploy applications without cumbersome configuration", + "guide_costcenter": "Find detailed info about your costs, usage, and manage your balance", + "guide_dbprovider": "Create multiple databases in seconds to meet different application needs", + "guide_devbox": "Easily create development environments that seamlessly connect with your favourite IDEs", + "guide_objectstorage": "Massive storage availability with near bare-metal performance", + "guide_workorder": "Your consultation portal for technical questions", + "handle": "Handle", + "have_read": "Read", + "healthy_pod": "Healthy Pods: {{count}}", + "hello_welcome": "Hello, welcome to", + "help_you_enable_high_availability_database": "Help you enable high availability database", + "home": "home", + "in_payment": "In Payment ...", + "in_time": "In Time", + "insufficient_balance": "Your account currently has outstanding payments", + "insufficient_balance_tips": "There is currently an outstanding balance in your account. In order to successfully complete the account cancellation process, please settle the outstanding balance first.", + "invaild_context": "This action cannot be performed on the active workspace, please switch to another workspace before continuing", + "invaild_name_of_team": "Invaild Name of Workspace", + "invalid_email": "Invalid email", + "invalid_invitation_link": "Invalid invitation link", + "invalid_phone_number": "Invalid phone number", + "invalid_user_id": "Invalid User ID", + "invalid_username_or_password": "Invalid username or password", + "invalid_verification_amount": "The verification amount format is invalid", + "invalid_verification_code": "The input verification code is incorrect, please check if there is an input error.", + "invalid_verification_code_with_attempts_one": "Incorrect verification code. {{count}} attempt remaining.", + "invalid_verification_code_with_attempts_other": "Incorrect verification code. {{count}} attempts remaining.", + "invitation_reminder": "Invitation reminder", + "invite_member": "Invite Member", + "invite_members_to_workspace": "Invite members to workspace {{workspace}}", + "irreversibleactiontips": "This action is irreversible, please proceed with caution.", + "jump_over": "Jump Over", + "laf_on_sealos": "Laf on Sealos", + "language": "Language", + "later": "Later", + "launch_various_third-party_applications_with_one_click": "Launch various third-party applications with one click", + "legal_person": "Legal Representative", + "license_buy": "License Buy", + "license_required": "License required, please contact administrator", + "link": "link", + "link_to_workorder": "Submit a Ticket", + "loading": "Loading", + "log_in": "Log In", + "log_out": "Log Out", + "login_to_your_account": "Login to your account", + "login_with_github": "Login with Github", + "login_with_google": "Login with Google", + "login_with_oauth2": "login with OAuth2.0", + "login_with_wechat": "Login with Wechat", + "manage_team": "Manage Workspaces", + "member_list": "Member List", + "memory": "Memory", + "merge": "merge", + "merge_account_tips1": "The account you are trying to bind has been used by another user. Due to conflicting binding methods, the accounts cannot be merged.", + "merge_account_tips2": "The account you are trying to bind has been used by another user. You can choose to merge accounts to manage your information and settings in a unified way. After the merge is completed, your private workspace may be converted into a regular workspace. Do you want to merge accounts now?", + "merge_account_title": "Account has been bound", + "message_center": "Message Center", + "modify_member": "Modify Member", + "monitor": "Monitor", + "more_apps": "More Apps", + "name": "Name", + "name_of_team": "Name of Workspace", + "nav_links": { + "balance": "Balance", + "cancelled": "Cancelled", + "plan_expired": "Expired", + "plan_suffix": "Plan", + "renew": "Renew", + "subscribe": "Subscribe", + "upgrade_plan": "Upgrade" + }, + "new_email": "New email", + "new_phone": "New mobile number", + "newpassword": "New Password", + "newuser_benefit": "Benefits", + "next": "Next", + "next_time": "Next", + "nickname": "Nickname", + "no_apps_found": "No Apps Found", + "no_realname_auth": "Identity Not Verified", + "notification": "Notification", + "no_notifications_yet": "No notifications yet", + "no_notifications_desc": "Recent notifications will be shown here. Everything is going well.", + "noworkspacecreated": "You haven't created a workspace yet", + "official_account_login": "Official account login", + "old_email": "Old email", + "old_phone": "old mobile number", + "online_service": "Help", + "operating": "Operating", + "order_number": "Order Number", + "password": "Password", + "password_login": "with Password", + "password_mis_match": "Passwords do not match", + "password_tips": "Password must be 8 characters or more", + "passwordchangesuccess": "Password changed successfully", + "pay_with_stripe": "Pay With Stripe", + "pay_with_wechat": "Pay With Wechat", + "payment_result": "Payment Result", + "payment_status": "Payment Status", + "payment_successful": "Payment Successful", + "personal_verification": "Individual", + "phone": "Phone", + "phone_binding_description": "To ensure the security of your account assets and comply with regulatory requirements, please bind your phone number to enable full services.", + "phone_binding_required": "Bind Phone", + "phone_number_tips": "Phone Number", + "phonechangesuccess": "Mobile phone number modified successfully", + "plan": "Plan", + "please_enter": "Please enter", + "please_enter_account_number": "Enter the bank account number", + "please_enter_bank_name": "Enter a bank name", + "please_enter_contact_info": "Enter the phone number", + "please_enter_enterprise_key": "Enter the Unified Social Credit Code", + "please_enter_legal_person": "Enter the name of the legal representative", + "please_enter_username": "Please enter your username", + "please_enter_verification_amount": "Enter the verification amount (in cents).", + "privacy_policy": "Privacy Policy", + "private_team_id_of_user": "User's ID", + "purchase_history": "Purchase History", + "purchase_license": "Purchase License", + "purchase_link_error": "Purchase Link Error", + "qr_code_refresh_note": "If the real name fails, you can try to refresh the QR code", + "qr_code_refreshed_successfully": "The QR code refreshed successfully", + "quick_application_switching_floating_ball": "Quick application switching floating ball", + "quit": "Quit", + "quit_workspace_tips": "Confirm leaving workspace?", + "read_all": "Read All", + "read_and_agree": "Please read and agree to the agreement below", + "realNameVerification": "Real Name Verification", + "realName_verification": "Identity Verification", + "realname_auth_now": "Click to verify your name", + "realname_auth_reminder": "Real-name authentication reminder", + "realname_auth_reminder_desc": "Real-name verification is required for regions in China. Without real-name verification, top-up will be restricted. Successful real-name verification will be rewarded with a {{reward}} Sealos balance.", + "realname_auth_reminder_desc_no_reward": "Real-name verification is required for regions in China. Without real-name verification, top-up will be restricted.", + "realname_info": "RealName", + "receive_tips": "{{managerName}} invites you to join {{teamName}} wtih the {{role}}", + "recharge_amount": "Recharge Amount", + "redirecting_to_homepage_in_3_seconds": "Redirecting to homepage in 3 seconds", + "refresh_qr_code": "Refresh the QR code", + "regenerate_link": "Regenerate", + "region": "Region", + "reject": "Reject", + "remain_app_tips": "There are still undeleted application resources in your account. Please backup any important data and manually delete all application resources.", + "remain_database_tips": "There are still undeleted database resources in your account. Please backup any important data and manually delete all database resources.", + "remain_objectstorage_tips": "There are still undeleted object storage resources in your account. Please backup any important data and manually delete all object storage resources.", + "remain_other_region_resource_tips": "There are still associated resources that have not been deleted in your account. Please backup any important data and manually delete all region resources.", + "remain_template_tips": "There are still undeleted template resources in your account. Please backup any important data and manually delete all template resources.", + "remain_workspace_tips": "There are still undeleted associated resources in your account. Please backup any important data and manually delete or transfer any workspaces.", + "remaining_attempts": "{{count}} times left", + "remaining_time": "Remaining Time: ", + "remove": "Remove", + "remove_member_tips": "Are you sure you wish to remove this member?", + "rename": "Rename", + "retry_get_qr_code": "reacquire", + "scan_qr_code_for_face_recognition": "Use WeChat on your mobile phone to scan the QR code to complete identity verification", + "scan_to_join_group": "Scan the QR code to add exclusive customer service", + "scan_with_wechat": "Scan with WeChat", + "sealos_copilot": "Sealos Copilot", + "sealos_document": "Sealos Document", + "sealos_newcomer_benefits": "Sealos New Member Benefits", + "search_apps": "Search Apps", + "select_amount": "Select Amount", + "sending": "Sending", + "service_agreement": "Service Agreement", + "set_alias_prompt": { + "cancel": "Cancel", + "confirm": "Confirm", + "title": "Set Alias" + }, + "spend": "spend", + "start_immediately": "Start", + "start_now": "Go Now", + "start_your_sealos_journey": "Start your Sealos journey", + "status": "Status", + "storage": "Storage", + "submit_error": "Submit Error", + "submit_verification": "Submit ", + "switching_disc": "Switching Disc", + "team": "Workspace", + "terminal": "Terminal", + "the_invited_user_must_be_others": "The invited user must be others", + "theme": "Theme", + "toggle_app_bar": "Toggle App Bar", + "total_amount": "Total Amount", + "transAmt_not_match": "The verification amount does not match. Please note that the verification amount is in cents. For example, if the received amount is 0.23 yuan, enter 23.", + "unbind": "Unbind", + "unbind_success": "Unbinding successfully", + "unbindemail": "Unbind Email", + "unbindphone": "Unbind Phone", + "unbound": "Not Linked", + "under_active_development": "Under active development 🚧", + "unread": "Unread", + "used_last_month": "Usage for the last 30 days", + "used_resources": "Resources Used", + "user_name": "User Name", + "user_name_required": "Please enter the correct name of the enterprise's legal representative", + "user_table_set_alias": "Set Alias", + "username": "Username", + "username_tips": "Username must be 3-16 characters, including letters, numbers", + "usertask": { + "task_appstore_desc": "Provides prefabricated multiple types of application and tool templates, click to install, and automatically deploy", + "task_appstore_title": "App Store", + "task_database_desc": "Distributed storage, compatible with multi-language ecology, no need to build complex multi-node architecture by yourself", + "task_database_title": "Create Database", + "task_launchpad_desc": "Create a container cluster with one click, automatically deploy container applications, and provide intranet/extranet access addresses", + "task_launchpad_title": "Deploy App" + }, + "verification_amount": "Verification Amount", + "verification_amount_required": "The verification amount cannot be empty", + "verification_amount_tips": "Shanghai UnionPay will deposit a random amount into your bank account for\nverification, typically arriving in real-time. If not received in 3 business day, please ", + "verification_amount_tips2": "Please confirm the deposited verification amount with your finance team. It usually arrives in real-time.", + "verification_attempts_exhausted": "Too many attempts. Request a new verification code.", + "verification_attempts_exhausted_with_retry": "Too many attempts. Request a new verification code in {{countdown}} seconds.", + "verification_code_expired": "The verification code has expired. Request a new one.", + "verification_send_rate_limited": "Too many requests. Try again in {{countdown}} seconds.", + "verification_code_login": "with Phone", + "verify_code_tips": "Please input 6-digit Verification Code", + "verify_password": "Verify password", + "verifycode": "Verification", + "view_discount_rules": "View recharge discount rules.", + "view_later": "Talk to You later", + "waiting": "Waiting", + "waiting_for_face_recognition": "Real name result query in progress...", + "warning": "Warning", + "wechat": "Wechat", + "work_order": "Work Order", + "workspace": "Workspace", + "year": "Year", + "you_can_complete_the_following_operations": "You can do the following", + "you_can_use_the_kubectl_command_directly_from_the_terminal": "You can use the kubectl command directly from the terminal", + "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", + "you_have_not_purchased_the_license": "You have not purchased the License", + "yuan": "Yuan", + "alert_settings": { + "menu_item": "Alert Settings", + "title": "Alert Settings", + "description": "When your balance is insufficient or service is abnormal, we will notify you through the following methods.", + "loading": "Loading...", + "phone": { + "section_title": "Send SMS", + "enable_all": "Enable (Select All)", + "no_phone_numbers": "No phone numbers", + "add_phone": "Add Phone Number", + "bound_badge": "Bound Phone" + }, + "email": { + "section_title": "Send Email", + "enable_all": "Enable (Select All)", + "no_emails": "No emails", + "add_email": "Add Email", + "bound_badge": "Bound Email" + }, + "bind": { + "phone_title": "Bind Phone Number", + "email_title": "Bind Email", + "phone_label": "Phone Number", + "email_label": "Email", + "phone_placeholder": "Please enter phone number", + "email_placeholder": "Please enter email", + "code_label": "Verification Code", + "code_placeholder": "Please enter verification code", + "send_code": "Send Verification Code", + "sending": "Sending...", + "resend": "Resend ({{count}}s)", + "complete_bind": "Complete Binding", + "verifying": "Verifying...", + "phone_required": "Please enter phone number", + "email_required": "Please enter email", + "phone_invalid": "Please enter a valid phone number", + "email_invalid": "Please enter a valid email", + "code_required": "Please enter verification code", + "code_invalid": "Verification code is incorrect", + "already_bound": "This phone number/email is already bound", + "account_bound_phone": "This phone number is already bound to your account, no need to bind again", + "account_bound_email": "This email is already bound to your account, no need to bind again", + "code_sent": "Verification code sent", + "send_code_failed": "Failed to send verification code", + "verify_failed": "Verification failed" + }, + "delete": { + "title": "Confirm Delete", + "confirm_phone": ["Are you sure you want to delete phone number ", "?"], + "confirm_email": ["Are you sure you want to delete email ", "?"], + "confirm_generic": "Are you sure you want to delete?", + "cancel": "Cancel", + "delete": "Delete" + }, + "messages": { + "fetch_failed": "Failed to fetch alert list", + "create_success": "Created successfully", + "create_failed": "Failed to create", + "delete_success": "Deleted successfully", + "delete_failed": "Failed to delete", + "update_success": "Updated successfully", + "update_failed": "Failed to update" + } + }, + "refresh_kubeconfig": "Refresh Kubeconfig", + "kubeconfig_rotating": "Rotating Kubeconfig...", + "kubeconfig_rotated_successfully": "Kubeconfig refreshed successfully", + "kubeconfig_rotation_failed": "Failed to refresh Kubeconfig", + "delete_account_subscription_warning_title": "Warning:", + "delete_account_subscription_warning": "Active subscriptions and resources were found in your account. We recommend canceling subscriptions in the Cost Center and removing your current work first. Alternatively, if you have confirmed everything, you can proceed to delete your account directly, and the related workspace & subscription will be canceled meanwhile:", + "delete_account_open_cost_center": "Open Cost Center", + "delete_account_delete_immediately": "Delete Immediately", + "delete_account_pending_title": "Account deletion is in progress", + "delete_account_pending_description": "We have started deleting your account. Keep this window open while we wait for the final result.", + "delete_account_pending_hint": "You will be signed out automatically after the deletion succeeds.", + "delete_account_success_title": "Account deleted successfully", + "delete_account_success_description": "Your account deletion has finished successfully.", + "delete_account_success_hint": "Signing you out now…", + "delete_account_failed_description": "We could not finish deleting your account. Review the failed workspaces below.", + "delete_account_failed_hint": "Workspace subscription cancellation failed before account deletion could finish.", + "delete_account_failed_no_details": "No workspace failure details were returned." +} diff --git a/frontend/desktop/public/locales/en/error.json b/frontend/desktop/public/locales/en/error.json new file mode 100644 index 000000000000..c835d5c6b37b --- /dev/null +++ b/frontend/desktop/public/locales/en/error.json @@ -0,0 +1,23 @@ +{ + "NOT_SUPPORT": "Not supported", + "OAUTH_PROVIDER_NOT_FOUND": "Current OAuth provider not found", + "EXIST_SAME_OAUTH_PROVIDER": "Same OAuth provider already exists", + "USER_NOT_FOUND": "User not found", + "RESULT_SUCCESS": "Success response", + "INTERNAL_SERVER_ERROR": "Internal server error", + "INSUFFICENT_BALANCE": "Insufficient balance", + "ACCOUNT_NOT_FOUND": "Account not found", + "MERGET_USER_INSUFFICENT_BALANCE":"Insufficient balance in the merged account", + "USER_CR_NOT_FOUND": "User CR not found", + "OAUTHPROVIDER_NOT_FOUND": "OAuth provider not found", + "PRIVATE_WORKSPACE_NOT_FOUND": "Private workspace not found", + "GET_RESOURCE_ERROR": "Get resource error", + "REMAIN_OTHER_REGION_RESOURCE": "Remaining resources in other region", + "REMAIN_WORKSACE_OWNER": "Remaining workspace owner in current region", + "REMAIN_CVM": "Remaining cloud virtual machine in current region", + "REMAIN_APP": "Remaining applications in current region", + "REMAIN_TEMPLATE": "Remaining templates in current region", + "REMAIN_OBJECT_STORAGE": "Remaining object storage in current region", + "REMAIN_DATABASE": "Remaining databases in current region", + "KUBECONFIG_NOT_FOUND": "Kubeconfig not found" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/v2.json b/frontend/desktop/public/locales/en/v2.json new file mode 100644 index 000000000000..c881e3d0d472 --- /dev/null +++ b/frontend/desktop/public/locales/en/v2.json @@ -0,0 +1,150 @@ +{ + "account_settings": "Account Settings", + "add_workspace": "Add Workspace", + "aiproxy_tips": "AI Proxy Major Update!", + "alert": "Alerts", + "back": "Back", + "can_request_new_link": "You can try again in {{countdown}}s", + "change_login_method": "Change the Login Method", + "check_your_email": "Check your email", + "check_your_phone": "Please input 6-digit Verification Code", + "choose_a_region": "Choose a region", + "confirm_password": "Confirm Password", + "continue": "Continue", + "copy_kubeconfig": "Copy Kubeconfig", + "copy_failed": "Copy failed", + "copy_success": "Copied!", + "create_workspace": "Create Workspace", + "database_create_desc": "Open database app to deploy a database", + "database_create_title": "Access Database", + "database_desc": "Provision high-availability databases with automated backups and scaling in seconds.", + "database_step_1": "Access Database", + "database_step_1_desc": "Open database app to deploy a database", + "database_step_2": "Deploy a New Database", + "database_step_2_desc": "Choose a database type, and adjust CPU & memory as needed", + "database_step_3": "Manage & Connect to the Database", + "database_step_3_desc": "Retrieve connection details and manage the database", + "database_title": "Deploy Managed Database", + "delete": "Delete", + "devbox_create_desc": "Enter DevBox to create a new development environment", + "devbox_create_title": "Create Devbox", + "devbox_desc": "Instantly spin up zero-config cloud environments for any language or framework.", + "devbox_step_1": "Create a New Project", + "devbox_step_1_desc": "Open the DevBox module and create a new project", + "devbox_step_2": "Configure Your Settings", + "devbox_step_2_desc": "Name your project and choose a predefined template", + "devbox_step_3": "Get Coding", + "devbox_step_3_desc": "Open your project in your favorite IDE", + "devbox_title": "Launch Dev Environment", + "didnt_get_it": "Didn't get it?", + "documentation_desc": "Get started from recommended scenarios below", + "double_first_deposit": "Double first", + "edit": "Edit", + "email": "Email Address", + "email_sign_in": "Continue with Email", + "first_name": "First Name", + "get_started": "Get Started", + "get_started_from_recommended": "Select a scenario to launch your first workload.", + "github_login_email_conflict_description": "The email address associated with the GitHub account you are attempting to log in with is already in use by another Sealos account. You can either select a different GitHub account or use an alternative login method.", + "github_login_failed": "GitHub Log-in Failed", + "github_reauth_select_account": "Log in with Another GitHub Account", + "got_it": "got it", + "guest_mode": "Guest Mode", + "guest_mode_login_tip": "Login to unlock full features", + "guide": "Guide", + "guide_info_text": "Try a guided demo - no charges, no real deployments. Click “Show Me” to begin.", + "guide_steps": "{{count}} steps", + "guide_title": "Hi, {{name}}. Welcome to your free plan.", + "includes_credits": "Includes ${{credits}} credits", + "invite_link_copied": "Invitation link copied!", + "join_discord_prompt": { + "cancel": "Maybe Later", + "description": "Got questions or feedback? Join our official Discord community to get real-time help from our team and other users.", + "do_not_show_again": "Don't show this again", + "join_discord": "Join our Discord", + "title": "Need Help? Join our Community!" + }, + "join_our_mailing_list": "Join our mailing list", + "kubeconfig": "Kubeconfig", + "last_name": "Last Name", + "launchpad_create_desc": "Open App Launchpad to deploy a Docker image", + "launchpad_create_title": "Create Launchpad", + "launchpad_desc": "Directly deploy any application from a Docker image and manage containers with ease.", + "launchpad_step_1": "Access App Launchpad", + "launchpad_step_1_desc": "Open App Launchpad to deploy a Docker image", + "launchpad_step_2": "Configure Launchpad", + "launchpad_step_2_desc": "Define image settings, and adjust CPU & memory as needed", + "launchpad_step_3": "Access Application", + "launchpad_step_3_desc": "Copy the Private or Public Address for access", + "launchpad_title": "Run Docker Image", + "login_sign_up": "Login / Sign Up", + "lost_password_reset": "Lost Password Reset", + "lost_password_reset_description": "Forgotten your password? Enter your email address below to begin the reset process.", + "most_popular": "Most popular", + "next": "Next", + "onboard_guide": "Not sure where to start? View the Quickstart Guide. ", + "or": "OR CONTINUE WITH", + "or_sign_in_with": "OR Sign in with", + "or_sign_up_with": "Or Sign up with", + "password": "Password", + "password_hint": "Forgot password?", + "password_reset_requested": "Password Reset Requested", + "password_reset_requested_description": "If the email address you entered matched an existing account, please check your email for instructions on what to do next.", + "personal_information": "Personal Information", + "personal_information_description": "Complete your profile with key details to personalize your experience.", + "phone_verification_message": "Verification code has been sent to {{phone}}", + "plan_and_billing": "Plan", + "plan_benefit_statement": "A short benefit statement that highlights the ideal user for this tier.", + "please_select_region_and_workspace_name": "Please select a region and workspace name", + "privacy_policy": "Privacy Policy", + "purchase_plan": "Purchase plan", + "quickstart_guide": "Quickstart Guide", + "quit_guide": "Explore More Guides", + "quit_guide_description": "To restart Guide, click here. If you're looking for extra support, select Docs to find articles that relate to your task.", + "register": "Register", + "remember_me": "Remember me", + "request_new_link": "Resend it", + "sending_code": "Sending verification code...", + "settings": "Settings", + "show_me": "Show me", + "sign_in": "Sign in", + "sign_in_failed": "Sign in failed", + "sign_in_success": "Sign in success", + "sign_in_with": "Sign in with {{provider}}", + "sign_up": "Sign up", + "sign_up_failed": "Sign up failed", + "sign_up_success": "Sign up success", + "sign_up_with": "Sign up with {{provider}}", + "start_deploying": "Start", + "step_title": "Skip to Dashboard", + "submit": "Submit", + "success": "Success", + "support": "Support", + "template_create_desc": "Launch pre-configured applications from our extensive App Store in just one click.", + "template_create_title": "Choose from template", + "template_desc": "Launch pre-configured applications from our extensive App Store in just one click.", + "template_step_1": "Choose from template", + "template_step_1_desc": "Explore App Store to deploy an application from a template", + "template_step_2": "Access Application", + "template_step_2_desc": "Get the private or public address from App Launchpad", + "template_title": "Deploy from App Store", + "terms_and_conditions": "Terms and Conditions", + "terms_and_privacy_policy_text": "By proceeding you acknowledge that you have read, understood and agree to our", + "ticket": "Ticket", + "unknown_error": "Unknown error", + "unlocked_credit": "Unlocked lifetime free access", + "unlocked_credit_description": "Please connect your GitHub account within 30 days. Failure to do so will result in service suspension.", + "upgrade": "Upgrade", + "upgrade_plan": "Upgrade Plan", + "usage_analysis": "Usage", + "username_password_signin": "Username / Password Signin", + "verification_code": "verification code", + "verification_message": "A verification code was sent to {{email}}. Enter it below to continue.", + "verifying": "verifying", + "workspace_create": "Let's create your workspace", + "workspace_deploy_failed": "Failed to deploy workspace", + "workspace_welcome_default": "Welcome to Sealos", + "worspace_heading_description": "Organize, collaborate, and manage apps efficiently with isolated workspaces for your team", + "you_may_invite_memebers_later": "You may invite members later", + "your_current_plan": "Your current plan" +} diff --git a/frontend/desktop/public/locales/zh/applist.json b/frontend/desktop/public/locales/zh/applist.json new file mode 100644 index 000000000000..0f0032601342 --- /dev/null +++ b/frontend/desktop/public/locales/zh/applist.json @@ -0,0 +1,12 @@ +{ + "terminal": "终端", + "job": "任务", + "other": "其他", + "object-storage": "对象存储", + "cloud-vm": "云主机", + "db": "数据库", + "app": "应用", + "app-store": "应用商店", + "db-backup": "数据库备份", + "app_type": "应用类型" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/cloudProviders.json b/frontend/desktop/public/locales/zh/cloudProviders.json new file mode 100644 index 000000000000..e88b5813933f --- /dev/null +++ b/frontend/desktop/public/locales/zh/cloudProviders.json @@ -0,0 +1,11 @@ +{ + "volcano_engine": "火山引擎", + "alibaba_cloud": "阿里云", + "tencent_cloud": "腾讯云", + "google_cloud": "谷歌云", + "provider": "供应商", + "beijing": "北京", + "singapore": "新加坡", + "guangzhou": "广州", + "hangzhou": "杭州" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json new file mode 100644 index 000000000000..01dc72b3ca53 --- /dev/null +++ b/frontend/desktop/public/locales/zh/common.json @@ -0,0 +1,486 @@ +{ + "abdication": "移交", + "accept": "接受", + "accept_invitation": "接受邀请", + "access": "权限", + "account_bank_required": "请输入正确的企业开户行", + "account_number": "银行账号", + "account_number_required": "请输入正确的企业开户行收款银行账号", + "account_settings": "账户设置", + "added": "已加入", + "agree_policy": "我已阅读并同意", + "alarm_pod": "告警 Pods: {{count}}", + "notification_alert": "通知", + "alerts": "告警", + "all_apps": "所有应用", + "already_sent_code": "已经发送验证码", + "amount_forecast": "根据近一天消耗金额进行预测", + "and": "和", + "app_info": "应用信息", + "application_desktop": "应用桌面", + "application_desktop_tips": "已安装应用入口", + "apps_running_alert": "检测到以下应用正在运行,请确认是否停止项目运行,避免消耗余额。", + "apps_running_app_applaunchpad": "App Launchpad", + "apps_running_app_database": "Database", + "apps_running_app_devbox": "DevBox", + "apps_running_desc_devbox_one": "{{count}} 个开发环境正在运行", + "apps_running_desc_devbox_other": "{{count}} 个开发环境正在运行", + "apps_running_do_not_show_again": "不再显示此提示", + "apps_running_prompt_desc_applaunchpad_one": "{{count}} 个应用运行中", + "apps_running_prompt_desc_applaunchpad_other": "{{count}} 个应用运行中", + "apps_running_prompt_desc_devbox_one": "{{count}} 个数据库服务运行中", + "apps_running_prompt_desc_devbox_other": "{{count}} 个数据库服务运行中", + "apps_running_running_badge": "运行中", + "apps_running_shutdown": "去关机", + "apps_running_title": "确认关闭页面?", + "attachment": "附件", + "avatar": "头像", + "balance_popover": { + "balance": "余额", + "cancelled_notice": "您的订阅已取消。资源将在 {{date}} 后删除。请及时备份数据或恢复订阅以避免损失。", + "check_costcenter_tip": "到费用中心查看详细信息", + "expired_at": "过期时间:", + "expires_on": "到期时间:", + "payg_plan": "按量付费", + "plan_suffix": "套餐", + "renew_subscription_button": "恢复订阅", + "subscription_status": { + "expired": "过期", + "limited_trial": "限时试用" + }, + "top_up": "充值", + "trial_expiry_tip_one": "你的试用将在 {{count}} 天后过期.", + "trial_expiry_tip_other": "你的试用将在 {{count}} 天后过期.", + "trial_expiry_upgrade_tip_one": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.", + "trial_expiry_upgrade_tip_other": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.", + "upgrade_button": "升级套餐", + "upgrade_tip": "跳转到费用中心以升级套餐", + "can_not_manage_payments": "请联系空间管理员执行取消或升级订阅操作" + }, + "balance_text": "余额", + "bank_name": "开户银行", + "banner": { + "button": "查看详情", + "date": ",活动日期 4月22日-4月28日", + "description": "最高立返", + "title": "首充折扣, 限时返场!", + "unit": "元" + }, + "billing": "费用中心", + "bind": "绑定", + "bind_failed": "绑定失败", + "bind_success": "绑定成功", + "provider_conflict_error": "当前手机号已被绑定,且不符合账户合并条件,请更换其他手机号尝试!", + "complete_binding": "完成绑定", + "bindemail": "绑定电子邮箱", + "bindphone": "绑定手机号", + "bonus": "赠", + "bound": "已绑定", + "business_license": "营业执照", + "cancel": "取消", + "go_back": "返回", + "captcha_init_failed": "验证码加载失败", + "change": "变更", + "change_binding": "改绑", + "changeemail": "修改电子邮箱", + "changepassword": "修改密码", + "changephone": "更改手机号", + "charge": "充值", + "click_anywhere_to_continue": "点击任意空白继续", + "click_on_any_shadow_to_skip": "点击任意阴影跳过", + "click_to_upload_file": "点击上传文件", + "completed": "完成", + "community": { + "menu": { + "title": "加入 Sealos 生态群", + "badge": "推荐", + "description": "产品更新 • 活动福利 • 案例共创" + }, + "modal": { + "title": "加入 Sealos 生态群", + "description": "与 Sealos 官方共建产品形态!", + "button": "即刻入群", + "qr_alt": "Sealos 生态群二维码" + } + }, + "completed_the_deployment_of_an_nginx_for_the_first_time": "部署一个 nginx ,首次完成 将", + "confirm": "确认", + "copy": "复制", + "confirm_again": "再次确认", + "confirmnewpassword": "确认新密码", + "contact_info": "联系方式 (手机号码)", + "contact_info_must_be_numeric": "必须是数字字符串", + "contact_info_required": "请输入正确的联系方式", + "core": "核", + "create_team": "创建工作空间", + "create_workspace": "创建工作空间", + "created_time": "创建时间", + "currentpassword": "当前密码", + "currentpasswordrequired": "当前密码不能为空", + "day": "天", + "default_team": "个人空间", + "delete_account": "注销账号", + "delete_account_button": "注销", + "delete_account_caution": "资源一旦删除,将不可恢复。因此,在执行以上操作前,请务必做好数据备份工作", + "delete_account_force_button": "直接注销", + "delete_account_remain_resources": "账号中仍有以下资源未删除,请您手动删除下列所有资源,确保无遗漏。", + "delete_account_tips": "将永久删除此账户及其所有内容", + "delete_account_title": "注销账户", + "deleteaccounttitle": "Sealos 将永久删除此账户。", + "deletemyaccount": "删除我的账号", + "deploy_an_application": "来部署一个应用吧~", + "description": "描述", + "detail": "详情", + "dissolve_team": "删除", + "dissovle_tips": "删除工作空间会清空所有资源,确定要删除吗?", + "doc": "文档", + "email": "电子邮箱", + "emailchangesuccess": "电子邮箱修改成功", + "enter": "输入", + "enter_confirm": "请输入 {{value}} 确认", + "enter_phone_number": "请输入手机号", + "enter_verify_code": "请输入验证码", + "enterprise_auth_tips": "我们会向你提交的对公账户进行小额打款, 请输入收到的金额完成验证。", + "enterprise_key": "统一社会信用代码", + "enterprise_key_required": "请输入正确的企业统一社会信用代码", + "enterprise_keyname": "企业名称 (需与银行开户名一致)", + "enterprise_keyname_placeholder": "请输入企业名称 (需与银行开户名一致)", + "enterprise_name": "企业名称", + "enterprise_name_required": "请输入正确的企业名字", + "enterprise_realname_cancel_failed": "取消失败", + "enterprise_realname_cancel_success": "取消成功", + "enterprise_realname_payment_failed": "验证金额打款失败", + "enterprise_realname_payment_success": "验证金额已经打款到您的企业开户银行", + "enterprise_realname_verify_failed": "企业实名失败", + "enterprise_realname_verify_success": "企业实名成功", + "enterprise_verification": "企业实名", + "expected_to_use_next_month": "未来30天预计使用", + "expected_used": "预计还能使用", + "face_recognition_failed": "个人实名失败", + "face_recognition_success": "个人实名成功", + "failed_to_generate_invitation_link": "生成邀请链接失败", + "failed_to_get_qr_code": "获取实名二维码失败", + "failed_to_refresh_qr_code": "二维码刷新失败", + "flow": "流量", + "force_delete_keywords": "注销后所有资源无法恢复", + "force_delete_tips": "您的账号中仍有未删除的资源。一旦注销,所有资源将无法恢复。请确保已经备份或转移了所有重要数据。", + "from": "来自", + "generate_invitation_link": "生成邀请链接", + "get_code": "获取验证码", + "get_code_failed": "获取验证码失败", + "get_code_too_frequent": "验证码获取过于频繁", + "get_verification_amount": "获取验证金额", + "gift_amount": "赠送 {{amount}} 余额.", + "github": "Github", + "google": "Google", + "guide": "引导", + "guide_applaunchpad": "快速部署应用,无需繁琐配置", + "guide_costcenter": "计费标准、资源消耗明细看板", + "guide_dbprovider": "多种数据库秒级创建,满足不同应用需求", + "guide_devbox": "自动化开发环境设置、与本地 IDE 无缝连接", + "guide_objectstorage": "海量存储空间,近乎裸机的速度体验", + "guide_workorder": "技术问题咨询入口", + "handle": "操作", + "have_read": "已读", + "healthy_pod": "健康 Pods: {{count}}", + "hello_welcome": "您好, 欢迎来到", + "help_you_enable_high_availability_database": "帮您启用高可用数据库", + "home": "首页", + "in_payment": "支付中 ...", + "in_time": "加入时间", + "insufficient_balance": "您的账户目前存在未结清的款项", + "insufficient_balance_tips": "您的账户目前存在未结清的款项,为了顺利完成账户注销流程,请先结清欠款。", + "invaild_context": "你需要切换到其他工作空间操作", + "invaild_name_of_team": "不合法的工作空间名称", + "invalid_email": "无效的电子邮箱", + "invalid_invitation_link": "邀请链接非法", + "invalid_phone_number": "无效的手机号", + "invalid_user_id": "用户的 ID 不合法", + "invalid_username_or_password": "用户名或密码错误", + "invalid_verification_amount": "验证金额格式不合法", + "invalid_verification_code": "输入的验证码不正确,请检查是否输入错误。", + "invalid_verification_code_with_attempts_one": "验证码不正确,还可尝试 {{count}} 次。", + "invalid_verification_code_with_attempts_other": "验证码不正确,还可尝试 {{count}} 次。", + "invitation_reminder": "受邀提醒", + "invite_member": "邀请成员", + "invite_members_to_workspace": "邀请成员至工作空间 {{workspace}}", + "irreversibleactiontips": "此操作不可逆转,请谨慎操作", + "jump_over": "跳过", + "language": "语言", + "later": "稍后", + "launch_various_third-party_applications_with_one_click": "一键启动各种第三方应用", + "legal_person": "法定代表人", + "license_buy": "License 购买", + "license_required": "缺少 license,请联系管理员", + "link": "链接", + "link_to_workorder": "提交工单", + "loading": "加载中", + "log_in": "登录", + "log_out": "登出", + "login_to_your_account": "登录您的帐户", + "login_with_github": "Github 登录", + "login_with_google": "Google 登录", + "login_with_oauth2": "OAuth2.0 登录", + "login_with_wechat": "Wechat 登录", + "manage_team": "管理工作空间", + "member_list": "成员列表", + "memory": "内存", + "merge": "合并", + "merge_account_tips1": "您尝试绑定的账号已被其他用户使用。由于存在冲突的其他绑定方式,无法合并账户。", + "merge_account_tips2": "您尝试绑定的账号已被其他用户使用。您可以选择合并账户,以统一管理您的信息和设置。 在合并完成后,个人空间有可能转化为普通的工作空间,是否现在合并账户?", + "merge_account_title": "账户已被绑定", + "message_center": "消息中心", + "modify_member": "修改权限", + "monitor": "监控", + "more_apps": "更多应用", + "name": "姓名", + "name_of_team": "工作空间名称", + "nav_links": { + "balance": "余额", + "cancelled": "已取消", + "plan_expired": "已过期", + "plan_suffix": "套餐", + "renew": "续费", + "subscribe": "订阅套餐", + "upgrade_plan": "升级" + }, + "new_email": "新电子邮箱", + "new_phone": "新手机号", + "newpassword": "新密码", + "newuser_benefit": "新手福利", + "next": "下一步", + "next_time": "下次吧", + "nickname": "昵称", + "no_apps_found": "未找到任何应​​用", + "no_realname_auth": "未实名认证", + "notification": "通知", + "no_notifications_yet": "暂无通知", + "no_notifications_desc": "最近的通知将显示在这里。一切正常。", + "noworkspacecreated": "您还没有创建工作空间", + "official_account_login": "公众号登录", + "old_email": "旧电子邮箱", + "old_phone": "旧手机号", + "online_service": "在线客服", + "operating": "操作", + "order_number": "订单号", + "password": "密码", + "password_login": "密码登录", + "password_mis_match": "密码不一致", + "password_tips": "密码为8位以上字符", + "passwordchangesuccess": "密码修改成功", + "pay_with_stripe": "Stripe 支付", + "pay_with_wechat": "微信支付", + "payment_result": "支付结果", + "payment_status": "支付状态", + "payment_successful": "支付成功", + "personal_verification": "个人实名", + "phone": "手机号", + "phone_binding_description": "为了保障您的账号资产安全及合规要求, 请绑定手机号开启完整服务。", + "phone_binding_required": "绑定手机号", + "phone_number_tips": "手机号", + "phonechangesuccess": "手机号修改成功", + "plan": "计划", + "please_enter": "请输入", + "please_enter_account_number": "请输入银行账号", + "please_enter_bank_name": "请输入开户行名,输入总行信息,不要输入支行信息,例如招商银行", + "please_enter_contact_info": "请输入联系方式", + "please_enter_enterprise_key": "请输入统一社会信用代码", + "please_enter_legal_person": "请输入法人姓名", + "please_enter_username": "请输入您的用户名", + "please_enter_verification_amount": "请输入验证金额(单位分)", + "privacy_policy": "隐私政策", + "private_team_id_of_user": "用户ID", + "purchase_history": "购买记录", + "purchase_license": "购买 License", + "purchase_link_error": "购买链接错误", + "qr_code_refresh_note": "如果实名失败可以尝试刷新二维码", + "qr_code_refreshed_successfully": "二维码刷新成功", + "quick_application_switching_floating_ball": "快捷应用切换悬浮球", + "quit": "退出", + "quit_workspace_tips": "确认要退出工作空间吗?", + "read_all": "全部已读", + "read_and_agree": "请阅读并同意下方协议", + "realNameVerification": "实名认证", + "realName_verification": "实名认证", + "realname_auth_now": "点击进行实名", + "realname_auth_reminder": "实名认证提醒", + "realname_auth_reminder_desc": "国内可用区需要实名认证,未实名认证将会被限制充值,实名认证成功奖励 Sealos 余额 {{reward}} 元。", + "realname_auth_reminder_desc_no_reward": "国内可用区需要实名认证,未实名认证将会被限制充值。", + "realname_info": "实名信息", + "receive_tips": "{{managerName}} 邀请你到 {{teamName}} 成为 {{role}}", + "recharge_amount": "充值金额", + "redirecting_to_homepage_in_3_seconds": "3秒后跳回主页", + "refresh_qr_code": "刷新二维码", + "regenerate_link": "重新生成", + "region": "可用区", + "reject": "拒绝", + "remain_app_tips": "您的账户中仍有未删除的应用资源,为了帮助您顺利完成账户注销流程,请您手动删除所有应用资源,以避免数据丢失", + "remain_database_tips": "您的账户中仍有未删除的数据库资源,为了帮助您顺利完成账户注销流程,请您手动删除所有数据库资源,以避免数据丢失", + "remain_objectstorage_tips": "您的账户中仍有未删除的对象存储资源,为了帮助您顺利完成账户注销流程,请您手动删除所有对象存储资源,以避免数据丢失", + "remain_other_region_resource_tips": "您好,您的账户中仍有未删除的关联资源,为了帮助您顺利完成账户注销流程,请您清理所有可用区资源,确保无遗漏。", + "remain_template_tips": "您的账户中仍有未删除的模板资源,为了帮助您顺利完成账户注销流程,请您手动删除所有模板资源,以避免数据丢失", + "remain_workspace_tips": "您的账户中仍有未删除的关联资源,为了帮助您顺利完成账户注销流程,请您清理或转移您的工作空间,以避免数据丢失", + "remaining_attempts": "剩余 {{count}} 次", + "remaining_time": "剩余激活时间: ", + "remove": "移除", + "remove_member_tips": "确认要移除该成员?", + "rename": "重命名", + "retry_get_qr_code": "重新获取", + "scan_qr_code_for_face_recognition": "使用手机微信扫描二维码,完成身份验证", + "scan_to_join_group": "扫码添加专属客服", + "scan_with_wechat": "微信扫码支付", + "sealos_copilot": "Sealos 小助理", + "sealos_newcomer_benefits": "Sealos 新手福利", + "search_apps": "搜索应用", + "select_amount": "选择金额", + "sending": "发送中", + "service_agreement": "服务协议", + "set_alias_prompt": { + "cancel": "取消", + "confirm": "确认", + "title": "设置别名" + }, + "spend": "花", + "start_immediately": "立即开始", + "start_now": "立即前往", + "start_your_sealos_journey": "开始您的 Sealos 之旅", + "status": "状态", + "storage": "存储", + "submit_error": "提交错误", + "submit_verification": "提交认证 ", + "switching_disc": "切换圆盘", + "team": "工作空间", + "the_invited_user_must_be_others": "只能邀请其他人", + "theme": "主题", + "toggle_app_bar": "切换应用栏", + "transAmt_not_match": "验证金额不匹配,注意验证金额单位是分,例如收到的打款金额为 0.23 元 则输入23。", + "unbind": "解绑", + "unbind_success": "解绑成功", + "unbindemail": "解绑电子邮箱", + "unbindphone": "解绑手机号", + "unbound": "未绑定", + "under_active_development": "正在积极开发中 🚧", + "unread": "未读", + "used_last_month": "过去30天已使用", + "used_resources": "已用资源", + "user_name": "用户名", + "user_name_required": "请输入正确的企业法人姓名", + "user_table_set_alias": "设置别名", + "username": "用户名", + "username_tips": "用户名为3-16位的英文或数字的字符", + "usertask": { + "task_appstore_desc": "提供预制的多类型应用、工具模板,点击安装,自动部署", + "task_appstore_title": "应用商店", + "task_database_desc": "分布式存储,兼容多语言生态,无须自行构建复杂的多节点架构", + "task_database_title": "创建数据库", + "task_launchpad_desc": "一键创建容器集群,自动化 部署容器应用,并提供内 网/外网访问地址", + "task_launchpad_title": "部署应用" + }, + "verification_amount": "验证金额", + "verification_amount_required": "验证金额不能为空", + "verification_amount_tips": "验证金额将由上海银联打入你提交的对公账号, 金额随机, 一般实时到账。若三个工作日内未收到, 请", + "verification_amount_tips2": "请联系公司财务确认企业银行账户收到的验证金额, 一般实时到账, 长期有效。", + "verification_attempts_exhausted": "尝试次数过多,请重新获取验证码。", + "verification_attempts_exhausted_with_retry": "尝试次数过多,请在 {{countdown}} 秒后重新获取验证码。", + "verification_code_expired": "验证码已失效,请重新获取验证码。", + "verification_send_rate_limited": "请求过于频繁,请在 {{countdown}} 秒后重试。", + "verification_code_login": "手机号登录", + "verify_code_tips": "请输入6位验证码", + "verify_password": "确认密码", + "verifycode": "验证码", + "view_discount_rules": "查看优惠规则", + "view_later": "稍后再说", + "waiting": "等待中", + "waiting_for_face_recognition": "实名结果查询中...", + "warning": "警告", + "wechat": "微信", + "work_order": "工单", + "workspace": "工作空间", + "year": "年", + "you_can_complete_the_following_operations": "您可以完成以下操作", + "you_can_use_the_kubectl_command_directly_from_the_terminal": "您可通过终端直接使用 kubectl 命令", + "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", + "you_have_not_purchased_the_license": "您还没有购买 License", + "yuan": "元", + "alert_settings": { + "menu_item": "告警设置", + "title": "告警设置", + "description": "当余额不足或服务异常时,我们将通过以下方式通知您。", + "loading": "加载中...", + "phone": { + "section_title": "发送手机短信", + "enable_all": "启用 (全选)", + "no_phone_numbers": "暂无手机号", + "add_phone": "添加手机号", + "bound_badge": "已绑定手机号" + }, + "email": { + "section_title": "发送邮件", + "enable_all": "启用 (全选)", + "no_emails": "暂无邮箱", + "add_email": "添加邮箱", + "bound_badge": "已绑定邮箱" + }, + "bind": { + "phone_title": "绑定手机号", + "email_title": "绑定邮箱", + "phone_label": "手机号", + "email_label": "邮箱", + "phone_placeholder": "请输入手机号", + "email_placeholder": "请输入邮箱", + "code_label": "验证码", + "code_placeholder": "请输入验证码", + "send_code": "发送验证码", + "sending": "发送中...", + "resend": "重新发送 ({{count}}s)", + "complete_bind": "完成绑定", + "verifying": "验证中...", + "phone_required": "请输入手机号", + "email_required": "请输入邮箱", + "phone_invalid": "请输入正确的手机号", + "email_invalid": "请输入正确的邮箱", + "code_required": "请输入验证码", + "code_invalid": "验证码错误", + "already_bound": "该手机号/邮箱已绑定", + "account_bound_phone": "该手机号已绑定到账户,无需重复绑定", + "account_bound_email": "该邮箱已绑定到账户,无需重复绑定", + "code_sent": "验证码已发送", + "send_code_failed": "发送验证码失败", + "verify_failed": "验证失败" + }, + "delete": { + "title": "确认删除", + "confirm_phone": ["确认要删除手机号 ", " 吗?"], + "confirm_email": ["确认要删除邮箱 ", " 吗?"], + "confirm_generic": "确认要删除吗?", + "cancel": "取消", + "delete": "删除" + }, + "messages": { + "fetch_failed": "获取告警列表失败", + "create_success": "创建成功", + "create_failed": "创建失败", + "delete_success": "删除成功", + "delete_failed": "删除失败", + "update_success": "更新成功", + "update_failed": "更新失败" + } + }, + "refresh_kubeconfig": "刷新 Kubeconfig", + "kubeconfig_rotating": "正在刷新 Kubeconfig...", + "kubeconfig_rotated_successfully": "Kubeconfig 刷新成功", + "kubeconfig_rotation_failed": "Kubeconfig 刷新失败", + "delete_account_subscription_warning_title": "警告:", + "delete_account_subscription_warning": "您的账号中发现了仍在生效的订阅和资源。我们建议您先前往 Cost Center 取消订阅,并先删除当前资源。如果您已经确认无误,也可以继续直接注销账号,相关工作空间和订阅届时将一并被取消:", + "delete_account_open_cost_center": "打开费用中心", + "delete_account_delete_immediately": "立即注销", + "delete_account_pending_title": "账号注销处理中", + "delete_account_pending_description": "我们已经开始注销您的账号。请保持此窗口开启,等待最终结果。", + "delete_account_pending_hint": "注销成功后,系统将自动为您退出登录。", + "delete_account_success_title": "账号已成功注销", + "delete_account_success_description": "您的账号注销流程已成功完成。", + "delete_account_success_hint": "正在为您退出登录…", + "delete_account_failed_description": "账号注销未能完成,请查看下方失败的工作空间。", + "delete_account_failed_hint": "系统在注销账号前取消工作空间订阅失败。", + "delete_account_failed_no_details": "未返回工作空间失败详情。" +} diff --git a/frontend/desktop/public/locales/zh/error.json b/frontend/desktop/public/locales/zh/error.json new file mode 100644 index 000000000000..3d93a8e47aee --- /dev/null +++ b/frontend/desktop/public/locales/zh/error.json @@ -0,0 +1,23 @@ +{ + "NOT_SUPPORT": "不支持", + "OAUTH_PROVIDER_NOT_FOUND": "当前绑定的登录方式不存在", + "EXIST_SAME_OAUTH_PROVIDER": "已存在相同的绑定的登录方式", + "USER_NOT_FOUND": "未找到用户", + "RESULT_SUCCESS": "响应成功", + "INTERNAL_SERVER_ERROR": "内部服务器错误", + "INSUFFICENT_BALANCE": "余额不足", + "MERGET_USER_INSUFFICENT_BALANCE": "被合并的账户余额不足", + "ACCOUNT_NOT_FOUND": "未找到账户", + "USER_CR_NOT_FOUND": "未找到用户CR", + "OAUTHPROVIDER_NOT_FOUND": "未找到OAuth提供程序", + "PRIVATE_WORKSPACE_NOT_FOUND": "未找到私有工作空间", + "GET_RESOURCE_ERROR": "获取资源错误", + "REMAIN_OTHER_REGION_RESOURCE": "其他可用区残留资源", + "REMAIN_WORKSACE_OWNER": "当前可用区残留工作空间", + "REMAIN_CVM": "当前可用区残留云主机", + "REMAIN_APP": "当前可用区残留应用", + "REMAIN_TEMPLATE": "当前可用区残留模板", + "REMAIN_OBJECT_STORAGE": "当前可用区残留对象存储", + "REMAIN_DATABASE": "当前可用区残留数据库", + "KUBECONFIG_NOT_FOUND": "未找到Kubeconfig" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/v2.json b/frontend/desktop/public/locales/zh/v2.json new file mode 100644 index 000000000000..3a3f5e39e8f8 --- /dev/null +++ b/frontend/desktop/public/locales/zh/v2.json @@ -0,0 +1,150 @@ +{ + "account_settings": "账户设置", + "add_workspace": "创建工作空间", + "aiproxy_tips": "AI Proxy 重大更新!", + "alert": "告警", + "back": "返回", + "can_request_new_link": "{{countdown}}s 后可重新发送验证码", + "change_login_method": "更换登录方式", + "check_your_email": "检查您的电子邮件", + "check_your_phone": "请输入6位验证码", + "choose_a_region": "选择一个可用区", + "confirm_password": "确认密码", + "continue": "继续", + "copy_kubeconfig": "复制 Kubeconfig", + "copy_failed": "复制失败", + "copy_success": "复制成功", + "create_workspace": "创建工作空间", + "database_create_desc": "点击应用,进行数据库创建", + "database_create_title": "创建数据库", + "database_desc": "全面兼容 Mysql/Redis/Pgsql 等生态,分布式存储,支持快速扩展与无缝迁移。", + "database_step_1": "创建数据库", + "database_step_1_desc": "打开数据库,点击新建", + "database_step_2": "配置数据库", + "database_step_2_desc": "选择数据库类型、版本,配置运行资源并开启备份", + "database_step_3": "连接数据库", + "database_step_3_desc": "开启外网地址,并通过工具连接管理", + "database_title": "数据库", + "delete": "删除", + "devbox_create_desc": "点击应用,创建一个云开发环境", + "devbox_create_title": "创建项目", + "devbox_desc": "深度结合容器技术,秒级创建云开发环境,集开发、测试、部署上线为一体。", + "devbox_step_1": "创建一个新项目", + "devbox_step_1_desc": "打开 DevBox,创建可共享的云开发环境", + "devbox_step_2": "配置语言框架", + "devbox_step_2_desc": "勾选所需的语言框架、版本,并配置主机资源", + "devbox_step_3": "本地连接开发", + "devbox_step_3_desc": "选择本地安装的 IDE 一键连接开发", + "devbox_title": "DevBox", + "didnt_get_it": "没收到?", + "documentation_desc": "从下面的推荐场景开始", + "double_first_deposit": "首充双倍", + "edit": "编辑", + "email": "邮箱地址", + "email_sign_in": "继续", + "first_name": "名", + "get_started": "开始使用", + "get_started_from_recommended": "从以下推荐方案开始", + "github_login_email_conflict_description": "你尝试登录的 GitHub 账户所使用的邮件地址已被另一个 Sealos 账户使用。你可以重新选择 GitHub 账户,或换用其他登录方式。", + "github_login_failed": "GitHub 登录失败", + "github_reauth_select_account": "使用其他 GitHub 账户登录", + "got_it": "明白", + "guest_mode": "游客模式", + "guest_mode_login_tip": "立即登录解锁完整功能", + "guide": "引导", + "guide_info_text": "体验引导式演示— 无需花费、无需真实部署。点击“展示”即可开始。", + "guide_steps": "{{count}}步骤", + "guide_title": "你好, {{name}}. 欢迎来到你的免费计划", + "includes_credits": "包含 ${{credits}} 积分", + "invite_link_copied": "邀请链接已复制", + "join_discord_prompt": { + "cancel": "以后再说", + "description": "有问题或者需要反馈?请加入我们的官方 Discord 社区,以获取来自我们团队以及其他用户的帮助。", + "do_not_show_again": "不再显示此提示", + "join_discord": "加入 Discord 社区", + "title": "需要帮助?请加入我们的社区!" + }, + "join_our_mailing_list": "加入我们的邮件列表", + "kubeconfig": "Kubeconfig", + "last_name": "姓", + "launchpad_create_desc": "点击应用,使用 Docker 进行部署", + "launchpad_create_title": "部署应用", + "launchpad_desc": "基于原生 K8s 架构,多应用模板化Docker部署,弹性伸缩,夜间自动释放资源。", + "launchpad_step_1": "创建应用", + "launchpad_step_1_desc": "打开应用管理,部署 Docker 应用", + "launchpad_step_2": "配置应用", + "launchpad_step_2_desc": "填入镜像地址,并配置主机资源", + "launchpad_step_3": "访问应用", + "launchpad_step_3_desc": "复制公网地址进行访问", + "launchpad_title": "应用管理", + "login_sign_up": "登录 / 注册", + "lost_password_reset": "密码重置", + "lost_password_reset_description": "忘记密码了吗?在下面输入您的电子邮件地址开始重置过程。", + "most_popular": "最受欢迎", + "next": "下一步", + "onboard_guide": "不知道从哪里开始?查看指南了解主要功能。", + "or": "或", + "or_sign_in_with": "或者用以下方式登录", + "or_sign_up_with": "或者通过以下方式登录", + "password": "密码", + "password_hint": "忘记密码?", + "password_reset_requested": "密码重置已请求", + "password_reset_requested_description": "如果您输入的电子邮件地址与现有帐户匹配,请检查您的电子邮件,以获取下一步操作的说明。", + "personal_information": "个人信息", + "personal_information_description": "完成您的个人资料来个性化您的体验", + "phone_verification_message": "已发送验证码到 {{phone}}", + "plan_and_billing": "计划与账单", + "plan_benefit_statement": "简短的收益说明,突出显示此套餐的理想用户。", + "please_select_region_and_workspace_name": "请选择一个可用区和工作空间名称", + "privacy_policy": "隐私政策", + "purchase_plan": "购买套餐", + "quickstart_guide": "快速入门指南", + "quit_guide": "退出指南", + "quit_guide_description": "要重新启动指南,请单击此处。\n如果您正在寻找额外的支持,请选择文档以查找与您的任务相关的文章。", + "register": "注册", + "remember_me": "记住我", + "request_new_link": "重新发送验证码", + "sending_code": "正在发送验证码...", + "settings": "设置", + "show_me": "展示", + "sign_in": "登录", + "sign_in_failed": "登录失败", + "sign_in_success": "登录成功", + "sign_in_with": "用{{provider}}登录", + "sign_up": "注册", + "sign_up_failed": "注册失败", + "sign_up_success": "注册成功", + "sign_up_with": "用{{provider}}注册", + "start_deploying": "开始", + "step_title": "或跳到仪表板", + "submit": "提交", + "success": "成功", + "support": "支持", + "template_create_desc": "发现、部署和管理各类云原生应用", + "template_create_title": "云原生应用市场", + "template_desc": "发现、部署和管理各类云原生应用,告别复杂配置,一键部署,即刻可用。", + "template_step_1": "选择应用", + "template_step_1_desc": "筛选所需要的工具应用,点击完成部署", + "template_step_2": "访问应用", + "template_step_2_desc": "复制公网地址,进行访问", + "template_title": "应用商店", + "terms_and_conditions": "服务条款", + "terms_and_privacy_policy_text": "继续表示您已阅读、理解并同意我们的", + "ticket": "工单", + "unknown_error": "未知错误", + "unlocked_credit": "Unlocked lifetime free access", + "unlocked_credit_description": "请在30天内连接您的GitHub账户。否则将导致服务暂停。", + "upgrade": "升级", + "upgrade_plan": "升级计划", + "usage_analysis": "使用分析", + "username_password_signin": "账号密码登录", + "verification_code": "验证码", + "verification_message": "已发送验证码到 {{email}}。在下方输入以继续。", + "verifying": "验证中", + "workspace_create": "创建您的工作空间", + "workspace_deploy_failed": "创建工作空间失败", + "workspace_welcome_default": "欢迎来到 Sealos", + "worspace_heading_description": "计算资源、应用数据和团队权限一体化", + "you_may_invite_memebers_later": "稍后可邀请成员共同协作", + "your_current_plan": "当前套餐" +} diff --git a/frontend/desktop/public/logo.svg b/frontend/desktop/public/logo.svg new file mode 100644 index 000000000000..f11bbfba39b7 --- /dev/null +++ b/frontend/desktop/public/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/scripts/cleanCluster.ps1 b/frontend/desktop/scripts/cleanCluster.ps1 new file mode 100644 index 000000000000..5b091746d48e --- /dev/null +++ b/frontend/desktop/scripts/cleanCluster.ps1 @@ -0,0 +1,20 @@ +$confirm = Read-Host "!!!会删除所有的user 和 ns 是否继续执行?[Y/N]" +if ($confirm -ne "Y") { + exit +} +$itmes = kubectl get user -o json | ConvertFrom-Json | Select-Object { $_.items } +foreach ($item in $items) { + $name = $item.metadata.name + if ($name -ne "admin") { + kubectl.exe delete user $name + } +} +$items = kubectl get ns -o json | +ConvertFrom-Json | +Select-Object { $_.items } | +Where-Object { $_.metadata.name -match "ns-*" -and $_.metadata.name -ne "ns-admin" } + +foreach ($item in $items) { + $name = $item.metadata.name + kubectl.exe delete ns $name +} diff --git a/frontend/desktop/scripts/createUser.ps1 b/frontend/desktop/scripts/createUser.ps1 new file mode 100644 index 000000000000..e8a743b411d9 --- /dev/null +++ b/frontend/desktop/scripts/createUser.ps1 @@ -0,0 +1,28 @@ +$desktopHostName = Read-Host "输入域名" +$url = "https://${desktopHostName}/api/auth/password" +$tokenUrl = "https://${desktopHostName}/api/auth/regionToken" +Write-Host $url +$requests = 500 +$batchSize = 20 +$delay = 5 + +for ($i = 0; $i -lt $requests; $i += $batchSize) { + + foreach ($num in ($i..($i + $batchSize))) { + $body = @{ + "user" = "test${num}test" + "password" = "test${num}test" + } | ConvertTo-Json + # Start-Job -ScriptBlock { + $result = Invoke-WebRequest -Uri $url -Body $body -ContentType 'application/json' + $token = [URI]::EscapeDataString( ($result.Content | ConvertFrom-Json).data.token) + Write-Host $token + $result2 = Invoke-WebRequest -Uri $tokenUrl -Headers @{ + 'Authorization' = $token + } + Write-Host $result2.Content + # } + } + + # Start-Sleep -Seconds $delay +} diff --git a/frontend/desktop/scripts/deployglobal-postgresql.ps1 b/frontend/desktop/scripts/deployglobal-postgresql.ps1 new file mode 100644 index 000000000000..093a6c6deec4 --- /dev/null +++ b/frontend/desktop/scripts/deployglobal-postgresql.ps1 @@ -0,0 +1,3 @@ +$env:GLOBAL_DATABASE_URL = Read-Host -Prompt "get GLOBAL_DATABASE_URL" + +pnpm.ps1 prisma migrate deploy --schema ./prisma/providers/postgresql/global/schema.prisma diff --git a/frontend/desktop/scripts/deployglobal.ps1 b/frontend/desktop/scripts/deployglobal.ps1 new file mode 100644 index 000000000000..652da2e188bc --- /dev/null +++ b/frontend/desktop/scripts/deployglobal.ps1 @@ -0,0 +1,3 @@ +$env:GLOBAL_DATABASE_URL = Read-Host -Prompt "get GLOBAL_DATABASE_URL" + +pnpm.ps1 prisma migrate deploy --schema ./prisma/global/schema.prisma diff --git a/frontend/desktop/scripts/deployregion-postgresql.ps1 b/frontend/desktop/scripts/deployregion-postgresql.ps1 new file mode 100644 index 000000000000..5cbe89b5cb0e --- /dev/null +++ b/frontend/desktop/scripts/deployregion-postgresql.ps1 @@ -0,0 +1,3 @@ +$env:REGION_DATABASE_URL = Read-Host -Prompt "get REGION_DATABASE_URL" + +pnpm.ps1 prisma migrate deploy --schema ./prisma/providers/postgresql/region/schema.prisma diff --git a/frontend/desktop/scripts/deployregion.ps1 b/frontend/desktop/scripts/deployregion.ps1 new file mode 100644 index 000000000000..8fad50d051c8 --- /dev/null +++ b/frontend/desktop/scripts/deployregion.ps1 @@ -0,0 +1,3 @@ +$env:REGION_DATABASE_URL = Read-Host -Prompt "get REGION_DATABASE_URL" + +pnpm.ps1 prisma migrate deploy --schema ./prisma/region/schema.prisma diff --git a/frontend/desktop/scripts/getMongodbUri.ps1 b/frontend/desktop/scripts/getMongodbUri.ps1 new file mode 100644 index 000000000000..8510cfe9722b --- /dev/null +++ b/frontend/desktop/scripts/getMongodbUri.ps1 @@ -0,0 +1,5 @@ +$_mongodbUri = (kubectl.exe get secret/desktop-frontend-secret -nsealos -o json | +ConvertFrom-Json).data.mongodb_uri +Write-Host $_mongodbUri +$mongodbUri = ConvertFrom-Base64ToString $_mongodbUri +Write-Host $mongodbUri diff --git a/frontend/desktop/scripts/getRegionSecret.ps1 b/frontend/desktop/scripts/getRegionSecret.ps1 new file mode 100644 index 000000000000..65f463b117a5 --- /dev/null +++ b/frontend/desktop/scripts/getRegionSecret.ps1 @@ -0,0 +1,3 @@ +$secret_data = (kubectl get secret desktop-frontend-secret -nsealos -ojson | ConvertFrom-Json).data +Write-Host "JWT_SECRET_REGION=$($secret_data.JWT_SECRET_REGION)" +Write-Host "PASSWORD_SALT=$($secret_data.PASSWORD_SALT)" \ No newline at end of file diff --git a/frontend/desktop/scripts/getSalt.ps1 b/frontend/desktop/scripts/getSalt.ps1 new file mode 100644 index 000000000000..0fa0103c5075 --- /dev/null +++ b/frontend/desktop/scripts/getSalt.ps1 @@ -0,0 +1,5 @@ +$_salt = (kubectl.exe get secret/desktop-frontend-secret -nsealos -o json | +ConvertFrom-Json).data.jwt_secret +Write-Host $_salt +$salt = ConvertFrom-Base64ToString $_salt +Write-Host $salt diff --git a/frontend/desktop/scripts/init.sql b/frontend/desktop/scripts/init.sql new file mode 100644 index 000000000000..9f443df51478 --- /dev/null +++ b/frontend/desktop/scripts/init.sql @@ -0,0 +1,11 @@ +ALTER DATABASE "sealos-desktop" SET PRIMARY REGION "us-east1"; +ALTER DATABASE "sealos-desktop" ADD region "us-west1"; +ALTER DATABASE "sealos-desktop" ADD region "europe-west1"; +ALTER TABLE "RegionUser" SET LOCALITY REGIONAL BY ROW; +ALTER TABLE "RegionUserToWorkspace" SET LOCALITY REGIONAL BY ROW; +ALTER TABLE "Workspace" SET LOCALITY REGIONAL BY ROW; +ALTER TABLE "OauthProvider" SET LOCALITY GLOBAL; +ALTER TABLE "RegionUser" SET LOCALITY GLOBAL; +ALTER TABLE "Region" SET LOCALITY GLOBAL; +insert into "Region" (uid, "displayName", location, "utcDelta", "domain") +values ('b7c94022-6f17-4252-bf24-b937108712a5', 'home', 'us-west1', 8, 'localhost'); diff --git a/frontend/desktop/scripts/testDeleteAccountTrnasaction.ps1 b/frontend/desktop/scripts/testDeleteAccountTrnasaction.ps1 new file mode 100644 index 000000000000..cd28b89349e4 --- /dev/null +++ b/frontend/desktop/scripts/testDeleteAccountTrnasaction.ps1 @@ -0,0 +1,38 @@ +$desktopHostName = Read-Host "输入域名" +$start = Read-Host "输入开始" +$url = "https://${desktopHostName}/api/auth/password" +$tokenUrl = "https://${desktopHostName}/api/auth/regionToken" +$deleteAccountUrl = "https://${desktopHostName}/api/auth/delete" +Write-Host $url +$requests = 500 +$batchSize = 20 +$delay = 5 + +for ($i = $start; $i -lt $requests; $i += $batchSize) { + + foreach ($num in ($i..($i + $batchSize))) { + $body = @{ + "user" = "test${num}test" + "password" = "test${num}test" + } | ConvertTo-Json + # Start-Job -ScriptBlock { + $result = Invoke-WebRequest -Uri $url -Body $body -ContentType 'application/json' + $token = [URI]::EscapeDataString( ($result.Content | ConvertFrom-Json).data.token) + Write-Host 'password result' + Write-Host $token + $result2 = Invoke-WebRequest -Uri $tokenUrl -Headers @{ + 'Authorization' = $token + } + Write-Host 'regiontoken result' + Write-Host $result2.Content + $token = [URI]::EscapeDataString( ($result2.Content | ConvertFrom-Json).data.token) + $result3 = Invoke-WebRequest -Uri $deleteAccountUrl -Headers @{ + 'Authorization' = $token + } + Write-Host 'delete result' + Write-Host $result3.Content + # } + } + + # Start-Sleep -Seconds $delay +} diff --git a/frontend/desktop/src/__tests__/components/.gitkeep b/frontend/desktop/src/__tests__/components/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frontend/desktop/src/__tests__/unit/appSort.test.ts b/frontend/desktop/src/__tests__/unit/appSort.test.ts new file mode 100644 index 000000000000..8f75789d4ca7 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/appSort.test.ts @@ -0,0 +1,89 @@ +import { compareSystemAppOrder } from '@/utils/appSort'; +import { APPTYPE, TAppConfig } from '@/types'; + +const createApp = (overrides: Partial & Pick): TAppConfig => ({ + name: overrides.key, + icon: '/logo.svg', + type: APPTYPE.IFRAME, + data: { + url: '', + desc: '' + }, + representativeMeta: { + forcedIconStyle: 'fill' + }, + displayType: 'normal', + ...overrides +}); + +describe('compareSystemAppOrder', () => { + it('sorts by display type, position, creation time, then key', () => { + const apps = [ + createApp({ + key: 'system-hidden', + displayType: 'hidden', + position: 0, + creationTimestamp: '2026-06-04T00:00:00Z' + }), + createApp({ + key: 'system-more', + displayType: 'more', + position: 0, + creationTimestamp: '2026-06-01T00:00:00Z' + }), + createApp({ + key: 'system-later', + position: 20, + creationTimestamp: '2026-06-03T00:00:00Z' + }), + createApp({ + key: 'system-first', + position: 10, + creationTimestamp: '2026-06-02T00:00:00Z' + }) + ].sort(compareSystemAppOrder); + + expect(apps.map((app) => app.key)).toEqual([ + 'system-first', + 'system-later', + 'system-more', + 'system-hidden' + ]); + }); + + it('treats missing position as zero', () => { + const apps = [ + createApp({ + key: 'system-positioned', + position: 1 + }), + createApp({ + key: 'system-default' + }) + ].sort(compareSystemAppOrder); + + expect(apps.map((app) => app.key)).toEqual(['system-default', 'system-positioned']); + }); + + it('uses creation time and key as deterministic tie breakers', () => { + const apps = [ + createApp({ + key: 'system-zulu', + position: 1, + creationTimestamp: '2026-06-01T00:00:00Z' + }), + createApp({ + key: 'system-alpha', + position: 1, + creationTimestamp: '2026-06-01T00:00:00Z' + }), + createApp({ + key: 'system-newest', + position: 1, + creationTimestamp: '2026-06-02T00:00:00Z' + }) + ].sort(compareSystemAppOrder); + + expect(apps.map((app) => app.key)).toEqual(['system-newest', 'system-alpha', 'system-zulu']); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/global-auth.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/global-auth.test.ts new file mode 100644 index 000000000000..6774479ce8cb --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/global-auth.test.ts @@ -0,0 +1,150 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ProviderType, UserStatus } from 'prisma/global/generated/client'; + +vi.mock('@/api/platform', () => ({ + uploadConvertData: vi.fn() +})); + +vi.mock('@/services/backend/auth', () => ({ + generateGlobalAccessToken: vi.fn(() => 'global-token') +})); + +vi.mock('@/services/backend/db/init', () => ({ + globalPrisma: { + oauthProvider: { + findUnique: vi.fn(), + findFirst: vi.fn() + }, + restrictedUser: { + findFirst: vi.fn() + }, + account: { + findUnique: vi.fn() + }, + userTask: { + findFirst: vi.fn() + }, + user: { + update: vi.fn() + }, + userInfo: { + findUnique: vi.fn() + } + } +})); + +vi.mock('@/services/enable', () => ({ + enableSignUp: vi.fn(() => true), + enableTracking: vi.fn(() => false), + getRegionUid: vi.fn(() => 'region-uid'), + getVersion: vi.fn(() => 'cn') +})); + +vi.mock('@/services/backend/tracking', () => ({ + trackSignUp: vi.fn() +})); + +vi.mock('@/services/backend/svc/bindProvider', () => ({ + addOauthProvider: vi.fn(), + bindEmailSvc: vi.fn() +})); + +import { generateGlobalAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { getGlobalToken } from '@/services/backend/globalAuth'; +import { addOauthProvider } from '@/services/backend/svc/bindProvider'; + +const mockPrisma = globalPrisma as any; +const mockGenerateGlobalAccessToken = vi.mocked(generateGlobalAccessToken); +const mockAddOauthProvider = vi.mocked(addOauthProvider); + +describe('getGlobalToken', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockPrisma.restrictedUser.findFirst.mockResolvedValue(null); + mockPrisma.account.findUnique.mockResolvedValue(null); + mockPrisma.oauthProvider.findFirst.mockResolvedValue(null); + mockPrisma.userInfo.findUnique.mockResolvedValue({ isInited: true }); + }); + + it('does not update existing user profile on sign in', async () => { + const existingUser = { + uid: 'user-uid', + id: 'user-id', + nickname: 'Admin Nickname', + avatarUri: 'old-avatar', + status: UserStatus.NORMAL_USER + }; + + mockPrisma.oauthProvider.findUnique + .mockResolvedValueOnce({ + providerId: 'github-id', + providerType: ProviderType.GITHUB, + userUid: existingUser.uid + }) + .mockResolvedValueOnce({ + providerId: 'github-id', + providerType: ProviderType.GITHUB, + userUid: existingUser.uid, + user: existingUser + }); + + const result = await getGlobalToken({ + provider: ProviderType.GITHUB, + providerId: 'github-id', + name: 'github-login', + avatar_url: 'new-avatar', + email: 'github-user@example.com' + }); + + expect(mockPrisma.user.update).not.toHaveBeenCalled(); + expect(mockGenerateGlobalAccessToken).toHaveBeenCalledWith({ + sub: existingUser.uid, + user_id: existingUser.id, + preferred_username: existingUser.nickname + }); + expect(result?.user).toEqual({ + name: existingUser.nickname, + avatar: existingUser.avatarUri, + userUid: existingUser.uid + }); + }); + + it('preserves automatic email provider binding on sign in', async () => { + const existingUser = { + uid: 'user-uid', + id: 'user-id', + nickname: 'Admin Nickname', + avatarUri: 'old-avatar', + status: UserStatus.NORMAL_USER + }; + + mockPrisma.oauthProvider.findUnique + .mockResolvedValueOnce({ + providerId: 'github-id', + providerType: ProviderType.GITHUB, + userUid: existingUser.uid + }) + .mockResolvedValueOnce({ + providerId: 'github-id', + providerType: ProviderType.GITHUB, + userUid: existingUser.uid, + user: existingUser + }); + mockPrisma.oauthProvider.findFirst.mockResolvedValue(null); + + await getGlobalToken({ + provider: ProviderType.GITHUB, + providerId: 'github-id', + name: 'github-login', + avatar_url: 'new-avatar', + email: 'github-user@example.com' + }); + + expect(mockAddOauthProvider).toHaveBeenCalledWith({ + providerType: ProviderType.EMAIL, + providerId: 'github-user@example.com', + userUid: existingUser.uid + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/global-token.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/global-token.test.ts new file mode 100644 index 000000000000..efe9f0e5fd35 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/global-token.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + GLOBAL_TOKEN_CLIENT_ID, + generateOAuth2AccessToken, + generateOAuth2RefreshToken, + verifyGlobalToken +} from '@/services/backend/auth'; +import { sign } from 'jsonwebtoken'; + +describe('verifyGlobalToken', () => { + beforeEach(() => { + (global as any).AppConfig = { + desktop: { + auth: { + jwt: { + global: 'test-global-secret', + regional: 'test-regional-secret', + internal: 'test-internal-secret' + } + } + }, + cloud: { + regionUID: 'test-region' + } + }; + }); + + const buildHeader = (token: string) => + ({ + authorization: encodeURIComponent(token) + } as any); + + it('maps oauth2 access token claims to legacy payload', async () => { + const token = generateOAuth2AccessToken({ + sub: 'oauth-uid', + user_id: 'oauth-user-id', + client_id: GLOBAL_TOKEN_CLIENT_ID + }); + + const payload = await verifyGlobalToken(buildHeader(token)); + + expect(payload).toEqual({ + userUid: 'oauth-uid', + userId: 'oauth-user-id' + }); + }); + + it('rejects oauth2 refresh token', async () => { + const token = generateOAuth2RefreshToken({ + sub: 'oauth-uid', + user_id: 'oauth-user-id', + client_id: GLOBAL_TOKEN_CLIENT_ID + }); + + const payload = await verifyGlobalToken(buildHeader(token)); + + expect(payload).toBeNull(); + }); + + it('rejects oauth2 access token without subject', async () => { + const token = sign( + { + client_id: GLOBAL_TOKEN_CLIENT_ID, + token_type: 'access_token', + user_id: 'oauth-user-id' + }, + 'test-global-secret', + { expiresIn: '7d' } + ); + + const payload = await verifyGlobalToken(buildHeader(token)); + + expect(payload).toBeNull(); + }); + + it('rejects oauth2 access token without user_id', async () => { + const token = sign( + { + sub: 'oauth-uid', + client_id: GLOBAL_TOKEN_CLIENT_ID, + token_type: 'access_token' + }, + 'test-global-secret', + { expiresIn: '7d' } + ); + + const payload = await verifyGlobalToken(buildHeader(token)); + + expect(payload).toBeNull(); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/google-onetap.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/google-onetap.api.test.ts new file mode 100644 index 000000000000..e5704411eaad --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/google-onetap.api.test.ts @@ -0,0 +1,243 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const verifyIdToken = vi.fn(); +const getPayload = vi.fn(); + +vi.mock('google-auth-library', () => ({ + OAuth2Client: vi.fn(function () { + return { + verifyIdToken + }; + }) +})); + +vi.mock('@/services/enable', () => ({ + enableGoogle: vi.fn(() => true) +})); + +vi.mock('@/services/backend/persistImage', () => ({ + persistImage: vi.fn(() => Promise.resolve('persisted-avatar')) +})); + +vi.mock('@/services/backend/globalAuth', () => ({ + getGlobalToken: vi.fn() +})); + +import handler from '@/pages/api/auth/google/onetap'; +import { persistImage } from '@/services/backend/persistImage'; +import { getGlobalToken } from '@/services/backend/globalAuth'; + +const mockPersistImage = vi.mocked(persistImage); +const mockGetGlobalToken = vi.mocked(getGlobalToken); + +const createMockRes = () => { + const res: any = { + headers: {}, + statusCode: 200, + body: undefined, + setHeader: vi.fn((name: string, value: string | string[]) => { + res.headers[name] = value; + }), + status: vi.fn((code: number) => { + res.statusCode = code; + return res; + }), + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }), + end: vi.fn(() => res) + }; + return res; +}; + +const setAppConfig = (oneTapOrigins = ['https://www.example.com']) => { + (global as any).AppConfig = { + desktop: { + auth: { + idp: { + google: { + clientID: 'google-client-id', + oneTapOrigins + } + } + } + } + }; +}; + +describe('google onetap api handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + setAppConfig(); + getPayload.mockReturnValue({ + sub: 'google-sub', + name: 'Ada Lovelace', + picture: 'https://example.com/avatar.png', + email: 'ada@example.com', + email_verified: true + }); + verifyIdToken.mockResolvedValue({ getPayload }); + mockGetGlobalToken.mockResolvedValue({ + token: 'global-token', + user: { + name: 'Ada', + avatar: 'persisted-avatar', + userUid: 'user-uid' + }, + needInit: false + }); + }); + + it('allows preflight from configured One Tap origin', async () => { + const req: any = { + method: 'OPTIONS', + headers: { + origin: 'https://www.example.com' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(204); + expect(res.headers['Access-Control-Allow-Origin']).toBe('https://www.example.com'); + expect(res.headers['Access-Control-Allow-Credentials']).toBe('true'); + expect(res.headers['Access-Control-Allow-Methods']).toBe('POST, OPTIONS'); + expect(res.headers['Access-Control-Allow-Headers']).toBe('Content-Type'); + expect(res.headers.Vary).toBe('Origin'); + expect(res.end).toHaveBeenCalled(); + }); + + it('rejects forbidden origins before verifying Google token', async () => { + const req: any = { + method: 'POST', + headers: { + origin: 'https://evil.example.com' + }, + body: { + credential: 'credential' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toMatchObject({ + code: 403, + message: 'Forbidden origin' + }); + expect(verifyIdToken).not.toHaveBeenCalled(); + }); + + it('returns 400 when credential is missing', async () => { + const req: any = { + method: 'POST', + headers: { + origin: 'https://www.example.com' + }, + body: {} + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toMatchObject({ + code: 400, + message: 'credential is invalid' + }); + }); + + it('signs in with verified Google payload and sets shared auth cookie', async () => { + const req: any = { + method: 'POST', + headers: { + origin: 'https://www.example.com', + 'x-forwarded-proto': 'https' + }, + body: { + credential: 'credential' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(verifyIdToken).toHaveBeenCalledWith({ + idToken: 'credential', + audience: 'google-client-id' + }); + expect(mockPersistImage).toHaveBeenCalledWith( + 'https://example.com/avatar.png', + 'avatar/GOOGLE/google-sub' + ); + expect(mockGetGlobalToken).toHaveBeenCalledWith({ + provider: 'GOOGLE', + providerId: 'google-sub', + name: 'Ada Lovelace', + avatar_url: 'persisted-avatar', + email: 'ada@example.com' + }); + expect(res.body).toMatchObject({ + code: 200, + data: { + token: 'global-token' + } + }); + expect(res.headers['Set-Cookie']).toContain('sealos_auth_token=global-token'); + expect(res.headers['Set-Cookie']).toContain('HttpOnly'); + expect(res.headers['Set-Cookie']).toContain('Secure'); + expect(res.headers['Set-Cookie']).toContain('SameSite=Lax'); + }); + + it('does not pass unverified email into account binding', async () => { + getPayload.mockReturnValue({ + sub: 'google-sub', + name: 'Ada Lovelace', + picture: '', + email: 'ada@example.com', + email_verified: false + }); + const req: any = { + method: 'POST', + headers: { + origin: 'https://www.example.com' + }, + body: { + credential: 'credential' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(mockGetGlobalToken).toHaveBeenCalledWith({ + provider: 'GOOGLE', + providerId: 'google-sub', + name: 'Ada Lovelace', + avatar_url: '', + email: undefined + }); + }); + + it('returns 401 when Google token verification fails', async () => { + verifyIdToken.mockRejectedValue(new Error('bad token')); + const req: any = { + method: 'POST', + headers: { + origin: 'https://www.example.com' + }, + body: { + credential: 'credential' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toMatchObject({ + code: 401, + message: 'Unauthorized' + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/marketing-consent.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/marketing-consent.test.ts new file mode 100644 index 000000000000..e75302636a25 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/marketing-consent.test.ts @@ -0,0 +1,174 @@ +import { verify } from 'jsonwebtoken'; + +import handler from '@/pages/api/marketing/consent-token'; +import { + generateGlobalAccessToken, + generateMarketingConsentToken, + generateRegionalToken, + marketingConsentJwtSecret +} from '@/services/backend/auth'; + +describe('marketing consent token', () => { + const previousConfig = global.AppConfig; + + beforeEach(() => { + global.AppConfig = { + cloud: { regionUID: 'region-test' }, + desktop: { + auth: { + jwt: { + global: 'global-secret-test', + marketingConsent: 'marketing-secret-test', + regional: 'regional-secret-test' + } + } + } + } as typeof global.AppConfig; + }); + + afterEach(() => { + global.AppConfig = previousConfig; + }); + + it('issues a short-lived token with the Brain contract claims', () => { + const token = generateMarketingConsentToken({ + ad_personalization: 'granted', + ad_user_data_consent: 'granted', + attribution_hash: 'a'.repeat(64), + region: 'region-test', + sub: 'user-test' + }); + const payload = verify(token, marketingConsentJwtSecret(), { + audience: 'brain-marketing-attribution', + issuer: 'sealos-desktop' + }); + + expect(payload).toMatchObject({ + ad_personalization: 'granted', + ad_user_data_consent: 'granted', + attribution_hash: 'a'.repeat(64), + consent_source: 'desktop_oauth', + region: 'region-test', + sub: 'user-test' + }); + expect(payload).toHaveProperty('jti'); + expect(payload).toHaveProperty('iat'); + expect(payload).toHaveProperty('exp'); + }); + + it('does not elevate browser-asserted consent', async () => { + const globalToken = generateGlobalAccessToken({ + preferred_username: 'user-test', + sub: 'user-test', + user_id: '10001' + }); + let responseBody: any; + const res = { + json: vi.fn((body) => { + responseBody = body; + }), + setHeader: vi.fn() + } as any; + const seaAttr = Buffer.from( + JSON.stringify({ + ad_personalization: 'granted', + ad_user_data_consent: true, + version: 2 + }) + ).toString('base64url'); + + await handler( + { + body: { sea_attr: seaAttr }, + cookies: {}, + headers: { authorization: encodeURIComponent(globalToken) }, + method: 'POST' + } as any, + res + ); + + expect( + verify(responseBody.data.token, marketingConsentJwtSecret(), { + audience: 'brain-marketing-attribution', + issuer: 'sealos-desktop' + }) + ).toMatchObject({ + ad_personalization: 'unspecified', + ad_user_data_consent: 'unspecified', + attribution_hash: expect.any(String), + sub: 'user-test' + }); + }); + + it('keeps opaque attribution input unspecified', async () => { + const globalToken = generateGlobalAccessToken({ + preferred_username: 'user-test', + sub: 'user-test', + user_id: '10001' + }); + let responseBody: any; + const res = { + json: vi.fn((body) => { + responseBody = body; + }), + setHeader: vi.fn() + } as any; + + await handler( + { + body: { sea_attr: 'opaque-attribution-test' }, + cookies: {}, + headers: { authorization: encodeURIComponent(globalToken) }, + method: 'POST' + } as any, + res + ); + + expect( + verify(responseBody.data.token, marketingConsentJwtSecret(), { + audience: 'brain-marketing-attribution', + issuer: 'sealos-desktop' + }) + ).toMatchObject({ + ad_personalization: 'unspecified', + ad_user_data_consent: 'unspecified', + sub: 'user-test' + }); + }); + + it('accepts an authenticated regional session without a shared global cookie', async () => { + const regionalToken = generateRegionalToken({ + regionUid: 'region-test', + userCrName: 'user-cr-test', + userCrUid: 'user-cr-uid-test', + userId: '10002', + userUid: 'regional-user-test', + workspaceId: 'workspace-test', + workspaceUid: 'workspace-uid-test' + }); + let responseBody: any; + const res = { + json: vi.fn((body) => { + responseBody = body; + }), + setHeader: vi.fn() + } as any; + + await handler( + { + body: { sea_attr: 'regional-attribution-test' }, + cookies: {}, + headers: { authorization: encodeURIComponent(regionalToken) }, + method: 'POST' + } as any, + res + ); + + expect( + verify(responseBody.data.token, marketingConsentJwtSecret(), { + audience: 'brain-marketing-attribution', + issuer: 'sealos-desktop' + }) + ).toMatchObject({ sub: 'regional-user-test' }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/namespace/modifyRole.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/namespace/modifyRole.api.test.ts new file mode 100644 index 000000000000..3ac77c0fb676 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/namespace/modifyRole.api.test.ts @@ -0,0 +1,423 @@ +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { JoinStatus, Role } from 'prisma/region/generated/client'; +import { UserRole } from '@/types/team'; + +vi.mock('@/services/backend/auth', () => ({ + verifyAccessToken: vi.fn() +})); + +vi.mock('@/services/backend/db/init', () => ({ + globalPrisma: {}, + prisma: { + userWorkspace: { + findMany: vi.fn() + } + } +})); + +vi.mock('@/services/backend/team', () => ({ + modifyBinding: vi.fn(), + modifyWorkspaceRole: vi.fn() +})); + +import handler, { + getModifyRolePermissionError, + validateModifyRoleRequest +} from '@/pages/api/auth/namespace/modifyRole'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { prisma } from '@/services/backend/db/init'; +import { modifyBinding, modifyWorkspaceRole } from '@/services/backend/team'; + +const mockVerifyAccessToken = verifyAccessToken as MockedFunction; +const mockPrisma = prisma as any; +const mockModifyBinding = modifyBinding as MockedFunction; +const mockModifyWorkspaceRole = modifyWorkspaceRole as MockedFunction; + +const createMockRes = () => { + const res: any = { + body: undefined, + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }) + }; + return res; +}; + +describe('namespace modifyRole api handler', () => { + const managerUserCrUid = '11111111-1111-4111-8111-111111111111'; + const developerUserCrUid = '22222222-2222-4222-8222-222222222222'; + const ownerUserCrUid = '44444444-4444-4444-8444-444444444444'; + const workspaceUid = '33333333-3333-4333-8333-333333333333'; + const workspace = { + id: 'ns-team' + }; + + const buildUserWorkspace = ({ + userCrUid, + role, + crName + }: { + userCrUid: string; + role: Role; + crName: string; + }) => ({ + userCrUid, + workspaceUid, + role, + status: JoinStatus.IN_WORKSPACE, + workspace, + userCr: { + crName + } + }); + + const createModifyRoleReq = ({ + targetUserCrUid, + tRole + }: { + targetUserCrUid: string; + tRole: UserRole; + }) => ({ + method: 'POST', + headers: {}, + body: { + ns_uid: workspaceUid, + targetUserCrUid, + tRole + } + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('validateModifyRoleRequest', () => { + const buildValidBody = () => ({ + ns_uid: workspaceUid, + targetUserCrUid: developerUserCrUid, + tRole: UserRole.Owner + }); + + it('rejects invalid target user ids', () => { + expect( + validateModifyRoleRequest( + { + ...buildValidBody(), + targetUserCrUid: 'bad-id' + }, + managerUserCrUid + ) + ).toEqual({ + ok: false, + error: { + code: 400, + message: 'tUserId is invalid' + } + }); + }); + + it('rejects missing or invalid target roles', () => { + expect( + validateModifyRoleRequest( + { + ...buildValidBody(), + tRole: undefined + }, + managerUserCrUid + ) + ).toEqual({ + ok: false, + error: { + code: 400, + message: 'tRole is required' + } + }); + + expect( + validateModifyRoleRequest( + { + ...buildValidBody(), + tRole: 99 as UserRole + }, + managerUserCrUid + ) + ).toEqual({ + ok: false, + error: { + code: 400, + message: 'tRole is required' + } + }); + }); + + it('accepts owner as a syntactically valid target role', () => { + expect(validateModifyRoleRequest(buildValidBody(), managerUserCrUid)).toEqual({ + ok: true, + data: buildValidBody() + }); + }); + + it('rejects invalid namespace ids', () => { + expect( + validateModifyRoleRequest( + { + ...buildValidBody(), + ns_uid: 'bad-id' + }, + managerUserCrUid + ) + ).toEqual({ + ok: false, + error: { + code: 400, + message: 'ns_uid is invalid' + } + }); + }); + + it('rejects changing the requester role', () => { + expect( + validateModifyRoleRequest( + { + ...buildValidBody(), + targetUserCrUid: managerUserCrUid + }, + managerUserCrUid + ) + ).toEqual({ + ok: false, + error: { + code: 403, + message: 'target user is not self' + } + }); + }); + }); + + describe('getModifyRolePermissionError', () => { + it('rejects manager promotion of a developer to owner', () => { + expect( + getModifyRolePermissionError({ + requesterRole: UserRole.Manager, + targetCurrentRole: UserRole.Developer, + requestedRole: UserRole.Owner, + isSelf: false + }) + ).toEqual({ + code: 403, + message: 'you are not owner' + }); + }); + + it('allows owner promotion of a developer to owner', () => { + expect( + getModifyRolePermissionError({ + requesterRole: UserRole.Owner, + targetCurrentRole: UserRole.Developer, + requestedRole: UserRole.Owner, + isSelf: false + }) + ).toBeNull(); + }); + + it('rejects manager modification of a developer to a non-owner role', () => { + expect( + getModifyRolePermissionError({ + requesterRole: UserRole.Manager, + targetCurrentRole: UserRole.Developer, + requestedRole: UserRole.Manager, + isSelf: false + }) + ).toEqual({ + code: 403, + message: 'you are not owner' + }); + }); + + it('allows owner modification of a developer to a non-owner role', () => { + expect( + getModifyRolePermissionError({ + requesterRole: UserRole.Owner, + targetCurrentRole: UserRole.Developer, + requestedRole: UserRole.Manager, + isSelf: false + }) + ).toBeNull(); + }); + + it('rejects manager modification of an owner', () => { + expect( + getModifyRolePermissionError({ + requesterRole: UserRole.Manager, + targetCurrentRole: UserRole.Owner, + requestedRole: UserRole.Developer, + isSelf: false + }) + ).toEqual({ + code: 403, + message: 'you are not owner' + }); + }); + }); + + it('rejects a manager promoting a developer to owner when tRole is Owner', async () => { + mockVerifyAccessToken.mockResolvedValue({ + userCrUid: managerUserCrUid + } as any); + mockPrisma.userWorkspace.findMany.mockResolvedValue([ + buildUserWorkspace({ + userCrUid: managerUserCrUid, + role: Role.MANAGER, + crName: 'manager-cr' + }), + buildUserWorkspace({ + userCrUid: developerUserCrUid, + role: Role.DEVELOPER, + crName: 'developer-cr' + }) + ]); + mockModifyWorkspaceRole.mockResolvedValue(undefined as any); + mockModifyBinding.mockResolvedValue({} as any); + + const req: any = createModifyRoleReq({ + targetUserCrUid: developerUserCrUid, + tRole: UserRole.Owner + }); + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toMatchObject({ + code: 403 + }); + expect(mockModifyWorkspaceRole).not.toHaveBeenCalled(); + expect(mockModifyBinding).not.toHaveBeenCalled(); + }); + + it('allows an owner to promote a developer to owner', async () => { + mockVerifyAccessToken.mockResolvedValue({ + userCrUid: ownerUserCrUid + } as any); + mockPrisma.userWorkspace.findMany.mockResolvedValue([ + buildUserWorkspace({ + userCrUid: ownerUserCrUid, + role: Role.OWNER, + crName: 'owner-cr' + }), + buildUserWorkspace({ + userCrUid: developerUserCrUid, + role: Role.DEVELOPER, + crName: 'developer-cr' + }) + ]); + mockModifyWorkspaceRole.mockResolvedValue(undefined as any); + mockModifyBinding.mockResolvedValue({} as any); + + const req: any = createModifyRoleReq({ + targetUserCrUid: developerUserCrUid, + tRole: UserRole.Owner + }); + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toEqual({ + code: 200, + message: 'Successfully', + data: null + }); + expect(mockModifyWorkspaceRole).toHaveBeenCalledWith({ + k8s_username: 'developer-cr', + role: UserRole.Owner, + action: 'Modify', + workspaceId: 'ns-team', + pre_role: UserRole.Developer + }); + expect(mockModifyBinding).toHaveBeenCalledWith({ + userCrUid: developerUserCrUid, + workspaceUid, + role: UserRole.Owner + }); + }); + + it('rejects a manager modifying a developer to a non-owner role', async () => { + mockVerifyAccessToken.mockResolvedValue({ + userCrUid: managerUserCrUid + } as any); + mockPrisma.userWorkspace.findMany.mockResolvedValue([ + buildUserWorkspace({ + userCrUid: managerUserCrUid, + role: Role.MANAGER, + crName: 'manager-cr' + }), + buildUserWorkspace({ + userCrUid: developerUserCrUid, + role: Role.DEVELOPER, + crName: 'developer-cr' + }) + ]); + mockModifyWorkspaceRole.mockResolvedValue(undefined as any); + mockModifyBinding.mockResolvedValue({} as any); + + const req: any = createModifyRoleReq({ + targetUserCrUid: developerUserCrUid, + tRole: UserRole.Manager + }); + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toMatchObject({ + code: 403 + }); + expect(mockModifyWorkspaceRole).not.toHaveBeenCalled(); + expect(mockModifyBinding).not.toHaveBeenCalled(); + }); + + it('allows an owner to modify a developer to a non-owner role', async () => { + mockVerifyAccessToken.mockResolvedValue({ + userCrUid: ownerUserCrUid + } as any); + mockPrisma.userWorkspace.findMany.mockResolvedValue([ + buildUserWorkspace({ + userCrUid: ownerUserCrUid, + role: Role.OWNER, + crName: 'owner-cr' + }), + buildUserWorkspace({ + userCrUid: developerUserCrUid, + role: Role.DEVELOPER, + crName: 'developer-cr' + }) + ]); + mockModifyWorkspaceRole.mockResolvedValue(undefined as any); + mockModifyBinding.mockResolvedValue({} as any); + + const req: any = createModifyRoleReq({ + targetUserCrUid: developerUserCrUid, + tRole: UserRole.Manager + }); + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toEqual({ + code: 200, + message: 'Successfully', + data: null + }); + expect(mockModifyWorkspaceRole).toHaveBeenCalledWith({ + k8s_username: 'developer-cr', + role: UserRole.Manager, + action: 'Modify', + workspaceId: 'ns-team', + pre_role: UserRole.Developer + }); + expect(mockModifyBinding).toHaveBeenCalledWith({ + userCrUid: developerUserCrUid, + workspaceUid, + role: UserRole.Manager + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/verification-code.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/verification-code.test.ts new file mode 100644 index 000000000000..ec2be043e598 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/verification-code.test.ts @@ -0,0 +1,206 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const collection = { + createIndex: vi.fn(), + updateOne: vi.fn(), + findOne: vi.fn(), + findOneAndDelete: vi.fn(), + findOneAndUpdate: vi.fn(), + deleteOne: vi.fn() +}; + +vi.mock('@/services/backend/db/mongodb', () => ({ + connectToDatabase: vi.fn(async () => ({ + db: () => ({ collection: () => collection }) + })) +})); + +vi.mock('uuid', () => ({ v4: vi.fn() })); + +import { v4 } from 'uuid'; +import { + addOrUpdateCode, + MAX_VERIFICATION_ATTEMPTS, + verifyAndConsumeCode +} from '@/services/backend/db/verifyCode'; + +const now = new Date('2026-07-20T08:00:00.000Z'); +const challengeId = '00000000-0000-4000-8000-000000000002'; +const record = (overrides: Record = {}) => ({ + uid: '00000000-0000-4000-8000-000000000001', + challengeId, + id: '13800138000', + smsType: 'phone_login', + code: '123456', + attemptCount: 0, + createdAt: new Date(now.getTime() - 21_000), + ...overrides +}); + +describe('Mongo verification code', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(now); + collection.createIndex.mockResolvedValue('index'); + }); + + it('atomically increments an incorrect attempt and returns the remaining count', async () => { + collection.findOneAndDelete.mockResolvedValue({ value: null }); + collection.findOneAndUpdate.mockResolvedValue({ value: record({ attemptCount: 1 }) }); + + await expect( + verifyAndConsumeCode({ + id: '13800138000', + smsType: 'phone_login', + code: '000000', + challengeId + }) + ).resolves.toEqual({ status: 'invalid', remainingAttempts: 9 }); + + expect(collection.findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + id: '13800138000', + smsType: 'phone_login', + challengeId, + code: { $ne: '000000' }, + $or: [ + { attemptCount: { $lt: MAX_VERIFICATION_ATTEMPTS } }, + { attemptCount: { $exists: false } } + ] + }), + { $inc: { attemptCount: 1 } }, + { returnDocument: 'after' } + ); + }); + + it('logically invalidates the code on the tenth failed attempt', async () => { + collection.findOneAndDelete.mockResolvedValue({ value: null }); + collection.findOneAndUpdate.mockResolvedValue({ + value: record({ attemptCount: MAX_VERIFICATION_ATTEMPTS }) + }); + + await expect( + verifyAndConsumeCode({ + id: '13800138000', + smsType: 'phone_login', + code: '000000', + challengeId + }) + ).resolves.toEqual({ status: 'locked', retryAfter: 39 }); + }); + + it('allows at most ten increments under concurrent incorrect attempts', async () => { + let attemptCount = 0; + collection.findOneAndDelete.mockResolvedValue({ value: null }); + collection.findOneAndUpdate.mockImplementation(async () => { + if (attemptCount >= MAX_VERIFICATION_ATTEMPTS) return { value: null }; + attemptCount += 1; + return { value: record({ attemptCount }) }; + }); + collection.findOne.mockImplementation(async () => record({ attemptCount })); + + const results = await Promise.all( + Array.from({ length: 20 }, () => + verifyAndConsumeCode({ + id: '13800138000', + smsType: 'phone_login', + code: '000000', + challengeId + }) + ) + ); + + expect(attemptCount).toBe(MAX_VERIFICATION_ATTEMPTS); + expect(results.filter((result) => result.status === 'invalid')).toHaveLength(9); + expect(results.filter((result) => result.status === 'locked')).toHaveLength(11); + }); + + it('returns a stable lock response after attempts are exhausted', async () => { + collection.findOneAndDelete.mockResolvedValue({ value: null }); + collection.findOneAndUpdate.mockResolvedValue({ value: null }); + collection.findOne.mockResolvedValue(record({ attemptCount: MAX_VERIFICATION_ATTEMPTS })); + + await expect( + verifyAndConsumeCode({ + id: '13800138000', + smsType: 'phone_login', + code: '123456', + challengeId + }) + ).resolves.toEqual({ status: 'locked', retryAfter: 39 }); + }); + + it('atomically consumes a correct code once', async () => { + collection.findOneAndDelete + .mockResolvedValueOnce({ value: record() }) + .mockResolvedValueOnce({ value: null }); + collection.findOneAndUpdate.mockResolvedValue({ value: null }); + collection.findOne.mockResolvedValue(null); + + const params = { + id: '13800138000', + smsType: 'phone_login' as const, + code: '123456', + challengeId + }; + const [first, second] = await Promise.all([ + verifyAndConsumeCode(params), + verifyAndConsumeCode(params) + ]); + + expect([first.status, second.status].sort()).toEqual(['expired', 'verified']); + }); + + it('does not let an old challenge affect a replacement code', async () => { + collection.findOneAndDelete.mockResolvedValue({ value: null }); + collection.findOneAndUpdate.mockResolvedValue({ value: null }); + collection.findOne.mockResolvedValue(null); + + await expect( + verifyAndConsumeCode({ + id: '13800138000', + smsType: 'phone_login', + code: '000000', + challengeId: '00000000-0000-4000-8000-000000000099' + }) + ).resolves.toEqual({ status: 'expired' }); + expect(collection.findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + challengeId: '00000000-0000-4000-8000-000000000099' + }), + expect.anything(), + expect.anything() + ); + }); + + it('resets attempts and rotates both internal and public IDs when resending', async () => { + vi.mocked(v4) + .mockReturnValueOnce('00000000-0000-4000-8000-000000000010') + .mockReturnValueOnce('00000000-0000-4000-8000-000000000011'); + collection.updateOne.mockResolvedValue({ acknowledged: true }); + + await expect( + addOrUpdateCode({ + id: 'user@example.com', + smsType: 'email_login', + code: '654321' + }) + ).resolves.toEqual({ + uid: '00000000-0000-4000-8000-000000000010', + challengeId: '00000000-0000-4000-8000-000000000011' + }); + expect(collection.updateOne).toHaveBeenCalledWith( + { id: 'user@example.com', smsType: 'email_login' }, + { + $set: expect.objectContaining({ + code: '654321', + attemptCount: 0, + uid: '00000000-0000-4000-8000-000000000010', + challengeId: '00000000-0000-4000-8000-000000000011' + }) + }, + { upsert: true } + ); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/verification-middleware.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/verification-middleware.test.ts new file mode 100644 index 000000000000..fd91908c3a71 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/verification-middleware.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('@/services/backend/db/verifyCode', () => ({})); +vi.mock('@/services/backend/db/verificationRateLimit', () => ({})); +vi.mock('@/services/backend/db/verificationTicket', () => ({})); + +import { filterCodeUid, filterPhoneVerifyParams } from '@/services/backend/middleware/sms'; + +describe('verification middleware', () => { + it('rejects a malformed flow ticket uid before querying the database', async () => { + const json = vi.fn(); + const next = vi.fn(); + + await filterCodeUid({ body: { uid: 'not-a-uuid' } } as any, { json } as any, next); + + expect(json).toHaveBeenCalledWith({ + code: 400, + message: 'uid is invalid', + data: null + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('passes a valid flow ticket uid to the next handler', async () => { + const uid = '00000000-0000-4000-8000-000000000001'; + const next = vi.fn(); + + await filterCodeUid({ body: { uid } } as any, { json: vi.fn() } as any, next); + + expect(next).toHaveBeenCalledWith({ uid }); + }); + + it('rejects verification without a valid challenge ID', async () => { + const json = vi.fn(); + const next = vi.fn(); + + await filterPhoneVerifyParams( + { body: { id: '13800138000', code: '123456', challengeId: 'invalid' } } as any, + { json } as any, + next + ); + + expect(json).toHaveBeenCalledWith({ + code: 400, + message: 'code is invalid', + data: null + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('rejects object-shaped codes before building a Mongo query', async () => { + const json = vi.fn(); + const next = vi.fn(); + + await filterPhoneVerifyParams( + { + body: { + id: '13800138000', + code: { $ne: null }, + challengeId: '00000000-0000-4000-8000-000000000001' + } + } as any, + { json } as any, + next + ); + + expect(json).toHaveBeenCalledWith({ + code: 400, + message: 'code is invalid', + data: null + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('passes the challenge ID to verification handlers', async () => { + const challengeId = '00000000-0000-4000-8000-000000000001'; + const next = vi.fn(); + + await filterPhoneVerifyParams( + { body: { id: '13800138000', code: '123456', challengeId } } as any, + { json: vi.fn() } as any, + next + ); + + expect(next).toHaveBeenCalledWith({ + phoneNumbers: '13800138000', + code: '123456', + challengeId, + semData: undefined, + adClickData: undefined + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/verification-rate-limit.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/verification-rate-limit.test.ts new file mode 100644 index 000000000000..3577c6df5eac --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/verification-rate-limit.test.ts @@ -0,0 +1,119 @@ +import { createHash } from 'crypto'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const collection = { + createIndex: vi.fn(), + findOne: vi.fn(), + findOneAndUpdate: vi.fn(), + updateOne: vi.fn() +}; + +vi.mock('@/services/backend/requestIp', () => ({ + getClientIp: vi.fn(() => '203.0.113.42') +})); + +vi.mock('@/services/backend/db/mongodb', () => ({ + connectToDatabase: vi.fn(async () => ({ + db: () => ({ collection: () => collection }) + })) +})); + +import { + releaseVerificationSend, + reserveVerificationSend +} from '@/services/backend/db/verificationRateLimit'; + +const now = new Date('2026-07-20T08:00:00.000Z'); +const sha256 = (value: string) => createHash('sha256').update(value).digest('hex'); + +describe('Mongo verification send rate limit', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(now); + collection.createIndex.mockResolvedValue('index'); + collection.updateOne.mockResolvedValue({ acknowledged: true }); + }); + + it('atomically reserves identifier and IP windows without plaintext keys', async () => { + collection.findOne.mockResolvedValue(null); + collection.findOneAndUpdate.mockImplementation(async (filter: { key: string }) => ({ + value: { key: filter.key, windowStartedAt: now } + })); + + const result = await reserveVerificationSend({} as any, ' User@Example.com ', 'EMAIL'); + + const identifierHash = sha256('EMAIL:user@example.com'); + const ipHash = sha256('203.0.113.42'); + const expectedKeys = [ + `verification:id:minute:${identifierHash}`, + `verification:id:hour:${identifierHash}`, + `verification:ip:ten-minute:${ipHash}` + ]; + expect(result).toEqual({ + allowed: true, + reservation: { + entries: expectedKeys.map((key) => ({ key, windowStartedAt: now })) + } + }); + for (const key of expectedKeys) { + expect(key).not.toContain('user@example.com'); + expect(key).not.toContain('203.0.113.42'); + } + }); + + it('returns retryAfter when the identifier minute window is full', async () => { + collection.findOne.mockResolvedValue({ + key: 'minute-key', + count: 1, + windowStartedAt: new Date(now.getTime() - 21_000), + expiresAt: new Date(now.getTime() + 39_000) + }); + + await expect(reserveVerificationSend({} as any, '13800138000', 'PHONE')).resolves.toEqual({ + allowed: false, + retryAfter: 39 + }); + expect(collection.findOneAndUpdate).not.toHaveBeenCalled(); + }); + + it('compensates earlier reservations when a later rule is full', async () => { + collection.findOne.mockResolvedValueOnce(null).mockResolvedValueOnce({ + key: 'hour-key', + count: 5, + windowStartedAt: now, + expiresAt: new Date(now.getTime() + 120_000) + }); + collection.findOneAndUpdate.mockImplementation(async (filter: { key: string }) => ({ + value: { key: filter.key, windowStartedAt: now } + })); + + await expect(reserveVerificationSend({} as any, '13800138000', 'PHONE')).resolves.toEqual({ + allowed: false, + retryAfter: 120 + }); + expect(collection.updateOne).toHaveBeenCalledOnce(); + }); + + it('releases each reserved counter only within the matching window', async () => { + const reservation = { + entries: [ + { key: 'verification:id:minute:a', windowStartedAt: now }, + { key: 'verification:id:hour:a', windowStartedAt: now }, + { key: 'verification:ip:ten-minute:b', windowStartedAt: now } + ] + }; + + await releaseVerificationSend(reservation); + + expect(collection.updateOne).toHaveBeenCalledTimes(3); + expect(collection.updateOne).toHaveBeenCalledWith( + { + key: reservation.entries[0].key, + windowStartedAt: now, + count: { $gt: 0 } + }, + { $inc: { count: -1 } } + ); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/auth/verification-ticket.test.ts b/frontend/desktop/src/__tests__/unit/backend/auth/verification-ticket.test.ts new file mode 100644 index 000000000000..eab4386912d2 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/auth/verification-ticket.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const collection = { + createIndex: vi.fn(), + findOneAndUpdate: vi.fn(), + findOne: vi.fn(), + findOneAndDelete: vi.fn() +}; + +vi.mock('@/services/backend/db/mongodb', () => ({ + connectToDatabase: vi.fn(async () => ({ + db: () => ({ collection: () => collection }) + })) +})); + +import { + consumeVerificationFlowTicket, + createVerificationFlowTicket, + getVerificationFlowTicket +} from '@/services/backend/db/verificationTicket'; + +const now = new Date('2026-07-20T08:00:00.000Z'); +const params = { + uid: '00000000-0000-4000-8000-000000000001', + userUid: '00000000-0000-4000-8000-000000000002', + providerType: 'PHONE', + oldProviderId: '13800138000', + scenario: 'change_binding' +}; + +describe('Mongo verification flow ticket', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(now); + collection.createIndex.mockResolvedValue('index'); + }); + + it('atomically replaces the ticket for the same user and provider', async () => { + collection.findOneAndUpdate.mockResolvedValue({ value: params }); + + await createVerificationFlowTicket(params); + + expect(collection.findOneAndUpdate).toHaveBeenCalledWith( + { + userUid: params.userUid, + providerType: 'PHONE', + scenario: 'change_binding' + }, + { + $set: { + ...params, + expiresAt: new Date(now.getTime() + 5 * 60_000) + } + }, + { upsert: true, returnDocument: 'after' } + ); + }); + + it('binds reads to the ticket, user, provider, scenario, and expiration', async () => { + collection.findOne.mockResolvedValue(null); + const selector = { + uid: params.uid, + userUid: params.userUid, + providerType: params.providerType, + scenario: params.scenario + }; + + await expect(getVerificationFlowTicket(selector)).resolves.toBeNull(); + expect(collection.findOne).toHaveBeenCalledWith({ + ...selector, + expiresAt: { $gt: now } + }); + }); + + it('atomically consumes a valid ticket once', async () => { + collection.findOneAndDelete + .mockResolvedValueOnce({ value: params }) + .mockResolvedValueOnce({ value: null }); + const selector = { + uid: params.uid, + userUid: params.userUid, + providerType: params.providerType, + scenario: params.scenario + }; + + await expect(consumeVerificationFlowTicket(selector)).resolves.toBe(true); + await expect(consumeVerificationFlowTicket(selector)).resolves.toBe(false); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/api-headers.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/api-headers.test.ts new file mode 100644 index 000000000000..cfbcfa36f55c --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/api-headers.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it, vi } from 'vitest'; +import { applyOAuth2NoStoreHeaders } from '@/pages/api/auth/oauth2/utils'; + +describe('applyOAuth2NoStoreHeaders', () => { + it('sets OAuth2 no-store cache headers', () => { + const setHeader = vi.fn(); + const res = { setHeader } as any; + + applyOAuth2NoStoreHeaders(res); + + expect(setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store'); + expect(setHeader).toHaveBeenCalledWith('Pragma', 'no-cache'); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.api.test.ts new file mode 100644 index 000000000000..9e98660dcda2 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.api.test.ts @@ -0,0 +1,169 @@ +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { OAuth2HttpError } from '@/services/backend/oauth2/errors'; + +vi.mock('@/services/backend/oauth2/auth', () => ({ + resolveOAuth2AuthUser: vi.fn() +})); + +vi.mock('@/services/backend/oauth2/service', () => ({ + getAuthorizeContext: vi.fn(), + submitAuthorizeDecision: vi.fn() +})); + +import contextHandler from '@/pages/api/auth/oauth2/authorize/context'; +import decisionHandler from '@/pages/api/auth/oauth2/authorize/decision'; +import { resolveOAuth2AuthUser } from '@/services/backend/oauth2/auth'; +import { getAuthorizeContext, submitAuthorizeDecision } from '@/services/backend/oauth2/service'; + +const mockResolveOAuth2AuthUser = resolveOAuth2AuthUser as MockedFunction< + typeof resolveOAuth2AuthUser +>; +const mockGetAuthorizeContext = getAuthorizeContext as MockedFunction; +const mockSubmitAuthorizeDecision = submitAuthorizeDecision as MockedFunction< + typeof submitAuthorizeDecision +>; + +const createMockRes = () => { + const res: any = { + headers: {}, + statusCode: 200, + body: undefined, + setHeader: vi.fn((name: string, value: string) => { + res.headers[name] = value; + }), + status: vi.fn((code: number) => { + res.statusCode = code; + return res; + }), + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }), + end: vi.fn(() => res) + }; + return res; +}; + +describe('oauth2 authorize context api handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 405 for non-GET method and still sets no-store headers', async () => { + const req: any = { method: 'POST', query: {} }; + const res = createMockRes(); + + await contextHandler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store'); + expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.end).toHaveBeenCalled(); + }); + + it('returns invalid_request when query schema validation fails', async () => { + const req: any = { method: 'GET', query: {} }; + const res = createMockRes(); + + await contextHandler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBe('invalid_request'); + }); + + it('maps OAuth2HttpError from getAuthorizeContext', async () => { + mockResolveOAuth2AuthUser.mockResolvedValue(null); + mockGetAuthorizeContext.mockRejectedValue( + new OAuth2HttpError(400, 'invalid_grant', 'not found') + ); + + const req: any = { + method: 'GET', + query: { + user_code: 'ABCD-EFGH' + } + }; + const res = createMockRes(); + + await contextHandler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body).toEqual({ + error: 'invalid_grant', + error_description: 'not found' + }); + }); +}); + +describe('oauth2 authorize decision api handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 405 for non-POST method and still sets no-store headers', async () => { + const req: any = { method: 'GET', body: {} }; + const res = createMockRes(); + + await decisionHandler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store'); + expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.end).toHaveBeenCalled(); + }); + + it('returns 401 invalid_request when user is unauthenticated', async () => { + mockResolveOAuth2AuthUser.mockResolvedValue(null); + const req: any = { + method: 'POST', + body: { + request_id: '550e8400-e29b-41d4-a716-446655440000', + decision: 'approve' + } + }; + const res = createMockRes(); + + await decisionHandler(req, res); + + expect(res.statusCode).toBe(401); + expect(res.body).toEqual({ + error: 'invalid_request', + error_description: 'Authentication required' + }); + }); + + it('returns invalid_request when request body schema validation fails', async () => { + mockResolveOAuth2AuthUser.mockResolvedValue({ userUid: 'user-uid' } as any); + const req: any = { method: 'POST', body: {} }; + const res = createMockRes(); + + await decisionHandler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBe('invalid_request'); + }); + + it('maps OAuth2HttpError from submitAuthorizeDecision', async () => { + mockResolveOAuth2AuthUser.mockResolvedValue({ userUid: 'user-uid' } as any); + mockSubmitAuthorizeDecision.mockRejectedValue( + new OAuth2HttpError(400, 'invalid_grant', 'bad request') + ); + + const req: any = { + method: 'POST', + body: { + request_id: '550e8400-e29b-41d4-a716-446655440000', + decision: 'approve' + } + }; + const res = createMockRes(); + + await decisionHandler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body).toEqual({ + error: 'invalid_grant', + error_description: 'bad request' + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.service.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.service.test.ts new file mode 100644 index 000000000000..a2cf30417d21 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/authorize.service.test.ts @@ -0,0 +1,199 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { DeviceGrantStatus } from 'prisma/global/generated/client'; + +vi.mock('@/services/backend/db/init', () => ({ + globalPrisma: { + oAuthDeviceGrant: { + findFirst: vi.fn(), + findUnique: vi.fn(), + update: vi.fn() + }, + oAuthUserConsent: { + findFirst: vi.fn(), + upsert: vi.fn() + }, + $transaction: vi.fn() + } +})); + +vi.mock('@/services/enable', () => ({ + enableOAuth2Idp: vi.fn(() => true) +})); + +import { getAuthorizeContext, submitAuthorizeDecision } from '@/services/backend/oauth2/service'; +import { globalPrisma } from '@/services/backend/db/init'; + +const mockPrisma = globalPrisma as any; + +const buildGrantWithClient = (status: DeviceGrantStatus) => ({ + id: '550e8400-e29b-41d4-a716-446655440000', + clientId: 'client-1', + deviceCodeHash: 'device-hash', + userCodeHash: 'user-code-hash', + userUid: null, + status, + expiresAt: new Date(Date.now() + 60 * 1000), + lastPollAt: null, + pollCount: 0, + createdAt: new Date(), + updatedAt: new Date(), + client: { + id: 'client-db-id', + clientId: 'client-1', + name: 'Test Client', + logoUrl: null + } +}); + +const buildGrant = (status: DeviceGrantStatus) => ({ + id: '550e8400-e29b-41d4-a716-446655440000', + clientId: 'client-1', + deviceCodeHash: 'device-hash', + userCodeHash: 'user-code-hash', + userUid: null, + status, + expiresAt: new Date(Date.now() + 60 * 1000), + lastPollAt: null, + pollCount: 0, + createdAt: new Date(), + updatedAt: new Date() +}); + +describe('oauth2 authorize service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('getAuthorizeContext returns invalid_grant for unknown request', async () => { + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue(null); + + await expect( + getAuthorizeContext({ + request_id: '550e8400-e29b-41d4-a716-446655440000' + }) + ).rejects.toMatchObject({ + error: 'invalid_grant' + }); + }); + + it('getAuthorizeContext returns expired_token for expired grant', async () => { + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue({ + ...buildGrantWithClient(DeviceGrantStatus.PENDING), + expiresAt: new Date(Date.now() - 1000) + }); + + await expect( + getAuthorizeContext({ + request_id: '550e8400-e29b-41d4-a716-446655440000' + }) + ).rejects.toMatchObject({ + error: 'expired_token' + }); + }); + + it('getAuthorizeContext marks has_existing_consent in authenticated context', async () => { + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue( + buildGrantWithClient(DeviceGrantStatus.PENDING) + ); + mockPrisma.oAuthUserConsent.findFirst.mockResolvedValue({ + id: 'consent-id' + }); + + const result = await getAuthorizeContext( + { + request_id: '550e8400-e29b-41d4-a716-446655440000' + }, + 'user-uid' + ); + + expect(result.request_id).toBe('550e8400-e29b-41d4-a716-446655440000'); + expect(result.client_id).toBe('client-1'); + expect(result.has_existing_consent).toBe(true); + }); + + it('submitAuthorizeDecision returns invalid_grant when request is not found', async () => { + mockPrisma.oAuthDeviceGrant.findUnique.mockResolvedValue(null); + + await expect( + submitAuthorizeDecision({ + requestId: '550e8400-e29b-41d4-a716-446655440000', + decision: 'approve', + userUid: 'user-uid' + }) + ).rejects.toMatchObject({ + error: 'invalid_grant' + }); + }); + + it('submitAuthorizeDecision returns invalid_grant when grant is not pending', async () => { + mockPrisma.oAuthDeviceGrant.findUnique.mockResolvedValue( + buildGrant(DeviceGrantStatus.APPROVED) + ); + + await expect( + submitAuthorizeDecision({ + requestId: '550e8400-e29b-41d4-a716-446655440000', + decision: 'approve', + userUid: 'user-uid' + }) + ).rejects.toMatchObject({ + error: 'invalid_grant' + }); + }); + + it('submitAuthorizeDecision approve uses transaction and updates consent + grant', async () => { + mockPrisma.oAuthDeviceGrant.findUnique.mockResolvedValue(buildGrant(DeviceGrantStatus.PENDING)); + mockPrisma.oAuthUserConsent.upsert.mockReturnValue('consent-op'); + mockPrisma.oAuthDeviceGrant.update.mockReturnValue('grant-op'); + mockPrisma.$transaction.mockResolvedValue([]); + + const result = await submitAuthorizeDecision({ + requestId: '550e8400-e29b-41d4-a716-446655440000', + decision: 'approve', + userUid: 'user-uid' + }); + + expect(mockPrisma.oAuthUserConsent.upsert).toHaveBeenCalledWith({ + where: { + userUid_clientId: { + userUid: 'user-uid', + clientId: 'client-1' + } + }, + update: {}, + create: { + userUid: 'user-uid', + clientId: 'client-1' + } + }); + expect(mockPrisma.oAuthDeviceGrant.update).toHaveBeenCalledWith({ + where: { id: '550e8400-e29b-41d4-a716-446655440000' }, + data: { + status: DeviceGrantStatus.APPROVED, + userUid: 'user-uid' + } + }); + expect(mockPrisma.$transaction).toHaveBeenCalledWith(['consent-op', 'grant-op']); + expect(result).toEqual({ status: 'approved' }); + }); + + it('submitAuthorizeDecision deny updates grant status to DENIED', async () => { + mockPrisma.oAuthDeviceGrant.findUnique.mockResolvedValue(buildGrant(DeviceGrantStatus.PENDING)); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + + const result = await submitAuthorizeDecision({ + requestId: '550e8400-e29b-41d4-a716-446655440000', + decision: 'deny', + userUid: 'user-uid' + }); + + expect(mockPrisma.oAuthDeviceGrant.update).toHaveBeenCalledWith({ + where: { id: '550e8400-e29b-41d4-a716-446655440000' }, + data: { + status: DeviceGrantStatus.DENIED, + userUid: 'user-uid' + } + }); + expect(result).toEqual({ status: 'denied' }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/device.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/device.api.test.ts new file mode 100644 index 000000000000..04584ab25c88 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/device.api.test.ts @@ -0,0 +1,115 @@ +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { OAuth2HttpError } from '@/services/backend/oauth2/errors'; + +vi.mock('@/services/backend/oauth2/service', () => ({ + createDeviceAuthorizationGrant: vi.fn() +})); + +import handler from '@/pages/api/auth/oauth2/device'; +import { createDeviceAuthorizationGrant } from '@/services/backend/oauth2/service'; + +const mockCreateDeviceAuthorizationGrant = createDeviceAuthorizationGrant as MockedFunction< + typeof createDeviceAuthorizationGrant +>; + +const createMockRes = () => { + const res: any = { + headers: {}, + statusCode: 200, + body: undefined, + setHeader: vi.fn((name: string, value: string) => { + res.headers[name] = value; + }), + status: vi.fn((code: number) => { + res.statusCode = code; + return res; + }), + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }), + end: vi.fn(() => res) + }; + return res; +}; + +describe('oauth2 device api handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 405 for non-POST method and still sets no-store headers', async () => { + const req: any = { method: 'GET', body: {} }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store'); + expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.end).toHaveBeenCalled(); + }); + + it('returns invalid_request when body schema validation fails', async () => { + const req: any = { method: 'POST', body: {} }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBe('invalid_request'); + }); + + it('maps OAuth2HttpError from service to OAuth2 error response', async () => { + mockCreateDeviceAuthorizationGrant.mockRejectedValue( + new OAuth2HttpError(401, 'invalid_client', 'invalid client') + ); + + const req: any = { + method: 'POST', + body: { + client_id: 'client-1' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(401); + expect(res.body).toEqual({ + error: 'invalid_client', + error_description: 'invalid client' + }); + }); + + it('returns success payload when service resolves', async () => { + mockCreateDeviceAuthorizationGrant.mockResolvedValue({ + device_code: 'device', + user_code: 'ABCD-EFGH', + verification_uri: 'https://example.com/oauth2/device', + verification_uri_complete: 'https://example.com/oauth2/device?user_code=ABCD-EFGH', + expires_in: 600, + interval: 5 + }); + + const req: any = { + method: 'POST', + body: { + client_id: 'client-1' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(200); + expect(res.body).toEqual({ + device_code: 'device', + user_code: 'ABCD-EFGH', + verification_uri: 'https://example.com/oauth2/device', + verification_uri_complete: 'https://example.com/oauth2/device?user_code=ABCD-EFGH', + expires_in: 600, + interval: 5 + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/service.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/service.test.ts new file mode 100644 index 000000000000..d87cbee8ad45 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/service.test.ts @@ -0,0 +1,371 @@ +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { DeviceGrantStatus, OAuthClientType } from 'prisma/global/generated/client'; +import { createHash } from 'crypto'; + +vi.mock('@/services/backend/db/init', () => ({ + globalPrisma: { + oAuthClient: { + findUnique: vi.fn() + }, + oAuthDeviceGrant: { + create: vi.fn(), + findFirst: vi.fn(), + findUnique: vi.fn(), + update: vi.fn() + }, + oAuthUserConsent: { + findFirst: vi.fn(), + upsert: vi.fn() + }, + user: { + findUnique: vi.fn() + }, + $transaction: vi.fn() + } +})); + +vi.mock('@/services/backend/auth', () => ({ + generateOAuth2AccessToken: vi.fn(() => 'mock-access-token'), + generateOAuth2RefreshToken: vi.fn(() => 'mock-refresh-token'), + verifyOAuth2RefreshToken: vi.fn() +})); + +vi.mock('@/services/enable', () => ({ + enableOAuth2Idp: vi.fn(() => true) +})); + +import { + createDeviceAuthorizationGrant, + exchangeDeviceCodeForToken, + exchangeRefreshTokenForToken +} from '@/services/backend/oauth2/service'; +import { globalPrisma } from '@/services/backend/db/init'; +import { verifyOAuth2RefreshToken } from '@/services/backend/auth'; + +const mockPrisma = globalPrisma as any; +const mockVerifyOAuth2RefreshToken = verifyOAuth2RefreshToken as MockedFunction< + typeof verifyOAuth2RefreshToken +>; + +const buildClient = () => ({ + id: 'c1', + clientId: 'client-1', + clientType: OAuthClientType.PUBLIC, + userUid: null, + clientSecretHash: null, + allowedGrantTypes: ['urn:ietf:params:oauth:grant-type:device_code'], + name: 'Test Client', + logoUrl: null, + createdAt: new Date(), + updatedAt: new Date() +}); + +const hashSecret = (secret: string) => createHash('sha256').update(secret).digest('hex'); + +const buildConfidentialClient = () => ({ + ...buildClient(), + clientType: OAuthClientType.CONFIDENTIAL, + clientSecretHash: hashSecret('top-secret') +}); + +const buildGrant = (status: DeviceGrantStatus, lastPollAt?: Date) => ({ + id: 'request-id', + clientId: 'client-1', + deviceCodeHash: 'hashed-device', + userCodeHash: 'hashed-user', + userUid: status === DeviceGrantStatus.APPROVED ? 'user-uid' : null, + status, + expiresAt: new Date(Date.now() + 60 * 1000), + lastPollAt: lastPollAt ?? new Date(Date.now() - 10 * 1000), + pollCount: 0, + createdAt: new Date(), + updatedAt: new Date() +}); + +describe('oauth2 service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates device authorization grant', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.create.mockResolvedValue({ + expiresAt: new Date(Date.now() + 60 * 1000) + }); + + const result = await createDeviceAuthorizationGrant({ + client_id: 'client-1' + }); + + expect(result.device_code).toBeTruthy(); + expect(result.user_code).toBeTruthy(); + expect(result.verification_uri).toContain('/oauth2/device'); + expect(result.verification_uri_complete).toContain('user_code='); + }); + + it('returns authorization_pending before approval', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue(buildGrant(DeviceGrantStatus.PENDING)); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'authorization_pending' + }); + }); + + it('returns slow_down when polling too frequently', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue( + buildGrant(DeviceGrantStatus.PENDING, new Date(Date.now() - 1000)) + ); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'slow_down' + }); + }); + + it('returns expired_token when grant is expired', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue({ + ...buildGrant(DeviceGrantStatus.PENDING), + expiresAt: new Date(Date.now() - 1000) + }); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'expired_token' + }); + }); + + it('returns access_denied when user denied request', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue(buildGrant(DeviceGrantStatus.DENIED)); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'access_denied' + }); + }); + + it('returns access token when grant is approved', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue(buildGrant(DeviceGrantStatus.APPROVED)); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + mockPrisma.user.findUnique.mockResolvedValue({ + uid: 'user-uid', + id: 'user-id', + name: 'user-name' + }); + + const result = await exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }); + + expect(result.access_token).toBe('mock-access-token'); + expect(result.refresh_token).toBe('mock-refresh-token'); + expect(result.token_type).toBe('Bearer'); + expect(result.expires_in).toBeGreaterThan(0); + }); + + it('returns expired_token when device code has already been consumed', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockPrisma.oAuthDeviceGrant.findFirst.mockResolvedValue(buildGrant(DeviceGrantStatus.CONSUMED)); + mockPrisma.oAuthDeviceGrant.update.mockResolvedValue({}); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'expired_token' + }); + }); + + it('returns unauthorized_client when client grant type is not allowed in device exchange', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue({ + ...buildClient(), + allowedGrantTypes: [] + }); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'unauthorized_client' + }); + }); + + it('rejects confidential client device exchange when client_secret is missing', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildConfidentialClient()); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'invalid_client' + }); + }); + + it('rejects confidential client device exchange when client_secret is invalid', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildConfidentialClient()); + + await expect( + exchangeDeviceCodeForToken({ + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device', + client_id: 'client-1', + client_secret: 'wrong-secret' + }) + ).rejects.toMatchObject({ + error: 'invalid_client' + }); + }); + + it('refreshes token when refresh token is valid', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockVerifyOAuth2RefreshToken.mockResolvedValue({ + sub: 'user-uid', + user_id: 'user-id', + client_id: 'client-1', + preferred_username: 'user-name', + token_type: 'refresh_token' + }); + + const result = await exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'valid-refresh-token', + client_id: 'client-1' + }); + + expect(result.access_token).toBe('mock-access-token'); + expect(result.refresh_token).toBe('mock-refresh-token'); + expect(result.token_type).toBe('Bearer'); + expect(result.expires_in).toBeGreaterThan(0); + }); + + it('rejects invalid refresh token', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockVerifyOAuth2RefreshToken.mockResolvedValue(null); + + await expect( + exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'invalid-refresh-token', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'invalid_grant' + }); + }); + + it('rejects refresh token when client does not match', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildClient()); + mockVerifyOAuth2RefreshToken.mockResolvedValue({ + sub: 'user-uid', + user_id: 'user-id', + client_id: 'another-client', + preferred_username: 'user-name', + token_type: 'refresh_token' + }); + + await expect( + exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'valid-refresh-token', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'invalid_grant' + }); + }); + + it('returns unauthorized_client when client grant type is not allowed in refresh exchange', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue({ + ...buildClient(), + allowedGrantTypes: [] + }); + + await expect( + exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'valid-refresh-token', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'unauthorized_client' + }); + }); + + it('rejects confidential client refresh exchange when client_secret is missing', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildConfidentialClient()); + + await expect( + exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'valid-refresh-token', + client_id: 'client-1' + }) + ).rejects.toMatchObject({ + error: 'invalid_client' + }); + }); + + it('rejects confidential client refresh exchange when client_secret is invalid', async () => { + mockPrisma.oAuthClient.findUnique.mockResolvedValue(buildConfidentialClient()); + + await expect( + exchangeRefreshTokenForToken({ + grant_type: 'refresh_token', + refresh_token: 'valid-refresh-token', + client_id: 'client-1', + client_secret: 'wrong-secret' + }) + ).rejects.toMatchObject({ + error: 'invalid_client' + }); + }); + + // [TODO] Invalidate used refresh tokens + it.todo( + 'should reject replayed refresh token after rotation (security enhancement pending implementation)' + ); + + // [TODO] Prevent device_code race + it.todo( + 'should prevent concurrent device_code exchanges from issuing multiple token pairs (race-condition hardening pending implementation)' + ); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/token.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/token.api.test.ts new file mode 100644 index 000000000000..aadd9cad5e05 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/oauth2-idp/token.api.test.ts @@ -0,0 +1,122 @@ +import { beforeEach, describe, expect, it, vi, type MockedFunction } from 'vitest'; +import { OAuth2HttpError } from '@/services/backend/oauth2/errors'; + +vi.mock('@/services/backend/oauth2/service', () => ({ + exchangeDeviceCodeForToken: vi.fn(), + exchangeRefreshTokenForToken: vi.fn() +})); + +import handler from '@/pages/api/auth/oauth2/token'; +import { + exchangeDeviceCodeForToken, + exchangeRefreshTokenForToken +} from '@/services/backend/oauth2/service'; + +const mockExchangeDeviceCodeForToken = exchangeDeviceCodeForToken as MockedFunction< + typeof exchangeDeviceCodeForToken +>; +const mockExchangeRefreshTokenForToken = exchangeRefreshTokenForToken as MockedFunction< + typeof exchangeRefreshTokenForToken +>; + +const createMockRes = () => { + const res: any = { + headers: {}, + statusCode: 200, + body: undefined, + setHeader: vi.fn((name: string, value: string) => { + res.headers[name] = value; + }), + status: vi.fn((code: number) => { + res.statusCode = code; + return res; + }), + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }), + end: vi.fn(() => res) + }; + return res; +}; + +describe('oauth2 token api handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns 405 for non-POST method and still sets no-store headers', async () => { + const req: any = { method: 'GET', body: {} }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store'); + expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.end).toHaveBeenCalled(); + }); + + it('returns invalid_request when body schema validation fails', async () => { + const req: any = { method: 'POST', body: {} }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBe('invalid_request'); + }); + + it('maps OAuth2HttpError from refresh exchange to OAuth2 error response', async () => { + mockExchangeRefreshTokenForToken.mockRejectedValue( + new OAuth2HttpError(401, 'invalid_client', 'bad client') + ); + + const req: any = { + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token: 'token', + client_id: 'client-1' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(401); + expect(res.body).toEqual({ + error: 'invalid_client', + error_description: 'bad client' + }); + }); + + it('returns success payload for device code grant', async () => { + mockExchangeDeviceCodeForToken.mockResolvedValue({ + access_token: 'a', + refresh_token: 'r', + token_type: 'Bearer', + expires_in: 3600 + }); + + const req: any = { + method: 'POST', + body: { + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: 'device-code', + client_id: 'client-1' + } + }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(200); + expect(res.body).toEqual({ + access_token: 'a', + refresh_token: 'r', + token_type: 'Bearer', + expires_in: 3600 + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/backend/workspace/getQuota.api.test.ts b/frontend/desktop/src/__tests__/unit/backend/workspace/getQuota.api.test.ts new file mode 100644 index 000000000000..8ad72e8c3c96 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/backend/workspace/getQuota.api.test.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { verifyAccessToken, generateBillingToken, fetchQuota } = vi.hoisted(() => ({ + verifyAccessToken: vi.fn(), + generateBillingToken: vi.fn(() => 'billing-token'), + fetchQuota: vi.fn() +})); + +vi.mock('@/services/backend/auth', () => ({ + generateBillingToken, + verifyAccessToken +})); + +import handler from '@/pages/api/workspace/getQuota'; + +const createMockRes = () => { + const res: any = { + body: undefined, + json: vi.fn((payload: unknown) => { + res.body = payload; + return res; + }) + }; + return res; +}; + +describe('workspace quota api', () => { + beforeEach(() => { + vi.clearAllMocks(); + (global as any).AppConfig = { + desktop: { + auth: { + billingUrl: 'https://billing.example.com' + } + } + }; + vi.stubGlobal('fetch', fetchQuota); + verifyAccessToken.mockResolvedValue({ + userId: 'user-id', + userUid: 'user-uid', + workspaceId: 'workspace-id' + }); + }); + + it('maps the ResourceQuota pods entry to a pod quota item', async () => { + fetchQuota.mockResolvedValue({ + clone: () => ({ + json: async () => ({ + quota: { + hard: { pods: '20' }, + used: { pods: '3' } + } + }) + }) + }); + + const res = createMockRes(); + await handler({ headers: {}, method: 'GET' } as any, res); + + expect(res.body).toEqual({ + code: 200, + data: { + quota: [{ limit: 20, type: 'pod', used: 3 }] + }, + message: '' + }); + }); + + it('keeps an empty quota response compatible when pods is absent', async () => { + fetchQuota.mockResolvedValue({ + clone: () => ({ + json: async () => ({ quota: { hard: {}, used: {} } }) + }) + }); + + const res = createMockRes(); + await handler({ headers: {}, method: 'GET' } as any, res); + + expect(res.body).toEqual({ code: 200, data: { quota: [] }, message: '' }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/components/app-window.test.tsx b/frontend/desktop/src/__tests__/unit/components/app-window.test.tsx new file mode 100644 index 000000000000..933c64aaa442 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/components/app-window.test.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +import AppWindow from '@/components/app_window'; +import type { AppInfo } from '@/stores/app'; +import { APPTYPE, type WindowSize } from '@/types'; + +const store = vi.hoisted(() => ({ + currentApp: undefined as AppInfo | undefined, + closeAppById: vi.fn(), + updateOpenedAppInfo: vi.fn(), + setToHighestLayerById: vi.fn() +})); + +vi.mock('@/stores/app', () => ({ + BRAIN_APP_KEY: 'system-brain', + default: () => ({ + closeAppById: store.closeAppById, + updateOpenedAppInfo: store.updateOpenedAppInfo, + setToHighestLayerById: store.setToHighestLayerById, + currentApp: () => store.currentApp, + findAppInfoById: () => store.currentApp, + maxZIndex: 10 + }) +})); + +vi.mock('@/stores/config', () => ({ + useConfigStore: () => ({ + layoutConfig: { + logo: '/logo.svg' + } + }) +})); + +vi.mock('@/stores/session', () => ({ + default: (selector: (state: { isGuest: () => boolean }) => unknown) => + selector({ isGuest: () => false }) +})); + +vi.mock('next-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: 'zh-Hans' } + }) +})); + +vi.mock('react-draggable', () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children} +})); + +const createApp = (key: AppInfo['key'], size: WindowSize): AppInfo => + ({ + pid: 1, + isShow: true, + zIndex: 10, + size, + cacheSize: size, + style: {}, + mouseDowning: false, + key, + name: 'Test app', + icon: '/test-app.svg', + type: APPTYPE.IFRAME, + data: { + url: 'https://example.com', + desc: '' + }, + displayType: 'normal', + representativeMeta: { + forcedIconStyle: 'fill' + } + } as AppInfo); + +afterEach(() => { + vi.clearAllMocks(); + store.currentApp = undefined; +}); + +describe('AppWindow 标题栏', () => { + test.each(['maximize', 'maxmin', 'minimize'])( + 'system-brain 在 %s 状态下不渲染窗口标题栏', + (size) => { + store.currentApp = createApp('system-brain', size); + + const html = renderToStaticMarkup(Brain content); + + expect(html).not.toContain('windowHeader'); + expect(html).toContain('Brain content'); + } + ); + + test('非 Brain app 保留窗口标题栏和三个窗口控制按钮', () => { + store.currentApp = createApp('system-devbox', 'maximize'); + + const html = renderToStaticMarkup(Devbox content); + + expect(html).toContain('windowHeader'); + expect(html.match(/_uicon_/g)).toHaveLength(3); + expect(html).toContain('data-type="close"'); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/components/guide-feature-flag.test.tsx b/frontend/desktop/src/__tests__/unit/components/guide-feature-flag.test.tsx new file mode 100644 index 000000000000..0f707f9d9fa8 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/components/guide-feature-flag.test.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import SecondaryLinks from '@/components/SecondaryLinks'; +import { GlobalAnnouncement } from '@/components/desktop_content/GlobalAnnouncement'; + +const mocks = vi.hoisted(() => ({ + config: { + layoutConfig: { + version: 'cn', + currencySymbol: 'shellCoin', + common: { + announcementEnabled: true, + subscriptionEnabled: false + } + }, + commonConfig: { + guideEnabled: false + } + }, + queryData: undefined as unknown, + openGuideModal: vi.fn(), + setInitGuide: vi.fn() +})); + +vi.mock('@/stores/config', () => ({ + useConfigStore: (selector?: (state: typeof mocks.config) => unknown) => + selector ? selector(mocks.config) : mocks.config +})); + +vi.mock('@/stores/app', () => ({ + default: () => ({ + openDesktopApp: vi.fn() + }) +})); + +vi.mock('@/stores/guideModal', () => ({ + useGuideModalStore: () => ({ + openGuideModal: mocks.openGuideModal, + setInitGuide: mocks.setInitGuide + }) +})); + +vi.mock('@/stores/session', () => ({ + default: () => ({ + session: undefined, + isGuest: () => false + }) +})); + +vi.mock('@/stores/subscription', () => ({ + useSubscriptionStore: () => ({ + subscriptionInfo: undefined, + fetchSubscriptionInfo: vi.fn() + }) +})); + +vi.mock('@tanstack/react-query', () => ({ + useQuery: () => ({ + data: mocks.queryData + }) +})); + +vi.mock('@/components/account/BalancePopover', () => ({ + BalancePopover: ({ children }: { children: React.ReactNode }) => <>{children}, + getPlanBackground: () => 'transparent' +})); + +vi.mock('@/components/account/JoinDiscordPrompt', () => ({ + JoinDiscordPrompt: ({ children }: { children: React.ReactNode }) => <>{children} +})); + +vi.mock('@sealos/ui', () => ({ + CurrencySymbol: () => currency +})); + +vi.mock('@sealos/shadcn-ui', () => ({ + cn: (...values: string[]) => values.filter(Boolean).join(' ') +})); + +vi.mock('next-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key + }) +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: 'zh-Hans' } + }) +})); + +vi.mock('dompurify', () => ({ + default: { + sanitize: (value: string) => value + } +})); + +vi.mock('@sealos/gtm', () => ({ + track: vi.fn() +})); + +vi.mock('@chakra-ui/react', async () => { + const actual = await vi.importActual('@chakra-ui/react'); + return { + ...actual, + useBreakpointValue: () => false + }; +}); + +describe('guide feature flag', () => { + beforeEach(() => { + mocks.config.commonConfig.guideEnabled = false; + mocks.queryData = undefined; + vi.clearAllMocks(); + }); + + it('hides the manual guide entry when guideEnabled is false', () => { + const html = renderToStaticMarkup(); + + expect(html).not.toContain('common:guide'); + }); + + it('shows the manual guide entry when guideEnabled is true', () => { + mocks.config.commonConfig.guideEnabled = true; + + const html = renderToStaticMarkup(); + + expect(html).toContain('common:guide'); + }); + + it('hides only the guide fallback when guideEnabled is false', () => { + const html = renderToStaticMarkup(); + + expect(html).not.toContain('v2:onboard_guide'); + }); + + it('keeps a real regional announcement visible when guideEnabled is false', () => { + mocks.queryData = [ + { + uid: 'announcement-1', + from: 'Desktop-Announcement', + i18n: { + 'zh-Hans': { + title: '中国区推荐公告' + } + } + } + ]; + + const html = renderToStaticMarkup(); + + expect(html).toContain('中国区推荐公告'); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/sdkParentBoundary.test.ts b/frontend/desktop/src/__tests__/unit/sdkParentBoundary.test.ts new file mode 100644 index 000000000000..76b7a76357e0 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/sdkParentBoundary.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { createSealosApp, sealosApp } from '../../../../packages/client-sdk/src/app'; + +describe('client SDK parent boundary', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('sends requests to the direct parent Desktop instead of the top-level host', async () => { + const parentPostMessage = vi.fn(); + const topPostMessage = vi.fn(); + let messageListener: ((event: MessageEvent) => void) | undefined; + + vi.stubGlobal('window', { + location: { origin: 'https://app.example.com' }, + parent: { postMessage: parentPostMessage }, + top: { postMessage: topPostMessage }, + addEventListener: vi.fn((type: string, listener: (event: MessageEvent) => void) => { + if (type === 'message') messageListener = listener; + }), + removeEventListener: vi.fn() + }); + + const cleanup = createSealosApp(); + const sessionPromise = sealosApp.getSession(); + + expect(parentPostMessage).toHaveBeenCalledOnce(); + expect(topPostMessage).not.toHaveBeenCalled(); + + const [request, targetOrigin] = parentPostMessage.mock.calls[0]; + expect(targetOrigin).toBe('*'); + expect(messageListener).toBeDefined(); + + messageListener?.({ + data: { + messageId: request.messageId, + success: true, + data: { user: 'test-user' } + }, + origin: 'https://desktop.example.com', + source: {} + } as MessageEvent); + + await expect(sessionPromise).resolves.toEqual({ user: 'test-user' }); + cleanup?.(); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/utils/gtm.test.ts b/frontend/desktop/src/__tests__/unit/utils/gtm.test.ts new file mode 100644 index 000000000000..478e5d38fd30 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/utils/gtm.test.ts @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { gtmLoginSuccess } from '@/utils/gtm'; + +describe('gtmLoginSuccess', () => { + beforeEach(() => { + const dataLayer = [] as any[]; + dataLayer.push = vi.fn<(...items: any[]) => number>(() => 0) as any; + + Object.defineProperty(globalThis, 'window', { + value: { + dataLayer: dataLayer as any[] & { push: ReturnType } + }, + writable: true, + configurable: true + }); + }); + + it('pushes direct product user traits for GTM identify variables', () => { + const push = vi.mocked(window.dataLayer.push); + + gtmLoginSuccess({ + method: 'oauth2', + oauth2Provider: 'GITHUB', + user_type: 'existing', + productUserTraits: { + user_username: 'octocat', + user_name: 'Octo Cat', + user_email: 'octo@example.com' + } + }); + + expect(push).toHaveBeenCalledWith({ + event: 'login_success', + method: 'oauth2', + oauth2_provider: 'GITHUB', + user_type: 'existing', + module: 'auth', + context: 'app', + user_username: 'octocat', + user_name: 'Octo Cat', + user_email: 'octo@example.com' + }); + }); + + it('omits product user traits when they are not provided', () => { + const push = vi.mocked(window.dataLayer.push); + + gtmLoginSuccess({ + method: 'email', + user_type: 'new' + }); + + expect(push).toHaveBeenCalledWith({ + event: 'login_success', + method: 'email', + oauth2_provider: undefined, + user_type: 'new', + module: 'auth', + context: 'app' + }); + }); + + it('queues login events before the GTM script initializes', () => { + window.dataLayer = undefined as unknown as any[]; + + gtmLoginSuccess({ + method: 'oauth2', + user_type: 'existing' + }); + + expect(window.dataLayer).toEqual([ + { + event: 'login_success', + method: 'oauth2', + oauth2_provider: undefined, + user_type: 'existing', + module: 'auth', + context: 'app' + } + ]); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/utils/initialAppTarget.test.ts b/frontend/desktop/src/__tests__/unit/utils/initialAppTarget.test.ts new file mode 100644 index 000000000000..8822bd0a813b --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/utils/initialAppTarget.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveInitialAppTarget } from '@/utils/initialAppTarget'; + +const installedAppKeys = ['system-brain', 'system-template', 'system-devbox']; + +const resolve = (overrides: Partial[0]> = {}) => + resolveInitialAppTarget({ + installedAppKeys, + hasOpenAppQuery: false, + restoreAppKeys: [], + defaultAppKey: 'system-brain', + ...overrides + }); + +describe('resolveInitialAppTarget', () => { + it('keeps an autolaunch target ahead of the normal root-path selection', () => { + expect( + resolve({ + autolaunchAppKey: 'system-devbox', + hasOpenAppQuery: true, + queryAppKey: 'system-template', + restoreAppKeys: ['system-brain'] + }) + ).toEqual({ + kind: 'app', + appKey: 'system-devbox', + source: 'autolaunch' + }); + }); + + it('treats an empty openapp query as an explicit Desktop target', () => { + expect( + resolve({ + hasOpenAppQuery: true, + queryAppKey: '', + restoreAppKeys: ['system-template'] + }) + ).toEqual({ + kind: 'desktop', + source: 'explicit-desktop' + }); + }); + + it('opens an explicitly requested app before a restorable or default app', () => { + expect( + resolve({ + hasOpenAppQuery: true, + queryAppKey: 'system-devbox', + restoreAppKeys: ['system-template'] + }) + ).toEqual({ + kind: 'app', + appKey: 'system-devbox', + source: 'query' + }); + }); + + it('stays on Desktop when an explicit app is unavailable', () => { + expect( + resolve({ + hasOpenAppQuery: true, + queryAppKey: 'system-missing', + restoreAppKeys: ['system-template'] + }) + ).toEqual({ + kind: 'desktop', + source: 'explicit-unavailable' + }); + }); + + it('restores a fullscreen app before selecting Brain as the default', () => { + expect(resolve({ restoreAppKeys: ['system-template'] })).toEqual({ + kind: 'app', + appKey: 'system-template', + source: 'restore' + }); + }); + + it('uses the first valid restore candidate', () => { + expect(resolve({ restoreAppKeys: ['system-missing', 'system-devbox'] })).toEqual({ + kind: 'app', + appKey: 'system-devbox', + source: 'restore' + }); + }); + + it('prefers the tab-local restore candidate when both restore candidates are valid', () => { + expect(resolve({ restoreAppKeys: ['system-template', 'system-brain'] })).toEqual({ + kind: 'app', + appKey: 'system-template', + source: 'restore' + }); + }); + + it('defaults to Brain only when there is no explicit or restorable target', () => { + expect(resolve()).toEqual({ + kind: 'app', + appKey: 'system-brain', + source: 'default' + }); + }); + + it('falls back to Desktop when Brain is not installed', () => { + expect(resolve({ installedAppKeys: ['system-template'] })).toEqual({ + kind: 'desktop', + source: 'fallback' + }); + }); + + it('does not open a target that is unavailable in guest mode', () => { + expect( + resolve({ + hasOpenAppQuery: true, + queryAppKey: 'system-template', + allowedAppKeys: ['system-brain'] + }) + ).toEqual({ + kind: 'desktop', + source: 'explicit-unavailable' + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/utils/marketing-attribution.test.ts b/frontend/desktop/src/__tests__/unit/utils/marketing-attribution.test.ts new file mode 100644 index 000000000000..a917b47af9de --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/utils/marketing-attribution.test.ts @@ -0,0 +1,70 @@ +import { + appendMarketingQuery, + clearPersistedMarketingQuery, + marketingQueryFromRecord, + mergeMarketingQuery, + resolveMarketingQuery +} from '@/utils/marketing-attribution'; + +describe('marketing attribution query propagation', () => { + it('keeps only the signed attribution parameters from router input', () => { + expect( + marketingQueryFromRecord({ + consent_token: 'token-1', + sea_attr: ['state-1', 'state-2'], + openapp: 'system-brain' + }) + ).toEqual({ consent_token: 'token-1', sea_attr: 'state-1' }); + }); + + it('merges current attribution over stale query parameters', () => { + expect( + mergeMarketingQuery('templateName=n8n&sea_attr=old-state&consent_token=old-token', { + consent_token: 'token-1', + sea_attr: 'state-1' + }) + ).toBe('templateName=n8n&sea_attr=state-1&consent_token=token-1'); + }); + + it('appends attribution to redirects', () => { + expect(appendMarketingQuery('/?openapp=system-brain', { sea_attr: 'state-1' })).toBe( + '/?openapp=system-brain&sea_attr=state-1' + ); + }); + + it('drops a persisted consent token when a new attribution payload arrives', () => { + const storage = new Map([ + [ + 'sealos_marketing_query_v1', + JSON.stringify({ consent_token: 'old-token', sea_attr: 'old-state' }) + ] + ]); + vi.stubGlobal('window', {}); + vi.stubGlobal('sessionStorage', { + getItem: (key: string) => storage.get(key) || null, + setItem: (key: string, value: string) => storage.set(key, value), + removeItem: (key: string) => storage.delete(key) + }); + + expect(resolveMarketingQuery({ sea_attr: 'new-state' })).toEqual({ + sea_attr: 'new-state' + }); + vi.unstubAllGlobals(); + }); + + it('clears persisted attribution on logout', () => { + const storage = new Map([ + ['sealos_marketing_query_v1', JSON.stringify({ sea_attr: 'state-1' })] + ]); + vi.stubGlobal('window', {}); + vi.stubGlobal('sessionStorage', { + getItem: (key: string) => storage.get(key) || null, + removeItem: (key: string) => storage.delete(key) + }); + + clearPersistedMarketingQuery(); + + expect(storage.has('sealos_marketing_query_v1')).toBe(false); + vi.unstubAllGlobals(); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/utils/sessionConfig.test.ts b/frontend/desktop/src/__tests__/unit/utils/sessionConfig.test.ts new file mode 100644 index 000000000000..c72f991ecb9b --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/utils/sessionConfig.test.ts @@ -0,0 +1,103 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@/api/auth', () => ({ + UserInfo: vi.fn(), + getPlanInfo: vi.fn() +})); + +vi.mock('jwt-decode', () => ({ + jwtDecode: vi.fn() +})); + +const mockSetToken = vi.fn(); +const mockSetSession = vi.fn(); +const mockSetHasEverLoggedIn = vi.fn(); + +vi.mock('@/stores/session', () => ({ + default: { + getState: vi.fn(() => ({ + setToken: mockSetToken, + setSession: mockSetSession, + setHasEverLoggedIn: mockSetHasEverLoggedIn + })) + } +})); + +import { UserInfo, getPlanInfo } from '@/api/auth'; +import { jwtDecode } from 'jwt-decode'; +import { sessionConfig } from '@/utils/sessionConfig'; + +describe('sessionConfig', () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(jwtDecode).mockReturnValue({ + userCrName: 'k8s-user', + workspaceId: 'workspace-id', + workspaceUid: 'workspace-uid', + userCrUid: 'user-cr-uid', + userId: 'user-id', + userUid: 'user-uid' + } as any); + + vi.mocked(UserInfo).mockResolvedValue({ + data: { + info: { + nickname: 'display-name', + avatarUri: 'avatar-url', + userRestrictedLevel: 1, + realName: 'Real Name', + enterpriseRealName: 'Enterprise Name', + oauthProvider: [ + { + providerType: 'EMAIL', + providerId: 'user@example.com' + } + ] + } + } + } as any); + + vi.mocked(getPlanInfo).mockResolvedValue({ + data: { + subscription: { planName: 'pro' } + } + } as any); + }); + + it('persists username and email into session for downstream analytics variables', async () => { + const productUserTraits = await sessionConfig({ + token: 'region-token', + kubeconfig: 'kube-config', + appToken: 'app-token' + }); + + expect(mockSetToken).toHaveBeenCalledWith('region-token'); + expect(mockSetSession).toHaveBeenCalledWith({ + token: 'app-token', + subscription: { planName: 'pro' }, + user: { + userRestrictedLevel: 1, + realName: 'Real Name', + enterpriseRealName: 'Enterprise Name', + k8s_username: 'k8s-user', + username: 'display-name', + email: 'user@example.com', + name: 'display-name', + avatar: 'avatar-url', + nsid: 'workspace-id', + ns_uid: 'workspace-uid', + userCrUid: 'user-cr-uid', + userId: 'user-id', + userUid: 'user-uid' + }, + kubeconfig: 'kube-config' + }); + expect(mockSetHasEverLoggedIn).toHaveBeenCalledWith(true); + expect(productUserTraits).toEqual({ + user_username: 'display-name', + user_name: 'display-name', + user_email: 'user@example.com' + }); + }); +}); diff --git a/frontend/desktop/src/__tests__/unit/utils/stripeCallback.test.ts b/frontend/desktop/src/__tests__/unit/utils/stripeCallback.test.ts new file mode 100644 index 000000000000..a7ff645e8a13 --- /dev/null +++ b/frontend/desktop/src/__tests__/unit/utils/stripeCallback.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveStripeCallbackTarget } from '@/utils/stripeCallback'; + +describe('resolveStripeCallbackTarget', () => { + it('opens Brain billing when the checkout declares system-brain', () => { + expect(resolveStripeCallbackTarget('system-brain')).toEqual({ + appKey: 'system-brain', + pathname: '/billing' + }); + }); + + it('normalizes a repeated app query param to its first value', () => { + expect(resolveStripeCallbackTarget(['system-brain', 'system-costcenter'])).toEqual({ + appKey: 'system-brain', + pathname: '/billing' + }); + }); + + it.each([undefined, 'system-costcenter', 'untrusted-app'])( + 'keeps the existing costcenter callback for %s', + (payApp) => { + expect(resolveStripeCallbackTarget(payApp)).toEqual({ + appKey: 'system-costcenter', + pathname: '/' + }); + } + ); +}); diff --git a/frontend/desktop/src/api/auth.ts b/frontend/desktop/src/api/auth.ts new file mode 100644 index 000000000000..0aa41830acb4 --- /dev/null +++ b/frontend/desktop/src/api/auth.ts @@ -0,0 +1,372 @@ +import { InitRegionTokenParams } from '@/schema/auth'; +import { ILoginParams, ILoginResult, IRegisterParams } from '@/schema/auth'; +import { IEmailCheckParams } from '@/schema/email'; +import { OAuth2AuthorizeContextResponse } from '@/schema/oauth2'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { RegionResourceType } from '@/services/backend/svc/checkResource'; +import request from '@/services/request'; +import { ApiResp, Region, VerificationChallenge } from '@/types'; +import { AdClickData } from '@/types/adClick'; +import { BIND_STATUS } from '@/types/response/bind'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { + DELETE_USER_STATUS, + DeleteUserFinalStatusResponse, + DeleteUserInitiateResponse +} from '@/types/response/deleteUser'; +import { EnterpriseAuthInfo, PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { UNBIND_STATUS } from '@/types/response/unbind'; +import { SemData } from '@/types/sem'; +import { ValueOf } from '@/types/tools'; +import { type AxiosInstance } from 'axios'; +import { ProviderType } from 'prisma/global/generated/client'; +import { SubscriptionInfoResponse, WorkspacesPlansResponse } from '@/types/plan'; + +export const issueMarketingConsentToken = (seaAttr?: string) => + request.post>('/api/marketing/consent-token', { + ...(seaAttr ? { sea_attr: seaAttr } : {}) + }); + +export const _getRegionToken = (request: AxiosInstance) => () => + request.post>( + '/api/auth/regionToken' + ); + +export const getRegionToken = _getRegionToken(request); + +export const _passwordLoginRequest = + (request: AxiosInstance) => + ( + data: + | { + user: string; + password: string; + semData: SemData | null | undefined; + adClickData: AdClickData | null | undefined; + } + | { + user: string; + password: string; + } + ) => + request.post>('/api/auth/password', data); + +export const _passwordModifyRequest = + (request: AxiosInstance) => (data: { oldPassword: string; newPassword: string }) => + request.post>('/api/auth/password/modify', data); + +export const _UserInfo = (request: AxiosInstance) => () => + request.post< + any, + ApiResp<{ + info: { + realName?: string; + enterpriseRealName?: string; + userRestrictedLevel?: number; + uid: string; + createdAt: Date; + updatedAt: Date; + avatarUri: string; + nickname: string; + id: string; + name: string; + oauthProvider: { providerId: string; providerType: Exclude }[]; + }; + }> + >('/api/auth/info'); +export const _regionList = (request: AxiosInstance) => () => + request.get< + any, + ApiResp<{ + regionList: Region[]; + }> + >('/api/auth/regionList'); +const _getSmsBindCodeRequest = + (request: AxiosInstance) => (smsType: SmsType) => (data: { id: string; cfToken?: string }) => + request.post>( + `/api/auth/${smsType}/bind/sms`, + data + ); + +export const _verifySmsBindRequest = + (request: AxiosInstance) => + (smsType: SmsType) => + (data: { id: string; code: string; challengeId: string }) => + request.post< + typeof data, + ApiResp<{ code: string | null | undefined }, ValueOf> + >(`/api/auth/${smsType}/bind/verify`, data); + +export const _verifySmsUnbindRequest = + (request: AxiosInstance) => + (smsType: SmsType) => + (data: { id: string; code: string; challengeId: string }) => + request.post(`/api/auth/${smsType}/unbind/verify`, data); +export const _getSmsUnbindCodeRequest = + (request: AxiosInstance) => (smsType: SmsType) => (data: { id: string; cfToken?: string }) => + request.post>( + `/api/auth/${smsType}/unbind/sms`, + data + ); +export const _verifyOldSmsRequest = + (request: AxiosInstance) => + (smsType: SmsType) => + (data: { id: string; code: string; challengeId: string }) => + request.post>( + `/api/auth/${smsType}/changeBinding/verifyOld`, + data + ); +export const _getOldSmsCodeRequest = + (request: AxiosInstance) => (smsType: SmsType) => (data: { id: string; cfToken?: string }) => + request.post>( + `/api/auth/${smsType}/changeBinding/oldSms`, + data + ); +export const _verifyNewSmsRequest = + (request: AxiosInstance) => + (smsType: SmsType) => + (data: { id: string; code: string; uid: string; challengeId: string }) => + request.post(`/api/auth/${smsType}/changeBinding/verifyNew`, data); +export const _getNewSmsCodeRequest = + (request: AxiosInstance) => + (smsType: SmsType) => + (data: { id: string; cfToken?: string; uid: string }) => + request.post>( + `/api/auth/${smsType}/changeBinding/newSms`, + data + ); + +export const _oauthProviderSignIn = + (request: AxiosInstance) => + (provider: ProviderType) => + (data: { code: string; semData?: SemData; adClickData?: AdClickData }) => + request.post< + typeof data, + ApiResp< + | { + token: string; + realUser: { + realUserUid: string; + }; + needInit: boolean; + } + | { error: string } + > + >(`/api/auth/oauth/${provider.toLocaleLowerCase()}`, data); +export const _oauthProviderBind = + (request: AxiosInstance) => (provider: ProviderType) => (data: { code: string }) => + request.post< + typeof data, + ApiResp<{ code: string | null | undefined }, ValueOf> + >(`/api/auth/oauth/${provider.toLocaleLowerCase()}/bind`, data); +export const _oauthProviderUnbind = + (request: AxiosInstance) => (provider: ProviderType) => (data: { code: string }) => + request.post>>( + `/api/auth/oauth/${provider.toLocaleLowerCase()}/unbind`, + data + ); + +export const _mergeUser = + (request: AxiosInstance) => (data: { code: string; providerType: ProviderType }) => + request.post>('/api/auth/mergeUser', data); + +export const _deleteUser = (request: AxiosInstance) => () => + request.post>('/api/auth/delete'); +export const _checkRemainResource = (request: AxiosInstance) => () => + request< + never, + ApiResp<{ regionResourceList: RegionResourceType[]; code: string }>, + RESOURCE_STATUS + >('/api/auth/delete/checkAllResource'); + +export const _forceDeleteUser = (request: AxiosInstance) => (data: { code: string }) => + request.post>( + '/api/auth/delete/force', + data + ); + +export const _getDeleteUserStatus = (request: AxiosInstance) => (deleteId: string) => + request>('/api/auth/delete/status', { + method: 'GET', + params: { deleteId } + }); + +export const _faceAuthGenerateQRcodeUriRequest = (request: AxiosInstance) => () => + request.get>( + '/api/account/generateRealNameQRcodeUri' + ); + +export const _refreshRealNameQRecodeUriRequest = (request: AxiosInstance) => () => + request.post>('/api/account/refreshRealNameQRecodeUri'); + +export const _getFaceAuthStatusRequest = (request: AxiosInstance) => (data: { bizToken: string }) => + request.post>( + '/api/account/getFaceAuthStatus', + data + ); + +export const _enterpriseRealNameAuthPaymentRequest = + (request: AxiosInstance) => + (data: { + key: string; + accountBank: string; + accountNo: string; + keyName: string; + usrName: string; + contactInfo: string; + }) => { + return request.post>( + '/api/account/enterpriseRealName', + data + ); + }; + +export const _enterpriseRealNameAuthVerifyRequest = + (request: AxiosInstance) => (data: { transAmt: string }) => { + return request.post< + typeof data, + ApiResp<{ authState: 'success' | 'failed'; enterpriseRealName: string }> + >('/api/account/enterpriseRealNameVerify', data); + }; + +export const _enterpriseRealNameAuthInfoRequest = (request: AxiosInstance) => () => { + return request.get>('/api/account/enterpriseRealName'); +}; + +export const _enterpriseRealNameAuthCancelRequest = (request: AxiosInstance) => () => { + return request.patch>( + '/api/account/enterpriseRealName' + ); +}; + +export const _getBanksListRequest = (request: AxiosInstance) => () => + request>>('/api/account/enterpriseRealName?type=banks'); + +export const _getAmount = (request: AxiosInstance) => () => + request>('/api/account/getAmount'); +export const _verifyToken = (request: AxiosInstance) => () => + request>('/api/auth/verify'); +export const _verifySharedToken = (request: AxiosInstance) => () => + request.get>('/api/auth/verifySharedToken'); + +export const _EmailSignIn = (request: AxiosInstance) => (data: ILoginParams) => + request.post>('/api/auth/email', data); + +export const _EmailSignUp = (request: AxiosInstance) => (data: IRegisterParams) => + request.post>('/api/auth/email/signUp', data); +export const _EmailSignUpCheck = (request: AxiosInstance) => (data: IEmailCheckParams) => + request.post>('/api/auth/email/signUp/check', data); + +export const _initRegionToken = (request: AxiosInstance) => (data: InitRegionTokenParams) => + request.post>( + '/api/auth/initRegionToken', + data + ); + +export const _autoInitRegionToken = (request: AxiosInstance) => () => + request.post>( + '/api/auth/autoInitRegionToken' + ); + +export const EmailSignIn = _EmailSignIn(request); +export const EmailSignUp = _EmailSignUp(request); +export const EmailSignUpCheck = _EmailSignUpCheck(request); +export const initRegionToken = _initRegionToken(request); +export const autoInitRegionToken = _autoInitRegionToken(request); + +export const passwordLoginRequest = _passwordLoginRequest(request); + +export const passwordModifyRequest = _passwordModifyRequest(request); +export const UserInfo = _UserInfo(request); +export const verifyToken = _verifyToken(request); +export const verifySharedTokenRequest = _verifySharedToken(request); +export const regionList = _regionList(request); +export const refreshRealNameQRecodeUriRequest = _refreshRealNameQRecodeUriRequest(request); +export const getSmsBindCodeRequest = _getSmsBindCodeRequest(request); +export const verifySmsBindRequest = _verifySmsBindRequest(request); +export const getSmsUnbindCodeRequest = _getSmsUnbindCodeRequest(request); +export const verifySmsUnbindRequest = _verifySmsUnbindRequest(request); +export const getOldSmsCodeRequest = _getOldSmsCodeRequest(request); +export const verifyOldSmsRequest = _verifyOldSmsRequest(request); +export const getNewSmsCodeRequest = _getNewSmsCodeRequest(request); +export const verifyNewSmsRequest = _verifyNewSmsRequest(request); +export const bindRequest = _oauthProviderBind(request); +export const unBindRequest = _oauthProviderUnbind(request); +export const signInRequest = _oauthProviderSignIn(request); +export const mergeUserRequest = _mergeUser(request); +export const deleteUserRequest = _deleteUser(request); +export const checkRemainResource = _checkRemainResource(request); +export const forceDeleteUser = _forceDeleteUser(request); +export const getDeleteUserStatus = _getDeleteUserStatus(request); +export const enterpriseRealNameAuthPaymentRequest = _enterpriseRealNameAuthPaymentRequest(request); +export const enterpriseRealNameAuthVerifyRequest = _enterpriseRealNameAuthVerifyRequest(request); +export const enterpriseRealNameAuthInfoRequest = _enterpriseRealNameAuthInfoRequest(request); +export const enterpriseRealNameAuthCancelRequest = _enterpriseRealNameAuthCancelRequest(request); + +export const faceAuthGenerateQRcodeUriRequest = _faceAuthGenerateQRcodeUriRequest(request); +export const getFaceAuthStatusRequest = _getFaceAuthStatusRequest(request); +export const getBanksListRequest = _getBanksListRequest(request); + +export const getAmount = _getAmount(request); + +export const getPlanInfo = (workspace: string) => + request('/api/plan/info', { + method: 'POST', + data: { workspace } + }); + +export const getWorkspacesPlans = (workspaces: string[]) => + request('/api/plan/workspaces-plans', { + method: 'POST', + data: { + workspaces + } + }); + +export const _rotateKubeconfig = (request: AxiosInstance) => () => + request.post>('/api/auth/rotateKubeconfig'); + +export const rotateKubeconfig = _rotateKubeconfig(request); + +const buildOauth2AuthHeaders = (authToken?: string): HeadersInit => + authToken ? { Authorization: encodeURIComponent(authToken) } : {}; + +export const oauth2AuthorizeContext = async ( + params: { user_code?: string; request_id?: string }, + authToken?: string +): Promise => { + const query = new URLSearchParams(); + if (params.user_code) query.set('user_code', params.user_code); + if (params.request_id) query.set('request_id', params.request_id); + const response = await fetch(`/api/auth/oauth2/authorize/context?${query.toString()}`, { + method: 'GET', + credentials: 'include', + headers: buildOauth2AuthHeaders(authToken) + }); + const data = await response.json(); + if (!response.ok) { + throw data; + } + return data as OAuth2AuthorizeContextResponse; +}; + +export const oauth2AuthorizeDecision = async ( + data: { request_id: string; decision: 'approve' | 'deny' }, + authToken?: string +): Promise<{ status: 'approved' | 'denied' }> => { + const response = await fetch('/api/auth/oauth2/authorize/decision', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...buildOauth2AuthHeaders(authToken) + }, + body: JSON.stringify(data) + }); + const payload = await response.json(); + if (!response.ok) { + throw payload; + } + return payload as { status: 'approved' | 'denied' }; +}; diff --git a/frontend/desktop/src/api/namespace.ts b/frontend/desktop/src/api/namespace.ts new file mode 100644 index 000000000000..0254f2677c11 --- /dev/null +++ b/frontend/desktop/src/api/namespace.ts @@ -0,0 +1,104 @@ +import request from '@/services/request'; +import { ApiResp } from '@/types'; +import { NamespaceDto, UserRole, teamMessageDto } from '@/types/team'; +import { TeamUserDto } from '@/types/user'; +import { AxiosInstance } from 'axios'; + +export const _abdicateRequest = + (request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) => + request.post>('/api/auth/namespace/abdicate', data); + +export const _createRequest = + (request: AxiosInstance) => + ({ teamName }: Record<'teamName', string>) => + request.post>('/api/auth/namespace/create', { + teamName + }); + +export const _deleteTeamRequest = + (request: AxiosInstance) => + ({ ns_uid }: { ns_uid: string }) => + request.post>('/api/auth/namespace/delete', { + ns_uid + }); +export const _inviteMemberRequest = + (request: AxiosInstance) => (props: { ns_uid: string; targetUserId: string; role: UserRole }) => + request.post>('/api/auth/namespace/invite', props); + +export const _setAliasRequest = + (request: AxiosInstance) => + (props: { ns_uid: string; targetUserCrUid: string; alias: string | null }) => + request.post>('/api/auth/namespace/setAlias', props); + +export const _modifyRoleRequest = + (request: AxiosInstance) => + (props: { ns_uid: string; targetUserCrUid: string; tRole: UserRole }) => + request.post>('/api/auth/namespace/modifyRole', props); + +export const _nsListRequest = (request: AxiosInstance) => () => + request>('/api/auth/namespace/list'); + +export enum reciveAction { + Accepte = 'accept', + Reject = 'reject' +} + +type verifyParam = { ns_uid: string; action: reciveAction }; +type verifyCodeParam = { code: string; action: reciveAction }; +export const _verifyInviteRequest = (request: AxiosInstance) => (data: verifyParam) => + request.post>('/api/auth/namespace/verifyInvite', data); +export const _verifyInviteCodeRequest = (request: AxiosInstance) => (data: verifyCodeParam) => + request.post>( + '/api/auth/namespace/verifyInviteCode', + data + ); +export const _getInviteCodeRequest = + (request: AxiosInstance) => (data: { ns_uid: string; role: UserRole }) => + request.post>( + '/api/auth/namespace/getInviteCode', + data + ); +export const _getInviteCodeInfoRequest = (request: AxiosInstance) => (data: { code: string }) => + request.post< + typeof data, + ApiResp<{ + workspace: string; + role: UserRole; + inviterNickname: string; + }> + >('/api/auth/namespace/getInviteCodeInfo', data); +export const _removeMemberRequest = + (request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) => + request.post>('/api/auth/namespace/removeUser', data); + +export const _teamDetailsRequest = (request: AxiosInstance) => (ns_uid: string) => + request.post>( + '/api/auth/namespace/details', + { ns_uid } + ); +export const _reciveMessageRequest = (request: AxiosInstance) => () => + request.post>('/api/auth/namespace/recive'); +export const _switchRequest = (request: AxiosInstance) => (ns_uid: string) => + request.post>('/api/auth/namespace/switch', { + ns_uid + }); +export const _renameRequest = + (request: AxiosInstance) => (data: { ns_uid: string; teamName: string }) => + request.post>('/api/auth/namespace/rename', data); +// for prod/dev +export const abdicateRequest = _abdicateRequest(request); +export const createRequest = _createRequest(request); +export const deleteTeamRequest = _deleteTeamRequest(request); +export const inviteMemberRequest = _inviteMemberRequest(request); +export const modifyRoleRequest = _modifyRoleRequest(request); +export const setAliasRequest = _setAliasRequest(request); +export const nsListRequest = _nsListRequest(request); +export const verifyInviteRequest = _verifyInviteRequest(request); +export const removeMemberRequest = _removeMemberRequest(request); +export const teamDetailsRequest = _teamDetailsRequest(request); +export const reciveMessageRequest = _reciveMessageRequest(request); +export const switchRequest = _switchRequest(request); +export const getInviteCodeRequest = _getInviteCodeRequest(request); +export const getInviteCodeInfoRequest = _getInviteCodeInfoRequest(request); +export const verifyInviteCodeRequest = _verifyInviteCodeRequest(request); +export const renameRequest = _renameRequest(request); diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts new file mode 100644 index 000000000000..f0db3fe15249 --- /dev/null +++ b/frontend/desktop/src/api/platform.ts @@ -0,0 +1,164 @@ +import request from '@/services/request'; +import { + ApiResp, + LayoutConfigType, + CloudConfigType, + AuthClientConfigType, + AppClientConfigType, + CommonClientConfigType, + TNotification, + VerificationChallenge +} from '@/types'; +import { AdClickData } from '@/types/adClick'; +import { LicenseCheckResponse } from '@/types/license'; +import { UserTask } from '@/types/task'; +import { WorkspaceQuotaResponse } from '@/types/workspace'; +import { + CreateAlertRequest, + CreateAlertResponse, + ListAlertsResponse, + ToggleAlertsRequest, + ToggleAlertsResponse, + DeleteAlertsRequest, + DeleteAlertsResponse +} from '@/types/alert'; + +/** + * Upload advertisement conversion data to the platform. + * @param data - The ad click data to be uploaded. + */ +export const uploadConvertData = (data: AdClickData) => { + const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`; + + return request.post(`${baseurl}/api/platform/uploadData`, { + data + }); +}; + +export const getUserTasks = () => { + return request.get('/api/account/getTasks'); +}; + +export const checkUserTask = () => { + return request.get('/api/account/checkTask'); +}; + +export const updateTask = (taskId: string) => { + return request.post('/api/account/updateTask', { taskId }); +}; + +export const getAppConfig = () => { + return request.get('/api/platform/getAppConfig'); +}; + +export const getCloudConfig = () => { + return request.get('/api/platform/getCloudConfig'); +}; + +export const getCommonConfig = () => { + return request.get('/api/platform/getCommonConfig'); +}; + +export const getLayoutConfig = () => { + return request.get('/api/platform/getLayoutConfig'); +}; + +export const getAuthConfig = () => { + return request.get('/api/platform/getAuthConfig'); +}; + +export const getPriceBonus = () => { + return request.get< + any, + ApiResp<{ + steps: string; + ratios: string; + activities: string; + }> + >('/api/price/bonus'); +}; + +export const getWechatQR = () => + request.get>( + '/api/auth/publicWechat/getWechatQR' + ); + +export const getWechatResult = (payload: { code: string }) => + request.get>('/api/auth/publicWechat/getWechatResult', { + params: payload + }); + +export const getGlobalNotification = () => { + return request.get>('/api/notification/global'); +}; + +export const listNotification = () => + request.get>('/api/notification/listNotification'); + +export const getResource = () => { + return request.get< + any, + ApiResp<{ + totalCpu: string; + totalMemory: string; + totalStorage: string; + runningPodCount: string; + totalGpuCount: string; + totalPodCount: string; + }> + >('/api/desktop/getResource'); +}; + +export const getUserBilling = () => { + return request.post< + any, + ApiResp<{ + prevMonthTime: number; + prevDayTime: number; + }> + >('/api/desktop/getBilling'); +}; + +export const getRunningApps = () => { + return request.post< + any, + ApiResp<{ + runningCount: { + devbox: number; + database: number; + applaunchpad: number; + }; + }> + >('/api/desktop/getRunningApps'); +}; + +export const getWorkspaceQuota = () => + request.get>('/api/workspace/getQuota'); + +export const checkLicense = () => + request.get>('/api/license/check'); + +export const createAlert = (data: CreateAlertRequest) => + request.post>('/api/account/alerts/createAlert', data); + +export const listAlerts = () => + request.post>('/api/account/alerts/listAlerts', {}); + +export const updateAlerts = (data: ToggleAlertsRequest) => + request.post>('/api/account/alerts/updateAlerts', data); + +export const deleteAlerts = (data: DeleteAlertsRequest) => + request.post>('/api/account/alerts/deleteAlerts', data); + +export const sendAlertBindEmailCode = (data: { id: string }) => + request.post>('/api/account/alerts/bind/email/sms', data); + +export const sendAlertBindPhoneCode = (data: { id: string }) => + request.post>('/api/account/alerts/bind/phone/sms', data); + +export const createTemplateInstance = (data: { + templateName: string; + templateForm: Record; +}) => { + return request.post>('/api/platform/createTemplateInstance', data); +}; diff --git a/frontend/desktop/src/components/AppDock/CustomTooltip.tsx b/frontend/desktop/src/components/AppDock/CustomTooltip.tsx new file mode 100644 index 000000000000..21b63ae77c5f --- /dev/null +++ b/frontend/desktop/src/components/AppDock/CustomTooltip.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Tooltip, TooltipProps } from '@chakra-ui/react'; + +type ArrowDirection = 'top' | 'bottom' | 'left' | 'right'; + +interface CustomTooltipProps extends Omit { + placement: ArrowDirection; + children: React.ReactNode; +} + +const CustomTooltip: React.FC = ({ placement = 'top', children, ...props }) => { + const getArrowStyles = (placement: ArrowDirection) => { + switch (placement) { + case 'top': + return { + bottom: '-16px', + left: '50%', + transform: 'translateX(-50%)', + borderColor: 'rgba(239, 239, 242, 0.7) transparent transparent transparent' + }; + case 'bottom': + return { + top: '-16px', + left: '50%', + transform: 'translateX(-50%)', + borderColor: 'transparent transparent rgba(239, 239, 242, 0.7) transparent' + }; + case 'left': + return { + right: '-16px', + top: '50%', + transform: 'translateY(-50%)', + borderColor: 'transparent transparent transparent rgba(239, 239, 242, 0.7)' + }; + case 'right': + return { + left: '-16px', + top: '50%', + transform: 'translateY(-50%)', + borderColor: 'transparent rgba(239, 239, 242, 0.7) transparent transparent' + }; + } + }; + + return ( + + {children} + + ); +}; + +export default CustomTooltip; diff --git a/frontend/desktop/src/components/AppDock/index.module.css b/frontend/desktop/src/components/AppDock/index.module.css new file mode 100644 index 000000000000..d33f3a385f00 --- /dev/null +++ b/frontend/desktop/src/components/AppDock/index.module.css @@ -0,0 +1,24 @@ +.contexify { + background: rgba(239, 239, 242, 0.7); + backdrop-filter: blur(80px) saturate(150%); + color: #111824; + padding: 6px; + font-size: 12px; + top: 0; + left: 0; + min-width: 140px; + position: absolute; +} + +.arrow { + position: absolute; + top: 100%; + left: 46%; + width: 0; + height: 0; + width: 16px; + height: 9px; + background: rgba(239, 239, 242, 0.7); + backdrop-filter: blur(80px) saturate(150%); + clip-path: polygon(50% 100%, 0 0, 100% 0); +} diff --git a/frontend/desktop/src/components/AppDock/index.tsx b/frontend/desktop/src/components/AppDock/index.tsx new file mode 100644 index 000000000000..d0c4c228c7cf --- /dev/null +++ b/frontend/desktop/src/components/AppDock/index.tsx @@ -0,0 +1,418 @@ +import { MoreAppsContext } from '@/pages/index'; +import { BRAIN_APP_KEY } from '@/constants/app'; +import useAppStore, { AppInfo } from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import { useLicenseCheck } from '@/hooks/useLicenseCheck'; +import useSessionStore from '@/stores/session'; +import { APPTYPE, TApp } from '@/types'; +import { I18nCommonKey } from '@/types/i18next'; +import { Box, Center, Flex, Image, useBreakpointValue } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useMessage } from '@sealos/ui'; +import { MouseEvent, useContext, useMemo, useRef, useState } from 'react'; +import { useContextMenu } from 'react-contexify'; +import { ChevronDownIcon } from '../icons'; +import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from 'framer-motion'; + +const APP_DOCK_MENU_ID = 'APP_DOCK_MENU_ID'; + +const MotionFlex = motion(Flex); +const MotionCenter = motion(Center); +const MotionBox = motion(Box); + +function StaticIcon({ item, logo, t, i18n, handleNavItem }: any) { + const [staticHovered, setStaticHovered] = useState(false); + + return ( + handleNavItem(e, item)} + position="relative" + onMouseEnter={() => setStaticHovered(true)} + onMouseLeave={() => setStaticHovered(false)} + > +
+ {item?.name} +
+ + + {staticHovered && ( + + {item?.i18n?.[i18n?.language]?.name + ? item?.i18n?.[i18n?.language]?.name + : t(item?.name as I18nCommonKey)} + + )} +
+ ); +} + +function AnimatedIcon({ item, logo, t, i18n, handleNavItem, mouseX }: any) { + const ref = useRef(null); + const [hovered, setHovered] = useState(false); + + const distance = useTransform(mouseX, (val: number) => { + const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; + return val - bounds.x - bounds.width / 2; + }); + + const widthTransform = useTransform(distance, [-150, 0, 150], [54, 70, 54]); + const heightTransform = useTransform(distance, [-150, 0, 150], [54, 70, 54]); + + const width = useSpring(widthTransform, { + mass: 0.1, + stiffness: 150, + damping: 12 + }); + + const height = useSpring(heightTransform, { + mass: 0.1, + stiffness: 150, + damping: 12 + }); + + return ( + handleNavItem(e, item)} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + {item?.name} + + + + {hovered && ( + + {item?.i18n?.[i18n?.language]?.name + ? item?.i18n?.[i18n?.language]?.name + : t(item?.name as I18nCommonKey)} + + )} + + + ); +} + +export default function AppDock() { + const { t, i18n } = useTranslation(); + const { message } = useMessage(); + const { + installedApps: apps, + runningInfo, + currentAppPid, + openApp, + switchAppById, + findAppInfoById, + updateOpenedAppInfo + } = useAppStore(); + const { layoutConfig, commonConfig } = useConfigStore(); + const logo = layoutConfig?.logo; + const { isUserLogin } = useSessionStore(); + const moreAppsContent = useContext(MoreAppsContext); + const { isNavbarVisible, toggleNavbarVisibility, getTransitionValue } = useDesktopConfigStore(); + const { hasLicense } = useLicenseCheck({ + enabled: isUserLogin() && !!commonConfig?.licenseCheckEnabled + }); + const [isMouseOverDock, setIsMouseOverDock] = useState(false); + const timeoutRef = useRef(null); + const isSmallScreen = useBreakpointValue({ base: true, sm: false }); + const mouseX = useMotionValue(Infinity); + const { isGuest, openGuestLoginModal } = useSessionStore(); + const { show } = useContextMenu({ + id: APP_DOCK_MENU_ID + }); + const { toggleShape } = useDesktopConfigStore(); + const normalApps = apps.filter((item: TApp) => item?.displayType === 'normal'); + + const AppMenuLists = useMemo(() => { + const initialApps: TApp[] = [ + { + name: 'home', + icon: '/icons/my-home.svg', + zIndex: 99999, + isShow: false, + pid: -9, + size: 'maxmin', + cacheSize: 'maxmin', + style: {}, + mouseDowning: false, + key: `system-sealos-home`, + type: APPTYPE.IFRAME, + data: { + url: '', + desc: '' + }, + representativeMeta: { + forcedIconStyle: 'fill' + }, + displayType: 'hidden' + }, + ...normalApps.slice(0, 5).map((app, index) => ({ ...app, pid: -2 })) + ]; + + const mergedApps = initialApps.map((app) => { + const runningApp = runningInfo.find((running) => running.key === app.key); + return runningApp ? { ...app, ...runningApp } : app; + }); + + return [ + ...mergedApps, + ...runningInfo.filter((running) => !initialApps.some((app) => app.key === running.key)) + ]; + }, [normalApps, runningInfo]); + + // Handle icon click event + const handleNavItem = (e: MouseEvent, item: AppInfo) => { + if (isGuest()) { + e.preventDefault(); + openGuestLoginModal(); + return; + } + + if (item.key === 'system-sealos-home') { + const isNotMinimized = runningInfo.some((item) => item.size !== 'minimize'); + runningInfo.forEach((item) => { + updateOpenedAppInfo({ + ...item, + size: isNotMinimized ? 'minimize' : item.cacheSize + }); + }); + return; + } + + if (item.key === 'system-sealos-apps') { + moreAppsContent?.setShowMoreApps(true); + return; + } + + if (commonConfig?.licenseCheckEnabled && hasLicense === false && item.key !== 'user-license') { + message({ + title: t('license_required'), + status: 'warning', + isClosable: true + }); + const licenseApp = apps.find((app) => app.key === 'user-license'); + if (licenseApp) { + openApp(licenseApp); + } + return; + } + + if (item.pid === currentAppPid && item.size !== 'minimize') { + updateOpenedAppInfo({ + ...item, + size: 'minimize', + cacheSize: item.size + }); + } else { + const app = findAppInfoById(item.pid); + if (!app) { + openApp(item); + } else { + switchAppById(item.pid); + } + } + }; + + const handleMouseEnter = () => { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setIsMouseOverDock(true); + }; + + const handleMouseLeave = () => { + mouseX.set(Infinity); + + timeoutRef.current = window.setTimeout(() => { + setIsMouseOverDock(false); + }, 500); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isSmallScreen) { + mouseX.set(e.clientX); + } + }; + + // Desktop Dock Suppression: while Brain is the topmost non-minimized window, + // the dock (toggle pill included) must not overlay it. It comes back as soon + // as Brain loses the top layer, is minimized, or is closed. + const topApp = runningInfo.find((item) => item.pid === currentAppPid); + if (topApp?.key === BRAIN_APP_KEY && topApp.size !== 'minimize') { + return null; + } + + return ( + +
{ + toggleNavbarVisibility(); + }} + > + +
+ + + {AppMenuLists.map((item: AppInfo, index: number) => + isSmallScreen ? ( + + ) : ( + + ) + )} + +
+ ); +} diff --git a/frontend/desktop/src/components/LangSelect/index.tsx b/frontend/desktop/src/components/LangSelect/index.tsx new file mode 100644 index 000000000000..e5f45b1da1f2 --- /dev/null +++ b/frontend/desktop/src/components/LangSelect/index.tsx @@ -0,0 +1,76 @@ +import { setCookie } from '@/utils/cookieUtils'; +import { + Button, + Flex, + FlexProps, + Menu, + MenuButton, + MenuButtonProps, + MenuItem, + MenuList, + Text +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { EVENT_NAME } from 'sealos-desktop-sdk'; +import { masterApp } from 'sealos-desktop-sdk/master'; +import { ExpanMoreIcon } from '../../../../packages/ui'; +import { ROLE_LIST } from '@/types/team'; +import { router } from 'next/client'; + +export default function LangSelectList(props: MenuButtonProps) { + const { t, i18n } = useTranslation(); + const langkv = { + en: 'English', + zh: '中文' + } as const; + const langList = [ + ['en', 'English'], + ['zh', '中文'] + ] as const; + return ( + + } + {...props} + > + {langkv[i18n.language as 'en' | 'zh']} + + + {langList.map(([lngKey, lngVal], idx) => ( + { + e.preventDefault(); + masterApp?.sendMessageToAll({ + apiName: 'event-bus', + eventName: EVENT_NAME.CHANGE_I18N, + data: { + currentLanguage: lngKey + } + }); + setCookie('NEXT_LOCALE', lngKey, { + expires: 30, + sameSite: 'None', + secure: true + }); + i18n?.changeLanguage(lngKey); + }} + key={lngKey} + > + {lngVal} + + ))} + + + ); +} diff --git a/frontend/desktop/src/components/LangSelect/simple.tsx b/frontend/desktop/src/components/LangSelect/simple.tsx new file mode 100644 index 000000000000..b366ff2b8b22 --- /dev/null +++ b/frontend/desktop/src/components/LangSelect/simple.tsx @@ -0,0 +1,32 @@ +import { Flex, FlexProps } from '@chakra-ui/react'; +import { useLanguageSwitcher } from '@/hooks/useLanguageSwitcher'; + +export default function LangSelectSimple(props: FlexProps) { + const { currentLanguage, switchLanguage } = useLanguageSwitcher(); + + return ( + switchLanguage(currentLanguage === 'en' ? 'zh' : 'en')} + > + {currentLanguage === 'en' ? 'En' : '中'} + + ); +} diff --git a/frontend/desktop/src/components/LoginModal/index.tsx b/frontend/desktop/src/components/LoginModal/index.tsx new file mode 100644 index 000000000000..23c6ca69e461 --- /dev/null +++ b/frontend/desktop/src/components/LoginModal/index.tsx @@ -0,0 +1,20 @@ +import { Modal, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react'; + +import SigninComponent from '../v2/Sign'; + +interface LoginModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function LoginModal({ isOpen, onClose }: LoginModalProps) { + return ( + + + + + + + + ); +} diff --git a/frontend/desktop/src/components/SecondaryLinks/index.tsx b/frontend/desktop/src/components/SecondaryLinks/index.tsx new file mode 100644 index 000000000000..2b31bbef4201 --- /dev/null +++ b/frontend/desktop/src/components/SecondaryLinks/index.tsx @@ -0,0 +1,398 @@ +import { useConfigStore } from '@/stores/config'; +import useAppStore from '@/stores/app'; +import { useGuideModalStore } from '@/stores/guideModal'; +import { + Center, + Divider, + Flex, + Text, + useBreakpointValue, + Menu, + MenuButton, + MenuList, + MenuItem +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo, useEffect } from 'react'; +import { getAmount } from '@/api/auth'; +import Decimal from 'decimal.js'; +import { CurrencySymbol } from '@sealos/ui'; +import { formatMoney } from '@/utils/format'; +import useSessionStore from '@/stores/session'; +import { useSubscriptionStore } from '@/stores/subscription'; +import { JoinDiscordPrompt } from '../account/JoinDiscordPrompt'; +import { MoreHorizontal, Sparkles } from 'lucide-react'; +import { BalancePopover, getPlanBackground } from '@/components/account/BalancePopover'; +import { cn } from '@sealos/shadcn-ui'; + +const baseItemStyle = { + minW: '36px', + h: '40px', + fontSize: '14px', + fontWeight: '500', + color: 'primary', + _hover: { + background: 'secondary' + } +}; + +export default function SecondaryLinks() { + const { layoutConfig, commonConfig } = useConfigStore(); + const { t } = useTranslation(); + const { openGuideModal, setInitGuide } = useGuideModalStore(); + const { openDesktopApp } = useAppStore(); + const { session } = useSessionStore(); + const currencySymbol = useConfigStore( + (state) => state.layoutConfig?.currencySymbol || 'shellCoin' + ); + const workspace = session?.user?.nsid || ''; + + const user = session?.user; + + const isCollapsed = useBreakpointValue({ + base: true, + lg: false + }); + + const openWorkOrderApp = () => { + openDesktopApp({ + appKey: 'system-workorder', + pathname: '/' + }); + }; + + const openCostCenterApp = (mode?: 'create' | 'upgrade' | 'topup') => { + if (!mode) { + openDesktopApp({ + appKey: 'system-costcenter', + pathname: '/' + }); + } else { + openDesktopApp({ + appKey: 'system-costcenter', + pathname: '/', + query: { + mode: mode + }, + messageData: { + type: 'InternalAppCall', + mode: mode + } + }); + } + }; + + const { data } = useQuery({ + queryKey: ['getAmount', { userId: user?.userCrUid }], + queryFn: getAmount, + enabled: !!user, + staleTime: 60 * 1000 + }); + + const { subscriptionInfo, fetchSubscriptionInfo } = useSubscriptionStore(); + const subscription = subscriptionInfo?.subscription; + const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free'; + const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan; + const isDebt = subscription?.Status?.toLowerCase() === 'debt'; + const canManagePayment = subscription?.type === 'PAYG' || subscription?.role === 'OWNER'; + + useEffect(() => { + if (workspace) { + fetchSubscriptionInfo(workspace); + } + }, [workspace, fetchSubscriptionInfo]); + + const balance = useMemo(() => { + let realBalance = new Decimal(data?.data?.balance || 0); + if (data?.data?.deductionBalance) { + realBalance = realBalance.minus(new Decimal(data.data.deductionBalance)); + } + return realBalance.toNumber(); + }, [data]); + + const handleGuideClick = () => { + if (!commonConfig?.guideEnabled) return; + openGuideModal(); + setInitGuide(false); + }; + + const handleDocsClick = () => { + if (layoutConfig?.common.docsUrl) { + window.open(layoutConfig.common.docsUrl); + } + }; + + if (!isCollapsed) { + return ( + + + layoutConfig?.common.subscriptionEnabled && canManagePayment + ? openCostCenterApp('upgrade') + : openCostCenterApp() + } + openCostCenterTopup={() => openCostCenterApp('topup')} + > +
+ layoutConfig?.common.subscriptionEnabled && canManagePayment + ? openCostCenterApp('upgrade') + : openCostCenterApp() + } + > + {subscription?.type === 'PAYG' ? ( +
+
+ + {!layoutConfig?.common.subscriptionEnabled && ( + <> + {t('common:nav_links.balance')} + + + )} + +
+ +
+ {formatMoney(balance).toFixed(2)} + + {layoutConfig?.common.subscriptionEnabled && ( + <> + + {t('common:nav_links.subscribe')} + + + )} +
+ ) : ( + <> +
+ + {subscription?.PlanName || 'Free'} {t('common:nav_links.plan_suffix')} + + {isDebt && ( +
+ {t('common:nav_links.plan_expired')} +
+ )} + {isCancelled && ( + + {t('common:nav_links.cancelled')} + + )} + + + + + {isCancelled ? t('common:nav_links.renew') : t('common:nav_links.upgrade_plan')} + + + + + )} +
+
+ + {commonConfig?.guideEnabled ? ( +
+ {t('common:guide')} +
+ ) : null} + + {layoutConfig?.common.docsUrl && ( +
+ {t('common:doc')} +
+ )} + + {layoutConfig?.common.workorderEnabled && ( +
+ {t('v2:ticket')} +
+ )} + + {layoutConfig?.discordInviteLink && ( + +
+ {t('v2:support')} +
+
+ )} +
+ ); + } + + return ( + + + + + + openCostCenterApp()} + > + + {t('common:nav_links.balance')} + + + {formatMoney(balance).toFixed(2)} + + + + + + {/* // [TODO] Guide is currently not compatible with narrow screen. */} + {/* + {t('common:guide')} + */} + + {layoutConfig?.common.docsUrl && ( + + {t('common:doc')} + + )} + + {layoutConfig?.common.workorderEnabled && ( + + {t('v2:ticket')} + + )} + + {layoutConfig?.discordInviteLink && ( + + + {t('v2:support')} + + + )} + + + ); +} diff --git a/frontend/desktop/src/components/ThemeToggle/index.tsx b/frontend/desktop/src/components/ThemeToggle/index.tsx new file mode 100644 index 000000000000..875fe8811a3f --- /dev/null +++ b/frontend/desktop/src/components/ThemeToggle/index.tsx @@ -0,0 +1,56 @@ +import { useState, useEffect } from 'react'; +import { useColorMode, IconButton } from '@chakra-ui/react'; +import { Sun, Moon } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { useAppDisplayConfigStore } from '@/stores/appDisplayConfig'; + +const MotionIconButton = motion(IconButton); + +export const ThemeToggle = () => { + const { colorMode, toggleColorMode } = useColorMode(); + const [isRotating, setIsRotating] = useState(false); + const { updateBackgroundImage } = useAppDisplayConfigStore(); + + useEffect(() => { + if (colorMode === 'light') { + updateBackgroundImage('/images/bg-light.svg'); + } else { + updateBackgroundImage('/images/bg-dark.svg'); + } + }, [colorMode, updateBackgroundImage]); + + const handleToggle = () => { + setIsRotating(true); + toggleColorMode(); + setTimeout(() => setIsRotating(false), 700); + }; + + return ( + : } + onClick={handleToggle} + variant="ghost" + borderRadius="full" + size="md" + animate={{ + rotate: isRotating ? [0, 360] : 0, + scale: isRotating ? [1, 0.8, 1.1, 1] : 1 + }} + transition={{ + duration: 0.7, + ease: 'easeInOut' + }} + _hover={{ + transform: 'scale(1.05)', + transition: 'transform 0.2s ease', + bg: 'muted' + }} + _active={{ + transform: 'scale(0.95)', + bg: 'secondary' + }} + color="foreground" + /> + ); +}; diff --git a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx new file mode 100644 index 000000000000..48b388b8955d --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx @@ -0,0 +1,152 @@ +import { useConfigStore } from '@/stores/config'; +import useSessionStore, { OauthAction } from '@/stores/session'; +import { OauthProvider } from '@/types/user'; +import { Center, Image, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import router from 'next/router'; +import { useMemo } from 'react'; +import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; + +export function AuthModifyList({ + isOnlyOne, + GITHUBIsBinding, + WECHATIsBinding, + GOOGLEIsBinding, + avatarUrl +}: { + isOnlyOne: boolean; + avatarUrl: string; + GOOGLEIsBinding: boolean; + GITHUBIsBinding: boolean; + WECHATIsBinding: boolean; +}) { + const { authConfig: conf, layoutConfig } = useConfigStore(); + const { setProvider, generateState } = useSessionStore(); + const { t } = useTranslation(); + const authActionList: { + title: string; + actionCb: (actino: OauthAction) => void; + isBinding: boolean; + }[] = useMemo(() => { + if (!conf) return []; + const actionCbGen = + ({ + url, + provider, + clientId, + proxyAddress + }: { + url: string; + provider: OauthProvider; + clientId: string; + proxyAddress?: string; + }) => + (action: T) => { + const state = generateState(action); + setProvider(provider); + if (proxyAddress) { + const target = new URL(proxyAddress); + const callback = new URL(conf.callbackURL); + target.searchParams.append( + 'oauthProxyState', + encodeURIComponent(callback.toString()) + '_' + state + ); + target.searchParams.append('oauthProxyClientID', clientId); + target.searchParams.append('oauthProxyProvider', provider); + router.replace(target.toString()); + } else { + const target = new URL(url); + target.searchParams.append('state', state); + router.replace(target); + } + }; + const result = []; + if (conf.idp.github.enabled) + result.push({ + title: t('common:github'), + isBinding: GITHUBIsBinding, + actionCb(action: Exclude) { + const githubConf = conf.idp.github; + return actionCbGen({ + provider: 'GITHUB', + clientId: githubConf.clientID, + proxyAddress: githubConf?.proxyAddress, + url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user` + })(action); + } + }); + + if (conf.idp.wechat.enabled) + result.push({ + title: t('common:wechat'), + isBinding: WECHATIsBinding, + actionCb(action: Exclude) { + const wechatConf = conf.idp.wechat; + return actionCbGen({ + provider: 'WECHAT', + clientId: wechatConf.clientID, + proxyAddress: wechatConf?.proxyAddress, + url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&scope=snsapi_login&#wechat_redirect` + })(action); + } + }); + if (conf.idp.google.enabled) + result.push({ + title: t('common:google'), + isBinding: GOOGLEIsBinding, + actionCb(action: Exclude) { + const googleConf = conf.idp.google; + const scope = encodeURIComponent( + `https://www.googleapis.com/auth/userinfo.profile openid email` + ); + return actionCbGen({ + provider: 'GOOGLE', + clientId: googleConf.clientID, + proxyAddress: googleConf?.proxyAddress, + url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&scope=${scope}&include_granted_scopes=true` + })(action); + } + }); + return result; + }, [conf, t, GITHUBIsBinding, WECHATIsBinding, GOOGLEIsBinding]); + return authActionList.map((auth) => { + return ( + {auth.title}} + RightElement={ + <> + {auth.isBinding ? ( +
+ user avator +
+ ) : ( + {t('common:unbound')} + )} + {(!auth.isBinding || !isOnlyOne) && ( + { + e.preventDefault(); + auth.isBinding ? auth.actionCb('UNBIND') : auth.actionCb('BIND'); + }} + modifyBehavior={ + auth.isBinding + ? BINDING_STATE_MODIFY_BEHAVIOR.UNBINDING + : BINDING_STATE_MODIFY_BEHAVIOR.BINDING + } + /> + )} + + } + >
+ ); + }); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx b/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx new file mode 100644 index 000000000000..cffd97dc1c99 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx @@ -0,0 +1,24 @@ +import { ButtonProps, Button, forwardRef } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +export enum BINDING_STATE_MODIFY_BEHAVIOR { + BINDING, + UNBINDING, + CHANGE_BINDING +} +export const BindingModifyButton = forwardRef< + ButtonProps & { modifyBehavior: BINDING_STATE_MODIFY_BEHAVIOR }, + 'button' +>(function ChangeButton({ modifyBehavior, ...props }, ref) { + const { t } = useTranslation(); + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx b/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx new file mode 100644 index 000000000000..18b5a98db34c --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx @@ -0,0 +1,30 @@ +import { Flex, Box, forwardRef, FlexProps } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +export const ConfigItem = forwardRef< + FlexProps & { LeftElement: ReactNode; RightElement: ReactNode }, + 'div' +>(function ConfigItem({ LeftElement, RightElement, ...props }, ref) { + return ( + + + {LeftElement} + + + {RightElement} + + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx new file mode 100644 index 000000000000..c35dbb168fef --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx @@ -0,0 +1,817 @@ +import { + checkRemainResource, + deleteUserRequest, + forceDeleteUser, + getAmount, + getDeleteUserStatus, + getWorkspacesPlans +} from '@/api/auth'; +import { nsListRequest } from '@/api/namespace'; +import { RegionResourceType, ResourceType } from '@/services/backend/svc/checkResource'; +import useAppStore from '@/stores/app'; +import useSessionStore from '@/stores/session'; +import { ApiResp, ValueOf } from '@/types'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { + DELETE_USER_EXECUTION_STATUS, + DeleteUserFailure, + DeleteUserFinalStatusResponse, + DeleteUserInitiateResponse +} from '@/types/response/deleteUser'; +import { getPlanBackgroundClass } from '@/utils/styling'; +import { clearPersistedMarketingQuery } from '@/utils/marketing-attribution'; +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, + Button, + ButtonProps, + Center, + Divider, + FormControl, + HStack, + IconButton, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + useDisclosure, + VStack +} from '@chakra-ui/react'; +import { Badge } from '@sealos/shadcn-ui/badge'; +import { cn } from '@sealos/shadcn-ui'; +import { InfoCircleIcon, LinkIcon, WarnTriangeIcon } from '@sealos/ui'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + buildSubscribedWorkspaceRows, + DeleteFlowStep, + getDeleteFlowStepFromStatus +} from './DeleteAccountModal.utils'; +import { SettingInput } from './SettingInput'; +import { SettingInputGroup } from './SettingInputGroup'; + +type DeleteExecutionState = { + deleteId: string; + status: DeleteUserFinalStatusResponse['status']; + failedWorkspaces: DeleteUserFailure[]; +}; + +const appKeyList = [ + '', + 'system-dbprovider', + 'system-applaunchpad', + '', + 'system-cronjob', + '', + 'system-objectstorage', + 'system-template' +] as const; + +const generateURL = (resource: ResourceType, domain: string) => { + const appKey = appKeyList[resource.type]; + if (!appKey) return ''; + + return `https://${domain}?workspaceUid=${resource.workspace.uid}&openapp=${appKey}`; +}; + +const createDefaultDeleteExecutionState = (): DeleteExecutionState => ({ + deleteId: '', + status: DELETE_USER_EXECUTION_STATUS.PENDING, + failedWorkspaces: [] +}); + +export default function DeleteAccount(props: ButtonProps) { + const { onOpen, isOpen, onClose: disclosureOnClose } = useDisclosure(); + const { t } = useTranslation(); + const { t: cloudProvidersT } = useTranslation('cloudProviders'); + const { t: appT } = useTranslation('applist'); + const queryClient = useQueryClient(); + const router = useRouter(); + const { delSession, setToken, session } = useSessionStore(); + const { openDesktopApp } = useAppStore(); + + const [flowStep, setFlowStep] = useState(DeleteFlowStep.BOOTSTRAP); + const [nickname, setNickname] = useState(''); + const [verifyValue, setVerifyValue] = useState(''); + const [forceDeleteValue, setForceDeleteValue] = useState(''); + const [code, setCode] = useState(''); + const [deleteExecution, setDeleteExecution] = useState( + createDefaultDeleteExecutionState + ); + + const verifyWords = t('common:deletemyaccount'); + const forceDeleteKeywords = t('common:force_delete_keywords'); + + const appType = [ + '', + appT('db'), + appT('app'), + appT('terminal'), + appT('job'), + appT('other'), + appT('object-storage'), + appT('cloud-vm'), + appT('app-store') + ]; + + const resetFormState = useCallback(() => { + setNickname(''); + setVerifyValue(''); + setForceDeleteValue(''); + setCode(''); + setDeleteExecution(createDefaultDeleteExecutionState()); + }, []); + + const closeModal = useCallback(() => { + resetFormState(); + setFlowStep(DeleteFlowStep.BOOTSTRAP); + disclosureOnClose(); + }, [disclosureOnClose, resetFormState]); + + const handleLogout = useCallback(async () => { + clearPersistedMarketingQuery(); + delSession(); + queryClient.clear(); + setToken(''); + await router.replace('/signin'); + }, [delSession, queryClient, router, setToken]); + + useEffect(() => { + if (flowStep !== DeleteFlowStep.SUCCESS) return; + + const timer = window.setTimeout(() => { + void handleLogout(); + }, 1500); + + return () => { + window.clearTimeout(timer); + }; + }, [flowStep, handleLogout]); + + const openCostCenterApp = useCallback(() => { + openDesktopApp({ + appKey: 'system-costcenter', + pathname: '/', + query: { + mode: 'create' + }, + messageData: { + type: 'InternalAppCall', + mode: 'create' + } + }); + }, [openDesktopApp]); + + const { data: amountData } = useQuery({ + queryKey: ['getAmount', { userId: session?.user?.userCrUid }], + queryFn: getAmount, + enabled: !!session?.user, + staleTime: 60 * 1000 + }); + + const { data: namespaces = [], isLoading: namespacesLoading } = useQuery({ + queryKey: ['teamList', 'deleteAccount'], + queryFn: nsListRequest, + enabled: !!session?.user, + select(data) { + return data.data?.namespaces || []; + }, + refetchOnWindowFocus: false + }); + + const namespaceIds = useMemo(() => namespaces.map((namespace) => namespace.id), [namespaces]); + + const { data: plans = [], isLoading: plansLoading } = useQuery({ + queryKey: ['planList', 'deleteAccount', ...namespaceIds], + queryFn: () => getWorkspacesPlans(namespaceIds), + enabled: namespaceIds.length > 0, + select(data) { + return data.data?.plans || []; + }, + refetchOnWindowFocus: false + }); + + const subscribedWorkspaceRows = useMemo( + () => buildSubscribedWorkspaceRows(namespaces, plans), + [namespaces, plans] + ); + + const hasSubscribedWorkspaces = subscribedWorkspaceRows.length > 0; + + const applyDeleteStatus = useCallback((data: DeleteUserFinalStatusResponse) => { + setDeleteExecution({ + deleteId: data.deleteId, + status: data.status, + failedWorkspaces: data.failedWorkspaces || [] + }); + setFlowStep(getDeleteFlowStepFromStatus(data.status)); + }, []); + + const mutationCheck = useMutation({ + mutationFn: checkRemainResource, + onSuccess(data) { + if (data.message === RESOURCE_STATUS.REMAIN_RESOURCE) { + setFlowStep(DeleteFlowStep.REMAIN_RESOURCES); + } + }, + onError(data: { code: number; message: ValueOf; data: RegionResourceType[] }) { + if (data.message === RESOURCE_STATUS.REMAIN_RESOURCE) { + setFlowStep(DeleteFlowStep.REMAIN_RESOURCES); + } + } + }); + + const mutationDelete = useMutation({ + mutationFn: deleteUserRequest, + onSuccess(data: ApiResp) { + if (!data.data) return; + applyDeleteStatus(data.data); + } + }); + + const mutationForceDelete = useMutation( + (payload: { code: string }) => + forceDeleteUser(payload) as unknown as Promise>, + { + onSuccess(data: ApiResp) { + if (!data.data) return; + applyDeleteStatus(data.data); + setCode(''); + setForceDeleteValue(''); + } + } + ); + + useQuery({ + queryKey: ['delete-user-status', deleteExecution.deleteId], + queryFn: () => getDeleteUserStatus(deleteExecution.deleteId), + enabled: + isOpen && + !!deleteExecution.deleteId && + deleteExecution.status === DELETE_USER_EXECUTION_STATUS.PENDING, + refetchInterval(data) { + return data?.data?.status === DELETE_USER_EXECUTION_STATUS.PENDING ? 3000 : false; + }, + refetchOnWindowFocus: false, + onSuccess(data) { + if (!data.data) return; + applyDeleteStatus(data.data); + } + }); + + useEffect(() => { + if (!isOpen) return; + if (flowStep !== DeleteFlowStep.BOOTSTRAP) return; + if (namespacesLoading || (namespaceIds.length > 0 && plansLoading)) return; + + setFlowStep( + hasSubscribedWorkspaces ? DeleteFlowStep.SUBSCRIPTION_WARNING : DeleteFlowStep.FINAL_CONFIRM + ); + }, [ + flowStep, + hasSubscribedWorkspaces, + isOpen, + namespaceIds.length, + namespacesLoading, + plansLoading + ]); + + const isInsufficientBalance = + (amountData?.data?.balance || 0) < (amountData?.data?.deductionBalance || 0); + + const isBusy = + mutationCheck.isLoading || + mutationDelete.isLoading || + mutationForceDelete.isLoading || + flowStep === DeleteFlowStep.BOOTSTRAP || + (flowStep === DeleteFlowStep.PENDING && !deleteExecution.deleteId); + + const canDismiss = ![DeleteFlowStep.PENDING, DeleteFlowStep.SUCCESS].includes(flowStep); + const isFinalConfirmationStep = flowStep === DeleteFlowStep.FINAL_CONFIRM; + const isForceDeleteStep = flowStep === DeleteFlowStep.FORCE_DELETE; + const isRemainResourcesStep = flowStep === DeleteFlowStep.REMAIN_RESOURCES; + const isSubscribedWarningStep = flowStep === DeleteFlowStep.SUBSCRIPTION_WARNING; + const isSuccessStep = flowStep === DeleteFlowStep.SUCCESS; + const isFailedStep = flowStep === DeleteFlowStep.FAILED; + const isPendingStep = flowStep === DeleteFlowStep.PENDING; + + const isFinalConfirmValid = + nickname === session?.user?.name && + verifyValue === verifyWords && + !isBusy && + !isInsufficientBalance; + const isForceDeleteValid = + forceDeleteValue === forceDeleteKeywords && !isBusy && !isInsufficientBalance; + + const remainResourceRows = (mutationCheck.data?.data?.regionResourceList || []).flatMap((item) => + item.resource + .filter((resource) => [1, 2, 4, 6, 7, 8].includes(resource.type)) + .map( + (resource) => + [ + item.region.displayName, + resource.workspace.displayName, + appType[resource.type], + generateURL(resource, item.region.domain) + ] as const + ) + ); + + const handleSubmitDelete = async () => { + if (isFinalConfirmationStep) { + const res = await mutationCheck.mutateAsync(); + const nextCode = res.data?.code || ''; + + if (res.message === RESOURCE_STATUS.REMAIN_RESOURCE) { + setCode(nextCode); + setFlowStep(DeleteFlowStep.REMAIN_RESOURCES); + } else if (res.message === RESOURCE_STATUS.RESULT_SUCCESS) { + mutationDelete.mutate(); + } + + return; + } + + if (isRemainResourcesStep) { + setFlowStep(DeleteFlowStep.FORCE_DELETE); + return; + } + + if (isForceDeleteStep && code) { + mutationForceDelete.mutate({ code }); + } + }; + + const getModalWidth = () => { + if (isRemainResourcesStep || isSubscribedWarningStep || isFailedStep) return '540px'; + return '400px'; + }; + + const getBodyDescription = () => { + if (isSubscribedWarningStep) return t('common:delete_account_subscription_warning'); + if (isFinalConfirmationStep) return t('common:deleteaccounttitle'); + if (isRemainResourcesStep) return t('common:delete_account_remain_resources'); + if (isForceDeleteStep) return t('common:force_delete_tips'); + if (isPendingStep) return t('common:delete_account_pending_description'); + if (isSuccessStep) return t('common:delete_account_success_description'); + if (isFailedStep) return t('common:delete_account_failed_description'); + return ''; + }; + + return ( + <> + + undefined} + isCentered + closeOnOverlayClick={canDismiss} + closeOnEsc={canDismiss} + > + + + + + {t('common:delete_account_title')} + + {canDismiss && } + + {isBusy ? ( +
+ +
+ ) : ( + + {isSubscribedWarningStep ? ( + <> + {t('common:deleteaccounttitle')} + + + + + {t('common:delete_account_subscription_warning_title')} + + + {t('common:delete_account_subscription_warning')} + + + + + ) : ( + {getBodyDescription()} + )} + + {(isFinalConfirmationStep || isRemainResourcesStep || isForceDeleteStep) && + isInsufficientBalance && ( + + + {t('common:insufficient_balance')} + + )} + + {(isFinalConfirmationStep || isRemainResourcesStep || isForceDeleteStep) && ( + + + {t('common:irreversibleactiontips')} + + )} + + {!isPendingStep && !isSuccessStep && !isFailedStep ? ( + + ) : null} + + {isSubscribedWarningStep && ( + + + + + {[t('common:workspace'), t('common:plan')].map((label, index, arr) => ( + + ))} + + + + {subscribedWorkspaceRows.map((workspace) => ( + + + + + ))} + +
+ {label} +
+ {workspace.workspaceName} + + + {workspace.planName} + +
+
+ )} + + {isFinalConfirmationStep && ( + <> + + + {t('common:please_enter_username')} + {session?.user?.name} + {t('common:confirm')} + + + { + setNickname(e.target.value); + }} + /> + + + + + {t('common:please_enter')} + {verifyWords} + {t('common:confirm_again')} + + + { + setVerifyValue(e.target.value); + }} + /> + + + + )} + + {isRemainResourcesStep && ( + + + + + {[ + t('common:region'), + t('common:team'), + appT('app_type'), + t('common:link') + ].map((label, index, arr) => ( + + ))} + + + + {remainResourceRows.map((item, index) => ( + + {[ + {cloudProvidersT(item[0] as any)}, + {item[1]}, + {item[2]}, + item[3] ? ( + } + minW={'auto'} + variant={'ghost'} + boxSize={'32px'} + color={'grayModern.600'} + aria-label={t('common:link')} + onClick={() => { + window.open(item[3], '_self'); + }} + /> + ) : ( + - + ) + ].map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {label} +
+ {cell} +
+
+ )} + + {isForceDeleteStep && ( + + + {t('common:please_enter')} + {forceDeleteKeywords} + {t('common:confirm')} + + + { + setForceDeleteValue(e.target.value); + }} + /> + + + )} + + {isPendingStep && ( +
+ + + {t('common:delete_account_pending_title')} + + {t('common:delete_account_pending_hint')} + + +
+ )} + + {isSuccessStep && ( +
+ + + {t('common:delete_account_success_title')} + + + {t('common:delete_account_success_hint')} + + +
+ )} + + {isFailedStep && ( + + + {t('common:delete_account_failed_hint')} + + {deleteExecution.failedWorkspaces.length > 0 ? ( + + + + + {[t('common:workspace'), t('common:status')].map( + (label, index, arr) => ( + + ) + )} + + + + {deleteExecution.failedWorkspaces.map((workspace, index) => ( + + + + + ))} + +
+ {label} +
+ + {workspace.workspaceName} + + {workspace.action} + + + + {workspace.message} +
+
+ ) : ( + + {t('common:delete_account_failed_no_details')} + + )} +
+ )} + + {isSubscribedWarningStep && ( + + + + + + )} + + {(isFinalConfirmationStep || isRemainResourcesStep || isForceDeleteStep) && ( + + + + + )} + + {isFailedStep && ( + + + + )} +
+ )} +
+
+
+ + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.utils.ts b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.utils.ts new file mode 100644 index 000000000000..98beaf18ae35 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.utils.ts @@ -0,0 +1,61 @@ +import { WorkspacesPlansResponse } from '@/types/plan'; +import { + DeleteUserFinalStatusResponse, + DELETE_USER_EXECUTION_STATUS +} from '@/types/response/deleteUser'; +import { NamespaceDto, UserRole } from '@/types/team'; + +export enum DeleteFlowStep { + BOOTSTRAP, + SUBSCRIPTION_WARNING, + FINAL_CONFIRM, + REMAIN_RESOURCES, + FORCE_DELETE, + PENDING, + SUCCESS, + FAILED +} + +export type SubscribedWorkspaceRow = { + namespace: string; + workspaceName: string; + planName: string; +}; + +export const buildSubscribedWorkspaceRows = ( + namespaces: NamespaceDto[] = [], + plans: WorkspacesPlansResponse['plans'] = [] +): SubscribedWorkspaceRow[] => { + const namespaceNameMap = new Map( + namespaces.map((namespace) => [namespace.id, namespace.teamName || namespace.id]) + ); + const ownerNamespaceIds = new Set( + namespaces + .filter((namespace) => namespace.role === UserRole.Owner) + .map((namespace) => namespace.id) + ); + + return plans + .filter((plan) => { + const normalizedPlanName = plan.planName?.trim().toUpperCase(); + return ( + ownerNamespaceIds.has(plan.namespace) && + Boolean(normalizedPlanName) && + normalizedPlanName !== 'PAYG' && + normalizedPlanName !== 'FREE' + ); + }) + .map((plan) => ({ + namespace: plan.namespace, + workspaceName: namespaceNameMap.get(plan.namespace) || plan.namespace, + planName: plan.planName + })); +}; + +export const getDeleteFlowStepFromStatus = ( + status: DeleteUserFinalStatusResponse['status'] +): DeleteFlowStep => { + if (status === DELETE_USER_EXECUTION_STATUS.SUCCESS) return DeleteFlowStep.SUCCESS; + if (status === DELETE_USER_EXECUTION_STATUS.FAILED) return DeleteFlowStep.FAILED; + return DeleteFlowStep.PENDING; +}; diff --git a/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx b/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx new file mode 100644 index 000000000000..684525a8ed9c --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx @@ -0,0 +1,125 @@ +import { + Button, + FlexProps, + FormControl, + FormLabel, + HStack, + Spinner, + VStack +} from '@chakra-ui/react'; +import { useMutation } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { ApiResp } from '@/types'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { strongPassword } from '@/utils/crypto'; +import { passwordModifyRequest } from '@/api/auth'; +import { SettingInput } from './SettingInput'; +import { SettingInputGroup } from './SettingInputGroup'; + +export default function PasswordModify( + props: FlexProps & { + onClose: () => void; + } +) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const { register, handleSubmit, reset, formState } = useForm<{ + oldPassword: string; + newPassword: string; + againPassword: string; + }>(); + const mutation = useMutation(passwordModifyRequest, { + onSuccess(data) { + if (data.code === 200) { + toast({ + status: 'success', + title: t('common:passwordchangesuccess') + }); + reset(); + props.onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ oldPassword, newPassword }); + }, + (errors) => { + if (errors.oldPassword) return toast({ title: t('common:currentpasswordrequired') }); + if (errors.newPassword) return toast({ title: t('common:password_tips') }); + if (errors.againPassword) return toast({ title: t('common:password_mis_match') }); + } + )} + > + + + + {t('common:currentpassword')} + + + + + + + + {t('common:newpassword')} + + + + + + + + {t('common:confirmnewpassword')} + + + + + + + +
+ + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/PhoneBindingModal.tsx b/frontend/desktop/src/components/account/AccountCenter/PhoneBindingModal.tsx new file mode 100644 index 000000000000..22bfd01fac20 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/PhoneBindingModal.tsx @@ -0,0 +1,430 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + VStack, + Text, + FormControl, + FormLabel, + Input, + Button, + useToast, + InputGroup, + InputRightElement, + Box, + Flex, + HStack, + Spinner, + ModalCloseButton +} from '@chakra-ui/react'; +import { useState, useCallback } from 'react'; +import { useTranslation } from 'next-i18next'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { getSmsBindCodeRequest, verifySmsBindRequest, mergeUserRequest } from '@/api/auth'; +import { ApiResp } from '@/types'; +import { MERGE_USER_READY } from '@/types/response/utils'; +import { ProviderType } from 'prisma/global/generated/client'; +import { WarnTriangeIcon } from '@sealos/ui'; +import { ValueOf } from '@/types'; +import { I18nErrorKey } from '@/types/i18next'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; + +type ModalStep = 'BINDING' | 'MERGE_CONFLICT' | 'MERGE_CONFIRM'; + +type MergeData = { + code: string; + providerType: ProviderType; +}; + +interface PhoneBindingModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function PhoneBindingModal({ isOpen, onClose }: PhoneBindingModalProps) { + const { t } = useTranslation(); + const toast = useToast(); + const queryClient = useQueryClient(); + + const [currentStep, setCurrentStep] = useState('BINDING'); + const [mergeData, setMergeData] = useState(null); + const [phoneNumber, setPhoneNumber] = useState(''); + const [verifyCode, setVerifyCode] = useState(''); + const [challengeId, setChallengeId] = useState(''); + const [countdown, setCountdown] = useState(0); + const [isSending, setIsSending] = useState(false); + + // Phone number validation + const isPhoneValid = useCallback((phone: string) => { + return /^1[3-9]\d{9}$/.test(phone); + }, []); + + // Countdown timer + const startCountdown = useCallback(() => { + setCountdown(60); + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + }, []); + + // Send verification code + const sendCodeMutation = useMutation({ + mutationFn: () => getSmsBindCodeRequest('phone')({ id: phoneNumber }), + onSuccess: (data) => { + setChallengeId(data.data?.challengeId || ''); + toast({ + position: 'top', + title: t('common:already_sent_code'), + status: 'success', + duration: 3000 + }); + startCountdown(); + }, + onError: (error: ApiResp) => { + toast({ + position: 'top', + title: error.message || t('common:get_code_failed'), + status: 'error', + duration: 3000 + }); + }, + onSettled: () => { + setIsSending(false); + } + }); + + // Merge user mutation + const mergeMutation = useMutation({ + mutationFn: mergeUserRequest, + onSuccess() { + queryClient.clear(); + onClose(); + }, + onError(err: { message: ValueOf }) { + toast({ + position: 'top', + title: t(err.message as I18nErrorKey, { ns: 'error' }), + status: 'error', + duration: 3000 + }); + } + }); + + // Verify and bind phone + const verifyMutation = useMutation({ + mutationFn: () => + verifySmsBindRequest('phone')({ + id: phoneNumber, + code: verifyCode, + challengeId + }), + onSuccess: (data) => { + const status = data.message || ''; + + if (data.code === 200) { + toast({ + position: 'top', + title: t('common:bind_success'), + status: 'success', + duration: 3000 + }); + // Refresh user info + queryClient.invalidateQueries(); + // Close modal + onClose(); + } else if (Object.values(MERGE_USER_READY).includes(status as MERGE_USER_READY)) { + // Handle merge user scenario - stay in same modal with different step + if (status === MERGE_USER_READY.MERGE_USER_CONTINUE) { + const code = data.data?.code; + if (!code) return; + setMergeData({ + code, + providerType: ProviderType.PHONE + }); + setCurrentStep('MERGE_CONFIRM'); + } else { + setCurrentStep('MERGE_CONFLICT'); + } + } else { + toast({ + position: 'top', + title: data.message || t('common:bind_failed'), + status: 'error', + duration: 3000 + }); + } + }, + onError: (error: ApiResp) => { + if (error.message === MERGE_USER_READY.MERGE_USER_PROVIDER_CONFLICT) { + toast({ + position: 'top', + title: t('common:provider_conflict_error'), + status: 'error', + duration: 5000 + }); + } else { + toast({ + position: 'top', + title: error.message || t('common:bind_failed'), + status: 'error', + duration: 3000 + }); + } + } + }); + + const handleSendCode = () => { + if (!isPhoneValid(phoneNumber)) { + toast({ + position: 'top', + title: t('common:invalid_phone_number'), + status: 'error', + duration: 3000 + }); + return; + } + if (countdown > 0) { + return; + } + setIsSending(true); + sendCodeMutation.mutate(); + }; + + const handleSubmit = () => { + if (!isPhoneValid(phoneNumber)) { + toast({ + position: 'top', + title: t('common:invalid_phone_number'), + status: 'error', + duration: 3000 + }); + return; + } + if (verifyCode.length !== 6) { + toast({ + position: 'top', + title: t('common:verify_code_tips'), + status: 'error', + duration: 3000 + }); + return; + } + verifyMutation.mutate(); + }; + + const handleMerge = () => { + if (!mergeData) { + toast({ + position: 'top', + title: 'Unknown Error', + status: 'error', + duration: 3000 + }); + return; + } + mergeMutation.mutate(mergeData); + }; + + const handleBackToBinding = () => { + setCurrentStep('BINDING'); + setMergeData(null); + }; + + // Get modal title based on current step + const getModalTitle = () => { + switch (currentStep) { + case 'BINDING': + return t('common:phone_binding_required'); + case 'MERGE_CONFLICT': + case 'MERGE_CONFIRM': + return ( + + + {t('common:merge_account_title')} + + ); + default: + return t('common:phone_binding_required'); + } + }; + + return ( + + + + + + {getModalTitle()} + + + {mergeMutation.isLoading ? ( + + + + ) : ( + <> + {/* BINDING Step */} + {currentStep === 'BINDING' && ( + + + + {t('common:phone_binding_description')} + + + + + + {t('common:phone')} + + + setPhoneNumber(e.target.value)} + maxLength={11} + disabled={verifyMutation.isLoading} + pr="110px" + /> + + + + + + + + + {t('common:verifycode')} + + setVerifyCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + maxLength={6} + disabled={verifyMutation.isLoading} + /> + + + + + + + )} + + {/* MERGE_CONFLICT Step */} + {currentStep === 'MERGE_CONFLICT' && ( + + {t('common:merge_account_tips1')} + + + + + )} + + {/* MERGE_CONFIRM Step */} + {currentStep === 'MERGE_CONFIRM' && ( + + {t('common:merge_account_tips2')} + + + + + + )} + + )} + + + + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx new file mode 100644 index 000000000000..e7f6f6b6c89a --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx @@ -0,0 +1,20 @@ +import { InputProps, Flex, Input, forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; + +export const SettingInput = forwardRef(function SettingInput( + { children, ...props }, + ref +) { + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx new file mode 100644 index 000000000000..defdfe6a7b7f --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx @@ -0,0 +1,23 @@ +import { forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; + +export const SettingInputGroup = forwardRef(function SettingInputGroup( + props, + ref +) { + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx new file mode 100644 index 000000000000..149e1ec2cb03 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx @@ -0,0 +1,25 @@ +import { + InputProps, + Flex, + Input, + forwardRef, + InputGroup, + InputGroupProps, + InputRightElement, + InputRightElementProps +} from '@chakra-ui/react'; + +export const SettingInputRightElement = forwardRef( + function SettingInputRightElement({ ...props }, ref) { + return ( + + ); + } +); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx new file mode 100644 index 000000000000..52f3328af9d7 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx @@ -0,0 +1,218 @@ +import { + Button, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { getSmsBindCodeRequest, verifySmsBindRequest } from '@/api/auth'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler, useState } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { ApiResp } from '@/types'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ProviderType } from 'prisma/global/generated/client'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; +import { MERGE_USER_READY } from '@/types/response/utils'; + +const smsBindGen = (smsType: SmsType) => + function SmsBindCore({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const [challengeId, setChallengeId] = useState(''); + + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + const { setMergeUserData, setMergeUserStatus } = useCallbackStore(); + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getSmsBindCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + setChallengeId(data.data?.challengeId || ''); + startTimer(); + toast({ + status: 'success', + title: t('common:already_sent_code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('common:get_code_failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('common:already_sent_code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: smsType === 'phone' ? t('common:invalid_phone_number') : t('common:invalid_email') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const queryClient = useQueryClient(); + const mutation = useMutation({ + async mutationFn({ + smsType, + ...data + }: { + id: string; + code: string; + challengeId: string; + smsType: SmsType; + }) { + return verifySmsBindRequest(smsType)(data); + }, + async onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + toast({ + status: 'success', + title: t('common:bind_success') + }); + reset(); + setChallengeId(''); + await queryClient.invalidateQueries(); + onClose?.(); + } else if (Object.values(MERGE_USER_READY).includes(status as MERGE_USER_READY)) { + if (status === MERGE_USER_READY.MERGE_USER_CONTINUE) { + const code = data.data?.code; + if (!code) return; + setMergeUserStatus(MergeUserStatus.CANMERGE); + setMergeUserData({ + code, + providerType: ProviderType.PHONE + }); + } else { + setMergeUserStatus(MergeUserStatus.CONFLICT); + } + onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message, status: 'error' }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + challengeId, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'email') return toast({ title: t('common:invalid_email') }); + else return toast({ title: t('common:invalid_phone_number') }); + } + if (errors.verifyCode) return toast({ title: t('common:verify_code_tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('common:phone') : t('common:email')} + + + + + { + + {t('common:get_code')} + + } + + + + + + + {t('common:verifycode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ + ); + }; +export const PhoneBind = smsBindGen('phone'); +export const EmailBind = smsBindGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx new file mode 100644 index 000000000000..5690ddfcedf3 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx @@ -0,0 +1,436 @@ +import { + Button, + FlexProps, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { ApiResp } from '@/types'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler, useState } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { + getNewSmsCodeRequest, + getOldSmsCodeRequest, + verifyNewSmsRequest, + verifyOldSmsRequest +} from '@/api/auth'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ProviderType } from 'prisma/global/generated/client'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; +import { MERGE_USER_READY } from '@/types/response/utils'; +enum PageState { + VERIFY_OLD, + VERIFY_NEW +} +function OldSms({ + smsType, + onSuccess, + oldId +}: { + smsType: SmsType; + onSuccess?: (uid: string) => void; + oldId: string; +}) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const [challengeId, setChallengeId] = useState(''); + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>({ + defaultValues: { + id: oldId + } + }); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ smsType, id }: { id: string; smsType: SmsType }) { + return getOldSmsCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + setChallengeId(data.data?.challengeId || ''); + startTimer(); + toast({ + status: 'success', + title: t('common:already_sent_code') + }); + }, + onError(err) { + getCodeMutation.reset(); + if ( + ['too_frequent', 'send_rate_limited'].includes(String((err as ApiResp)?.data?.error || '')) + ) { + toast({ + status: 'error', + title: t('common:get_code_too_frequent') + }); + } else { + toast({ + status: 'error', + title: t('common:get_code_failed') + }); + } + } + }); + + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('common:already_sent_code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: t('common:invalid_phone_number') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ id, smsType }); + }; + const mutation = useMutation({ + mutationFn({ + smsType, + ...data + }: { + smsType: SmsType; + id: string; + code: string; + challengeId: string; + }) { + return verifyOldSmsRequest(smsType)(data); + }, + onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + reset(); + setChallengeId(''); + onSuccess?.(data?.data?.uid || ''); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + challengeId, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'phone') return toast({ title: t('common:invalid_phone_number') }); + else return toast({ title: t('common:invalid_email') }); + } + if (errors.verifyCode) return toast({ title: t('common:verify_code_tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('common:old_phone') : t('common:old_email')} + + + + + { + + } + + + + + + + {t('common:verifycode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ ); +} +function NewSms({ + smsType = 'phone', + uid, + onSuccess: onClose, + onError +}: { + uid: string; + smsType?: SmsType; + onSuccess?: () => void; + onError?: () => void; +}) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const [challengeId, setChallengeId] = useState(''); + const { setMergeUserData, setMergeUserStatus } = useCallbackStore(); + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getNewSmsCodeRequest(smsType)({ id, uid }); + }, + onSuccess(data) { + setChallengeId(data.data?.challengeId || ''); + startTimer(); + toast({ + status: 'success', + title: t('common:already_sent_code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('common:get_code_failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('common:already_sent_code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: smsType === 'phone' ? t('common:invalid_phone_number') : t('common:invalid_email') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const mutation = useMutation(verifyNewSmsRequest(smsType), { + onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + toast({ + status: 'success', + title: + smsType === 'phone' ? t('common:phonechangesuccess') : t('common:emailchangesuccess') + }); + reset(); + onClose?.(); + } else if (Object.values(MERGE_USER_READY).includes(status as MERGE_USER_READY)) { + // onSuccess?.() + if (status === MERGE_USER_READY.MERGE_USER_CONTINUE) { + setMergeUserStatus(MergeUserStatus.CANMERGE); + setMergeUserData({ + code: data.data.code, + providerType: ProviderType.PHONE + }); + } else { + setMergeUserStatus(MergeUserStatus.CONFLICT); + } + } + }, + onError(error) { + console.log('error', error); + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + uid, + challengeId + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'phone') return toast({ title: t('common:invalid_phone_number') }); + else return toast({ title: t('common:invalid_email') }); + } + if (errors.verifyCode) return toast({ title: t('common:verify_code_tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('common:new_phone') : t('common:new_email')} + + + + + { + + } + + + + + + + {t('common:verifycode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ ); +} + +const smsChangeGen = (smsType: SmsType) => + function SmsChangeCore({ onClose, oldId }: { onClose: () => void; oldId: string }) { + const [pageState, setPageState] = useState(PageState.VERIFY_OLD); + const [codeUid, setCodeUid] = useState(''); + return pageState === PageState.VERIFY_OLD ? ( + { + setCodeUid(uid); + setPageState(PageState.VERIFY_NEW); + }} + oldId={oldId} + /> + ) : ( + { + setPageState(PageState.VERIFY_OLD); + onClose(); + }} + /> + ); + }; + +export const PhoneChange = smsChangeGen('phone'); +export const EmailChange = smsChangeGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx new file mode 100644 index 000000000000..59a933a2124f --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx @@ -0,0 +1,200 @@ +import { + Button, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { getSmsUnbindCodeRequest, verifySmsUnbindRequest } from '@/api/auth'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler, useState } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { ApiResp } from '@/types'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; + +const smsUnBindGen = (smsType: SmsType) => + function UnBindCore({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const [challengeId, setChallengeId] = useState(''); + + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getSmsUnbindCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + setChallengeId(data.data?.challengeId || ''); + startTimer(); + toast({ + status: 'success', + title: t('common:already_sent_code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('common:get_code_failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('common:already_sent_code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: t('common:get_code_failed') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn({ + smsType, + ...data + }: { + id: string; + smsType: SmsType; + code: string; + challengeId: string; + }) { + return verifySmsUnbindRequest(smsType)(data); + }, + async onSuccess(data) { + if (data.code === 200) { + toast({ + status: 'success', + title: t('common:unbind_success') + }); + reset(); + setChallengeId(''); + await queryClient.invalidateQueries(); + onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + challengeId, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'email') return toast({ title: t('common:invalid_email') }); + else return toast({ title: t('common:invalid_phone_number') }); + } + if (errors.verifyCode) return toast({ title: t('common:verify_code_tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('common:phone') : t('common:email')} + + + + + { + + {t('common:get_code')} + + } + + + + + + + {t('common:verifycode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ + ); + }; +export const PhoneUnBind = smsUnBindGen('phone'); +export const EmailUnBind = smsUnBindGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts b/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts new file mode 100644 index 000000000000..44433a9d3d0c --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts @@ -0,0 +1,20 @@ +import { SmsType } from '@/services/backend/db/verifyCode'; + +export const smsIdValid = (smsType: SmsType) => { + if (smsType === 'phone') + return { + pattern: { + value: /^1[3-9]\d{9}$/, + message: 'Invalid Phone Number' + }, + required: 'Phone number can not be blank' + }; + else + return { + pattern: { + value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + message: 'Invalid Email Address' + }, + required: 'Email Address can not be blank' + }; +}; diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx new file mode 100644 index 000000000000..49fa6a4752e7 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -0,0 +1,474 @@ +import { UserInfo } from '@/api/auth'; +import PasswordModify from '@/components/account/AccountCenter/PasswordModify'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import { ValueOf } from '@/types'; +import { + Badge, + Center, + Flex, + HStack, + IconButton, + IconButtonProps, + Image, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + Text, + useDisclosure, + VStack +} from '@chakra-ui/react'; +import { CloseIcon, LeftArrowIcon, SettingIcon } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useMemo, useState, ReactNode } from 'react'; +import { RealNameAuthForm } from '../RealNameModal'; +import { AuthModifyList } from './AuthModifyList'; +import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; +import DeleteAccount from './DeleteAccountModal'; +import { EmailBind, PhoneBind } from './SmsModify/SmsBind'; +import { EmailChange, PhoneChange } from './SmsModify/SmsChange'; +import { EmailUnBind, PhoneUnBind } from './SmsModify/SmsUnbind'; + +enum _PageState { + INDEX = 0 + // WECHAT_BIND, + // WECHAT_UNBIND, + // GITHUB_UNBIND, + // GITHUB_BIND +} +enum PasswordState { + PASSWORD = 10 +} +enum PhoneState { + PHONE_BIND = 20, + PHONE_UNBIND, + PHONE_CHANGE_BIND +} +enum EmailState { + EMAIL_BIND = 30, + EMAIL_UNBIND, + EMAIL_CHANGE_BIND +} + +enum RealNameState { + REALNAME_AUTH = 40 +} + +const PageState = Object.assign( + Object.assign({}, _PageState, EmailState, PhoneState), + PasswordState, + RealNameState +); + +interface AccountCenterProps extends Omit { + children?: ReactNode; +} + +export default function AccountCenter(props: AccountCenterProps) { + const { children, ...restProps } = props; + const { commonConfig } = useConfigStore(); + const { session } = useSessionStore((s) => s); + const conf = useConfigStore(); + const { t } = useTranslation(); + const logo = '/images/default-user.svg'; + const { isOpen, onOpen, onClose } = useDisclosure(); + const [pageState, setPageState] = useState>(PageState.INDEX); + + const resetPageState = () => { + setPageState(PageState.INDEX); + infoData.refetch(); + }; + + const infoData = useQuery({ + queryFn: UserInfo, + queryKey: [session?.token, 'UserInfo'], + select(d) { + return d.data?.info; + } + }); + + const providerState = useMemo(() => { + const providerList = ['PHONE', 'PASSWORD', 'GITHUB', 'WECHAT', 'GOOGLE', 'EMAIL'] as const; + const state = (infoData.data?.oauthProvider || []).reduce( + (pre, cur) => { + const { providerType, providerId } = cur; + // @ts-ignore + if (!providerList.includes(providerType)) return pre; + // @ts-ignore + pre[providerType].isBinding = true; + if (providerType === 'PHONE' || providerType === 'EMAIL') pre[providerType].id = providerId; + pre.total += 1; + return pre; + }, + { + total: 0, + PHONE: { + isBinding: false, + id: '' + }, + EMAIL: { + isBinding: false, + id: '' + }, + GITHUB: { + isBinding: false + }, + WECHAT: { + isBinding: false + }, + GOOGLE: { + isBinding: false + }, + PASSWORD: { + isBinding: false + }, + AVATAR_URL: '' + } + ); + state.AVATAR_URL = infoData.data?.avatarUri || ''; + return state; + }, [infoData.data?.oauthProvider]); + + const modalTitle = useMemo(() => { + if (pageState === PageState.INDEX) return t('common:account_settings'); + else if (pageState === PageState.PASSWORD) return t('common:changepassword'); + else if (pageState === PageState.EMAIL_BIND) return t('common:bindemail'); + else if (pageState === PageState.EMAIL_UNBIND) return t('common:unbindemail'); + else if (pageState === PageState.EMAIL_CHANGE_BIND) return t('common:changeemail'); + else if (pageState === PageState.PHONE_BIND) return t('common:bindphone'); + else if (pageState === PageState.PHONE_UNBIND) return t('common:unbindphone'); + else if (pageState === PageState.PHONE_CHANGE_BIND) return t('common:changephone'); + else if (pageState === PageState.REALNAME_AUTH) return t('common:realName_verification'); + else return ''; + }, [t, pageState]); + + return ( + <> + {children ? ( +
{ + e.preventDefault(); + onOpen(); + }} + > + {children} +
+ ) : ( + { + e.preventDefault(); + return onOpen(); + }} + {...restProps} + variant={'white-bg-icon'} + icon={} + /> + )} + { + resetPageState(); + onClose(); + }} + isCentered + > + + + + + + {pageState !== PageState.INDEX && ( + { + setPageState(PageState.INDEX); + }} + /> + )} + + {modalTitle} + + + + {infoData.isSuccess && infoData.data ? ( + + {pageState === PageState.INDEX ? ( + + {/* */} + + {t('common:avatar')} + +
+ user avator +
+
+
+ + {t('common:nickname')} + {infoData.data.nickname} + + + {'ID'} + {infoData.data.id} + + {commonConfig?.realNameAuthEnabled && ( + {t('common:realname_info')}} + RightElement={ + infoData.data.enterpriseRealName || infoData.data.realName ? ( + + + + + + {infoData?.data.enterpriseRealName || infoData?.data.realName} + + + ) : ( + setPageState(PageState.REALNAME_AUTH)} + display="flex" + padding="4px 4px 4px 8px" + justifyContent="center" + alignItems="center" + gap="2px" + borderRadius="6px" + bg="var(--Red-50, #FEF3F2)" + color="var(--Red-500, #F04438)" + fontFamily="PingFang SC" + fontSize="14px" + fontStyle="normal" + fontWeight="500" + lineHeight="20px" + letterSpacing="0.1px" + textTransform="none" + > + {t('common:no_realname_auth')} + + + + + ) + } + /> + )} + {conf.authConfig?.idp.password.enabled && providerState.PASSWORD.isBinding && ( + {t('common:password')}} + RightElement={ + <> + ********* + { + setPageState(PageState.PASSWORD); + }} + /> + + } + /> + )} + {conf.authConfig?.idp.sms.enabled && ( + {t('common:phone')}} + RightElement={ + <> + + {providerState.PHONE.isBinding + ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('common:unbound')} + + + { + providerState.PHONE.isBinding + ? setPageState(PageState.PHONE_CHANGE_BIND) + : setPageState(PageState.PHONE_BIND); + }} + /> + {providerState.PHONE.isBinding && + providerState.total > 1 && + conf.layoutConfig?.version !== 'cn' && ( + { + setPageState(PageState.PHONE_UNBIND); + }} + /> + )} + + + } + /> + )} + {conf.authConfig?.idp.email.enabled && ( + {t('common:email')}} + RightElement={ + <> + + {providerState.EMAIL.isBinding + ? providerState.EMAIL.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('common:unbound')} + + + { + providerState.EMAIL.isBinding + ? setPageState(PageState.EMAIL_CHANGE_BIND) + : setPageState(PageState.EMAIL_BIND); + }} + /> + {providerState.EMAIL.isBinding && providerState.total > 1 && ( + { + setPageState(PageState.EMAIL_UNBIND); + }} + /> + )} + + + } + /> + )} + + {t('common:delete_account')}} + RightElement={ + <> + {t('common:delete_account_tips')} + + + } + /> +
+ ) : ( + + {pageState === PageState.PASSWORD ? ( + + ) : pageState === PageState.PHONE_BIND ? ( + + ) : pageState === PageState.PHONE_UNBIND ? ( + + ) : pageState === PageState.PHONE_CHANGE_BIND ? ( + + ) : pageState === PageState.EMAIL_BIND ? ( + + ) : pageState === PageState.EMAIL_UNBIND ? ( + + ) : pageState === PageState.EMAIL_CHANGE_BIND ? ( + + ) : pageState === PageState.REALNAME_AUTH ? ( + + ) : null} + + )} +
+ ) : ( +
+ +
+ )} +
+
+ + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx new file mode 100644 index 000000000000..0bb7c6c6bf62 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx @@ -0,0 +1,152 @@ +import { mergeUserRequest } from '@/api/auth'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ValueOf } from '@/types'; +import { I18nErrorKey } from '@/types/i18next'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { + BoxProps, + Button, + HStack, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + Text, + VStack +} from '@chakra-ui/react'; +import { WarnTriangeIcon } from '@sealos/ui'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useState } from 'react'; + +function NeedToMerge({ ...props }: BoxProps & {}) { + const { mergeUserStatus, mergeUserData, setMergeUserStatus, setMergeUserData } = + useCallbackStore(); + const [isOpen, setIsOpen] = useState(false); + + const onClose = () => { + setMergeUserStatus(MergeUserStatus.IDLE); + }; + + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { toast } = useCustomToast({ status: 'error' }); + const mutation = useMutation({ + mutationFn: mergeUserRequest, + onSuccess() { + queryClient.clear(); + }, + onError(err: { message: ValueOf }) { + const errMessage = + err.message === USER_MERGE_STATUS.INSUFFICENT_BALANCE + ? err.message + : 'MERGET_USER_INSUFFICENT_BALANCE'; + + toast({ + status: 'error', + title: t(err.message as I18nErrorKey, { ns: 'error' }) + }); + }, + onSettled() { + setMergeUserData(); + setMergeUserStatus(MergeUserStatus.IDLE); + } + }); + useEffect(() => { + setIsOpen(!![MergeUserStatus.CONFLICT, MergeUserStatus.CANMERGE].includes(mergeUserStatus)); + }, [mergeUserStatus]); + return ( + + + + + + + {t('common:merge_account_title')} + + {mutation.isLoading ? ( + + ) : ( + + + + {mergeUserStatus === MergeUserStatus.CONFLICT + ? t('common:merge_account_tips1') + : t('common:merge_account_tips2')} + + {mergeUserStatus === MergeUserStatus.CONFLICT ? ( + + + + ) : ( + + + + + )} + + + )} + + + ); +} +export default NeedToMerge; diff --git a/frontend/desktop/src/components/account/AlertSettings/AlertSettings.tsx b/frontend/desktop/src/components/account/AlertSettings/AlertSettings.tsx new file mode 100644 index 000000000000..018dffafe949 --- /dev/null +++ b/frontend/desktop/src/components/account/AlertSettings/AlertSettings.tsx @@ -0,0 +1,543 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { Plus, Trash2 } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from '@sealos/shadcn-ui'; +import { Checkbox } from '@sealos/shadcn-ui'; +import { Button } from '@sealos/shadcn-ui/button'; +import { Badge } from '@sealos/shadcn-ui/badge'; +import { DeleteConfirmDialog } from './DeleteConfirmDialog'; +import { BindDialog } from './BindDialog'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { listAlerts, createAlert, deleteAlerts, updateAlerts } from '@/api/platform'; +import { AlertNotificationAccount, ProviderType } from '@/types/alert'; +import useSessionStore from '@/stores/session'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { UserInfo } from '@/api/auth'; +import { useTranslation } from 'next-i18next'; + +interface PhoneNumber { + id: string; + number: string; + isBound: boolean; + checked: boolean; + isFromAlert: boolean; +} + +interface Email { + id: string; + address: string; + isBound: boolean; + checked: boolean; + isFromAlert: boolean; +} + +interface AlertSettingsProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + emailEnabled?: boolean; + phoneEnabled?: boolean; +} + +export function AlertSettings({ + open = false, + onOpenChange, + emailEnabled = false, + phoneEnabled = false +}: AlertSettingsProps) { + const { t } = useTranslation(); + const { session } = useSessionStore(); + const queryClient = useQueryClient(); + const { toast } = useCustomToast({ status: 'error' }); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState<{ + type: 'phone' | 'email'; + id: string; + value: string; + } | null>(null); + const [bindDialogOpen, setBindDialogOpen] = useState(false); + const [bindDialogType, setBindDialogType] = useState<'phone' | 'email'>('phone'); + + const { data: userInfo } = useQuery({ + queryKey: [session?.token, 'UserInfo'], + queryFn: UserInfo, + enabled: open && !!session?.token, + select: (d) => d.data?.info + }); + + const userPhone = useMemo(() => { + const phoneProvider = userInfo?.oauthProvider?.find((p) => p.providerType === 'PHONE'); + return phoneProvider?.providerId || ''; + }, [userInfo]); + + const userEmail = useMemo(() => { + const emailProvider = userInfo?.oauthProvider?.find((p) => p.providerType === 'EMAIL'); + return emailProvider?.providerId || ''; + }, [userInfo]); + + const { + data: alertsData, + isLoading, + error: alertsError + } = useQuery({ + queryKey: ['alert-notification-accounts'], + queryFn: async () => { + const res = await listAlerts(); + if (res.code !== 200) { + throw new Error(res.message || 'Failed to fetch alerts'); + } + return res.data || []; + }, + enabled: open + }); + + useEffect(() => { + if (open && alertsError) { + toast({ + title: + alertsError instanceof Error + ? alertsError.message + : t('common:alert_settings.messages.fetch_failed') + }); + } + }, [open, alertsError, toast, t]); + + const { phoneNumbers, emails } = useMemo(() => { + const phones: PhoneNumber[] = []; + const emailsList: Email[] = []; + + // Add alerts from the list + if (alertsData) { + alertsData.forEach((alert: AlertNotificationAccount) => { + if (alert.providerType === 'PHONE') { + phones.push({ + id: alert.id, + number: alert.providerId, + isBound: alert.providerId === userPhone, + checked: alert.isEnabled, + isFromAlert: true + }); + } else if (alert.providerType === 'EMAIL') { + emailsList.push({ + id: alert.id, + address: alert.providerId, + isBound: alert.providerId === userEmail, + checked: alert.isEnabled, + isFromAlert: true + }); + } + }); + } + + // Add user phone if it exists but not in alerts list + if (userPhone && !phones.find((p) => p.number === userPhone)) { + phones.push({ + id: `bound-phone-${userPhone}`, + number: userPhone, + isBound: true, + checked: true, + isFromAlert: false + }); + } + + // Add user email if it exists but not in alerts list + if (userEmail && !emailsList.find((e) => e.address === userEmail)) { + emailsList.push({ + id: `bound-email-${userEmail}`, + address: userEmail, + isBound: true, + checked: true, + isFromAlert: false + }); + } + + return { phoneNumbers: phones, emails: emailsList }; + }, [alertsData, userPhone, userEmail]); + + const createMutation = useMutation({ + mutationFn: async ({ + providerType, + providerId, + code, + challengeId + }: { + providerType: ProviderType; + providerId: string; + code: string; + challengeId: string; + }) => { + const res = await createAlert({ providerType, providerId, code, challengeId }); + if (res.code !== 200) { + const error = new Error(res.message || 'Failed to create alert') as Error & { + code?: number; + }; + error.code = res.code; + throw error; + } + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-notification-accounts'] }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: async (ids: string[]) => { + const res = await deleteAlerts({ ids }); + if (res.code !== 200) { + throw new Error(res.message || 'Failed to delete alerts'); + } + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-notification-accounts'] }); + toast({ title: t('common:alert_settings.messages.delete_success'), status: 'success' }); + setDeleteDialogOpen(false); + setDeleteTarget(null); + }, + onError: (error) => { + toast({ + title: + error instanceof Error ? error.message : t('common:alert_settings.messages.delete_failed') + }); + } + }); + + const toggleMutation = useMutation({ + mutationFn: async ({ ids, isEnabled }: { ids: string[]; isEnabled: boolean }) => { + const res = await updateAlerts({ ids, isEnabled }); + if (res.code !== 200) { + throw new Error(res.message || 'Failed to update alerts'); + } + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['alert-notification-accounts'] }); + toast({ title: t('common:alert_settings.messages.update_success'), status: 'success' }); + }, + onError: (error) => { + toast({ + title: + error instanceof Error ? error.message : t('common:alert_settings.messages.update_failed') + }); + } + }); + + const handlePhoneSelectAll = (checked: boolean) => { + // Only select non-virtual entries (from alerts) + const ids = phoneNumbers.filter((p) => p.isFromAlert).map((p) => p.id); + if (ids.length > 0) { + toggleMutation.mutate({ ids, isEnabled: checked }); + } + }; + + const handleEmailSelectAll = (checked: boolean) => { + // Only select non-virtual entries (from alerts) + const ids = emails.filter((e) => e.isFromAlert).map((e) => e.id); + if (ids.length > 0) { + toggleMutation.mutate({ ids, isEnabled: checked }); + } + }; + + const handlePhoneCheck = (id: string, checked: boolean) => { + // Virtual entries (bound-phone-xxx) cannot be enabled/disabled + if (id.startsWith('bound-phone-')) { + return; + } + toggleMutation.mutate({ ids: [id], isEnabled: checked }); + }; + + const handleEmailCheck = (id: string, checked: boolean) => { + // Virtual entries (bound-email-xxx) cannot be enabled/disabled + if (id.startsWith('bound-email-')) { + return; + } + toggleMutation.mutate({ ids: [id], isEnabled: checked }); + }; + + const handleDeletePhone = (id: string) => { + const phone = phoneNumbers.find((p) => p.id === id); + if (phone) { + setDeleteTarget({ type: 'phone', id, value: phone.number }); + setDeleteDialogOpen(true); + } + }; + + const handleDeleteEmail = (id: string) => { + const email = emails.find((e) => e.id === id); + if (email) { + setDeleteTarget({ type: 'email', id, value: email.address }); + setDeleteDialogOpen(true); + } + }; + + const handleConfirmDelete = () => { + if (deleteTarget) { + deleteMutation.mutate([deleteTarget.id]); + } + }; + + const handleAddPhoneNumber = () => { + setBindDialogType('phone'); + setBindDialogOpen(true); + }; + + const handleAddEmail = () => { + setBindDialogType('email'); + setBindDialogOpen(true); + }; + + const handleBindConfirm = async ( + value: string, + code: string, + challengeId: string + ): Promise => { + return new Promise((resolve, reject) => { + const providerType: ProviderType = bindDialogType === 'phone' ? 'PHONE' : 'EMAIL'; + createMutation.mutate( + { providerType, providerId: value, code, challengeId }, + { + onSuccess: () => { + toast({ title: t('common:alert_settings.messages.create_success'), status: 'success' }); + setBindDialogOpen(false); + resolve(); + }, + onError: (error) => { + // Don't show toast here - let BindDialog handle error display + reject(error); + } + } + ); + }); + }; + + const phoneSelectAllChecked = phoneNumbers.filter((p) => p.isFromAlert).every((p) => p.checked); + const emailSelectAllChecked = emails.filter((e) => e.isFromAlert).every((e) => e.checked); + + return ( + + +
+ + + {t('common:alert_settings.title')} + + + {t('common:alert_settings.description')} + + + + {isLoading ? ( +
+
{t('common:alert_settings.loading')}
+
+ ) : ( +
+ {phoneEnabled && ( +
+

+ {t('common:alert_settings.phone.section_title')} +

+
+
+
+ e.isFromAlert).length === 0 || + toggleMutation.isLoading + } + /> +

+ {t('common:alert_settings.phone.enable_all')} +

+
+
+ +
+ {phoneNumbers.length === 0 ? ( +
+ {t('common:alert_settings.phone.no_phone_numbers')} +
+ ) : ( + phoneNumbers.map((phone, index) => ( +
+
+ + handlePhoneCheck(phone.id, checked === true) + } + className="w-4 h-4" + disabled={ + toggleMutation.isLoading || phone.id.startsWith('bound-phone-') + } + /> +

{phone.number}

+
+
+ {phone.isBound ? ( + + {t('common:alert_settings.phone.bound_badge')} + + ) : ( + + )} +
+
+ )) + )} + +
+ +
+
+
+
+ )} + + {emailEnabled && ( +
+

+ {t('common:alert_settings.email.section_title')} +

+
+
+
+ e.isFromAlert).length === 0 || + toggleMutation.isLoading + } + /> +

+ {t('common:alert_settings.email.enable_all')} +

+
+
+ +
+ {emails.length === 0 ? ( +
+ {t('common:alert_settings.email.no_emails')} +
+ ) : ( + emails.map((email, index) => ( +
+
+ + handleEmailCheck(email.id, checked === true) + } + className="w-4 h-4" + disabled={ + toggleMutation.isLoading || email.id.startsWith('bound-email-') + } + /> +

{email.address}

+
+
+ {email.isBound ? ( + + {t('common:alert_settings.email.bound_badge')} + + ) : ( + + )} +
+
+ )) + )} + +
+ +
+
+
+
+ )} +
+ )} +
+
+ + + + +
+ ); +} diff --git a/frontend/desktop/src/components/account/AlertSettings/BindDialog.tsx b/frontend/desktop/src/components/account/AlertSettings/BindDialog.tsx new file mode 100644 index 000000000000..3b5a97d77c24 --- /dev/null +++ b/frontend/desktop/src/components/account/AlertSettings/BindDialog.tsx @@ -0,0 +1,321 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@sealos/shadcn-ui'; +import { Button } from '@sealos/shadcn-ui/button'; +import { Input } from '@sealos/shadcn-ui/input'; +import { Label } from '@sealos/shadcn-ui/label'; +import { ProviderType } from '@/types/alert'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useTranslation } from 'next-i18next'; +import { sendAlertBindEmailCode, sendAlertBindPhoneCode } from '@/api/platform'; + +interface BindDialogProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + type: 'phone' | 'email'; + onConfirm?: (value: string, code: string, challengeId: string) => Promise; + userPhone?: string; + userEmail?: string; +} + +export function BindDialog({ + open = false, + onOpenChange, + type, + onConfirm, + userPhone, + userEmail +}: BindDialogProps) { + const { t } = useTranslation(); + const [value, setValue] = useState(''); + const [code, setCode] = useState(''); + const [challengeId, setChallengeId] = useState(''); + const [valueError, setValueError] = useState(''); + const [codeError, setCodeError] = useState(''); + const [cooldown, setCooldown] = useState(0); + const [sendingCode, setSendingCode] = useState(false); + const [verifying, setVerifying] = useState(false); + const { toast } = useCustomToast({ status: 'error' }); + + useEffect(() => { + if (cooldown <= 0) return; + const timer = setInterval(() => setCooldown((prev) => prev - 1), 1000); + return () => clearInterval(timer); + }, [cooldown]); + + useEffect(() => { + if (!open) { + // Reset all state when dialog closes + setValue(''); + setCode(''); + setChallengeId(''); + setValueError(''); + setCodeError(''); + setCooldown(0); + setSendingCode(false); + setVerifying(false); + } + }, [open]); + + const handleValueChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + if (valueError && newValue) { + setValueError(''); + } + }; + + const handleCodeChange = (e: React.ChangeEvent) => { + const newCode = e.target.value; + setCode(newCode); + if (codeError && newCode) { + setCodeError(''); + } + }; + + const handleSendCode = async () => { + if (!value) { + setValueError( + type === 'phone' + ? t('common:alert_settings.bind.phone_required') + : t('common:alert_settings.bind.email_required') + ); + return; + } + if (type === 'phone') { + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(value)) { + setValueError(t('common:alert_settings.bind.phone_invalid')); + return; + } + // Check if phone is already bound to account + if (userPhone && value === userPhone) { + setValueError(t('common:alert_settings.bind.account_bound_phone')); + return; + } + } else { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + setValueError(t('common:alert_settings.bind.email_invalid')); + return; + } + // Check if email is already bound to account + if (userEmail && value === userEmail) { + setValueError(t('common:alert_settings.bind.account_bound_email')); + return; + } + } + + setSendingCode(true); + try { + if (type === 'phone') { + const res = await sendAlertBindPhoneCode({ id: value }); + if (res.code !== 200 || !res.data?.challengeId) { + throw new Error(res.message || t('common:alert_settings.bind.send_code_failed')); + } + setChallengeId(res.data.challengeId); + } else { + const res = await sendAlertBindEmailCode({ id: value }); + if (res.code !== 200 || !res.data?.challengeId) { + throw new Error(res.message || t('common:alert_settings.bind.send_code_failed')); + } + setChallengeId(res.data.challengeId); + } + setCooldown(60); + toast({ title: t('common:alert_settings.bind.code_sent'), status: 'success' }); + } catch (error) { + toast({ + title: + error instanceof Error ? error.message : t('common:alert_settings.bind.send_code_failed') + }); + } finally { + setSendingCode(false); + } + }; + + const handleConfirm = async () => { + let hasError = false; + + if (!value) { + setValueError( + type === 'phone' + ? t('common:alert_settings.bind.phone_required') + : t('common:alert_settings.bind.email_required') + ); + hasError = true; + } else { + if (type === 'phone') { + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(value)) { + setValueError(t('common:alert_settings.bind.phone_invalid')); + hasError = true; + } else if (userPhone && value === userPhone) { + setValueError(t('common:alert_settings.bind.account_bound_phone')); + hasError = true; + } + } else { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + setValueError(t('common:alert_settings.bind.email_invalid')); + hasError = true; + } else if (userEmail && value === userEmail) { + setValueError(t('common:alert_settings.bind.account_bound_email')); + hasError = true; + } + } + } + + if (!code || !challengeId) { + setCodeError(t('common:alert_settings.bind.code_required')); + hasError = true; + } + + if (hasError) { + return; + } + + setVerifying(true); + setCodeError(''); + try { + await onConfirm?.(value, code, challengeId); + setValue(''); + setCode(''); + setChallengeId(''); + setValueError(''); + setCodeError(''); + onOpenChange?.(false); + } catch (error) { + const errorCode = (error as Error & { code?: number })?.code; + + // 409 = SMS code is wrong (from verifyCodeGuard) + // 410 = Already bound (from billing service) + if (errorCode === 409) { + // Verification code error - show in input field, no toast + setCodeError(t('common:alert_settings.bind.code_invalid')); + } else if (errorCode === 410) { + // Already bound - show toast + toast({ + title: t('common:alert_settings.bind.already_bound') + }); + } else { + // Other errors - show toast + const errorMessage = + error instanceof Error ? error.message : t('common:alert_settings.bind.verify_failed'); + toast({ + title: errorMessage + }); + } + } finally { + setVerifying(false); + } + }; + + const handleClose = () => { + setValue(''); + setCode(''); + setChallengeId(''); + setValueError(''); + setCodeError(''); + setCooldown(0); + setSendingCode(false); + setVerifying(false); + onOpenChange?.(false); + }; + + const title = + type === 'phone' + ? t('common:alert_settings.bind.phone_title') + : t('common:alert_settings.bind.email_title'); + const label = + type === 'phone' + ? t('common:alert_settings.bind.phone_label') + : t('common:alert_settings.bind.email_label'); + const placeholder = + type === 'phone' + ? t('common:alert_settings.bind.phone_placeholder') + : t('common:alert_settings.bind.email_placeholder'); + + return ( + + + + + {title} + + + +
+
+ +
+
+ + +
+ {valueError &&

{valueError}

} +
+
+ +
+ +
+
+ +
+ {codeError &&

{codeError}

} +
+
+
+ + + + +
+
+ ); +} diff --git a/frontend/desktop/src/components/account/AlertSettings/DeleteConfirmDialog.tsx b/frontend/desktop/src/components/account/AlertSettings/DeleteConfirmDialog.tsx new file mode 100644 index 000000000000..7895e8d5ddce --- /dev/null +++ b/frontend/desktop/src/components/account/AlertSettings/DeleteConfirmDialog.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { TriangleAlert } from 'lucide-react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@sealos/shadcn-ui'; +import { Button } from '@sealos/shadcn-ui/button'; +import { useTranslation } from 'next-i18next'; + +interface DeleteConfirmDialogProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + phoneNumber?: string; + email?: string; + onConfirm?: () => void; + onCancel?: () => void; +} + +export function DeleteConfirmDialog({ + open = false, + onOpenChange, + phoneNumber, + email, + onConfirm, + onCancel +}: DeleteConfirmDialogProps) { + const { t } = useTranslation(); + const handleConfirm = () => { + onConfirm?.(); + onOpenChange?.(false); + }; + + const handleCancel = () => { + onCancel?.(); + onOpenChange?.(false); + }; + + return ( + + + + + + {t('common:alert_settings.delete.title')} + + + +
+ {phoneNumber ? ( + <> + { + // @ts-expect-error i18n typing issues + t('common:alert_settings.delete.confirm_phone.0') + } + {phoneNumber} + { + // @ts-expect-error i18n typing issues + t('common:alert_settings.delete.confirm_phone.1') + } + + ) : email ? ( + <> + { + // @ts-expect-error i18n typing issues + t('common:alert_settings.delete.confirm_email.0') + } + {email} + { + // @ts-expect-error i18n typing issues + t('common:alert_settings.delete.confirm_email.1') + } + + ) : ( + t('common:alert_settings.delete.confirm_generic') + )} +
+ + + + + +
+
+ ); +} diff --git a/frontend/desktop/src/components/account/AlertSettings/index.tsx b/frontend/desktop/src/components/account/AlertSettings/index.tsx new file mode 100644 index 000000000000..f032e89813e5 --- /dev/null +++ b/frontend/desktop/src/components/account/AlertSettings/index.tsx @@ -0,0 +1,3 @@ +export { AlertSettings } from './AlertSettings'; +export { DeleteConfirmDialog } from './DeleteConfirmDialog'; +export { BindDialog } from './BindDialog'; diff --git a/frontend/desktop/src/components/account/BalancePopover.tsx b/frontend/desktop/src/components/account/BalancePopover.tsx new file mode 100644 index 000000000000..00393f2da579 --- /dev/null +++ b/frontend/desktop/src/components/account/BalancePopover.tsx @@ -0,0 +1,301 @@ +import { + Box, + Text, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + VStack, + HStack, + Flex +} from '@chakra-ui/react'; +import useSessionStore from '@/stores/session'; +import { useSubscriptionStore } from '@/stores/subscription'; +import { useMemo, useEffect } from 'react'; +import { Sparkles } from 'lucide-react'; +import { Tooltip, Button, cn, Separator, TooltipContent, TooltipTrigger } from '@sealos/shadcn-ui'; +import { WorkspaceSubscription } from '@/types/plan'; +import Decimal from 'decimal.js'; +import { useQuery } from '@tanstack/react-query'; +import { getAmount } from '@/api/auth'; +import { formatMoney } from '@/utils/format'; +import { CurrencySymbol } from '@sealos/ui'; +import { useConfigStore } from '@/stores/config'; +import { useTranslation } from 'next-i18next'; + +interface BalancePopoverProps { + openCostCenterApp: () => void; + openCostCenterTopup: () => void; + children: React.ReactNode; +} + +/** + * @deprecated - Move to `getPlanBackgroundClass` if possible. + */ +export function getPlanBackground(subscription?: WorkspaceSubscription) { + if (!subscription) return 'var(--background-image-plan-payg)'; + const name = subscription?.PlanName ? subscription?.PlanName.toLowerCase() : 'Free'; + const status = subscription?.Status?.toLowerCase(); + if (status === 'debt') return 'var(--background-image-plan-debt)'; + if (name.includes('free')) return 'var(--background-image-plan-hobby)'; + if (name.includes('hobby')) return 'var(--background-image-plan-hobby)'; + if (name.includes('starter')) return 'var(--background-image-plan-starter)'; + if (name.includes('pro')) return 'var(--background-image-plan-pro)'; + if (name.includes('team')) return 'var(--background-image-plan-team)'; + if (name.includes('standard')) return 'var(--background-image-plan-hobby-plus)'; + if (name.includes('enterprise')) return 'var(--background-image-plan-enterprise)'; + return 'var(--background-image-plan-payg)'; +} + +export function BalancePopover({ + openCostCenterApp, + openCostCenterTopup, + children +}: BalancePopoverProps) { + const { t } = useTranslation(); + const { session } = useSessionStore(); + const subscriptionEnabled = useConfigStore( + (state) => state.layoutConfig?.common.subscriptionEnabled + ); + const currencySymbol = useConfigStore( + (state) => state.layoutConfig?.currencySymbol || 'shellCoin' + ); + + const workspace = session?.user?.nsid || ''; + + const { subscriptionInfo, fetchSubscriptionInfo } = useSubscriptionStore(); + + useEffect(() => { + if (workspace) { + fetchSubscriptionInfo(workspace); + } + }, [workspace, fetchSubscriptionInfo]); + + const subscription = subscriptionInfo?.subscription; + const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free'; + const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan; + + const canManagePayment = subscription?.type === 'PAYG' || subscription?.role === 'OWNER'; + + const formatDate = (dateStr?: string) => { + if (!dateStr) return 'N/A'; + return new Date(dateStr).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const formatCancelledNoticeDate = (dateStr?: string) => { + const d = dateStr ? new Date(dateStr) : null; + if (!d || Number.isNaN(d.getTime())) return 'N/A'; + return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' }); + }; + + const calculateRemainingDays = (endDateStr?: string) => { + if (!endDateStr) return 0; + + const endDate = new Date(endDateStr); + const currentDate = new Date(); + const timeDiff = endDate.getTime() - currentDate.getTime(); + const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); + + return Math.max(0, daysDiff); + }; + + const remainingDays = calculateRemainingDays(subscription?.CurrentPeriodEndAt); + + const { data } = useQuery({ + queryKey: ['getAmount', { userId: session?.user?.userCrUid }], + queryFn: getAmount, + enabled: !!session?.user, + staleTime: 60 * 1000, + refetchOnMount: true + }); + + const balance = useMemo(() => { + let realBalance = new Decimal(data?.data?.balance || 0); + if (data?.data?.deductionBalance) { + realBalance = realBalance.minus(new Decimal(data.data.deductionBalance)); + } + return realBalance.toNumber(); + }, [data]); + + return ( + + {children} + + + + + {/* Show plan name only if subscription enabled */} + {subscriptionEnabled && ( + <> +
+ + {subscription?.PlanName + ? `${subscription?.PlanName} ${t('common:balance_popover.plan_suffix')}` + : t('common:balance_popover.payg_plan')} + + {isCancelled && ( + + {t('common:nav_links.cancelled')} + + )} + {subscriptionInfo?.subscription?.Status?.toLowerCase() === 'debt' && ( +
+ {t('common:balance_popover.subscription_status.expired')} +
+ )} + {subscription?.PlanName === 'Free' && ( +
+ {t('common:balance_popover.subscription_status.limited_trial')} +
+ )} +
+ + {!subscription?.PlanName && } + + )} + {!subscription?.PlanName && ( +
+
+
+ {t('common:balance_popover.balance')} +
+
+ + + + {formatMoney(balance).toFixed(2)} +
+
+ + {/* Show topup button if subscription is enabled */} + {!subscriptionEnabled && ( + + )} +
+ )} + {!!subscription?.PlanName && + (subscription?.PlanName !== 'Free' && + subscriptionInfo?.subscription?.Status?.toLowerCase() === 'debt' && + subscription?.ExpireAt ? ( + + + {t('common:balance_popover.expired_at')} + + + {formatDate(subscription?.ExpireAt)} + + + ) : ( + <> + ))} + {subscriptionEnabled && + isCancelled && + subscription?.PlanName && + subscription?.PlanName !== 'Free' && + subscription?.CurrentPeriodEndAt && ( +
+ {t('common:balance_popover.expires_on')}{' '} + {formatDate(subscription.CurrentPeriodEndAt)} +
+ )} + {subscription?.PlanName === 'Free' && ( +
+ {t('common:balance_popover.trial_expiry_tip', { count: remainingDays })} +
+ )} +
+ + {subscriptionEnabled && ( + <> + {isCancelled ? ( + <> +
+ {t('common:balance_popover.cancelled_notice', { + date: formatCancelledNoticeDate(subscription?.CurrentPeriodEndAt) + })} +
+ + + ) : ( + <> + {subscription?.PlanName !== 'Free' ? ( +
+ {t('common:balance_popover.upgrade_tip')} +
+ ) : ( +
+ {t('common:balance_popover.trial_expiry_upgrade_tip', { + count: remainingDays + })} +
+ )} + + + +
+ +
+
+ +

{t('common:balance_popover.can_not_manage_payments')}

+
+
+ + )} + + )} + + + {t('common:balance_popover.check_costcenter_tip')} + + + +
+
+
+
+ ); +} diff --git a/frontend/desktop/src/components/account/GuideModal.tsx b/frontend/desktop/src/components/account/GuideModal.tsx new file mode 100644 index 000000000000..4f3a3a7c67d5 --- /dev/null +++ b/frontend/desktop/src/components/account/GuideModal.tsx @@ -0,0 +1,508 @@ +import { + Modal, + ModalOverlay, + ModalContent, + Text, + Box, + Flex, + Icon, + Center, + Grid, + Divider, + Button +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { ArrowLeft, ChevronLeft, ChevronRight, CircleAlert, X } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; +import { UserInfo } from '@/api/auth'; +import useSessionStore from '@/stores/session'; +import { useCallback, useEffect } from 'react'; +import useAppStore from '@/stores/app'; +import { + devboxDriverObj, + quitGuideDriverObj, + startDriver, + appLaunchpadDriverObj, + templateDriverObj, + databaseDriverObj +} from './driver'; +import { WindowSize } from '@/types'; +import { Image } from '@chakra-ui/react'; +import { useGuideModalStore } from '@/stores/guideModal'; +import { track } from '@sealos/gtm'; + +const GuideModal = () => { + const { t } = useTranslation(); + const { session } = useSessionStore((s) => s); + const { installedApps, runningInfo, openApp, setToHighestLayerById, closeAppAll } = useAppStore(); + const { + isOpen, + selectedGuide, + activeStep, + initGuide: guideModalInitGuide, + closeGuideModal, + setSelectedGuide, + setActiveStep, + setInitGuide + } = useGuideModalStore(); + + useEffect(() => { + if (isOpen) { + track('module_open', { + module: 'guide', + trigger: guideModalInitGuide ? 'onboarding' : 'manual' + }); + } + }, [isOpen, guideModalInitGuide]); + + const infoData = useQuery({ + queryFn: UserInfo, + queryKey: [session?.token, 'UserInfo'], + select(d) { + return d.data?.info; + } + }); + + const openDesktopApp = useCallback( + ({ + appKey, + query = {}, + messageData = {}, + pathname = '/', + appSize = 'maximize' + }: { + appKey: string; + query?: Record; + messageData?: Record; + pathname: string; + appSize?: WindowSize; + }) => { + const app = installedApps.find((item) => item.key === appKey); + const runningApp = runningInfo.find((item) => item.key === appKey); + if (!app) return; + openApp(app, { query, pathname, appSize }); + if (runningApp) { + setToHighestLayerById(runningApp.pid); + } + // post message + const iframe = document.getElementById(`app-window-${appKey}`) as HTMLIFrameElement; + if (!iframe) return; + iframe.contentWindow?.postMessage(messageData, app.data.url); + }, + [installedApps, openApp, runningInfo, setToHighestLayerById] + ); + + const guideLinks: { + key: string; + icon: string; + title: string; + description: string; + steps: { + title: string; + description: string; + image: string; + }[]; + stepNumbers: number; + }[] = [ + { + key: 'system-devbox', + icon: installedApps.find((app) => app.key === 'system-devbox')?.icon || '', + title: t('v2:devbox_title'), + description: t('v2:devbox_desc'), + steps: [ + { + title: t('v2:devbox_step_1'), + description: t('v2:devbox_step_1_desc'), + image: '/images/onboarding/devbox-1.png' + }, + { + title: t('v2:devbox_step_2'), + description: t('v2:devbox_step_2_desc'), + image: '/images/onboarding/devbox-2.png' + }, + { + title: t('v2:devbox_step_3'), + description: t('v2:devbox_step_3_desc'), + image: '/images/onboarding/devbox-3.png' + } + ], + stepNumbers: 3 + }, + { + key: 'system-dbprovider', + icon: installedApps.find((app) => app.key === 'system-dbprovider')?.icon || '', + title: t('v2:database_title'), + description: t('v2:database_desc'), + steps: [ + { + title: t('v2:database_step_1'), + description: t('v2:database_step_1_desc'), + image: '/images/onboarding/database-1.png' + }, + { + title: t('v2:database_step_2'), + description: t('v2:database_step_2_desc'), + image: '/images/onboarding/database-2.png' + }, + { + title: t('v2:database_step_3'), + description: t('v2:database_step_3_desc'), + image: '/images/onboarding/database-3.png' + } + ], + stepNumbers: 3 + }, + { + key: 'system-applaunchpad', + icon: installedApps.find((app) => app.key === 'system-applaunchpad')?.icon || '', + title: t('v2:launchpad_title'), + description: t('v2:launchpad_desc'), + steps: [ + { + title: t('v2:launchpad_step_1'), + description: t('v2:launchpad_step_1_desc'), + image: '/images/onboarding/app-launchpad-1.png' + }, + { + title: t('v2:launchpad_step_2'), + description: t('v2:launchpad_step_2_desc'), + image: '/images/onboarding/app-launchpad-2.png' + }, + { + title: t('v2:launchpad_step_3'), + description: t('v2:launchpad_step_3_desc'), + image: '/images/onboarding/app-launchpad-3.png' + } + ], + stepNumbers: 3 + }, + { + key: 'system-template', + icon: installedApps.find((app) => app.key === 'system-template')?.icon || '', + title: t('v2:template_title'), + description: t('v2:template_desc'), + steps: [ + { + title: t('v2:template_step_1'), + description: t('v2:template_step_1_desc'), + image: '/images/onboarding/appstore-1.png' + }, + { + title: t('v2:template_step_2'), + description: t('v2:template_step_2_desc'), + image: '/images/onboarding/appstore-2.png' + } + ], + stepNumbers: 2 + } + ]; + + const StepCard = ({ + step, + index, + isActive, + onClick + }: { + step: { + title: string; + description: string; + image: string; + }; + index: number; + isActive: boolean; + onClick: () => void; + }) => { + return ( + + +
+ {index + 1} +
+ + + {step.title} + + + + {step.description} + + + +
+ + guide + +
+ ); + }; + + const handleCloseGuideModal = () => { + setInitGuide(false); + closeGuideModal(); + startDriver(quitGuideDriverObj(t)); + + track('guide_exit', { + module: 'guide', + progress_step: activeStep + }); + }; + + return ( + + + + + {selectedGuide !== null ? ( + + + + + {t('v2:guide_info_text')} + + + + + setSelectedGuide(null)} + _hover={{ color: 'blue.500' }} + /> + + + {guideLinks[selectedGuide].title} + + + {guideLinks[selectedGuide].description} + + + + + + + + {guideLinks[selectedGuide].steps.map((step, index) => ( + setActiveStep(index)} + /> + ))} + + + ) : ( + +
+ +
+
+ + {guideModalInitGuide + ? t('v2:guide_title', { name: infoData.data?.nickname || '' }) + : t('v2:quickstart_guide')} + + + {t('v2:documentation_desc')} + +
+ + + {guideLinks + .filter((app) => installedApps.find((i) => i.key === app.key)) + .map((item, index) => ( + + setSelectedGuide( + // Guide index will shift if some apps were missing + guideLinks.findIndex((guide) => guide.title === item.title) + ) + } + p={'20px'} + borderRadius="16px" + border={'1px solid #E4E4E7'} + background={'#FFF'} + boxShadow={'0px 1px 2px 0px rgba(0, 0, 0, 0.05)'} + _hover={{ + boxShadow: 'lg', + transform: 'scale(1.002)', + transition: 'all 0.2s' + }} + cursor={'pointer'} + gap={'16px'} + > +
+ guide +
+ + + {item.title} + + + {item.description} + + + {t('v2:guide_steps', { count: item.stepNumbers })} + + +
+ ))} +
+ + + {t('v2:step_title')} + +
+ )} +
+
+
+ ); +}; + +export default GuideModal; diff --git a/frontend/desktop/src/components/account/JoinDiscordPrompt.tsx b/frontend/desktop/src/components/account/JoinDiscordPrompt.tsx new file mode 100644 index 000000000000..8330839ef7a1 --- /dev/null +++ b/frontend/desktop/src/components/account/JoinDiscordPrompt.tsx @@ -0,0 +1,110 @@ +import { useConfigStore } from '@/stores/config'; +import { useJoinDiscordPromptStore } from '@/stores/joinDiscordPrompt'; +import { + Button, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Checkbox +} from '@chakra-ui/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function JoinDiscordPrompt({ children }: { children: React.ReactNode }) { + const { t } = useTranslation(); + + const store = useJoinDiscordPromptStore(); + const { layoutConfig } = useConfigStore(); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (!isClient || !layoutConfig?.discordInviteLink) { + return; + } + + if (!store.dontShowAgain && !store.open && !store.autoOpenBlocked) { + store.setOpen(true); + } + }, [store, isClient, layoutConfig?.discordInviteLink]); + + const handleClose = () => { + store.setOpen(false); + // Pervent auto open + store.blockAutoOpen(); + }; + + const handleOpenDiscord = () => { + window.open(layoutConfig?.discordInviteLink, '_blank'); + }; + + if (!layoutConfig?.discordInviteLink) return null; + return ( + <> +
store.setOpen(true)}>{children}
+ + + + + {t('v2:join_discord_prompt.title')} + + + +

{t('v2:join_discord_prompt.description')}

+
+ + + store.setDontShowAgain(e.target.checked)} + style={{ + color: '#71717A' + }} + > + {t('v2:join_discord_prompt.do_not_show_again')} + + +
+ + +
+
+
+
+ + ); +} diff --git a/frontend/desktop/src/components/account/RealNameModal.tsx b/frontend/desktop/src/components/account/RealNameModal.tsx new file mode 100644 index 000000000000..fc72c6890696 --- /dev/null +++ b/frontend/desktop/src/components/account/RealNameModal.tsx @@ -0,0 +1,1626 @@ +import { + Text, + Modal, + ModalOverlay, + ModalContent, + ModalCloseButton, + ModalHeader, + ModalBody, + useDisclosure, + VStack, + FormControl, + FormLabel, + Button, + Input, + useToast, + UseToastOptions, + Box, + Flex, + Center, + Spinner, + Link, + FormErrorMessage, + FlexProps, + Menu, + MenuButton, + MenuList, + MenuItem, + Icon +} from '@chakra-ui/react'; +import { CloseIcon, RefreshIcon, useMessage, WarningIcon } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useState, + useMemo, + useRef +} from 'react'; +import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + enterpriseRealNameAuthCancelRequest, + enterpriseRealNameAuthInfoRequest, + enterpriseRealNameAuthPaymentRequest, + enterpriseRealNameAuthVerifyRequest, + faceAuthGenerateQRcodeUriRequest, + getFaceAuthStatusRequest, + refreshRealNameQRecodeUriRequest, + getBanksListRequest +} from '@/api/auth'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import useSessionStore from '@/stores/session'; +import { useQuery } from '@tanstack/react-query'; +import QRCode from 'qrcode.react'; +import { useConfigStore } from '@/stores/config'; +import { PAYMENTSTATUS } from '@/types/response/enterpriseRealName'; + +export function useRealNameAuthNotification(props?: UseToastOptions) { + const { t } = useTranslation(); + const { commonConfig } = useConfigStore((s) => s); + const realNameReward = commonConfig?.realNameReward; + + const realNameAuthNotification = useToast({ + position: 'top', + ...props, + render: (props) => { + return ( + + + + +
+ +
+ + {t('common:realname_auth_reminder')} + +
+ {props?.isClosable && ( +
+ +
+ )} +
+ + + {realNameReward?.toString() === '0' + ? t('common:realname_auth_reminder_desc_no_reward') + : t('common:realname_auth_reminder_desc', { + reward: realNameReward + })} + + + {t('common:realname_auth_now')} + + + + +
+
+ ); + } + }); + return { + realNameAuthNotification + }; +} + +const RealNameModal = forwardRef< + { onOpen: () => void }, + { + children?: React.ReactElement; + onModalOpen?: () => void; + onModalClose?: () => void; + onFormSuccess?: () => void; + } +>(function RealNameModal(props, ref) { + const { t } = useTranslation(); + const { children } = props; + const { isOpen, onOpen, onClose } = useDisclosure(); + + const handleClose = () => { + onClose(); + if (props.onModalClose && typeof props.onModalClose === 'function') { + props.onModalClose(); + } + }; + + useImperativeHandle(ref, () => ({ + onOpen + })); + + return ( + <> + {children && + React.cloneElement(children, { + onClick: () => { + onOpen(); + if (props.onModalOpen && typeof props.onModalOpen === 'function') { + props.onModalOpen(); + } + } + })} + + + + + + + {t('common:realName_verification')} + + + + + + {t('common:personal_verification')} + + + + {t('common:enterprise_verification')} + + + + + { + onClose(); + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + + { + onClose(); + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + + + + + + + ); +}); + +export function RealNameAuthForm( + props: FlexProps & { + onFormSuccess?: () => void; + } +) { + const { t } = useTranslation(); + return ( + + + + {t('common:personal_verification')} + + + + {t('common:enterprise_verification')} + + + + + { + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + + { + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + + + ); +} + +export function FaceIdRealNameAuthORcode( + props: FlexProps & { + onFormSuccess?: () => void; + } +) { + const { t } = useTranslation(); + const { message } = useMessage(); + const queryClient = useQueryClient(); + const [isPolling, setIsPolling] = useState(false); + const { session } = useSessionStore((s) => s); + const { setSessionProp } = useSessionStore(); + const [refetchCount, setRefetchCount] = useState(0); + + const { data, isLoading, error, refetch } = useQuery( + ['faceIdAuth'], + faceAuthGenerateQRcodeUriRequest, + { + retry: false, + refetchOnWindowFocus: false + } + ); + + const refreshQRMutation = useMutation(refreshRealNameQRecodeUriRequest, { + onSuccess: () => { + message({ + title: t('common:qr_code_refreshed_successfully'), + status: 'success', + duration: 2000, + isClosable: true + }); + queryClient.invalidateQueries(['faceIdAuth']); + }, + onError: (error: any) => { + message({ + title: error.message || t('common:failed_to_refresh_qr_code'), + status: 'error', + duration: 2000, + isClosable: true + }); + } + }); + + const handleRefetch = useCallback(() => { + setRefetchCount((prev) => prev + 1); + refetch(); + }, [refetch]); + + const handleRefreshQR = useCallback(() => { + refreshQRMutation.mutate(); + }, [refreshQRMutation]); + + useEffect(() => { + let intervalId: NodeJS.Timeout; + + const bizToken = data?.data?.bizToken; + + if (!bizToken) { + return; + } + + const stopPolling = () => { + if (intervalId) clearInterval(intervalId); + setIsPolling(false); + }; + + const startPolling = () => { + if (!isPolling) { + setIsPolling(true); + intervalId = setInterval(async () => { + try { + const result = await getFaceAuthStatusRequest({ bizToken }); + if (result.data?.status === 'Success') { + message({ + title: t('common:face_recognition_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + + setSessionProp('user', { + ...useSessionStore.getState().session!.user!, + realName: result.data?.realName + }); + + // refetch user info + queryClient.invalidateQueries([session?.token, 'UserInfo']); + // refetch user amount + queryClient.invalidateQueries(['getAmount']); + + stopPolling(); + + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + } + if (result.data?.status === 'Failed') { + message({ + title: t('common:face_recognition_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + + stopPolling(); + handleRefetch(); + } + } catch (error: any) { + console.error('Error checking face ID auth status:', error); + message({ + title: error.message, + status: 'error', + duration: 2000, + isClosable: true + }); + } + }, 2000); + } + }; + + startPolling(); + + return stopPolling; + }, [session?.token, data, data?.data?.bizToken, refetchCount]); + + if (error) { + return ( + + {(error as Error).message || t('common:failed_to_get_qr_code')} + + + + + ); + } + + if (isLoading) { + return ( +
+ + {t('common:loading')} +
+ ); + } + + return ( + + {data?.data?.url && ( + <> + + + {t('common:scan_qr_code_for_face_recognition')} + +
+ +
+ + + {t('common:qr_code_refresh_note')} + +
+ + )} +
+ ); +} + +function EnterpriseVerification( + props: FlexProps & { + onFormSuccess?: () => void; + } +) { + const { t, i18n } = useTranslation(); + const { message } = useMessage(); + const { session } = useSessionStore((s) => s); + const { setSessionProp } = useSessionStore(); + const queryClient = useQueryClient(); + const domain = useConfigStore((state) => state.cloudConfig?.domain); + + const [selectedBank, setSelectedBank] = useState(''); + + const [searchKeyword, setSearchKeyword] = useState(''); + + const searchInputRef = useRef(null); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const clearSearch = useCallback(() => { + setSearchKeyword(''); + }, []); + + const { + data: banksResponse, + isLoading: banksLoading, + error: banksError + } = useQuery(['banksList'], getBanksListRequest, { + refetchOnWindowFocus: false, + select: (response) => { + const banksData = response?.data || {}; + return Object.entries(banksData).map(([key, value]) => ({ + code: key, + name: String(value || ''), + shortName: String(key || '') + })); + } + }); + + const banksList = banksResponse || []; + const filteredBanksList = useMemo(() => { + if (!searchKeyword.trim()) { + return banksList; + } + return banksList.filter((bank) => { + const bankShortName = bank.shortName.toLowerCase(); + const keyword = searchKeyword.toLowerCase(); + return bankShortName.includes(keyword); + }); + }, [banksList, searchKeyword]); + + const schema = z.object({ + key: z.string().min(1, { message: t('common:enterprise_key_required') }), + accountBank: z.string().min(1, { message: t('common:account_bank_required') }), + accountNo: z.string().min(1, { message: t('common:account_number_required') }), + keyName: z.string().min(1, { message: t('common:enterprise_name_required') }), + usrName: z.string().min(1, { message: t('common:user_name_required') }), + contactInfo: z + .string() + .min(1, { message: t('common:contact_info_required') }) + .regex(/^\d+$/, { message: t('common:contact_info_must_be_numeric') }) + }); + + const verificationSchema = z.object({ + transAmt: z + .string() + .refine((val) => /^\d{1,3}$/.test(val), { message: t('common:invalid_verification_amount') }) + }); + + const { data: enterpriseRealNameAuthInfo, isLoading: enterpriseRealNameAuthInfoLoading } = + useQuery(['enterpriseRealNameAuthInfo'], enterpriseRealNameAuthInfoRequest, { + refetchOnWindowFocus: false + }); + + type FormData = z.infer; + + const { + register: registerMain, + handleSubmit: handleMainSubmit, + reset: resetMain, + setValue: setMainValue, + formState: { errors: mainErrors } + } = useForm>({ + resolver: zodResolver(schema), + mode: 'onChange' + }); + + const { + register: registerVerification, + watch: watchVerification, + reset: resetVerification, + formState: { errors: verificationErrors } + } = useForm>({ + resolver: zodResolver(verificationSchema), + mode: 'onChange' + }); + + const transAmt = watchVerification('transAmt'); + + useEffect(() => { + if ( + enterpriseRealNameAuthInfo?.data && + enterpriseRealNameAuthInfo.data.paymentStatus === PAYMENTSTATUS.PROCESSING + ) { + const { key, accountBank, accountNo, keyName, usrName, contactInfo } = + enterpriseRealNameAuthInfo.data; + resetMain({ + key, + accountBank, + accountNo, + keyName, + usrName, + contactInfo + }); + setSelectedBank(accountBank || ''); + } + }, [enterpriseRealNameAuthInfo?.data, resetMain]); + + const handleBankSelect = useCallback( + (bankName: string) => { + setSelectedBank(bankName); + setMainValue('accountBank', bankName); + setSearchKeyword(''); + }, + [setMainValue] + ); + + const resetSearchState = useCallback(() => { + clearSearch(); + setSelectedBank(''); + }, [clearSearch]); + + const canPayment = enterpriseRealNameAuthInfo?.data?.paymentStatus !== PAYMENTSTATUS.PROCESSING; + + const canVerify = enterpriseRealNameAuthInfo?.data?.paymentStatus === PAYMENTSTATUS.PROCESSING; + + const canCancel = enterpriseRealNameAuthInfo?.data?.paymentStatus === PAYMENTSTATUS.PROCESSING; + + const canInput = enterpriseRealNameAuthInfo?.data?.paymentStatus !== PAYMENTSTATUS.PROCESSING; + + const remainingAttempts = enterpriseRealNameAuthInfo?.data?.remainingAttempts; + + const [errorMessage, setErrorMessage] = useState(''); + + const enterpriseRealNameAuthPaymentMutation = useMutation(enterpriseRealNameAuthPaymentRequest, { + onSuccess: (data) => { + if (data.code === 200 && data.data?.paymentStatus === PAYMENTSTATUS.PROCESSING) { + message({ + title: t('common:enterprise_realname_payment_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + setErrorMessage(''); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + } else { + message({ + title: t('common:enterprise_realname_payment_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(data.message || t('common:enterprise_realname_payment_failed')); + } + }, + onError: (error: any) => { + message({ + title: t('common:enterprise_realname_payment_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(error.message || t('common:enterprise_realname_payment_failed')); + } + }); + + const enterpriseRealNameAuthVerifyMutation = useMutation(enterpriseRealNameAuthVerifyRequest, { + onSuccess: (data) => { + if (data.code === 200 && data.data?.authState === 'success') { + message({ + title: t('common:enterprise_realname_verify_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + + setErrorMessage(''); + + setSessionProp('user', { + ...useSessionStore.getState().session!.user!, + enterpriseRealName: data.data?.enterpriseRealName + }); + + queryClient.invalidateQueries([session?.token, 'UserInfo']); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + resetMain(); + resetVerification(); + resetSearchState(); + + if (props.onFormSuccess) { + props.onFormSuccess(); + } + } else { + message({ + title: t('common:enterprise_realname_verify_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage( + data.message ? t(data.message as any) : t('common:enterprise_realname_verify_failed') + ); + } + }, + onError: (error: any) => { + message({ + title: t('common:enterprise_realname_verify_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(error.message || t('common:enterprise_realname_verify_failed')); + } + }); + + const enterpriseRealNameAuthCancelMutation = useMutation(enterpriseRealNameAuthCancelRequest, { + onSuccess: (data) => { + if (data.code === 200 && data.data?.paymentStatus === PAYMENTSTATUS.CANCEL) { + message({ + title: t('common:enterprise_realname_cancel_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + setErrorMessage(''); + queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); + } else { + message({ + title: t('common:enterprise_realname_cancel_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(t('common:enterprise_realname_cancel_failed')); + } + }, + onError: (error: any) => { + message({ + title: t('common:enterprise_realname_cancel_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(error.message || t('common:enterprise_realname_cancel_failed')); + } + }); + + const handleVerifyClick = () => { + if (!transAmt || !/^\d{1,3}$/.test(transAmt)) { + message({ + title: t('common:invalid_verification_amount'), + status: 'error', + duration: 2000, + isClosable: true + }); + setErrorMessage(t('common:invalid_verification_amount')); + return; + } + + enterpriseRealNameAuthVerifyMutation.mutate({ transAmt }); + }; + + if (enterpriseRealNameAuthInfoLoading) { + return ( +
+ + {t('common:loading')} +
+ ); + } + + return ( + + { + enterpriseRealNameAuthPaymentMutation.mutate(data); + })} + > + + + {t('common:enterprise_auth_tips')} + + + + + + + {t('common:enterprise_keyname')} + + + + + {mainErrors.keyName?.message} + + + + + + + {t('common:enterprise_key')} + + + + + {mainErrors.key?.message} + + + + + + + {t('common:legal_person')} + + + + + {mainErrors.usrName?.message} + + + + + + + {t('common:account_number')} + + + + + {mainErrors.accountNo?.message} + + + + + + + {t('common:bank_name')} + + + { + const value = e.target.value; + setSearchKeyword(value); + + if (selectedBank && value !== selectedBank) { + setSelectedBank(''); + setMainValue('accountBank', ''); + } + + if (value) { + setIsMenuOpen(true); + } + }} + onFocus={() => { + if (filteredBanksList.length > 0) { + setIsMenuOpen(true); + } + }} + onBlur={(e) => { + setTimeout(() => { + setIsMenuOpen(false); + }, 150); + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.preventDefault(); + clearSearch(); + setIsMenuOpen(false); + } else if (e.key === 'Enter' && filteredBanksList.length > 0) { + e.preventDefault(); + + if (filteredBanksList.length === 1) { + handleBankSelect(filteredBanksList[0].shortName); + setIsMenuOpen(false); + } + } + }} + h="32px" + px="12px" + pr="40px" + w="full" + borderRadius="6px" + borderColor={isMenuOpen ? 'brightBlue.600' : 'grayModern.200'} + bg="grayModern.50" + fontSize="14px" + fontWeight={400} + lineHeight="20px" + color="grayModern.900" + backgroundColor={selectedBank ? 'rgb(231, 240, 254)' : 'grayModern.50'} + _disabled={{ + bg: 'grayModern.100', + color: 'grayModern.400', + cursor: 'not-allowed', + opacity: 0.6 + }} + autoComplete="off" + /> + + + {(searchKeyword || selectedBank) && ( + { + e.stopPropagation(); + setSelectedBank(''); + setMainValue('accountBank', ''); + setSearchKeyword(''); + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + }} + _hover={{ color: 'grayModern.600' }} + > + + + )} + { + e.stopPropagation(); + if (isMenuOpen) { + setIsMenuOpen(false); + } else { + setIsMenuOpen(true); + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + } + }} + > + + + + + {isMenuOpen && ( + + + {Array.isArray(filteredBanksList) && filteredBanksList.length > 0 ? ( + filteredBanksList.map((bank, index) => ( + { + handleBankSelect(bank.shortName); + setIsMenuOpen(false); + }} + h="48px" + px="12px" + py="8px" + cursor="pointer" + bg={selectedBank === bank.shortName ? 'brightBlue.50' : 'white'} + _hover={{ + bg: + selectedBank === bank.shortName ? 'brightBlue.100' : 'grayModern.50' + }} + transition="background-color 0.1s ease" + > + + + + {bank.shortName.charAt(0)} + + + + + {bank.shortName} + + + {selectedBank === bank.shortName && ( + + + + )} + + + )) + ) : searchKeyword ? ( + + 未找到相关银行 + + ) : ( + + {banksError ? t('common:get_code_failed') : '请选择银行'} + + )} + + + )} + + + + + {mainErrors.accountBank?.message} + + + + + + + {t('common:contact_info')} + + + + + {mainErrors.contactInfo?.message} + + + + {(errorMessage || !canPayment) && ( + + + {errorMessage || t('common:verification_amount_tips2')} + + + )} + + + {t('common:verification_amount')} + + + + + + + + {verificationErrors.transAmt?.message} + + + + {t('common:verification_amount_tips')} + + {t('common:link_to_workorder')} + + {i18n.language === 'zh' && '。'} + + + + + + + + + + + + ); +} + +export default RealNameModal; diff --git a/frontend/desktop/src/components/account/driver.tsx b/frontend/desktop/src/components/account/driver.tsx new file mode 100644 index 000000000000..4471cb6c2146 --- /dev/null +++ b/frontend/desktop/src/components/account/driver.tsx @@ -0,0 +1,559 @@ +import { Flex, Text, Box, Center } from '@chakra-ui/react'; +import { driver } from '@sealos/driver'; +import { Config } from '@sealos/driver/src/config'; +import { X } from 'lucide-react'; +import { TFunction } from 'next-i18next'; +import { useGuideModalStore } from '@/stores/guideModal'; +import { track } from '@sealos/gtm'; + +export let currentDriver: any = null; + +export function destroyDriver() { + if (currentDriver) { + currentDriver.destroy(); + currentDriver = null; + } +} + +export function startDriver(config: Config) { + if (currentDriver) { + currentDriver.destroy(); + currentDriver = null; + } + const driverObj = driver(config); + currentDriver = driverObj; + useGuideModalStore.getState().setIsDriverActive(true); + driverObj.drive(); + return driverObj; +} + +export const devboxDriverObj = (openDesktopApp: any, t: TFunction): Config => ({ + onPopoverRender() { + track('guide_start', { + module: 'guide', + guide_name: 'devbox' + }); + }, + + showProgress: true, + allowClose: false, + allowClickMaskNextStep: false, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: false, + stagePadding: 0, + stageRadius: 12, + + // @ts-ignore + steps: [ + { + element: '.system-devbox', + popover: { + side: 'right', + align: 'center', + borderRadius: '12px 12px 12px 12px', + PopoverBody: ( + + + + {t('v2:devbox_create_title')} + + { + track('guide_exit', { + module: 'guide', + guide_name: 'devbox' + }); + + currentDriver.destroy(); + currentDriver = null; + startDriver(quitGuideDriverObj(t)); + }} + > + + + + + {t('v2:devbox_create_desc')} + + + + 1/5 + +
{ + currentDriver.destroy(); + currentDriver = null; + openDesktopApp({ + appKey: 'system-devbox', + pathname: '/', + query: { + action: 'guide' + }, + messageData: {}, + appSize: 'maximize' + }); + }} + > + {t('v2:next')} +
+
+
+ ) + } + } + ], + onHighlightStarted: (element) => { + const el = element as any; + if (el) { + el._originalBorderRadius = el.style.borderRadius; + el._originalBorder = el.style.border; + + el.style.borderRadius = '8px'; + el.style.border = '1.5px solid #1C4EF5'; + } + }, + onDeselected: (element?: Element) => { + if (element) { + const el = element as any; + el.style.borderRadius = el._originalBorderRadius || ''; + el.style.border = el._originalBorder || ''; + } + }, + onDestroyed: () => { + useGuideModalStore.getState().setIsDriverActive(false); + } +}); + +export const appLaunchpadDriverObj = (openDesktopApp: any, t: TFunction): Config => ({ + onPopoverRender() { + track('guide_start', { + module: 'guide', + guide_name: 'applaunchpad' + }); + }, + + showProgress: true, + allowClose: false, + allowClickMaskNextStep: false, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: false, + stagePadding: 0, + stageRadius: 12, + + // @ts-ignore + steps: [ + { + element: '.system-applaunchpad', + popover: { + side: 'right', + align: 'center', + borderRadius: '12px 12px 12px 12px', + + PopoverBody: ( + + + + {t('v2:launchpad_create_title')} + + { + track('guide_exit', { + module: 'guide', + guide_name: 'applaunchpad' + }); + + currentDriver.destroy(); + currentDriver = null; + startDriver(quitGuideDriverObj(t)); + }} + > + + + + + {t('v2:launchpad_create_desc')} + + + + 1/4 + +
{ + currentDriver.destroy(); + currentDriver = null; + openDesktopApp({ + appKey: 'system-applaunchpad', + pathname: '/redirect', + query: { + action: 'guide' + }, + messageData: {}, + appSize: 'maximize' + }); + }} + > + {t('v2:next')} +
+
+
+ ) + } + } + ], + onHighlightStarted: (element) => { + const el = element as any; + if (el) { + el._originalBorderRadius = el.style.borderRadius; + el._originalBorder = el.style.border; + + el.style.borderRadius = '8px'; + el.style.border = '1.5px solid #1C4EF5'; + } + }, + onDeselected: (element?: Element) => { + if (element) { + const el = element as any; + el.style.borderRadius = el._originalBorderRadius || ''; + el.style.border = el._originalBorder || ''; + } + }, + onDestroyed: () => { + useGuideModalStore.getState().setIsDriverActive(false); + } +}); + +export const templateDriverObj = (openDesktopApp: any, t: TFunction): Config => ({ + onPopoverRender() { + track('guide_start', { + module: 'guide', + guide_name: 'appstore' + }); + }, + + showProgress: true, + allowClose: false, + allowClickMaskNextStep: false, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: false, + stagePadding: 0, + stageRadius: 12, + + // @ts-ignore + steps: [ + { + element: '.system-template', + popover: { + side: 'right', + align: 'center', + borderRadius: '12px 12px 12px 12px', + PopoverBody: ( + + + + {t('v2:template_create_title')} + + { + track('guide_exit', { + module: 'guide', + guide_name: 'appstore' + }); + + currentDriver.destroy(); + currentDriver = null; + startDriver(quitGuideDriverObj(t)); + }} + > + + + + + {t('v2:template_create_desc')} + + + + 1/4 + +
{ + currentDriver.destroy(); + currentDriver = null; + openDesktopApp({ + appKey: 'system-template', + pathname: '/', + query: { + action: 'guide' + }, + messageData: {}, + appSize: 'maximize' + }); + }} + > + {t('v2:next')} +
+
+
+ ) + } + } + ], + onHighlightStarted: (element) => { + const el = element as any; + if (el) { + el._originalBorderRadius = el.style.borderRadius; + el._originalBorder = el.style.border; + + el.style.borderRadius = '8px'; + el.style.border = '1.5px solid #1C4EF5'; + } + }, + onDeselected: (element?: Element) => { + if (element) { + const el = element as any; + el.style.borderRadius = el._originalBorderRadius || ''; + el.style.border = el._originalBorder || ''; + } + }, + onDestroyed: () => { + useGuideModalStore.getState().setIsDriverActive(false); + } +}); + +export const databaseDriverObj = (openDesktopApp: any, t: TFunction): Config => ({ + onPopoverRender() { + track('guide_start', { + module: 'guide', + guide_name: 'database' + }); + }, + + showProgress: true, + allowClose: false, + allowClickMaskNextStep: false, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: false, + stagePadding: 0, + stageRadius: 12, + + // @ts-ignore + steps: [ + { + element: '.system-dbprovider', + popover: { + side: 'right', + align: 'center', + borderRadius: '12px 12px 12px 12px', + PopoverBody: ( + + + + {t('v2:database_create_title')} + + { + track('guide_exit', { + module: 'guide', + guide_name: 'database' + }); + + currentDriver.destroy(); + currentDriver = null; + startDriver(quitGuideDriverObj(t)); + }} + > + + + + + {t('v2:database_create_desc')} + + + + 1/4 + +
{ + currentDriver.destroy(); + currentDriver = null; + openDesktopApp({ + appKey: 'system-dbprovider', + pathname: '/redirect', + query: { + action: 'guide' + }, + messageData: {}, + appSize: 'maximize' + }); + }} + > + {t('v2:next')} +
+
+
+ ) + } + } + ], + onHighlightStarted: (element) => { + const el = element as any; + if (el) { + el._originalBorderRadius = el.style.borderRadius; + el._originalBorder = el.style.border; + + el.style.borderRadius = '8px'; + el.style.border = '1.5px solid #1C4EF5'; + } + }, + onDeselected: (element?: Element) => { + if (element) { + const el = element as any; + el.style.borderRadius = el._originalBorderRadius || ''; + el.style.border = el._originalBorder || ''; + } + }, + onDestroyed: () => { + useGuideModalStore.getState().setIsDriverActive(false); + } +}); + +export const quitGuideDriverObj = (t: TFunction): Config => ({ + showProgress: false, + allowClose: false, + allowClickMaskNextStep: true, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: true, + overlayColor: 'transparent', + + steps: [ + { + element: '.guide-button', + popover: { + side: 'bottom', + align: 'end', + PopoverBody: ( + + + + + + {t('v2:guide')} + { + currentDriver.destroy(); + currentDriver = null; + }} + > + + + + + + {t('v2:quit_guide_description')} + +
{ + currentDriver.destroy(); + currentDriver = null; + }} + > + {t('v2:got_it')} +
+
+ ) + } + } + ], + onHighlightStarted: (element) => { + const el = element as any; + if (el) { + el.style.borderRadius = '8px'; + el.style.border = '1px solid #1C4EF5'; + } + }, + onDeselected: (element?: Element) => { + if (element) { + const el = element as any; + el.style.borderRadius = ''; + el.style.border = ''; + } + }, + onDestroyed: (element?: Element) => { + useGuideModalStore.getState().setIsDriverActive(false); + if (element) { + const el = element as any; + el.style.borderRadius = ''; + el.style.border = ''; + } + } +}); diff --git a/frontend/desktop/src/components/account/github.tsx b/frontend/desktop/src/components/account/github.tsx new file mode 100644 index 000000000000..7207b89b7c8b --- /dev/null +++ b/frontend/desktop/src/components/account/github.tsx @@ -0,0 +1,29 @@ +import { Flex, FlexProps, Icon } from '@chakra-ui/react'; + +export default function GithubComponent(props: FlexProps) { + return ( + window.open('https://github.com/labring/sealos')} + > + + + + + ); +} diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx new file mode 100644 index 000000000000..e6fcd133150d --- /dev/null +++ b/frontend/desktop/src/components/account/index.tsx @@ -0,0 +1,746 @@ +import Notification from '@/components/notification'; +import { useCopyData } from '@/hooks/useCopyData'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import download from '@/utils/downloadFIle'; +import { clearSharedAuthCookie } from '@/utils/cookieUtils'; +import { clearPersistedMarketingQuery } from '@/utils/marketing-attribution'; +import { + Box, + Center, + Divider, + Flex, + Image, + Menu, + MenuButton, + MenuItem, + MenuList, + Modal, + ModalCloseButton, + ModalContent, + ModalOverlay, + Text, + useBreakpointValue, + useDisclosure, + useToast +} from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; +import RegionToggle from '../region/RegionToggle'; +import WorkspaceToggle from '../team/WorkspaceToggle'; +import useAppStore from '@/stores/app'; +import { + ArrowLeftRight, + ArrowRight, + Bell, + ChevronRight, + Copy, + Dock, + FileCode, + Globe, + LogOut, + ReceiptText, + RefreshCw, + User +} from 'lucide-react'; +import AccountCenter from './AccountCenter'; +import { useLanguageSwitcher } from '@/hooks/useLanguageSwitcher'; +import { useGuideModalStore } from '@/stores/guideModal'; +import SecondaryLinks from '../SecondaryLinks'; +import { useSubscriptionStore } from '@/stores/subscription'; +import { Badge } from '@sealos/shadcn-ui/badge'; +import { Button } from '@sealos/shadcn-ui/button'; +import { cn } from '@sealos/shadcn-ui'; +import { getPlanBackgroundClass } from '@/utils/styling'; +import { AlertSettings } from './AlertSettings'; +import { rotateKubeconfig } from '@/api/auth'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; + +const baseItemStyle = { + minW: '36px', + h: '40px', + fontSize: '14px', + fontWeight: '500', + color: 'primary', + _hover: { + background: 'secondary' + } +}; + +export default function Account() { + const { layoutConfig, authConfig, commonConfig, isLoaded: configLoaded } = useConfigStore(); + const router = useRouter(); + const { copyData } = useCopyData(); + const { t } = useTranslation(); + const { delSession, session, setToken, setSessionProp } = useSessionStore(); + const user = session?.user; + const queryclient = useQueryClient(); + const kubeconfig = session?.kubeconfig || ''; + const toast = useToast(); + const showDisclosure = useDisclosure(); + const communityDisclosure = useDisclosure(); + const [, setNotificationAmount] = useState(0); + const { openDesktopApp, autolaunch } = useAppStore(); + const { canShowGuide } = useDesktopConfigStore(); + const { openGuideModal, initGuide, autoOpenBlocked, blockAutoOpen } = useGuideModalStore(); + const { toggleLanguage, currentLanguage } = useLanguageSwitcher(); + const onAmount = useCallback((amount: number) => setNotificationAmount(amount), []); + const [showNsId, setShowNsId] = useState(false); + const [alertSettingsOpen, setAlertSettingsOpen] = useState(false); + const [isRotatingKubeconfig, setIsRotatingKubeconfig] = useState(false); + + const emailAlertEnabled = layoutConfig?.common.emailAlertEnabled && authConfig?.idp.email.enabled; + const phoneAlertEnabled = layoutConfig?.common.phoneAlertEnabled && authConfig?.idp.sms.enabled; + const alertSettingsEnabled = emailAlertEnabled || phoneAlertEnabled; + const communityEnabled = layoutConfig?.common.communityEnabled !== false; + const communityQRCodeImage = layoutConfig?.common.communityQRCodeImage; + const communityLink = layoutConfig?.common.communityLink; + + const logout = (e: React.MouseEvent) => { + e.preventDefault(); + clearSharedAuthCookie(); // Clear shared cookie for cross-domain logout + clearPersistedMarketingQuery(); + delSession(); + queryclient.clear(); + router.replace('/signin'); + setToken(''); + }; + + const handleRotateKubeconfig = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (isRotatingKubeconfig) return; + + try { + setIsRotatingKubeconfig(true); + toast({ + title: t('kubeconfig_rotating'), + status: 'info', + duration: 3000, + isClosable: true, + position: 'top' + }); + + const res = await rotateKubeconfig(); + + if (res.code === 200 && res.data?.kubeconfig) { + // Update session with new kubeconfig + setSessionProp('kubeconfig', res.data.kubeconfig); + + toast({ + title: t('kubeconfig_rotated_successfully'), + status: 'success', + duration: 3000, + isClosable: true, + position: 'top' + }); + } else { + throw new Error(res.message || 'Failed to rotate kubeconfig'); + } + } catch (error: any) { + console.error('Failed to rotate kubeconfig:', error); + toast({ + title: t('kubeconfig_rotation_failed'), + description: error?.message || 'An error occurred', + status: 'error', + duration: 5000, + isClosable: true, + position: 'top' + }); + } finally { + setIsRotatingKubeconfig(false); + } + }; + + const openCostcenterApp = ({ page = 'plan', mode = '' }: { page?: string; mode?: string }) => { + openDesktopApp({ + appKey: 'system-costcenter', + pathname: '/', + query: { + page: page, + mode: mode + } + }); + }; + + const isNarrowScreen = + useBreakpointValue({ + base: true, + md: false + }) ?? true; + + useEffect(() => { + // [TODO] Guide is currently not compatible with narrow screen. + // Do not show guide above auto opened windows. + if (Object.hasOwn(router.query, 'openapp') || autolaunch) { + blockAutoOpen(); + return; + } + + if ( + commonConfig?.guideEnabled && + canShowGuide && + initGuide && + !isNarrowScreen && + !autoOpenBlocked + ) { + openGuideModal(); + } + }, [ + router.query, + autolaunch, + blockAutoOpen, + canShowGuide, + commonConfig?.guideEnabled, + initGuide, + openGuideModal, + isNarrowScreen, + autoOpenBlocked + ]); + const { subscriptionInfo } = useSubscriptionStore(); + + return ( + + + +
+ Logo +
+ +
+ + + +
+ +
+ + + {/* */} + {/* +
+
+
*/} + + {/* {layoutConfig?.common.githubStarEnabled && ( + +
+ +
+
+ )} */} + + {/* */} + + + + +
showDisclosure.onToggle()} + > + +
+
+ + + +
+ user avator +
+
+ + + + {user?.name} + + + setShowNsId((s) => !s)} + > + {showNsId ? `NS: ${user?.nsid}` : `ID: ${user?.userId}`} + + { + if (user?.userId && user.nsid) copyData(showNsId ? user?.nsid : user?.userId); + }} + size={12} + color={'#71717A'} + /> + + + + + {layoutConfig?.common.accountSettingEnabled && ( + + + +
+ +
+ + {t('common:account_settings')} + +
+
+
+ )} + setAlertSettingsOpen(true)} + display={alertSettingsEnabled ? 'block' : 'none'} + > + +
+ +
+ + {t('common:alert_settings.menu_item')} + +
+
+ {layoutConfig?.common.subscriptionEnabled && ( + openCostcenterApp({ page: 'plan' })} + > + +
+ +
+ + {t('common:plan')} + + + {subscriptionInfo?.subscription?.PlanName || 'payg'} + +
+
+ )} + + openCostcenterApp({ page: 'billing' })} + > + +
+ +
+ + {t('common:billing')} + +
+
+
+ {communityEnabled && ( + <> + + + + + +
+
+ + {t('common:community.menu.title')} + + + {t('common:community.menu.badge')} + +
+ +
+ + {t('common:community.menu.description')} + +
+
+
+ +
+
+
+
+ + )} + + + + + {/* kubeconfig && download('kubeconfig.yaml', kubeconfig)} + > + + +
+ +
+ + {t('common:theme')} + +
+
+
*/} + kubeconfig && download('kubeconfig.yaml', kubeconfig)} + > + + +
+ +
+ + Kubeconfig + +
+ + {layoutConfig?.common?.kcRotationEnabled && ( + + + + )} + ) => { + e.stopPropagation(); + kubeconfig && copyData(kubeconfig); + }} + _hover={{ + color: 'blue.600' + }} + color="#737373" + > + + + +
+
+ + {layoutConfig?.version === 'cn' && ( + + + +
+ +
+ + {currentLanguage === 'zh' ? '中文' : 'English'} + +
+ + + +
+
+ )} +
+ + + + + + +
+ +
+ + {t('common:log_out')} + +
+
+
+
+
+
+
+ + + + + + +
+
+ +

+ {t('common:community.modal.title')} +

+
+ {communityQRCodeImage && ( + {t('common:community.modal.qr_alt')} + )} +
+ + {t('common:community.modal.description')} + + + + + + {/* + {layoutConfig?.common.workorderEnabled && ( + + {t('common:work_order')} + } + aria-label={'setting'} + /> + + )} + + Kubeconfig + + kubeconfig && download('kubeconfig.yaml', kubeconfig)} + icon={ + + } + aria-label={'Download kc'} + /> + kubeconfig && copyData(kubeconfig)} + icon={} + aria-label={'copy kc'} + /> + + */} + + + ); +} diff --git a/frontend/desktop/src/components/account/trigger.tsx b/frontend/desktop/src/components/account/trigger.tsx new file mode 100644 index 000000000000..eb93240b5fa9 --- /dev/null +++ b/frontend/desktop/src/components/account/trigger.tsx @@ -0,0 +1,44 @@ +import useSessionStore from '@/stores/session'; +import { Center, Flex, Image } from '@chakra-ui/react'; +import { Dispatch, SetStateAction } from 'react'; + +export default function Trigger({ + setShowAccount, + showAccount +}: { + showAccount: boolean; + setShowAccount: Dispatch>; +}) { + const user = useSessionStore((state) => state.session)?.user; + + return ( + { + setShowAccount(true); + }} + cursor={'pointer'} + > +
+ user avator +
+
+ ); +} diff --git a/frontend/desktop/src/components/app_window/index.module.css b/frontend/desktop/src/components/app_window/index.module.css new file mode 100644 index 000000000000..87f97fb50378 --- /dev/null +++ b/frontend/desktop/src/components/app_window/index.module.css @@ -0,0 +1,86 @@ +.appMask { + position: absolute; + inset: 0; + z-index: 4; +} + +.windowContainer { + will-change: transform; + position: fixed; + box-shadow: + 0px 10px 20px rgba(0, 0, 0, 0.25), + 0px 0px 1px rgba(0, 0, 0, 0.25); + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 6px; + transition: all cubic-bezier(0.85, 0.14, 0.14, 0.85) 150ms; + transform-origin: center; + display: flex; + flex-direction: column; + background: #f7f8fa; +} + +.windowContainer[data-size='maximize'] { + .closeBtn, + .toolbar, + .windowScreen { + border-radius: 0; + } +} + +.windowContainer[data-hide='true'] { + opacity: 0; + pointer-events: none; +} + +.windowContainer[data-size='maxmin'] { + top: 10%; + left: 15%; + width: 70%; + height: 80%; +} + +.windowContainer[data-size='minimize'] { + top: 10%; + left: 15%; + width: 70%; + height: 80%; + transform: scale(0.1) translate(0, 0) !important; + transform-origin: bottom; + opacity: 0; + pointer-events: none; +} + +.notrans { + transition: none !important; +} + +.uicon { + width: 46px; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + transition: all ease-in-out 60ms; +} + +.uicon img { + transition: all ease-in-out 60ms; +} + +.uicon:hover { + background: rgba(136, 136, 136, 0.2); +} + +.uicon[data-type='close']:hover { + background: rgba(255, 0, 0, 0.8); +} + +.windowScreen { + width: 100%; + flex-grow: 1; + overflow: hidden; + border-radius: 0 0 6px 6px; +} diff --git a/frontend/desktop/src/components/app_window/index.tsx b/frontend/desktop/src/components/app_window/index.tsx new file mode 100644 index 000000000000..2b9cd439df6a --- /dev/null +++ b/frontend/desktop/src/components/app_window/index.tsx @@ -0,0 +1,266 @@ +/* eslint-disable jsx-a11y/alt-text */ +import useAppStore, { BRAIN_APP_KEY } from '@/stores/app'; +import { Box, Flex, Image, Text } from '@chakra-ui/react'; +import clsx from 'clsx'; +import React, { useRef, useState } from 'react'; +import Draggable, { DraggableEventHandler } from 'react-draggable'; +import styles from './index.module.css'; +import { useTranslation } from 'next-i18next'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; + +export default function AppWindow(props: { + style?: React.CSSProperties; + pid: number; + children: any; +}) { + const { pid } = props; + const { + closeAppById, + updateOpenedAppInfo, + setToHighestLayerById, + currentApp, + findAppInfoById, + maxZIndex + } = useAppStore(); + const logo = useConfigStore().layoutConfig?.logo; + const { t, i18n } = useTranslation(); + const wnapp = findAppInfoById(pid); + const isGuest = useSessionStore((state) => state.isGuest); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const dragDom = useRef(null); + const [dragging, setDragging] = useState(false); + if (!wnapp) return null; + + const handleDragBoundary: DraggableEventHandler = (e, position) => { + const { x, y } = position; + const desktopHeight = document.getElementById('desktop')?.clientHeight; + if (!desktopHeight) { + setPosition({ x: 0, y: 0 }); + return; + } + const appHeaderHeight = dragDom.current?.querySelector('.windowHeader')?.clientHeight || 30; + const appHeaderWidth = dragDom.current?.querySelector('.windowHeader')?.clientWidth || 3000; + + if (currentApp()?.size === 'maxmin') { + let upperBoundary = -desktopHeight * 0.1; + let lowerBoundary = desktopHeight * 0.9 - appHeaderHeight; + setPosition({ + x: + x < 0 + ? x < -0.9 * appHeaderWidth // (0.8width + width/0.70*0.15) + ? 0 + : x + : x > 0.9 * appHeaderWidth + ? 0 + : x, + y: y < upperBoundary ? upperBoundary : y > lowerBoundary ? 0 : y + }); + } else { + setPosition({ + x: x < 0 ? (x < -0.8 * appHeaderWidth ? 0 : x) : x > 0.8 * appHeaderWidth ? 0 : x, + y: y < 0 ? 0 : y > desktopHeight - appHeaderHeight ? 0 : y + }); + } + }; + + const handleMinimize = () => { + updateOpenedAppInfo({ + ...wnapp, + size: 'minimize', + cacheSize: wnapp.size + }); + }; + + const handleMaximizeRestore = () => { + setPosition({ x: 0, y: 0 }); + updateOpenedAppInfo({ + ...wnapp, + size: wnapp?.size === 'maxmin' ? 'maximize' : 'maxmin', + cacheSize: wnapp?.size === 'maxmin' ? 'maximize' : 'maxmin' + }); + }; + + const handleClose = () => { + updateOpenedAppInfo({ + ...wnapp, + isShow: false + }); + closeAppById(pid); + }; + + return ( + { + setDragging(true); + }} + onDrag={(e, position) => { + setPosition(position); + }} + onStop={(e, position) => { + handleDragBoundary(e, position); + setDragging(false); + }} + handle=".windowHeader" + position={position} + > +
+ {/* app window header */} + {wnapp.key !== BRAIN_APP_KEY && ( + { + setToHighestLayerById(pid); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + > + + {wnapp?.name} + + {wnapp?.i18n?.[i18n?.language]?.name + ? wnapp.i18n?.[i18n?.language]?.name + : wnapp?.name} + + {wnapp?.menuData && + wnapp?.menuData?.length > 0 && + wnapp?.menuData?.map((item) => ( + { + typeof item?.link === 'string' && window.open(item?.link); + }} + > + {item.name} + + ))} + + + { + e.stopPropagation(); + e.preventDefault(); + handleMinimize(); + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.preventDefault(); + handleMinimize(); + }} + > + {wnapp?.name} + + { + e.stopPropagation(); + e.preventDefault(); + handleMaximizeRestore(); + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.preventDefault(); + handleMaximizeRestore(); + }} + > + {wnapp?.name} + + { + e.stopPropagation(); + e.preventDefault(); + if (isGuest()) { + handleMinimize(); + } else { + handleClose(); + } + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.preventDefault(); + if (isGuest()) { + handleMinimize(); + } else { + handleClose(); + } + }} + > + {wnapp?.name} + + + + )} + {/* app switch mask */} +
{ + setToHighestLayerById(pid); + }} + style={{ pointerEvents: wnapp.zIndex !== maxZIndex ? 'unset' : 'none' }} + >
+ {/* app window content */} + + {/* Drag necessary to improve fluency */} + {dragging && ( + + )} + {props.children} + +
+
+ ); +} diff --git a/frontend/desktop/src/components/banner/index.tsx b/frontend/desktop/src/components/banner/index.tsx new file mode 100644 index 000000000000..2d77f313587f --- /dev/null +++ b/frontend/desktop/src/components/banner/index.tsx @@ -0,0 +1,120 @@ +import { Box, Center, Fade, Flex, Text, useMediaQuery } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { CurrencySymbol } from '@sealos/ui'; + +interface SaleBannerProps { + isBannerVisible: boolean; + setIsBannerVisible: (value: boolean) => void; +} + +export default function SaleBanner({ isBannerVisible, setIsBannerVisible }: SaleBannerProps) { + const [isMobileSize] = useMediaQuery('(max-width: 780px)'); + const { t } = useTranslation(); + + const closeBanner = () => { + setIsBannerVisible(false); + const today = new Date().toLocaleDateString(); + localStorage.setItem('bannerLastClosed', today); + }; + + const goDetail = () => { + window.open(`https://mp.weixin.qq.com/s/bnLMItoH2NVUZ_smYzhfEg`, '_blank'); + }; + + if (!isBannerVisible || isMobileSize) return null; + + return ( + + +
+ 🎉 + + {t('common:banner.title')} + + + {t('common:banner.description')} + + 10000 + + + {t('common:banner.date')} + +
+ + {t('common:banner.button')} + + + + +
+
+ + + +
+
+
+
+ ); +} diff --git a/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx b/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx new file mode 100644 index 000000000000..e15b604cf35e --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx @@ -0,0 +1,34 @@ +import { useBreakpointValue, Center } from '@chakra-ui/react'; + +export function ChakraIndicator() { + const breakpoint = useBreakpointValue({ + base: 'base 0', + xs: 'xs 375', + sm: 'sm 640', + md: 'md 768', + lg: 'lg 1024', + xl: 'xl 1280', + '2xl': '2xl 1440' + }); + + if (process.env.NODE_ENV === 'production') { + return null; + } + + return ( +
+ {breakpoint} +
+ ); +} diff --git a/frontend/desktop/src/components/desktop_content/GlobalAnnouncement.tsx b/frontend/desktop/src/components/desktop_content/GlobalAnnouncement.tsx new file mode 100644 index 000000000000..4c146cc8ec25 --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/GlobalAnnouncement.tsx @@ -0,0 +1,107 @@ +import { getGlobalNotification } from '@/api/platform'; +import { Box, Center, Text } from '@chakra-ui/react'; +import { ArrowRight, Volume2 } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { useGlobalNotificationStore } from '@/stores/globalNotification'; +import DOMPurify from 'dompurify'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import { useGuideModalStore } from '@/stores/guideModal'; +import { track } from '@sealos/gtm'; + +function GlobalAnnouncementComponent() { + const { i18n, t } = useTranslation(); + const { layoutConfig, commonConfig } = useConfigStore(); + const { isGuest } = useSessionStore(); + const { openGuideModal } = useGuideModalStore(); + + // Hooks must be called unconditionally + const { data: notifications } = useQuery({ + queryKey: ['globalNotification'], + queryFn: async () => { + const { data } = await getGlobalNotification(); + return data || []; + }, + staleTime: 5 * 60 * 1000, + cacheTime: 10 * 60 * 1000, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchInterval: 30 * 60 * 1000, + retry: 1, + enabled: !!layoutConfig?.common.announcementEnabled + }); + + // Check total switch + if (!layoutConfig?.common.announcementEnabled) { + return null; + } + + // Filter 'Desktop-Announcement' type + const announcementNotification = notifications?.find((n) => n?.from === 'Desktop-Announcement'); + + // Determine content and click handler + let content: string; + let onClick: () => void; + let isHtmlContent = false; + + if (announcementNotification?.uid) { + // Show notification content (cannot be closed) + const rawContent = announcementNotification?.i18n[i18n?.language]?.title || ''; + content = DOMPurify.sanitize(rawContent); + isHtmlContent = true; + + onClick = () => {}; + } else { + // Show guide message (fallback) + if (isGuest() || !commonConfig?.guideEnabled) { + return null; + } + content = t('v2:onboard_guide'); + onClick = () => { + track('announcement_click', { + module: 'dashboard', + announcement_id: 'onboarding_guide_prompt' + }); + openGuideModal(); + }; + } + + return ( +
+
+ + + + + + + +
+
+ ); +} + +export const GlobalAnnouncement = memo(GlobalAnnouncementComponent); diff --git a/frontend/desktop/src/components/desktop_content/GlobalNotification.tsx b/frontend/desktop/src/components/desktop_content/GlobalNotification.tsx new file mode 100644 index 000000000000..44af12c8fcd8 --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/GlobalNotification.tsx @@ -0,0 +1,86 @@ +import { getGlobalNotification } from '@/api/platform'; +import { Alert, AlertIcon, AlertDescription, CloseButton, Box } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { Info, X } from 'lucide-react'; +import { useEffect, memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { useGlobalNotificationStore } from '@/stores/globalNotification'; +import DOMPurify from 'dompurify'; + +function GlobalNotificationComponent() { + const { i18n } = useTranslation(); + const { message } = useMessage(); + const { closedNotificationId, setClosedNotificationId } = useGlobalNotificationStore(); + + const { data: notifications } = useQuery({ + queryKey: ['globalNotification'], + queryFn: async () => { + const { data } = await getGlobalNotification(); + return data || []; + }, + staleTime: 5 * 60 * 1000, + cacheTime: 10 * 60 * 1000, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchInterval: 30 * 60 * 1000, + retry: 1, + enabled: true + }); + + // Filter 'Desktop-Alert' type + const notification = notifications?.find((n) => n?.from === 'Desktop-Alert'); + + // [TODO] This is not the correct place to do popup notifications + useEffect(() => { + if (!notification || !notification.licenseFrontend) return; + + const title = notification?.i18n[i18n?.language]?.title; + message({ + title: title, + status: 'info', + isClosable: true + }); + }, [notification, i18n?.language, message]); + + if (!notification) return null; + + const notificationId = notification?.uid; + if (!notificationId) { + return null; + } + if (closedNotificationId === notificationId) { + return null; + } + + const rawContent = notification?.i18n[i18n?.language]?.title || ''; + const sanitizedContent = DOMPurify.sanitize(rawContent); + + const handleClose = () => { + setClosedNotificationId(notificationId); + }; + + return ( + + + + + + + + + + + + ); +} + +export const GlobalNotification = memo(GlobalNotificationComponent); diff --git a/frontend/desktop/src/components/desktop_content/apps.tsx b/frontend/desktop/src/components/desktop_content/apps.tsx new file mode 100644 index 000000000000..554e4bbda30a --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/apps.tsx @@ -0,0 +1,984 @@ +import useAppStore from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; +import { useLicenseCheck } from '@/hooks/useLicenseCheck'; +import useSessionStore from '@/stores/session'; +import { TApp, displayType } from '@/types'; +import { + Box, + Flex, + Grid, + Image, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalCloseButton, + ModalBody, + Center, + useBreakpointValue, + GridProps, + FlexProps +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useMessage } from '@sealos/ui'; +import { + DragEventHandler, + MouseEvent, + MouseEventHandler, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import { useAppDisplayConfigStore } from '@/stores/appDisplayConfig'; +import styles from './index.module.css'; +import { useGuideModalStore } from '@/stores/guideModal'; +import { GlobalAnnouncement } from './GlobalAnnouncement'; +import { currentDriver, destroyDriver } from '../account/driver'; +import { throttle } from 'lodash'; + +const AppItem = ({ + app, + onClick, + onDragStart, + onDragEnd +}: { + app: TApp; + onClick?: (e: MouseEvent, app: TApp) => void; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; +}) => { + const wrapperRef = useRef(null); + + const { i18n } = useTranslation(); + const fallbackIcon = useConfigStore().layoutConfig?.logo || '/logo.svg'; + const forcedIconStyle = app.representativeMeta.forcedIconStyle; + const iconSize = forcedIconStyle === 'contain' ? '60px' : '100%'; + + return ( + +
{ + if (onClick) { + onClick(e, app); + } + }} + onDragStart={onDragStart} + onDragEnd={onDragEnd} + draggable + > + app logo +
+ + {app?.i18n?.[i18n?.language]?.name ? app?.i18n?.[i18n?.language]?.name : app?.name} + +
+ ); +}; + +const AppGrid = ({ + children, + gridGap, + rows, + appHeight, + columns, + gridProps +}: { + children: ReactNode; + gridGap?: number; + rows: number; + appHeight: number; + columns: number; + gridProps?: Omit< + GridProps, + 'templateColumns' | 'templateRows' | 'gap' | 'flexGrow' | 'flexShrink' | 'flexBasis' + >; +}) => { + return ( + + {children} + + ); +}; + +const AppGridPagingContainer = ({ + children, + gridGap, + appHeight, + columns, + totalPages, + currentPage, + handleNavigation, + pageGap, + onChange, + dragContainerProps +}: { + children: ReactNode; + dragContainerProps?: FlexProps; + gridGap: number; + appHeight: number; + columns: number; + totalPages: number; + currentPage: number; + handleNavigation: boolean; + pageGap: number; + onChange: (currentPage: number, pageSize: number) => void; +}) => { + const dragContainerRef = useRef(null); + const gridContainerRef = useRef(null); + + const [pageWidth, setPageWidth] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(8); + const [dragDelta, setDragDelta] = useState(0); + const [dragStartX, setDragStartX] = useState(0); + const [dragStartY, setDragStartY] = useState(0); + + const clampedCurrentPage = Math.min(Math.max(currentPage, 0), totalPages - 1); + const scrollPosition = + (pageWidth + pageGap) * clampedCurrentPage - dragDelta > + (totalPages - 1) * (pageWidth + pageGap) + ? (pageWidth + pageGap) * clampedCurrentPage + : (pageWidth + pageGap) * clampedCurrentPage - dragDelta; + + const calculateItemsPerPage = useCallback(() => { + if (!gridContainerRef.current) return 8; + + const height = gridContainerRef.current.clientHeight; + // At least 1 row in a grid. + const rows = Math.max(1, Math.floor((height + gridGap) / (appHeight + gridGap))); + + return rows * columns; + }, [columns, gridGap, appHeight]); + + const calculatePageWidth = useCallback(() => { + if (!gridContainerRef.current) return 0; + + return (gridContainerRef.current.scrollWidth + pageGap) / totalPages - pageGap; + }, [totalPages, pageGap]); + + // Calculate items per page and scroll position on initial render and on screen size changes + useEffect(() => { + const handleResize = throttle( + () => { + setItemsPerPage(calculateItemsPerPage()); + }, + 200, + { leading: true, trailing: false } + ); + + handleResize(); + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [columns, calculateItemsPerPage]); + + // Calculate grid width on screen size changes + useEffect(() => { + const handleResize = throttle( + () => { + setPageWidth(calculatePageWidth()); + }, + 50, + { leading: true, trailing: false } + ); + + handleResize(); + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [calculatePageWidth]); + + // Call onChange callback when something changes + useEffect(() => { + onChange(clampedCurrentPage, itemsPerPage); + }, [clampedCurrentPage, itemsPerPage, onChange]); + + // Keyboard pagination + useEffect(() => { + const handleKeyboardPagination = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + onChange(Math.max(0, currentPage - 1), itemsPerPage); + } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + onChange(Math.min(totalPages - 1, currentPage + 1), itemsPerPage); + } + }; + + if (handleNavigation) { + document.addEventListener('keydown', handleKeyboardPagination); + + return () => { + document.removeEventListener('keydown', handleKeyboardPagination); + }; + } + }, [handleNavigation, currentPage, totalPages, itemsPerPage, onChange]); + + // Pointer drag pagination + useEffect(() => { + const wrapper = dragContainerRef.current; + if (!wrapper) return; + + const handlePointerDown = (e: PointerEvent) => { + if (!dragContainerRef.current) return; + + setDragStartX(e.screenX); + setDragStartY(e.screenY); + }; + + const handlePointerMove = (e: PointerEvent) => { + if (e.pressure <= 0) return; + + // Handles dragging outside of the wrapper + // Prevents the pointer from being captured by the wrapper when *clicking* on the icons + if ( + !wrapper.hasPointerCapture(e.pointerId) && + Math.hypot(e.screenX - dragStartX, e.screenY - dragStartY) > 48 + ) { + wrapper.setPointerCapture(e.pointerId); + } + + setDragDelta(e.screenX - dragStartX); + }; + + const handlePointerUp = (e: PointerEvent) => { + if (wrapper.hasPointerCapture(e.pointerId)) { + wrapper.releasePointerCapture(e.pointerId); + } + + if (Math.abs(dragDelta) > 96) { + if (dragDelta < -96) { + onChange(Math.min(totalPages - 1, currentPage + 1), itemsPerPage); + } else if (dragDelta > 96) { + onChange(Math.max(0, currentPage - 1), itemsPerPage); + } + } + + setDragDelta(0); + }; + + if (handleNavigation) { + wrapper.addEventListener('pointerdown', handlePointerDown, { passive: true }); + wrapper.addEventListener('pointermove', handlePointerMove); + wrapper.addEventListener('pointerup', handlePointerUp); + wrapper.addEventListener('pointercancel', handlePointerUp); + + return () => { + wrapper.removeEventListener('pointerdown', handlePointerDown); + wrapper.removeEventListener('pointermove', handlePointerMove); + wrapper.removeEventListener('pointerup', handlePointerUp); + wrapper.removeEventListener('pointercancel', handlePointerUp); + }; + } + }, [ + handleNavigation, + currentPage, + totalPages, + itemsPerPage, + onChange, + dragStartX, + dragStartY, + dragDelta + ]); + + return ( + + + {children} + + + ); +}; + +const MoreAppsFolder = ({ + apps, + onClick, + onDrop +}: { + apps: TApp[]; + onClick?: MouseEventHandler; + onDrop?: DragEventHandler; +}) => { + const folderIconRef = useRef(null); + + const fallbackIcon = useConfigStore().layoutConfig?.logo || '/logo.svg'; + + const handleDragOverFolder = (e: React.DragEvent) => { + e.preventDefault(); + + if (folderIconRef.current) { + folderIconRef.current.classList.add(styles.folderPulse); + } + }; + + const handleDragLeaveFolder = (e: React.DragEvent) => { + e.preventDefault(); + + if (folderIconRef.current) { + folderIconRef.current.classList.remove(styles.folderPulse); + } + }; + + const handleDropOnFolder = (e: React.DragEvent) => { + e.preventDefault(); + + if (folderIconRef.current) { + folderIconRef.current.classList.remove(styles.folderPulse); + } + }; + + return ( +
+ + { + handleDropOnFolder(e); + + if (onDrop) { + onDrop(e); + } + }} + > + + {apps.length > 0 + ? apps.slice(0, 4).map((app, idx) => ( + + app icon + + )) + : Array(4) + .fill(null) + .map((_, idx) => ( + + ))} + + + +
+ ); +}; + +const PageSwitcher = ({ + pages, + current, + onChange +}: { + pages: number; + current: number; + onChange: (target: number) => void; +}) => { + return ( + + {Array.from({ length: pages }).map((_, index) => ( + onChange(index)} + _hover={{ bg: index === current ? 'gray.400' : 'gray.300' }} + /> + ))} + + ); +}; + +export default function Apps() { + const { t } = useTranslation(); + const { message } = useMessage(); + const { installedApps, openApp, openDesktopApp } = useAppStore(); + const { appDisplayConfigs, updateAppDisplayType } = useAppDisplayConfigStore(); + const { layoutConfig, commonConfig } = useConfigStore(); + const { isUserLogin } = useSessionStore(); + const { hasLicense } = useLicenseCheck({ + enabled: isUserLogin() && !!commonConfig?.licenseCheckEnabled + }); + const [draggedFromFolder, setDraggedFromFolder] = useState(false); + const [isFolderOpen, setIsFolderOpen] = useState(false); + const { isGuest, openGuestLoginModal } = useSessionStore(); + const desktopRef = useRef(null); + const folderRef = useRef(null); + const modalContentRef = useRef(null); + + // For hiding the flickers when opening the folder + const [isModalContentVisible, setIsModalContentVisible] = useState(false); + + // [TODO] Guide is currently not compatible with narrow screen. + const isNarrowScreen = + useBreakpointValue({ + base: true, + lg: false + }) ?? true; + + // min = 1 line app name + // max = 2 lines app name + const appHeight = + useBreakpointValue({ + base: 112, + md: 126 + }) ?? 112; + const columns = + useBreakpointValue({ + base: 2, + sm: 3, + lg: 5 + }) ?? 2; + + const gridGap = 10; + + const [itemsPerPageInGrid, setItemsPerPageInGrid] = useState(0); + const [currentPageInGrid, setCurrentPageInGrid] = useState(0); + + const [itemsPerPageInFolder, setItemsPerPageInFolder] = useState(0); + const [totalPagesInFolder, setTotalPagesInFolder] = useState(0); + const [currentPageInFolder, setCurrentPageInFolder] = useState(0); + + const getAppDisplayType = useCallback( + (app: TApp): displayType => { + return appDisplayConfigs[app.key] || app.displayType; + }, + [appDisplayConfigs] + ); + + const normalApps = useMemo(() => { + return installedApps.filter((app) => getAppDisplayType(app) === 'normal'); + }, [installedApps, getAppDisplayType]); + + const moreApps = useMemo(() => { + return installedApps.filter((app) => getAppDisplayType(app) === 'more'); + }, [installedApps, getAppDisplayType]); + + // Placed on desktop, but there's not enough space to show these apps on desktop + const dynamicApps = useMemo(() => { + if (itemsPerPageInGrid === 0) return []; + return normalApps.slice(itemsPerPageInGrid - 1); + }, [normalApps, itemsPerPageInGrid]); + + const folderApps = useMemo(() => { + return [...dynamicApps, ...moreApps]; + }, [dynamicApps, moreApps]); + + const desktopPages = useMemo(() => { + const firstPageApps = itemsPerPageInGrid > 0 ? normalApps.slice(0, itemsPerPageInGrid - 1) : []; + return [firstPageApps]; + }, [normalApps, itemsPerPageInGrid]); + + const folderPages = useMemo(() => { + const getPageInFolder = (pageIndex: number) => { + const start = pageIndex * itemsPerPageInFolder; + const end = start + itemsPerPageInFolder; + return folderApps.slice(start, end); + }; + + const pages = Array.from({ length: totalPagesInFolder }).map((_, index) => + getPageInFolder(index) + ); + + return pages; + }, [folderApps, itemsPerPageInFolder, totalPagesInFolder]); + + const { isDriverActive } = useGuideModalStore(); + + const handleAppClick = (e: MouseEvent, item: TApp) => { + if (isGuest()) { + const guestAllowedApps = ['system-brain']; + if (!guestAllowedApps.includes(item.key)) { + e.preventDefault(); + openGuestLoginModal(); + return; + } + } + + if (commonConfig?.licenseCheckEnabled && hasLicense === false && item.key !== 'user-license') { + message({ + title: t('license_required'), + status: 'warning', + isClosable: true + }); + const licenseApp = installedApps.find((app) => app.key === 'user-license'); + if (licenseApp) { + closeFolder(); + openApp(licenseApp); + } + return; + } + + if (isDriverActive) { + const guidedElements = [ + 'system-devbox', + 'system-applaunchpad', + 'system-template', + 'system-dbprovider' + ]; + if (guidedElements.includes(item.key)) { + if (openDesktopApp) { + openDesktopApp({ + appKey: item.key, + pathname: + item.key === 'system-applaunchpad' || item.key === 'system-dbprovider' + ? '/redirect' + : '/', + query: { + action: 'guide' + }, + messageData: {}, + appSize: 'maximize' + }); + } + if (currentDriver) { + destroyDriver(); + } + return; + } + } + + e.preventDefault(); + closeFolder(); + if (item?.name) { + openApp(item); + } + }; + + const handleMoreAppsClick = (e: MouseEvent) => { + if (isGuest()) { + e.preventDefault(); + openGuestLoginModal(); + return; + } + e.stopPropagation(); + + if (e.currentTarget) { + const rect = e.currentTarget.getBoundingClientRect(); + } + + setIsFolderOpen(true); + }; + + const closeFolder = () => { + setIsFolderOpen(false); + setCurrentPageInFolder(0); + }; + + const handleDragStart = ( + e: React.DragEvent, + app: TApp, + source: 'desktop' | 'folder' + ) => { + // these apps are not allowed to be dragged + const notDraggableApps = [ + 'system-devbox', + 'system-applaunchpad', + 'system-template', + 'system-dbprovider' + ]; + if (notDraggableApps.includes(app.key)) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + e.dataTransfer.setData('application/json', JSON.stringify({ app, source })); + + if (source === 'folder') { + setDraggedFromFolder(true); + } + }; + + const handleDragEnd = () => { + setDraggedFromFolder(false); + }; + + const handleDrag = (e: React.DragEvent) => { + if (draggedFromFolder) { + const mouseX = e.clientX; + const mouseY = e.clientY; + + if (modalContentRef.current) { + const rect = modalContentRef.current.getBoundingClientRect(); + const isOutside = + mouseX < rect.left - 5 || + mouseX > rect.right + 5 || + mouseY < rect.top - 5 || + mouseY > rect.bottom + 5; + + if (isOutside) { + closeFolder(); + } + } + } + }; + + const handleDesktopDrop = (e: React.DragEvent) => { + console.log('handleDesktopDrop'); + + e.preventDefault(); + + try { + const data: { app: TApp; source: 'desktop' | 'folder' } = JSON.parse( + e.dataTransfer.getData('application/json') + ); + + if (data && data.app) { + console.log(data.source, 'data.source', isFolderOpen, 'isFolderOpen'); + if (data.source === 'folder' && !isFolderOpen) { + updateAppDisplayType(data.app.key, 'normal'); + console.log('将应用移动到桌面:', data.app.name); + } + } + } catch (error) {} + }; + + const handleMoreAppsDrop = (e: React.DragEvent) => { + try { + const data: { app: TApp; source: 'desktop' | 'folder' } = JSON.parse( + e.dataTransfer.getData('application/json') + ); + + if (data && data.app) { + if (data.source === 'desktop') { + updateAppDisplayType(data.app.key, 'more'); + } + } + } catch (error) {} + }; + + // Close folder when clicking outside + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + if (folderRef.current && !folderRef.current.contains(e.target as Node) && isFolderOpen) { + closeFolder(); + } + }; + + document.addEventListener('mousedown', handleOutsideClick as any); + + return () => { + document.removeEventListener('mousedown', handleOutsideClick as any); + }; + }, [isFolderOpen]); + + // Fade in Modal content on open to mask layout calculation flicker + useEffect(() => { + if (isFolderOpen) { + setIsModalContentVisible(false); + const raf = requestAnimationFrame(() => { + setIsModalContentVisible(true); + }); + return () => cancelAnimationFrame(raf); + } + setIsModalContentVisible(false); + }, [isFolderOpen]); + + const gradientIconStyle = { + '.gradient-icon': { + svg: { + stroke: 'url(#iconGradient)' + }, + '&::before': { + content: '""', + position: 'absolute', + width: '0', + height: '0' + } + } + }; + + return ( + e.preventDefault()} + onDrag={handleDrag} + onDrop={handleDesktopDrop} + sx={gradientIconStyle} + > + + + + + + + + + + + {layoutConfig?.common.announcementEnabled && } + + + { + setCurrentPageInGrid(currentPage); + setItemsPerPageInGrid(pageSize); + }} + > + {desktopPages.map((page, pageIndex) => ( + + {page.map((app, index) => ( + handleDragStart(e, app, 'desktop')} + onDragEnd={handleDragEnd} + /> + ))} + + + + ))} + + + + + + + + + + { + setCurrentPageInFolder(currentPage); + setItemsPerPageInFolder(pageSize); + setTotalPagesInFolder(Math.ceil(folderApps.length / pageSize)); + }} + > + {folderPages.map((page, pageIndex) => ( + + {page.map((app, index) => ( + handleDragStart(e, app, 'folder')} + onDragEnd={handleDragEnd} + /> + ))} + + ))} + + + + setCurrentPageInFolder(page)} + /> + + + + + + ); +} diff --git a/frontend/desktop/src/components/desktop_content/assistant.tsx b/frontend/desktop/src/components/desktop_content/assistant.tsx new file mode 100644 index 000000000000..21d2a3d7938a --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/assistant.tsx @@ -0,0 +1,53 @@ +import { Flex, Icon, Text } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; + +export default function Assistant() { + const { t } = useTranslation(); + const { message } = useMessage(); + + return ( + { + message({ + title: t('common:under_active_development') + }); + }} + > + + + + + 🤖 + + + {t('common:sealos_copilot')} + + ); +} diff --git a/frontend/desktop/src/components/desktop_content/iframe_window.tsx b/frontend/desktop/src/components/desktop_content/iframe_window.tsx new file mode 100644 index 000000000000..5af936adb6be --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/iframe_window.tsx @@ -0,0 +1,20 @@ +import useAppStore from '@/stores/app'; +import { useMemo } from 'react'; +import styles from './index.module.css'; + +export default function Iframe_window({ pid, onLoad }: { pid: number; onLoad?: () => void }) { + const findAppInfo = useAppStore((state) => state.findAppInfoById); + const app = findAppInfo(pid); + const url = useMemo(() => app?.data?.url || '', [app?.data?.url]); + if (!url) return null; + + return ( +