├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── actions │ ├── set-up-git-config │ │ └── action.yml │ └── upload-coverage-artifact │ │ └── action.yml └── workflows │ ├── _coverage_report.yml │ ├── _lint.yml │ ├── _test_unit.yml │ ├── issues_delayed-auto-close.yml │ ├── issues_new-add-triage-label.yml │ ├── release_release-please.yml │ ├── release_trdl-publish.yml │ ├── release_trdl-release.yml │ └── test_pr.yml ├── .gitignore ├── .golangci.yaml ├── .prettierignore ├── .prettierrc.yaml ├── CHANGELOG.md ├── LICENSE ├── OWNERS ├── README.md ├── Taskfile.dist.yaml ├── cmd └── nelm │ ├── chart.go │ ├── chart_dependency.go │ ├── chart_dependency_download.go │ ├── chart_dependency_update.go │ ├── chart_download.go │ ├── chart_lint.go │ ├── chart_pack.go │ ├── chart_render.go │ ├── chart_secret.go │ ├── chart_secret_file.go │ ├── chart_secret_file_decrypt.go │ ├── chart_secret_file_edit.go │ ├── chart_secret_file_encrypt.go │ ├── chart_secret_key.go │ ├── chart_secret_key_create.go │ ├── chart_secret_key_rotate.go │ ├── chart_secret_values_file.go │ ├── chart_secret_values_file_decrypt.go │ ├── chart_secret_values_file_edit.go │ ├── chart_secret_values_file_encrypt.go │ ├── chart_upload.go │ ├── common.go │ ├── groups.go │ ├── main.go │ ├── release.go │ ├── release_get.go │ ├── release_history.go │ ├── release_install.go │ ├── release_list.go │ ├── release_list_legacy.go │ ├── release_plan.go │ ├── release_plan_install.go │ ├── release_rollback.go │ ├── release_uninstall.go │ ├── release_uninstall_legacy.go │ ├── repo.go │ ├── repo_add.go │ ├── repo_login.go │ ├── repo_logout.go │ ├── repo_remove.go │ ├── repo_update.go │ ├── root.go │ ├── usage.go │ └── version.go ├── go.mod ├── go.sum ├── internal ├── chart │ ├── capabilities.go │ ├── chart_downloader.go │ ├── chart_tree.go │ └── notes.go ├── common │ └── common.go ├── kube │ ├── common.go │ ├── discovery_kube_client.go │ ├── dynamic_kube_client.go │ ├── factory.go │ ├── interface.go │ ├── kube_client.go │ ├── kube_config.go │ ├── kube_mapper.go │ ├── legacy_client_getter.go │ └── static_kube_client.go ├── legacy │ └── deploy │ │ ├── resources_waiter.go │ │ └── stages_splitter.go ├── lock │ └── lock_manager.go ├── plan │ ├── calculate_planned_changes.go │ ├── common.go │ ├── dependency │ │ ├── external_dependency.go │ │ ├── internal_dependency.go │ │ ├── internal_dependency_detector.go │ │ └── resource_state.go │ ├── deploy_failure_plan_builder.go │ ├── deploy_plan_builder.go │ ├── log_planned_changes.go │ ├── operation │ │ ├── apply_resource_operation.go │ │ ├── create_pending_release_operation.go │ │ ├── create_resource_operation.go │ │ ├── delete_release_operation.go │ │ ├── delete_resource_operation.go │ │ ├── fail_release_operation.go │ │ ├── interface.go │ │ ├── pending_uninstall_release_operation.go │ │ ├── recreate_resource_operation.go │ │ ├── stage_operation.go │ │ ├── succeed_release_operation.go │ │ ├── supersede_release_operation.go │ │ ├── track_resource_absence.go │ │ ├── track_resource_presence.go │ │ ├── track_resource_readiness_operation.go │ │ └── update_resource_operation.go │ ├── plan.go │ ├── plan_executor.go │ ├── resourceinfo │ │ ├── build_deployable_resources_infos.go │ │ ├── deployable_general_resource_info.go │ │ ├── deployable_hook_resource_info.go │ │ ├── deployable_prev_release_general_resource_info.go │ │ ├── deployable_prev_release_hook_resource_info.go │ │ ├── deployable_release_namespace_info.go │ │ ├── deployable_resources_processor.go │ │ ├── deployable_standalone_crd_info.go │ │ └── util.go │ ├── uninstall_plan_builder.go │ └── util.go ├── release │ ├── histories.go │ ├── history.go │ ├── legacy_release.go │ ├── release.go │ ├── release_differ.go │ └── storage.go ├── resource │ ├── common.go │ ├── drop_invalid_annotations_and_labels_transformer.go │ ├── extra_metadata_patcher.go │ ├── general_resource.go │ ├── hook_resource.go │ ├── id │ │ └── resource_id.go │ ├── matcher │ │ └── resource_matcher.go │ ├── release_metadata_patcher.go │ ├── release_namespace.go │ ├── remote_resource.go │ ├── resource_lists_transformer.go │ ├── resource_patcher.go │ ├── resource_transformer.go │ ├── sort.go │ ├── standalone_crd.go │ └── util.go ├── track │ └── printer.go └── util │ ├── diff.go │ ├── groupversion.go │ ├── json.go │ ├── multierror.go │ ├── namespace.go │ ├── properties.go │ └── string.go ├── nelm.asc ├── pkg ├── action │ ├── action_suite_test.go │ ├── chart_lint.go │ ├── chart_render.go │ ├── common.go │ ├── errors.go │ ├── release_get.go │ ├── release_install.go │ ├── release_list.go │ ├── release_plan_install.go │ ├── release_rollback.go │ ├── release_uninstall.go │ ├── release_uninstall_legacy.go │ ├── report.go │ ├── secret_file_decrypt.go │ ├── secret_file_edit.go │ ├── secret_file_encrypt.go │ ├── secret_key_create.go │ ├── secret_key_rotate.go │ ├── secret_values_file_decrypt.go │ ├── secret_values_file_edit.go │ ├── secret_values_file_encrypt.go │ ├── util.go │ └── version.go ├── featgate │ └── feat.go ├── log │ ├── common.go │ ├── interface.go │ ├── logboek_logger.go │ └── null_logger.go └── secret │ ├── common.go │ ├── decrypt.go │ ├── edit.go │ ├── encrypt.go │ └── rotate.go ├── resources └── images │ ├── graph.png │ ├── graph.svg │ ├── nelm-release-install.gif │ └── nelm-release-plan-install.gif ├── scripts ├── builder │ └── Dockerfile └── verify-dist-binaries.sh ├── trdl.yaml └── trdl_channels.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # Hidden files and directories 2 | .* 3 | 4 | # Dockerfiles 5 | Dockerfile* 6 | 7 | # Documentation 8 | *.md 9 | 10 | # Configuration (except Taskfile.dist.yaml) 11 | *.yaml 12 | !Taskfile.dist.yaml 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | ; golang 15 | [*.go] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | ; yaml 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | indent_style = space 23 | 24 | ;python pep8 indentation 25 | [*.py] 26 | indent_style = space 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🪲 Bug 2 | description: Submit a bug report 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: "Before proceeding" 7 | options: 8 | - label: "I didn't find a similar [issue](https://github.com/werf/nelm/issues)" 9 | required: true 10 | - type: input 11 | attributes: 12 | label: Version 13 | placeholder: "1.2.3" 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: How to reproduce 19 | placeholder: | 20 | 1. In this environment... 21 | 2. With this configuration... 22 | 3. Run '...' 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Result 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Expected result 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Additional information 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Discussions 4 | url: https://github.com/werf/nelm/discussions/categories/general 5 | about: Ask a question 6 | - name: 💬 Telegram channel [EN] 7 | url: https://t.me/werf_io 8 | about: Ask a question 9 | - name: 💬 Telegram channel [RU] 10 | url: https://t.me/werf_ru 11 | about: Ask a question 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature 2 | description: Submit a feature request or suggestion 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: "Before proceeding" 7 | options: 8 | - label: "I didn't find a similar [issue](https://github.com/werf/nelm/issues)" 9 | required: true 10 | - type: textarea 11 | attributes: 12 | label: Problem 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Solution (if you have one) 18 | - type: textarea 19 | attributes: 20 | label: Additional information 21 | -------------------------------------------------------------------------------- /.github/actions/set-up-git-config/action.yml: -------------------------------------------------------------------------------- 1 | name: Set up git config 2 | runs: 3 | using: composite 4 | steps: 5 | - name: Set up git config 6 | run: | 7 | git config --global user.name "borya" 8 | git config --global user.email "borya@flant.com" 9 | shell: bash 10 | -------------------------------------------------------------------------------- /.github/actions/upload-coverage-artifact/action.yml: -------------------------------------------------------------------------------- 1 | name: Upload coverage artifact 2 | inputs: 3 | coverage: 4 | default: false 5 | type: string 6 | runs: 7 | using: composite 8 | steps: 9 | - if: inputs.coverage == 'true' 10 | name: Set timestamp 11 | shell: bash 12 | run: echo "TIMESTAMP=$(date +%H%M%S%N)" >> $GITHUB_ENV 13 | 14 | - if: inputs.coverage == 'true' 15 | name: Upload coverage artifact 16 | uses: actions/upload-artifact@v4 17 | with: 18 | name: coverage-${{ env.TIMESTAMP }} 19 | path: coverage 20 | -------------------------------------------------------------------------------- /.github/workflows/_coverage_report.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | env: 11 | DEBIAN_FRONTEND: "noninteractive" 12 | 13 | jobs: 14 | _: 15 | runs-on: ubuntu-22.04 16 | timeout-minutes: 30 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version-file: go.mod 25 | 26 | - name: Download Code Climate test-reporter 27 | run: | 28 | curl -sSL https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 --output reporter 29 | chmod +x ./reporter 30 | 31 | - name: Download coverage artifact 32 | uses: actions/download-artifact@v4 33 | with: 34 | path: coverage 35 | 36 | - name: Install gocovmerge 37 | run: go install github.com/wadey/gocovmerge@latest 38 | 39 | - name: Merge coverage files into one 40 | run: | 41 | coverage_files=$(find coverage -name '*.out') 42 | gocovmerge ${coverage_files[@]} > coverage.out 43 | 44 | - name: Format and upload coverage report 45 | run: | 46 | export GIT_BRANCH="${GITHUB_REF:11}" 47 | export GIT_COMMIT_SHA="$GITHUB_SHA" 48 | 49 | ./reporter format-coverage -t=gocov -p=github.com/werf/nelm/ coverage.out 50 | ./reporter upload-coverage 51 | env: 52 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 53 | -------------------------------------------------------------------------------- /.github/workflows/_lint.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | forceSkip: 7 | default: false 8 | type: string 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | env: 15 | DEBIAN_FRONTEND: "noninteractive" 16 | 17 | jobs: 18 | _: 19 | if: inputs.forceSkip == 'false' 20 | runs-on: ubuntu-22.04 21 | timeout-minutes: 30 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version-file: go.mod 30 | 31 | - name: Install Task 32 | uses: arduino/setup-task@v2 33 | with: 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Install golangci-lint 37 | run: task -p deps:install:golangci-lint 38 | 39 | - name: Lint 40 | run: task -p lint 41 | -------------------------------------------------------------------------------- /.github/workflows/_test_unit.yml: -------------------------------------------------------------------------------- 1 | name: xxxxx(internal) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | packages: 7 | description: Comma-separated package paths to test 8 | type: string 9 | excludePackages: 10 | description: Comma-separated package paths to exclude from testing 11 | type: string 12 | coverage: 13 | default: false 14 | type: string 15 | forceSkip: 16 | default: false 17 | type: string 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | env: 24 | DEBIAN_FRONTEND: "noninteractive" 25 | 26 | jobs: 27 | _: 28 | if: inputs.forceSkip == 'false' 29 | runs-on: ubuntu-22.04 30 | timeout-minutes: 60 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | cache: true 39 | go-version-file: go.mod 40 | 41 | - name: Install Task 42 | uses: arduino/setup-task@v2 43 | with: 44 | repo-token: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Set up git config 47 | uses: ./.github/actions/set-up-git-config 48 | 49 | # TODO: don't build ginkgo everytime? We need distributable binaries 50 | - name: Install ginkgo 51 | run: task -p deps:install:ginkgo 52 | 53 | - name: Test 54 | run: | 55 | if ${{ inputs.coverage }}; then 56 | task -p test:unit paths="$(echo ${{ inputs.packages }} | tr , ' ')" -- --coverprofile="$(openssl rand -hex 6)-coverage.out" --keep-going --skip-package '${{ inputs.excludePackages }}' 57 | mv *-coverage.out "$GITHUB_WORKSPACE/coverage/" 58 | else 59 | task -p test:unit paths="$(echo ${{ inputs.packages }} | tr , ' ')" -- --keep-going --skip-package '${{ inputs.excludePackages }}' 60 | fi 61 | echo loadavg: $(cat /proc/loadavg) 62 | 63 | - name: Upload coverage artifact 64 | uses: ./.github/actions/upload-coverage-artifact 65 | with: 66 | coverage: ${{ inputs.coverage }} 67 | -------------------------------------------------------------------------------- /.github/workflows/issues_delayed-auto-close.yml: -------------------------------------------------------------------------------- 1 | name: issues:delayed-auto-close 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * *" 6 | issue_comment: 7 | types: 8 | - created 9 | issues: 10 | types: 11 | - labeled 12 | pull_request_target: 13 | types: 14 | - labeled 15 | repository_dispatch: 16 | types: ["issues:delayed-auto-close"] 17 | workflow_dispatch: 18 | 19 | jobs: 20 | manage: 21 | runs-on: ubuntu-22.04 22 | timeout-minutes: 20 23 | steps: 24 | - uses: tiangolo/issue-manager@0.4.0 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | config: > 28 | { 29 | "solved": { 30 | "delay": "604800", 31 | "message": "This issue appears to be resolved, so we’re closing it for now. If you encounter further issues, please feel free to reopen or create a new issue at any time.", 32 | "remove_label_on_comment": true, 33 | "remove_label_on_close": true 34 | }, 35 | "awaiting response": { 36 | "delay": "604800", 37 | "message": "As we haven’t received additional information, we’re closing this issue for now. If there’s more to add, feel free to reopen or open a new issue whenever needed.", 38 | "remove_label_on_comment": true, 39 | "remove_label_on_close": true 40 | } 41 | } 42 | 43 | notify: 44 | if: github.event_name == 'schedule' && always() 45 | needs: manage 46 | uses: werf/common-ci/.github/workflows/notification.yml@main 47 | secrets: 48 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 49 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 50 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 51 | -------------------------------------------------------------------------------- /.github/workflows/issues_new-add-triage-label.yml: -------------------------------------------------------------------------------- 1 | name: issues:new-add-triage-label 2 | on: 3 | issues: 4 | types: 5 | - reopened 6 | - opened 7 | 8 | jobs: 9 | label_issues: 10 | runs-on: ubuntu-22.04 11 | permissions: 12 | issues: write 13 | steps: 14 | - run: gh issue edit "$NUMBER" --add-label "$LABELS" 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | GH_REPO: ${{ github.repository }} 18 | NUMBER: ${{ github.event.issue.number }} 19 | LABELS: triage 20 | -------------------------------------------------------------------------------- /.github/workflows/release_release-please.yml: -------------------------------------------------------------------------------- 1 | name: release:release-please 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "[0-9]+.[0-9]+.[0-9]+*" 7 | - "[0-9]+.[0-9]+" 8 | - "[0-9]+" 9 | repository_dispatch: 10 | types: ["release:release-please"] 11 | workflow_dispatch: 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | jobs: 18 | release-please: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - name: Extract branch name 22 | run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT 23 | id: extract_branch 24 | 25 | - name: Release 26 | uses: werf/third-party-release-please-action@werf 27 | with: 28 | default-branch: ${{ steps.extract_branch.outputs.branch }} 29 | release-type: go 30 | token: ${{ secrets.RELEASE_PLEASE_TOKEN }} 31 | release-notes-header: "## Changelog" 32 | release-notes-footer: | 33 | ## Install via trdl (with autoupdates, highly secure) 34 | 35 | 1. [Install trdl client binary](https://github.com/werf/trdl/releases/latest), preferably to `~/bin`. 36 | 2. Add Nelm TUF repository to trdl: 37 | 38 | ```shell 39 | trdl add nelm https://tuf.nelm.sh 1 2122fb476c48de4609fe6d3636759645996088ff6796857fc23ba4b8331a6e3a58fc40f1714c31bda64c709ef6f49bcc4691d091bad6cb1b9a631d8e06e1f308 40 | ``` 41 | 42 | 3. Make `nelm` binary available in the current shell: 43 | 44 | ```shell 45 | source "$(trdl use nelm 1 stable)" 46 | ``` 47 | 48 | ## Install binaries directly (no autoupdates) 49 | 50 | Download `nelm` binaries from here: 51 | * [Linux amd64](https://tuf.nelm.sh/targets/releases/{{> version }}/linux-amd64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/{{> version }}/linux-amd64/bin/nelm.sig)) 52 | * [Linux arm64](https://tuf.nelm.sh/targets/releases/{{> version }}/linux-arm64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/{{> version }}/linux-arm64/bin/nelm.sig)) 53 | * [macOS amd64](https://tuf.nelm.sh/targets/releases/{{> version }}/darwin-amd64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/{{> version }}/darwin-amd64/bin/nelm.sig)) 54 | * [macOS arm64](https://tuf.nelm.sh/targets/releases/{{> version }}/darwin-arm64/bin/nelm) ([PGP signature](https://tuf.nelm.sh/targets/signatures/{{> version }}/darwin-arm64/bin/nelm.sig)) 55 | * [Windows amd64](https://tuf.nelm.sh/targets/releases/{{> version }}/windows-amd64/bin/nelm.exe) ([PGP signature](https://tuf.nelm.sh/targets/signatures/{{> version }}/windows-amd64/bin/nelm.exe.sig)) 56 | 57 | These binaries were signed with PGP and could be verified with the [Nelm PGP public key](https://raw.githubusercontent.com/werf/nelm/refs/heads/main/nelm.asc). For example, `nelm` binary can be downloaded and verified with `gpg` on Linux with these commands: 58 | 59 | ```shell 60 | curl -sSLO "https://tuf.nelm.sh/targets/releases/{{> version }}/linux-amd64/bin/nelm" -O "https://tuf.nelm.sh/targets/signatures/{{> version }}/linux-amd64/bin/nelm.sig" 61 | curl -sSL https://raw.githubusercontent.com/werf/nelm/refs/heads/main/nelm.asc | gpg --import 62 | gpg --verify nelm.sig nelm 63 | ``` 64 | 65 | notify: 66 | if: failure() 67 | needs: release-please 68 | uses: werf/common-ci/.github/workflows/notification.yml@main 69 | secrets: 70 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 71 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 72 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 73 | -------------------------------------------------------------------------------- /.github/workflows/release_trdl-publish.yml: -------------------------------------------------------------------------------- 1 | name: release:trdl-publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - trdl_channels.yaml 8 | repository_dispatch: 9 | types: ["release:trdl-publish"] 10 | workflow_dispatch: 11 | 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | jobs: 17 | publish: 18 | name: Publish release channels using trdl server 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - name: Notify 22 | uses: mattermost/action-mattermost-notify@master 23 | with: 24 | MATTERMOST_WEBHOOK_URL: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 25 | MATTERMOST_CHANNEL: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 26 | TEXT: | 27 | ${{ vars.LOOP_NOTIFICATION_GROUP }} [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) nelm task sign pls 28 | 29 | - name: Publish with retry 30 | uses: werf/trdl-vault-actions/publish@main 31 | with: 32 | vault-addr: ${{ secrets.TRDL_VAULT_ADDR }} 33 | project-name: nelm 34 | vault-auth-method: approle 35 | vault-role-id: ${{ secrets.TRDL_VAULT_ROLE_ID }} 36 | vault-secret-id: ${{ secrets.TRDL_VAULT_SECRET_ID }} 37 | 38 | notify: 39 | if: always() 40 | needs: 41 | - publish 42 | uses: werf/common-ci/.github/workflows/notification.yml@main 43 | secrets: 44 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 45 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 46 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 47 | -------------------------------------------------------------------------------- /.github/workflows/release_trdl-release.yml: -------------------------------------------------------------------------------- 1 | name: release:trdl-release 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+*" 6 | repository_dispatch: 7 | types: ["release:trdl-release"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | release: 12 | name: Perform release using trdl server 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: Notify 16 | uses: mattermost/action-mattermost-notify@master 17 | with: 18 | MATTERMOST_WEBHOOK_URL: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 19 | MATTERMOST_CHANNEL: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 20 | TEXT: | 21 | ${{ vars.LOOP_NOTIFICATION_GROUP }} [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) nelm task sign pls 22 | 23 | - name: Release with retry 24 | uses: werf/trdl-vault-actions/release@main 25 | with: 26 | vault-addr: ${{ secrets.TRDL_VAULT_ADDR }} 27 | project-name: nelm 28 | git-tag: ${{ github.ref_name }} 29 | vault-auth-method: approle 30 | vault-role-id: ${{ secrets.TRDL_VAULT_ROLE_ID }} 31 | vault-secret-id: ${{ secrets.TRDL_VAULT_SECRET_ID }} 32 | 33 | notify: 34 | if: always() 35 | needs: release 36 | uses: werf/common-ci/.github/workflows/notification.yml@main 37 | secrets: 38 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 39 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 40 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 41 | -------------------------------------------------------------------------------- /.github/workflows/test_pr.yml: -------------------------------------------------------------------------------- 1 | name: test:pr 2 | 3 | on: 4 | pull_request: 5 | repository_dispatch: 6 | types: ["test:pr"] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | detect-changes: 15 | runs-on: ubuntu-22.04 16 | timeout-minutes: 10 17 | permissions: 18 | pull-requests: read 19 | outputs: 20 | workflow_proceed: ${{ steps.changes.outputs.workflow_proceed }} 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Detect changes 26 | uses: dorny/paths-filter@v3 27 | id: changes 28 | with: 29 | filters: | 30 | workflow_proceed: 31 | - 'go.mod' 32 | - 'Taskfile.dist.yaml' 33 | - '.github/**' 34 | - 'cmd/nelm/**' 35 | - 'internal/**' 36 | - 'pkg/**' 37 | - 'scripts/**' 38 | 39 | lint: 40 | needs: detect-changes 41 | uses: ./.github/workflows/_lint.yml 42 | with: 43 | forceSkip: ${{ github.event_name == 'pull_request' && needs.detect-changes.outputs.workflow_proceed == 'false' }} 44 | 45 | unit: 46 | needs: detect-changes 47 | uses: ./.github/workflows/_test_unit.yml 48 | with: 49 | forceSkip: ${{ github.event_name == 'pull_request' && needs.detect-changes.outputs.workflow_proceed == 'false' }} 50 | 51 | build: 52 | if: ${{ !(github.event_name == 'pull_request' && needs.detect-changes.outputs.workflow_proceed == 'false') }} 53 | needs: detect-changes 54 | strategy: 55 | fail-fast: false 56 | runs-on: ubuntu-22.04 57 | timeout-minutes: 60 58 | steps: 59 | - name: Install build dependencies 60 | run: | 61 | sudo apt update 62 | sudo apt install -y gcc-aarch64-linux-gnu file 63 | 64 | - name: Checkout code 65 | uses: actions/checkout@v4 66 | 67 | - name: Set up Go 68 | uses: actions/setup-go@v5 69 | with: 70 | cache: true 71 | go-version-file: go.mod 72 | 73 | - name: Install Task 74 | uses: arduino/setup-task@v2 75 | with: 76 | repo-token: ${{ secrets.GITHUB_TOKEN }} 77 | 78 | - name: Build 79 | run: task -p build:dev:all 80 | 81 | notify: 82 | if: | 83 | (github.event_name == 'pull_request' && github.event.pull_request.draft == false && failure()) || 84 | (github.event_name != 'pull_request' && failure()) 85 | needs: 86 | - lint 87 | - unit 88 | uses: werf/common-ci/.github/workflows/notification.yml@main 89 | secrets: 90 | loopNotificationGroup: ${{ vars.LOOP_NOTIFICATION_GROUP }} 91 | webhook: ${{ secrets.LOOP_NOTIFICATION_WEBHOOK }} 92 | notificationChannel: ${{ vars.LOOP_NOTIFICATION_CHANNEL }} 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.swp 3 | 4 | node_modules/ 5 | 6 | /.vscode/ 7 | /.idea/ 8 | /bin/ 9 | /build/ 10 | /dist/ 11 | /Taskfile.yaml 12 | /go.work 13 | /go.work.sum 14 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | linters-settings: 5 | gofumpt: 6 | extra-rules: true 7 | gci: 8 | sections: 9 | - standard 10 | - default 11 | - prefix(github.com/werf/) 12 | gocritic: 13 | disabled-checks: 14 | - ifElseChain 15 | errorlint: 16 | comparison: false 17 | asserts: false 18 | misspell: 19 | locale: US 20 | 21 | linters: 22 | disable-all: true 23 | enable: 24 | # Default linters. 25 | - ineffassign 26 | - typecheck 27 | # - unused 28 | 29 | # Extra linters. 30 | - asciicheck 31 | - bidichk 32 | - bodyclose 33 | - errname 34 | - errorlint 35 | - exportloopref 36 | - gci 37 | - gocritic 38 | - gofumpt 39 | - misspell 40 | - nolintlint 41 | 42 | issues: 43 | # Show all errors. 44 | max-issues-per-linter: 0 45 | max-same-issues: 0 46 | exclude-dirs: 47 | - scripts 48 | - docs 49 | 50 | exclude: 51 | # TODO use %w in the future. 52 | - "non-wrapping format verb for fmt.Errorf" # errorlint 53 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/.svn 3 | **/.hg 4 | werf*.yaml 5 | werf*.yml 6 | **/templates 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 9999 2 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - ilya-lesikov 3 | - alexey-igrychev 4 | 5 | reviewers: 6 | - ilya-lesikov 7 | - alexey-igrychev 8 | 9 | -------------------------------------------------------------------------------- /cmd/nelm/chart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "chart", 15 | "Manage charts.", 16 | "Manage charts.", 17 | chartCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartRenderCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartDependencyCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartDownloadCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | cmd.AddCommand(newChartUploadCommand(ctx, afterAllCommandsBuiltFuncs)) 25 | cmd.AddCommand(newChartPackCommand(ctx, afterAllCommandsBuiltFuncs)) 26 | cmd.AddCommand(newChartLintCommand(ctx, afterAllCommandsBuiltFuncs)) 27 | cmd.AddCommand(newChartSecretCommand(ctx, afterAllCommandsBuiltFuncs)) 28 | 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartDependencyCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "dependency", 15 | "Manage chart dependencies.", 16 | "Manage chart dependencies.", 17 | dependencyCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartDependencyUpdateCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartDependencyDownloadCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency_download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newChartDependencyDownloadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | dependencyCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "dependency") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(dependencyCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "build") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Use = "download CHART" 27 | cmd.Short = "Download chart dependencies from Chart.lock." 28 | cmd.Long = "Download chart dependencies from Chart.lock." 29 | cmd.Aliases = []string{} 30 | cli.SetSubCommandAnnotations(cmd, 50, dependencyCmdGroup) 31 | 32 | originalRunE := cmd.RunE 33 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 34 | helmSettings := helm_v3.Settings 35 | 36 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 37 | 38 | loader.NoChartLockWarning = "" 39 | 40 | if err := originalRunE(cmd, args); err != nil { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | return cmd 48 | } 49 | -------------------------------------------------------------------------------- /cmd/nelm/chart_dependency_update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newChartDependencyUpdateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | dependencyCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "dependency") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(dependencyCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "update") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Update Chart.lock and download chart dependencies." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 40, dependencyCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nelm/chart_download.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newChartDownloadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "pull") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "download [chart URL | repo/chartname] [...]" 23 | cmd.Short = "Download a chart from a repository." 24 | cmd.Aliases = []string{} 25 | cli.SetSubCommandAnnotations(cmd, 40, chartCmdGroup) 26 | 27 | originalRunE := cmd.RunE 28 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 29 | helmSettings := helm_v3.Settings 30 | 31 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 32 | 33 | loader.NoChartLockWarning = "" 34 | 35 | if err := originalRunE(cmd, args); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/nelm/chart_pack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newChartPackCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "package") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "pack [CHART_PATH] [...]" 23 | cmd.Short = "Pack a chart into an archive to distribute via a repository." 24 | cmd.Long = strings.ReplaceAll(cmd.Long, "helm package", "nelm chart pack") 25 | cmd.Aliases = []string{} 26 | cli.SetSubCommandAnnotations(cmd, 30, chartCmdGroup) 27 | 28 | originalRunE := cmd.RunE 29 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 30 | helmSettings := helm_v3.Settings 31 | 32 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 33 | 34 | loader.NoChartLockWarning = "" 35 | 36 | if err := originalRunE(cmd, args); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | } 42 | 43 | return cmd 44 | } 45 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "secret", 15 | "Manage chart secrets.", 16 | "Manage chart secrets.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretKeyCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretFileCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretValuesFileCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretFileCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "file", 15 | "Manage chart secret files.", 16 | "Manage chart secret files.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretFileEncryptCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretFileDecryptCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretFileEditCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretFileDecryptOptions struct { 15 | action.SecretFileDecryptOptions 16 | 17 | File string 18 | LogColorMode string 19 | LogLevel string 20 | } 21 | 22 | func newChartSecretFileDecryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretFileDecryptOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "decrypt [options...] --secret-key secret-key file", 28 | "Decrypt file and print result to stdout.", 29 | "Decrypt file and print result to stdout.", 30 | 10, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretFileDecryptLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | LogIsParseable: true, 42 | }) 43 | 44 | cfg.File = args[0] 45 | 46 | if err := action.SecretFileDecrypt(ctx, cfg.File, cfg.SecretFileDecryptOptions); err != nil { 47 | return fmt.Errorf("secret file decrypt: %w", err) 48 | } 49 | 50 | return nil 51 | }, 52 | ) 53 | 54 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 55 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 56 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 57 | Group: miscFlagGroup, 58 | }); err != nil { 59 | return fmt.Errorf("add flag: %w", err) 60 | } 61 | 62 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretFileDecryptLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 63 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 64 | Group: miscFlagGroup, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save decrypted output to a file", cli.AddFlagOptions{ 70 | Type: cli.FlagTypeFile, 71 | Group: mainFlagGroup, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 78 | Group: mainFlagGroup, 79 | Required: true, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 85 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 86 | Group: miscFlagGroup, 87 | Type: cli.FlagTypeDir, 88 | }); err != nil { 89 | return fmt.Errorf("add flag: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return cmd 96 | } 97 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_edit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretFileEditOptions struct { 15 | action.SecretFileEditOptions 16 | 17 | File string 18 | LogColorMode string 19 | LogLevel string 20 | } 21 | 22 | func newChartSecretFileEditCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretFileEditOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "edit [options...] --secret-key secret-key file", 28 | "Interactively edit encrypted file.", 29 | "Interactively edit encrypted file.", 30 | 30, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretFileEditLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | }) 42 | 43 | cfg.File = args[0] 44 | 45 | if err := action.SecretFileEdit(ctx, cfg.File, cfg.SecretFileEditOptions); err != nil { 46 | return fmt.Errorf("secret file edit: %w", err) 47 | } 48 | 49 | return nil 50 | }, 51 | ) 52 | 53 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 54 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs", cli.AddFlagOptions{ 55 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 56 | Group: miscFlagGroup, 57 | }); err != nil { 58 | return fmt.Errorf("add flag: %w", err) 59 | } 60 | 61 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretFileEditLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 62 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 63 | Group: miscFlagGroup, 64 | }); err != nil { 65 | return fmt.Errorf("add flag: %w", err) 66 | } 67 | 68 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 69 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 70 | Group: mainFlagGroup, 71 | Required: true, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 78 | Group: miscFlagGroup, 79 | Type: cli.FlagTypeDir, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | return nil 85 | } 86 | 87 | return cmd 88 | } 89 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretFileEncryptOptions struct { 15 | action.SecretFileEncryptOptions 16 | 17 | File string 18 | LogColorMode string 19 | LogLevel string 20 | } 21 | 22 | func newChartSecretFileEncryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretFileEncryptOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "encrypt [options...] --secret-key secret-key file", 28 | "Encrypt file and print result to stdout.", 29 | "Encrypt file and print result to stdout.", 30 | 20, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretFileEncryptLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | LogIsParseable: true, 42 | }) 43 | 44 | cfg.File = args[0] 45 | 46 | if err := action.SecretFileEncrypt(ctx, cfg.File, cfg.SecretFileEncryptOptions); err != nil { 47 | return fmt.Errorf("secret file encrypt: %w", err) 48 | } 49 | 50 | return nil 51 | }, 52 | ) 53 | 54 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 55 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 56 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 57 | Group: miscFlagGroup, 58 | }); err != nil { 59 | return fmt.Errorf("add flag: %w", err) 60 | } 61 | 62 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretFileEncryptLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 63 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 64 | Group: miscFlagGroup, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save encrypted output to a file", cli.AddFlagOptions{ 70 | Type: cli.FlagTypeFile, 71 | Group: mainFlagGroup, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 78 | Group: mainFlagGroup, 79 | Required: true, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 85 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 86 | Group: miscFlagGroup, 87 | Type: cli.FlagTypeDir, 88 | }); err != nil { 89 | return fmt.Errorf("add flag: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return cmd 96 | } 97 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretKeyCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "key", 15 | "Manage chart secret keys.", 16 | "Manage chart secret keys.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretKeyCreateCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretKeyRotateCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretKeyCreateOptions struct { 15 | action.SecretKeyCreateOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | } 20 | 21 | func newChartSecretKeyCreateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 22 | cfg := &chartSecretKeyCreateOptions{} 23 | 24 | cmd := cli.NewSubCommand( 25 | ctx, 26 | "create [options...]", 27 | "Create a new chart secret key.", 28 | "Create a new chart secret key.", 29 | 80, 30 | secretCmdGroup, 31 | cli.SubCommandOptions{}, 32 | func(cmd *cobra.Command, args []string) error { 33 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretKeyCreateLogLevel), action.SetupLoggingOptions{ 34 | ColorMode: cfg.LogColorMode, 35 | LogIsParseable: true, 36 | }) 37 | 38 | if _, err := action.SecretKeyCreate(ctx, cfg.SecretKeyCreateOptions); err != nil { 39 | return fmt.Errorf("secret key create: %w", err) 40 | } 41 | 42 | return nil 43 | }, 44 | ) 45 | 46 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 47 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 48 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 49 | Group: miscFlagGroup, 50 | }); err != nil { 51 | return fmt.Errorf("add flag: %w", err) 52 | } 53 | 54 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretKeyCreateLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 55 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 56 | Group: miscFlagGroup, 57 | }); err != nil { 58 | return fmt.Errorf("add flag: %w", err) 59 | } 60 | 61 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 62 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 63 | Group: miscFlagGroup, 64 | Type: cli.FlagTypeDir, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | return cmd 73 | } 74 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_key_rotate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretKeyRotateOptions struct { 15 | action.SecretKeyRotateOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | } 20 | 21 | func newChartSecretKeyRotateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 22 | cfg := &chartSecretKeyRotateOptions{} 23 | 24 | cmd := cli.NewSubCommand( 25 | ctx, 26 | "rotate [options...] --old-secret-key secret-key --new-secret-key secret-key [chart-dir]", 27 | "Reencrypt secret files with a new secret key.", 28 | "Decrypt with an old secret key, then encrypt with a new secret key chart files secret-values.yaml and secret/*.", 29 | 70, 30 | secretCmdGroup, 31 | cli.SubCommandOptions{ 32 | Args: cobra.MaximumNArgs(1), 33 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 34 | return nil, cobra.ShellCompDirectiveFilterDirs 35 | }, 36 | }, 37 | func(cmd *cobra.Command, args []string) error { 38 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretKeyRotateLogLevel), action.SetupLoggingOptions{ 39 | ColorMode: cfg.LogColorMode, 40 | }) 41 | 42 | if len(args) > 0 { 43 | cfg.ChartDirPath = args[0] 44 | } 45 | 46 | if err := action.SecretKeyRotate(ctx, cfg.SecretKeyRotateOptions); err != nil { 47 | return fmt.Errorf("secret key rotate: %w", err) 48 | } 49 | 50 | return nil 51 | }, 52 | ) 53 | 54 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 55 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 56 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 57 | Group: miscFlagGroup, 58 | }); err != nil { 59 | return fmt.Errorf("add flag: %w", err) 60 | } 61 | 62 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretKeyRotateLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 63 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 64 | Group: miscFlagGroup, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | if err := cli.AddFlag(cmd, &cfg.NewSecretKey, "new-secret-key", "", "New secret key", cli.AddFlagOptions{ 70 | Group: mainFlagGroup, 71 | Required: true, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.OldSecretKey, "old-secret-key", "", "Old secret key", cli.AddFlagOptions{ 77 | Group: mainFlagGroup, 78 | Required: true, 79 | }); err != nil { 80 | return fmt.Errorf("add flag: %w", err) 81 | } 82 | 83 | if err := cli.AddFlag(cmd, &cfg.SecretValuesPaths, "secret-values", []string{}, "Secret values files paths", cli.AddFlagOptions{ 84 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 85 | Group: mainFlagGroup, 86 | Type: cli.FlagTypeFile, 87 | }); err != nil { 88 | return fmt.Errorf("add flag: %w", err) 89 | } 90 | 91 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 92 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 93 | Group: miscFlagGroup, 94 | Type: cli.FlagTypeDir, 95 | }); err != nil { 96 | return fmt.Errorf("add flag: %w", err) 97 | } 98 | 99 | return nil 100 | } 101 | 102 | return cmd 103 | } 104 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newChartSecretValuesFileCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "values-file", 15 | "Manage chart secret values files.", 16 | "Manage chart secret values files.", 17 | secretCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newChartSecretValuesFileEncryptCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newChartSecretValuesFileDecryptCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newChartSecretValuesFileEditCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretValuesFileDecryptOptions struct { 15 | action.SecretValuesFileDecryptOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | ValuesFile string 20 | } 21 | 22 | func newChartSecretValuesFileDecryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretValuesFileDecryptOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "decrypt [options...] --secret-key secret-key values-file", 28 | "Decrypt values file and print result to stdout.", 29 | "Decrypt values file and print result to stdout.", 30 | 40, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretValuesFileDecryptLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | LogIsParseable: true, 42 | }) 43 | 44 | cfg.ValuesFile = args[0] 45 | 46 | if err := action.SecretValuesFileDecrypt(ctx, cfg.ValuesFile, cfg.SecretValuesFileDecryptOptions); err != nil { 47 | return fmt.Errorf("secret values file decrypt: %w", err) 48 | } 49 | 50 | return nil 51 | }, 52 | ) 53 | 54 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 55 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 56 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 57 | Group: miscFlagGroup, 58 | }); err != nil { 59 | return fmt.Errorf("add flag: %w", err) 60 | } 61 | 62 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretValuesFileDecryptLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 63 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 64 | Group: miscFlagGroup, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save decrypted output to a file", cli.AddFlagOptions{ 70 | Type: cli.FlagTypeFile, 71 | Group: mainFlagGroup, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 78 | Group: mainFlagGroup, 79 | Required: true, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 85 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 86 | Group: miscFlagGroup, 87 | Type: cli.FlagTypeDir, 88 | }); err != nil { 89 | return fmt.Errorf("add flag: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return cmd 96 | } 97 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_edit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretValuesFileEditOptions struct { 15 | action.SecretValuesFileEditOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | ValuesFile string 20 | } 21 | 22 | func newChartSecretValuesFileEditCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretValuesFileEditOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "edit [options...] --secret-key secret-key values-file", 28 | "Interactively edit encrypted values file.", 29 | "Interactively edit encrypted values file.", 30 | 60, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretValuesFileEditLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | }) 42 | 43 | cfg.ValuesFile = args[0] 44 | 45 | if err := action.SecretValuesFileEdit(ctx, cfg.ValuesFile, cfg.SecretValuesFileEditOptions); err != nil { 46 | return fmt.Errorf("secret values file edit: %w", err) 47 | } 48 | 49 | return nil 50 | }, 51 | ) 52 | 53 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 54 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 55 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 56 | Group: miscFlagGroup, 57 | }); err != nil { 58 | return fmt.Errorf("add flag: %w", err) 59 | } 60 | 61 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretValuesFileEditLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 62 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 63 | Group: miscFlagGroup, 64 | }); err != nil { 65 | return fmt.Errorf("add flag: %w", err) 66 | } 67 | 68 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 69 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 70 | Group: mainFlagGroup, 71 | Required: true, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 78 | Group: miscFlagGroup, 79 | Type: cli.FlagTypeDir, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | return nil 85 | } 86 | 87 | return cmd 88 | } 89 | -------------------------------------------------------------------------------- /cmd/nelm/chart_secret_values_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type chartSecretValuesFileEncryptOptions struct { 15 | action.SecretValuesFileEncryptOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | ValuesFile string 20 | } 21 | 22 | func newChartSecretValuesFileEncryptCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 23 | cfg := &chartSecretValuesFileEncryptOptions{} 24 | 25 | cmd := cli.NewSubCommand( 26 | ctx, 27 | "encrypt [options...] --secret-key secret-key values-file", 28 | "Encrypt values file and print result to stdout.", 29 | "Encrypt values file and print result to stdout.", 30 | 50, 31 | secretCmdGroup, 32 | cli.SubCommandOptions{ 33 | Args: cobra.ExactArgs(1), 34 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 35 | return nil, cobra.ShellCompDirectiveDefault 36 | }, 37 | }, 38 | func(cmd *cobra.Command, args []string) error { 39 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultSecretValuesFileEncryptLogLevel), action.SetupLoggingOptions{ 40 | ColorMode: cfg.LogColorMode, 41 | LogIsParseable: true, 42 | }) 43 | 44 | cfg.ValuesFile = args[0] 45 | 46 | if err := action.SecretValuesFileEncrypt(ctx, cfg.ValuesFile, cfg.SecretValuesFileEncryptOptions); err != nil { 47 | return fmt.Errorf("secret values file encrypt: %w", err) 48 | } 49 | 50 | return nil 51 | }, 52 | ) 53 | 54 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 55 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 56 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 57 | Group: miscFlagGroup, 58 | }); err != nil { 59 | return fmt.Errorf("add flag: %w", err) 60 | } 61 | 62 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultSecretValuesFileEncryptLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 63 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 64 | Group: miscFlagGroup, 65 | }); err != nil { 66 | return fmt.Errorf("add flag: %w", err) 67 | } 68 | 69 | if err := cli.AddFlag(cmd, &cfg.OutputFilePath, "save-output-to", "", "Save encrypted output to a file", cli.AddFlagOptions{ 70 | Type: cli.FlagTypeFile, 71 | Group: mainFlagGroup, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key", cli.AddFlagOptions{ 77 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 78 | Group: mainFlagGroup, 79 | Required: true, 80 | }); err != nil { 81 | return fmt.Errorf("add flag: %w", err) 82 | } 83 | 84 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 85 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 86 | Group: miscFlagGroup, 87 | Type: cli.FlagTypeDir, 88 | }); err != nil { 89 | return fmt.Errorf("add flag: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return cmd 96 | } 97 | -------------------------------------------------------------------------------- /cmd/nelm/chart_upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newChartUploadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "push") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Use = "upload [chart] [remote]" 23 | cmd.Short = "Upload a chart to a repository." 24 | cmd.Aliases = []string{} 25 | cli.SetSubCommandAnnotations(cmd, 40, chartCmdGroup) 26 | 27 | originalRunE := cmd.RunE 28 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 29 | helmSettings := helm_v3.Settings 30 | 31 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 32 | 33 | loader.NoChartLockWarning = "" 34 | 35 | if err := originalRunE(cmd, args); err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/nelm/common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/nelm/pkg/action" 9 | ) 10 | 11 | const ( 12 | releaseNameStub = "release-stub" 13 | releaseNamespaceStub = "namespace-stub" 14 | ) 15 | 16 | var helmRootCmd *cobra.Command 17 | 18 | func allowedLogColorModesHelp() string { 19 | return "Allowed: " + strings.Join(action.LogColorModes, ", ") 20 | } 21 | 22 | func allowedLogLevelsHelp() string { 23 | return "Allowed: " + strings.Join(action.LogLevels, ", ") 24 | } 25 | -------------------------------------------------------------------------------- /cmd/nelm/groups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/werf/common-go/pkg/cli" 5 | ) 6 | 7 | var ( 8 | releaseCmdGroup = cli.NewCommandGroup("release", "Release commands:", 100) 9 | chartCmdGroup = cli.NewCommandGroup("chart", "Chart commands:", 90) 10 | secretCmdGroup = cli.NewCommandGroup("secret", "Secret commands:", 80) 11 | dependencyCmdGroup = cli.NewCommandGroup("dependency", "Dependency commands:", 70) 12 | repoCmdGroup = cli.NewCommandGroup("repo", "Repo commands:", 60) 13 | miscCmdGroup = cli.NewCommandGroup("misc", "Other commands:", 0) 14 | 15 | mainFlagGroup = cli.NewFlagGroup("main", "Options:", 100) 16 | valuesFlagGroup = cli.NewFlagGroup("values", "Values options:", 90) 17 | secretFlagGroup = cli.NewFlagGroup("secret", "Secret options:", 80) 18 | patchFlagGroup = cli.NewFlagGroup("patch", "Patch options:", 70) 19 | progressFlagGroup = cli.NewFlagGroup("progress", "Progress options:", 65) 20 | chartRepoFlagGroup = cli.NewFlagGroup("chart-repo", "Chart repository options:", 60) 21 | kubeConnectionFlagGroup = cli.NewFlagGroup("kube-connection", "Kubernetes connection options:", 50) 22 | performanceFlagGroup = cli.NewFlagGroup("performance", "Performance options:", 40) 23 | miscFlagGroup = cli.NewFlagGroup("misc", "Other options:", 0) 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/nelm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "github.com/chanced/caps" 12 | "github.com/pkg/errors" 13 | "github.com/samber/lo" 14 | "github.com/spf13/cobra" 15 | 16 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 17 | "github.com/werf/common-go/pkg/cli" 18 | "github.com/werf/logboek" 19 | "github.com/werf/nelm/internal/common" 20 | "github.com/werf/nelm/pkg/action" 21 | "github.com/werf/nelm/pkg/featgate" 22 | "github.com/werf/nelm/pkg/log" 23 | ) 24 | 25 | func main() { 26 | if featgate.FeatGatePeriodicStackTraces.Enabled() { 27 | periodicStackTraces() 28 | } 29 | 30 | ctx := logboek.NewContext(context.Background(), logboek.DefaultLogger()) 31 | 32 | cli.FlagEnvVarsPrefix = caps.ToScreamingSnake(common.Brand) + "_" 33 | afterAllCommandsBuiltFuncs := make(map[*cobra.Command]func(cmd *cobra.Command) error) 34 | 35 | // Needed for embedding original Helm 3 commands. 36 | var err error 37 | helmRootCmd, err = helm_v3.Init() 38 | if err != nil { 39 | abort(ctx, fmt.Errorf("init helm: %w", err), 1) 40 | } 41 | 42 | rootCmd := NewRootCommand(ctx, afterAllCommandsBuiltFuncs) 43 | 44 | for cmd, fn := range afterAllCommandsBuiltFuncs { 45 | if err := fn(cmd); err != nil { 46 | abort(ctx, err, 1) 47 | } 48 | } 49 | 50 | featGatesEnvVars := lo.Map(featgate.FeatGates, func(fg *featgate.FeatGate, index int) string { 51 | return fg.EnvVarName() 52 | }) 53 | 54 | if unsupportedEnvVars := lo.Without(cli.FindUndefinedFlagEnvVarsInEnviron(), featGatesEnvVars...); len(unsupportedEnvVars) > 0 { 55 | abort(ctx, fmt.Errorf("unsupported environment variable(s): %s", strings.Join(unsupportedEnvVars, ",")), 1) 56 | } 57 | 58 | if err := rootCmd.ExecuteContext(ctx); err != nil { 59 | var exitCode int 60 | if errors.Is(err, action.ErrChangesPlanned) { 61 | exitCode = 2 62 | } else { 63 | exitCode = 1 64 | } 65 | 66 | abort(ctx, err, exitCode) 67 | } 68 | } 69 | 70 | func abort(ctx context.Context, err error, exitCode int) { 71 | log.Default.WarnPop(ctx, "final") 72 | log.Default.Error(ctx, "Error: %s", err) 73 | os.Exit(exitCode) 74 | } 75 | 76 | func periodicStackTraces() { 77 | go func() { 78 | for { 79 | buf := make([]byte, 1<<16) 80 | runtime.Stack(buf, true) 81 | fmt.Printf("%s", buf) 82 | 83 | time.Sleep(time.Second * time.Duration(10)) 84 | } 85 | }() 86 | } 87 | -------------------------------------------------------------------------------- /cmd/nelm/release.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | "github.com/werf/nelm/pkg/featgate" 10 | ) 11 | 12 | func newReleaseCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 13 | cmd := cli.NewGroupCommand( 14 | ctx, 15 | "release", 16 | "Manage Helm releases.", 17 | "Manage Helm releases.", 18 | releaseCmdGroup, 19 | cli.GroupCommandOptions{}, 20 | ) 21 | 22 | cmd.AddCommand(newReleaseInstallCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newReleaseRollbackCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | 25 | if featgate.FeatGateNativeReleaseUninstall.Enabled() || featgate.FeatGatePreviewV2.Enabled() { 26 | cmd.AddCommand(newReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) 27 | } else { 28 | cmd.AddCommand(newLegacyReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) 29 | } 30 | 31 | cmd.AddCommand(newReleaseHistoryCommand(ctx, afterAllCommandsBuiltFuncs)) 32 | 33 | if featgate.FeatGateNativeReleaseList.Enabled() || featgate.FeatGatePreviewV2.Enabled() { 34 | cmd.AddCommand(newReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) 35 | } else { 36 | cmd.AddCommand(newLegacyReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) 37 | } 38 | 39 | cmd.AddCommand(newReleaseGetCommand(ctx, afterAllCommandsBuiltFuncs)) 40 | cmd.AddCommand(newPlanCommand(ctx, afterAllCommandsBuiltFuncs)) 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/nelm/release_history.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newReleaseHistoryCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "history") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Short = "Show release history." 23 | cmd.Aliases = []string{} 24 | cli.SetSubCommandAnnotations(cmd, 30, releaseCmdGroup) 25 | 26 | originalRunE := cmd.RunE 27 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 28 | helmSettings := helm_v3.Settings 29 | 30 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 31 | 32 | loader.NoChartLockWarning = "" 33 | 34 | if err := originalRunE(cmd, args); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/nelm/release_list_legacy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newLegacyReleaseListCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "list") 19 | })) 20 | 21 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 22 | cmd.Short = "List all releases in a namespace." 23 | cmd.Aliases = []string{} 24 | cli.SetSubCommandAnnotations(cmd, 40, releaseCmdGroup) 25 | 26 | originalRunE := cmd.RunE 27 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 28 | helmSettings := helm_v3.Settings 29 | 30 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 31 | 32 | loader.NoChartLockWarning = "" 33 | 34 | if err := originalRunE(cmd, args); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/nelm/release_plan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newPlanCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "plan", 15 | "Show planned changes.", 16 | "Show planned changes.", 17 | releaseCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newReleasePlanInstallCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/nelm/repo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/werf/common-go/pkg/cli" 9 | ) 10 | 11 | func newRepoCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 12 | cmd := cli.NewGroupCommand( 13 | ctx, 14 | "repo", 15 | "Manage chart repositories.", 16 | "Manage chart repositories.", 17 | repoCmdGroup, 18 | cli.GroupCommandOptions{}, 19 | ) 20 | 21 | cmd.AddCommand(newRepoAddCommand(ctx, afterAllCommandsBuiltFuncs)) 22 | cmd.AddCommand(newRepoRemoveCommand(ctx, afterAllCommandsBuiltFuncs)) 23 | cmd.AddCommand(newRepoUpdateCommand(ctx, afterAllCommandsBuiltFuncs)) 24 | cmd.AddCommand(newRepoLoginCommand(ctx, afterAllCommandsBuiltFuncs)) 25 | cmd.AddCommand(newRepoLogoutCommand(ctx, afterAllCommandsBuiltFuncs)) 26 | 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /cmd/nelm/repo_add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newRepoAddCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "add") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Set up a new chart repository." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 60, repoCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nelm/repo_login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newRepoLoginCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | registryCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "registry") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(registryCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "login") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Log in to an OCI registry with charts." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 30, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/nelm/repo_logout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newRepoLogoutCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | registryCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "registry") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(registryCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "logout") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Log out from an OCI registry with charts." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 20, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/nelm/repo_remove.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newRepoRemoveCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "remove") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Remove a chart repository." 27 | cmd.Aliases = []string{} 28 | cli.SetSubCommandAnnotations(cmd, 50, repoCmdGroup) 29 | 30 | originalRunE := cmd.RunE 31 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 32 | helmSettings := helm_v3.Settings 33 | 34 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 35 | 36 | loader.NoChartLockWarning = "" 37 | 38 | if err := originalRunE(cmd, args); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/nelm/repo_update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "github.com/spf13/cobra" 9 | 10 | helm_v3 "github.com/werf/3p-helm/cmd/helm" 11 | "github.com/werf/3p-helm/pkg/chart/loader" 12 | "github.com/werf/common-go/pkg/cli" 13 | "github.com/werf/nelm/pkg/action" 14 | ) 15 | 16 | func newRepoUpdateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 17 | repoCmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { 18 | return strings.HasPrefix(c.Use, "repo") 19 | })) 20 | 21 | cmd := lo.Must(lo.Find(repoCmd.Commands(), func(c *cobra.Command) bool { 22 | return strings.HasPrefix(c.Use, "update") 23 | })) 24 | 25 | cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) 26 | cmd.Short = "Update info about available charts for all chart repositories." 27 | cmd.Long = "" 28 | cmd.Aliases = []string{} 29 | cli.SetSubCommandAnnotations(cmd, 40, repoCmdGroup) 30 | 31 | originalRunE := cmd.RunE 32 | cmd.RunE = func(cmd *cobra.Command, args []string) error { 33 | helmSettings := helm_v3.Settings 34 | 35 | ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, action.DebugLogLevel, action.InfoLogLevel), action.SetupLoggingOptions{}) 36 | 37 | loader.NoChartLockWarning = "" 38 | 39 | if err := originalRunE(cmd, args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | return cmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/nelm/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/internal/common" 12 | ) 13 | 14 | func NewRootCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 15 | cobra.EnableErrorOnUnknownSubcommand = true 16 | 17 | cmd := cli.NewRootCommand( 18 | ctx, 19 | strings.ToLower(common.Brand), 20 | fmt.Sprintf("%s is a Helm 3 alternative. %s manages and deploys Helm Charts to Kubernetes just like Helm, but provides a lot of features, improvements and bug fixes on top of what Helm 3 offers.", common.Brand, common.Brand), 21 | ) 22 | 23 | cmd.SetUsageFunc(usageFunc) 24 | cmd.SetUsageTemplate(usageTemplate) 25 | cmd.SetHelpTemplate(helpTemplate) 26 | 27 | cmd.AddCommand(newReleaseCommand(ctx, afterAllCommandsBuiltFuncs)) 28 | cmd.AddCommand(newChartCommand(ctx, afterAllCommandsBuiltFuncs)) 29 | cmd.AddCommand(newRepoCommand(ctx, afterAllCommandsBuiltFuncs)) 30 | cmd.AddCommand(newVersionCommand(ctx, afterAllCommandsBuiltFuncs)) 31 | 32 | return cmd 33 | } 34 | -------------------------------------------------------------------------------- /cmd/nelm/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/werf/common-go/pkg/cli" 11 | "github.com/werf/nelm/pkg/action" 12 | ) 13 | 14 | type versionConfig struct { 15 | action.VersionOptions 16 | 17 | LogColorMode string 18 | LogLevel string 19 | } 20 | 21 | func newVersionCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { 22 | cfg := &versionConfig{} 23 | 24 | cmd := cli.NewSubCommand( 25 | ctx, 26 | "version [options...]", 27 | "Show version.", 28 | "Show version.", 29 | 0, 30 | miscCmdGroup, 31 | cli.SubCommandOptions{}, 32 | func(cmd *cobra.Command, args []string) error { 33 | ctx = action.SetupLogging(ctx, cmp.Or(cfg.LogLevel, action.DefaultVersionLogLevel), action.SetupLoggingOptions{ 34 | ColorMode: cfg.LogColorMode, 35 | LogIsParseable: true, 36 | }) 37 | 38 | if _, err := action.Version(ctx, cfg.VersionOptions); err != nil { 39 | return fmt.Errorf("version: %w", err) 40 | } 41 | 42 | return nil 43 | }, 44 | ) 45 | 46 | afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { 47 | if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", action.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ 48 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 49 | Group: miscFlagGroup, 50 | }); err != nil { 51 | return fmt.Errorf("add flag: %w", err) 52 | } 53 | 54 | if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", action.DefaultVersionLogLevel, "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ 55 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 56 | Group: miscFlagGroup, 57 | }); err != nil { 58 | return fmt.Errorf("add flag: %w", err) 59 | } 60 | 61 | if err := cli.AddFlag(cmd, &cfg.OutputFormat, "output-format", action.DefaultVersionOutputFormat, "Result output format", cli.AddFlagOptions{ 62 | GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, 63 | Group: miscFlagGroup, 64 | }); err != nil { 65 | return fmt.Errorf("add flag: %w", err) 66 | } 67 | 68 | if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ 69 | GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, 70 | Group: miscFlagGroup, 71 | Type: cli.FlagTypeDir, 72 | }); err != nil { 73 | return fmt.Errorf("add flag: %w", err) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | return cmd 80 | } 81 | -------------------------------------------------------------------------------- /internal/chart/capabilities.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/client-go/discovery" 8 | 9 | helmaction "github.com/werf/3p-helm/pkg/action" 10 | helmchartutil "github.com/werf/3p-helm/pkg/chartutil" 11 | "github.com/werf/nelm/pkg/log" 12 | ) 13 | 14 | type BuildCapabilitiesOptions struct { 15 | APIVersions *helmchartutil.VersionSet 16 | DiscoveryClient discovery.CachedDiscoveryInterface 17 | KubeVersion *helmchartutil.KubeVersion 18 | } 19 | 20 | func BuildCapabilities(ctx context.Context, opts BuildCapabilitiesOptions) (*helmchartutil.Capabilities, error) { 21 | capabilities := &helmchartutil.Capabilities{ 22 | HelmVersion: helmchartutil.DefaultCapabilities.HelmVersion, 23 | } 24 | 25 | if opts.DiscoveryClient != nil { 26 | opts.DiscoveryClient.Invalidate() 27 | 28 | if opts.KubeVersion != nil { 29 | capabilities.KubeVersion = *opts.KubeVersion 30 | } else { 31 | kubeVersion, err := opts.DiscoveryClient.ServerVersion() 32 | if err != nil { 33 | return nil, fmt.Errorf("get kubernetes server version: %w", err) 34 | } 35 | 36 | capabilities.KubeVersion = helmchartutil.KubeVersion{ 37 | Version: kubeVersion.GitVersion, 38 | Major: kubeVersion.Major, 39 | Minor: kubeVersion.Minor, 40 | } 41 | } 42 | 43 | if opts.APIVersions != nil { 44 | capabilities.APIVersions = *opts.APIVersions 45 | } else { 46 | apiVersions, err := helmaction.GetVersionSet(opts.DiscoveryClient) 47 | if err != nil { 48 | if discovery.IsGroupDiscoveryFailedError(err) { 49 | log.Default.Warn(ctx, "Discovery failed: %s", err.Error()) 50 | } else { 51 | return nil, fmt.Errorf("get version set: %w", err) 52 | } 53 | } 54 | 55 | capabilities.APIVersions = apiVersions 56 | } 57 | } else { 58 | if opts.KubeVersion != nil { 59 | capabilities.KubeVersion = *opts.KubeVersion 60 | } else { 61 | capabilities.KubeVersion = helmchartutil.DefaultCapabilities.KubeVersion 62 | } 63 | 64 | if opts.APIVersions != nil { 65 | capabilities.APIVersions = *opts.APIVersions 66 | } else { 67 | capabilities.APIVersions = helmchartutil.DefaultCapabilities.APIVersions 68 | } 69 | } 70 | 71 | return capabilities, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/chart/chart_downloader.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/werf/3p-helm/pkg/cli" 11 | helmdownloader "github.com/werf/3p-helm/pkg/downloader" 12 | helmgetter "github.com/werf/3p-helm/pkg/getter" 13 | "github.com/werf/3p-helm/pkg/helmpath" 14 | helmregistry "github.com/werf/3p-helm/pkg/registry" 15 | helmrepo "github.com/werf/3p-helm/pkg/repo" 16 | log2 "github.com/werf/nelm/pkg/log" 17 | ) 18 | 19 | // TODO(ilya-lesikov): pass all missing options 20 | type ChartDownloaderOptions struct { 21 | KeyringFile string 22 | PassCredentialsAll bool 23 | CertFile string 24 | KeyFile string 25 | CaFile string 26 | SkipTLSVerify bool 27 | Insecure bool 28 | RepoURL string 29 | Username string 30 | Password string 31 | Version string 32 | } 33 | 34 | func NewChartDownloader(ctx context.Context, chartRef string, registryClient *helmregistry.Client, opts ChartDownloaderOptions) (*helmdownloader.ChartDownloader, string, error) { 35 | var out io.Writer 36 | if log2.Default.AcceptLevel(ctx, log2.WarningLevel) { 37 | out = os.Stdout 38 | } else { 39 | out = io.Discard 40 | } 41 | 42 | downloader := &helmdownloader.ChartDownloader{ 43 | Out: out, 44 | Keyring: opts.KeyringFile, 45 | Getters: helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}, 46 | Options: []helmgetter.Option{ 47 | helmgetter.WithPassCredentialsAll(opts.PassCredentialsAll), 48 | helmgetter.WithTLSClientConfig(opts.CertFile, opts.KeyFile, opts.CaFile), 49 | helmgetter.WithInsecureSkipVerifyTLS(opts.SkipTLSVerify), 50 | helmgetter.WithPlainHTTP(opts.Insecure), 51 | helmgetter.WithRegistryClient(registryClient), 52 | }, 53 | RegistryClient: registryClient, 54 | RepositoryConfig: cli.EnvOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), 55 | RepositoryCache: cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), 56 | } 57 | 58 | if opts.PassCredentialsAll || opts.RepoURL == "" { 59 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth(opts.Username, opts.Password)) 60 | } else { 61 | chartURL, err := helmrepo.FindChartInAuthAndTLSAndPassRepoURL(opts.RepoURL, opts.Username, opts.Password, chartRef, opts.Version, opts.CertFile, opts.KeyFile, opts.CaFile, opts.SkipTLSVerify, opts.PassCredentialsAll, helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}) 62 | if err != nil { 63 | return nil, "", fmt.Errorf("get chart URL: %w", err) 64 | } 65 | 66 | rUrl, err := url.Parse(opts.RepoURL) 67 | if err != nil { 68 | return nil, "", fmt.Errorf("parse repo URL: %w", err) 69 | } 70 | 71 | cUrl, err := url.Parse(chartURL) 72 | if err != nil { 73 | return nil, "", fmt.Errorf("parse chart URL: %w", err) 74 | } 75 | 76 | if rUrl.Scheme == cUrl.Scheme && rUrl.Host == cUrl.Host { 77 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth(opts.Username, opts.Password)) 78 | } else { 79 | downloader.Options = append(downloader.Options, helmgetter.WithBasicAuth("", "")) 80 | } 81 | 82 | chartRef = chartURL 83 | } 84 | 85 | return downloader, chartRef, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/chart/notes.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "path" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/werf/3p-helm/pkg/action" 10 | ) 11 | 12 | type BuildNotesOptions struct { 13 | RenderSubchartNotes bool 14 | } 15 | 16 | func BuildNotes(chartName string, renderedTemplates map[string]string, opts BuildNotesOptions) string { 17 | var resultBuf bytes.Buffer 18 | 19 | for filePath, fileContent := range renderedTemplates { 20 | if !strings.HasSuffix(filePath, action.NotesFileSuffix) { 21 | continue 22 | } 23 | 24 | fileContent = strings.TrimRightFunc(fileContent, unicode.IsSpace) 25 | if fileContent == "" { 26 | continue 27 | } 28 | 29 | isTopLevelNotes := filePath == path.Join(chartName, "templates", action.NotesFileSuffix) 30 | 31 | if !isTopLevelNotes && !opts.RenderSubchartNotes { 32 | continue 33 | } 34 | 35 | if resultBuf.Len() > 0 { 36 | resultBuf.WriteString("\n") 37 | } 38 | 39 | resultBuf.WriteString(fileContent) 40 | } 41 | 42 | return resultBuf.String() 43 | } 44 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/Masterminds/sprig/v3" 5 | ) 6 | 7 | var ( 8 | Brand = "Nelm" 9 | Version = "0.0.0" 10 | ) 11 | 12 | const ( 13 | DefaultFieldManager = "helm" 14 | KubectlEditFieldManager = "kubectl-edit" 15 | OldFieldManagerPrefix = "werf" 16 | ) 17 | 18 | type DeployType string 19 | 20 | const ( 21 | // Activated for the first revision of the release. 22 | DeployTypeInitial DeployType = "Initial" 23 | // Activated when no successful revision found. But for the very first revision 24 | // DeployTypeInitial is used instead. 25 | DeployTypeInstall DeployType = "Install" 26 | // Activated when a successful revision found. 27 | DeployTypeUpgrade DeployType = "Upgrade" 28 | DeployTypeRollback DeployType = "Rollback" 29 | DeployTypeUninstall DeployType = "Uninstall" 30 | ) 31 | 32 | type DeletePolicy string 33 | 34 | const ( 35 | DeletePolicySucceeded DeletePolicy = "succeeded" 36 | DeletePolicyFailed DeletePolicy = "failed" 37 | DeletePolicyBeforeCreation DeletePolicy = "before-creation" 38 | ) 39 | 40 | var SprigFuncs = sprig.TxtFuncMap() 41 | -------------------------------------------------------------------------------- /internal/kube/common.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "k8s.io/cli-runtime/pkg/genericclioptions" 7 | "k8s.io/client-go/tools/clientcmd" 8 | "k8s.io/client-go/util/homedir" 9 | ) 10 | 11 | var ( 12 | DefaultKubectlCacheDir = filepath.Join(homedir.HomeDir(), ".kube", "cache") 13 | KubectlCacheDirEnv = "KUBECACHEDIR" 14 | KubectlHttpCacheSubdir = "http" 15 | KubectlDiscoveryCacheSubdir = "discovery" 16 | ) 17 | 18 | func init() { 19 | genericclioptions.ErrEmptyConfig = clientcmd.NewEmptyConfigError("missing or incomplete kubeconfig") 20 | } 21 | -------------------------------------------------------------------------------- /internal/kube/discovery_kube_client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "time" 9 | 10 | "k8s.io/client-go/discovery/cached/disk" 11 | ) 12 | 13 | func NewDiscoveryKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*disk.CachedDiscoveryClient, error) { 14 | var cacheDir string 15 | if dir := os.Getenv(KubectlCacheDirEnv); dir != "" { 16 | cacheDir = dir 17 | } else { 18 | cacheDir = DefaultKubectlCacheDir 19 | } 20 | 21 | httpCacheDir := filepath.Join(cacheDir, KubectlHttpCacheSubdir) 22 | discoveryCacheDir := computeDiscoveryCacheDir(filepath.Join(cacheDir, KubectlDiscoveryCacheSubdir), kubeConfig.RestConfig.Host) 23 | 24 | return disk.NewCachedDiscoveryClientForConfig(kubeConfig.RestConfig, discoveryCacheDir, httpCacheDir, time.Duration(6*time.Hour)) 25 | } 26 | 27 | // Taken from: https://github.com/kubernetes/cli-runtime/blob/e447e205e17575154e7108dbd67e6965499488a0/pkg/genericclioptions/config_flags.go#L485 28 | func computeDiscoveryCacheDir(parentDir, host string) string { 29 | schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) 30 | 31 | safeHost := regexp.MustCompile(`[^(\w/.)]`).ReplaceAllString(schemelessHost, "_") 32 | 33 | return filepath.Join(parentDir, safeHost) 34 | } 35 | -------------------------------------------------------------------------------- /internal/kube/dynamic_kube_client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/client-go/dynamic" 5 | ) 6 | 7 | func NewDynamicKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*dynamic.DynamicClient, error) { 8 | return dynamic.NewForConfig(kubeConfig.RestConfig) 9 | } 10 | -------------------------------------------------------------------------------- /internal/kube/factory.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | 9 | "github.com/samber/lo" 10 | apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 | apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 12 | "k8s.io/apimachinery/pkg/api/meta" 13 | "k8s.io/client-go/discovery" 14 | "k8s.io/client-go/dynamic" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/kubernetes/scheme" 17 | ) 18 | 19 | var addToScheme sync.Once 20 | 21 | func NewClientFactory(ctx context.Context, kubeConfig *KubeConfig) (*ClientFactory, error) { 22 | addToScheme.Do(func() { 23 | lo.Must0(apiextv1.AddToScheme(scheme.Scheme)) 24 | lo.Must0(apiextv1beta1.AddToScheme(scheme.Scheme)) 25 | }) 26 | 27 | staticClient, err := NewStaticKubeClientFromKubeConfig(kubeConfig) 28 | if err != nil { 29 | return nil, fmt.Errorf("construct static kubernetes client: %w", err) 30 | } 31 | 32 | if _, err := staticClient.ServerVersion(); err != nil { 33 | return nil, fmt.Errorf("check kubernetes cluster version to check kubernetes connectivity: %w", err) 34 | } 35 | 36 | dynamicClient, err := NewDynamicKubeClientFromKubeConfig(kubeConfig) 37 | if err != nil { 38 | return nil, fmt.Errorf("construct dynamic kubernetes client: %w", err) 39 | } 40 | 41 | discoveryClient, err := NewDiscoveryKubeClientFromKubeConfig(kubeConfig) 42 | if err != nil { 43 | return nil, fmt.Errorf("construct discovery kubernetes client: %w", err) 44 | } 45 | 46 | mapper := reflect.ValueOf(NewKubeMapper(ctx, discoveryClient)).Interface().(meta.ResettableRESTMapper) 47 | 48 | kubeClient := NewKubeClient(staticClient, dynamicClient, discoveryClient, mapper) 49 | 50 | legacyClientGetter := NewLegacyClientGetter(discoveryClient, mapper, kubeConfig.RestConfig, kubeConfig.LegacyClientConfig) 51 | 52 | clientFactory := &ClientFactory{ 53 | discoveryClient: discoveryClient, 54 | dynamicClient: dynamicClient, 55 | kubeClient: kubeClient, 56 | kubeConfig: kubeConfig, 57 | legacyClientGetter: legacyClientGetter, 58 | mapper: mapper, 59 | staticClient: staticClient, 60 | } 61 | 62 | return clientFactory, nil 63 | } 64 | 65 | type ClientFactory struct { 66 | discoveryClient discovery.CachedDiscoveryInterface 67 | dynamicClient dynamic.Interface 68 | kubeClient KubeClienter 69 | kubeConfig *KubeConfig 70 | legacyClientGetter *LegacyClientGetter 71 | mapper meta.ResettableRESTMapper 72 | staticClient kubernetes.Interface 73 | } 74 | 75 | func (f *ClientFactory) KubeClient() KubeClienter { 76 | return f.kubeClient 77 | } 78 | 79 | func (f *ClientFactory) Static() kubernetes.Interface { 80 | return f.staticClient 81 | } 82 | 83 | func (f *ClientFactory) Dynamic() dynamic.Interface { 84 | return f.dynamicClient 85 | } 86 | 87 | func (f *ClientFactory) Discovery() discovery.CachedDiscoveryInterface { 88 | return f.discoveryClient 89 | } 90 | 91 | func (f *ClientFactory) Mapper() meta.ResettableRESTMapper { 92 | return f.mapper 93 | } 94 | 95 | func (f *ClientFactory) LegacyClientGetter() *LegacyClientGetter { 96 | return f.legacyClientGetter 97 | } 98 | 99 | func (f *ClientFactory) KubeConfig() *KubeConfig { 100 | return f.kubeConfig 101 | } 102 | -------------------------------------------------------------------------------- /internal/kube/interface.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | "github.com/werf/nelm/internal/resource/id" 9 | ) 10 | 11 | type KubeClienter interface { 12 | Get(ctx context.Context, resource *id.ResourceID, opts KubeClientGetOptions) (*unstructured.Unstructured, error) 13 | Create(ctx context.Context, resource *id.ResourceID, unstruct *unstructured.Unstructured, opts KubeClientCreateOptions) (*unstructured.Unstructured, error) 14 | Apply(ctx context.Context, resource *id.ResourceID, unstruct *unstructured.Unstructured, opts KubeClientApplyOptions) (*unstructured.Unstructured, error) 15 | MergePatch(ctx context.Context, resource *id.ResourceID, patch []byte) (*unstructured.Unstructured, error) 16 | Delete(ctx context.Context, resource *id.ResourceID, opts KubeClientDeleteOptions) error 17 | } 18 | -------------------------------------------------------------------------------- /internal/kube/kube_mapper.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | "k8s.io/client-go/discovery" 8 | "k8s.io/client-go/restmapper" 9 | 10 | "github.com/werf/nelm/pkg/log" 11 | ) 12 | 13 | func NewKubeMapper(ctx context.Context, discoveryClient discovery.CachedDiscoveryInterface) meta.RESTMapper { 14 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 15 | 16 | expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(msg string) { 17 | log.Default.Warn(ctx, msg) 18 | }) 19 | 20 | return expander 21 | } 22 | -------------------------------------------------------------------------------- /internal/kube/legacy_client_getter.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/cli-runtime/pkg/genericclioptions" 6 | "k8s.io/client-go/discovery" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/clientcmd" 9 | ) 10 | 11 | var _ genericclioptions.RESTClientGetter = (*LegacyClientGetter)(nil) 12 | 13 | func NewLegacyClientGetter(discoveryClient discovery.CachedDiscoveryInterface, mapper meta.ResettableRESTMapper, restConfig *rest.Config, legacyClientConfig clientcmd.ClientConfig) *LegacyClientGetter { 14 | return &LegacyClientGetter{ 15 | discoveryClient: discoveryClient, 16 | mapper: mapper, 17 | restConfig: restConfig, 18 | legacyClientConfig: legacyClientConfig, 19 | } 20 | } 21 | 22 | type LegacyClientGetter struct { 23 | discoveryClient discovery.CachedDiscoveryInterface 24 | mapper meta.ResettableRESTMapper 25 | restConfig *rest.Config 26 | legacyClientConfig clientcmd.ClientConfig 27 | } 28 | 29 | func (g *LegacyClientGetter) ToRESTConfig() (*rest.Config, error) { 30 | return g.restConfig, nil 31 | } 32 | 33 | func (g *LegacyClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { 34 | return g.discoveryClient, nil 35 | } 36 | 37 | func (g *LegacyClientGetter) ToRESTMapper() (meta.RESTMapper, error) { 38 | return g.mapper, nil 39 | } 40 | 41 | func (g *LegacyClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { 42 | return g.legacyClientConfig 43 | } 44 | -------------------------------------------------------------------------------- /internal/kube/static_kube_client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | ) 6 | 7 | func NewStaticKubeClientFromKubeConfig(kubeConfig *KubeConfig) (*kubernetes.Clientset, error) { 8 | return kubernetes.NewForConfig(kubeConfig.RestConfig) 9 | } 10 | -------------------------------------------------------------------------------- /internal/legacy/deploy/stages_splitter.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/cli-runtime/pkg/resource" 10 | 11 | "github.com/werf/3p-helm/pkg/kube" 12 | "github.com/werf/3p-helm/pkg/phases/stages" 13 | ) 14 | 15 | var metadataAccessor = meta.NewAccessor() 16 | 17 | func NewStagesSplitter() *StagesSplitter { 18 | return &StagesSplitter{} 19 | } 20 | 21 | type StagesSplitter struct{} 22 | 23 | func (s *StagesSplitter) Split(resources kube.ResourceList) (stages.SortedStageList, error) { 24 | stageList := stages.SortedStageList{} 25 | 26 | if err := resources.Visit(func(resInfo *resource.Info, err error) error { 27 | if err != nil { 28 | return err 29 | } 30 | 31 | annotations, err := metadataAccessor.Annotations(resInfo.Object) 32 | if err != nil { 33 | return fmt.Errorf("error getting annotations for object: %w", err) 34 | } 35 | 36 | var weight int 37 | if w, ok := annotations[StageWeightAnnoName]; ok { 38 | weight, err = strconv.Atoi(w) 39 | if err != nil { 40 | return fmt.Errorf("error parsing annotation \"%s: %s\" — value should be an integer: %w", StageWeightAnnoName, w, err) 41 | } 42 | } 43 | 44 | stage := stageList.StageByWeight(weight) 45 | 46 | if stage == nil { 47 | stage = &stages.Stage{ 48 | Weight: weight, 49 | } 50 | stageList = append(stageList, stage) 51 | } 52 | 53 | stage.DesiredResources.Append(resInfo) 54 | 55 | return nil 56 | }); err != nil { 57 | return nil, fmt.Errorf("error visiting resources list: %w", err) 58 | } 59 | 60 | sort.Sort(stageList) 61 | 62 | return stageList, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/plan/common.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | type StageOpNamePrefix string 4 | 5 | const ( 6 | StageOpNameSuffixStart = "start" 7 | StageOpNameSuffixEnd = "end" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/plan/dependency/external_dependency.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | 7 | "github.com/werf/nelm/internal/resource/id" 8 | ) 9 | 10 | func NewExternalDependency(name, namespace string, gvk schema.GroupVersionKind, opts ExternalDependencyOptions) *ExternalDependency { 11 | resID := id.NewResourceID(name, namespace, gvk, id.ResourceIDOptions{ 12 | DefaultNamespace: opts.DefaultNamespace, 13 | FilePath: opts.FilePath, 14 | Mapper: opts.Mapper, 15 | }) 16 | 17 | return &ExternalDependency{ 18 | ResourceID: resID, 19 | } 20 | } 21 | 22 | type ExternalDependencyOptions struct { 23 | DefaultNamespace string 24 | FilePath string 25 | Mapper meta.ResettableRESTMapper 26 | } 27 | 28 | type ExternalDependency struct { 29 | *id.ResourceID 30 | } 31 | -------------------------------------------------------------------------------- /internal/plan/dependency/internal_dependency.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | import ( 4 | "github.com/werf/nelm/internal/resource/matcher" 5 | ) 6 | 7 | func NewInternalDependency(matchNames, matchNamespaces, matchGroups, matchVersions, matchKinds []string, opts InternalDependencyOptions) *InternalDependency { 8 | var resourceState ResourceState 9 | if opts.ResourceState == "" { 10 | resourceState = ResourceStatePresent 11 | } else { 12 | resourceState = opts.ResourceState 13 | } 14 | 15 | resMatcher := matcher.NewResourceMatcher(matchNames, matchNamespaces, matchGroups, matchVersions, matchKinds, matcher.ResourceMatcherOptions{ 16 | DefaultNamespace: opts.DefaultNamespace, 17 | }) 18 | 19 | return &InternalDependency{ 20 | ResourceMatcher: resMatcher, 21 | ResourceState: resourceState, 22 | } 23 | } 24 | 25 | type InternalDependencyOptions struct { 26 | DefaultNamespace string 27 | ResourceState ResourceState 28 | } 29 | 30 | type InternalDependency struct { 31 | *matcher.ResourceMatcher 32 | ResourceState ResourceState 33 | } 34 | -------------------------------------------------------------------------------- /internal/plan/dependency/resource_state.go: -------------------------------------------------------------------------------- 1 | package dependency 2 | 3 | type ResourceState string 4 | 5 | const ( 6 | ResourceStateAbsent ResourceState = "absent" 7 | ResourceStatePresent ResourceState = "present" 8 | ResourceStateReady ResourceState = "ready" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/plan/operation/apply_resource_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | 9 | "github.com/werf/nelm/internal/kube" 10 | "github.com/werf/nelm/internal/resource" 11 | "github.com/werf/nelm/internal/resource/id" 12 | ) 13 | 14 | var _ Operation = (*ApplyResourceOperation)(nil) 15 | 16 | const ( 17 | TypeApplyResourceOperation = "apply" 18 | TypeExtraPostApplyResourceOperation = "extra-post-apply" 19 | ) 20 | 21 | func NewApplyResourceOperation( 22 | resource *id.ResourceID, 23 | unstruct *unstructured.Unstructured, 24 | kubeClient kube.KubeClienter, 25 | opts ApplyResourceOperationOptions, 26 | ) (*ApplyResourceOperation, error) { 27 | return &ApplyResourceOperation{ 28 | resource: resource, 29 | unstruct: unstruct, 30 | kubeClient: kubeClient, 31 | manageableBy: opts.ManageableBy, 32 | extraPost: opts.ExtraPost, 33 | }, nil 34 | } 35 | 36 | type ApplyResourceOperationOptions struct { 37 | ManageableBy resource.ManageableBy 38 | ExtraPost bool 39 | } 40 | 41 | type ApplyResourceOperation struct { 42 | resource *id.ResourceID 43 | unstruct *unstructured.Unstructured 44 | kubeClient kube.KubeClienter 45 | manageableBy resource.ManageableBy 46 | extraPost bool 47 | status Status 48 | } 49 | 50 | func (o *ApplyResourceOperation) Execute(ctx context.Context) error { 51 | if _, err := o.kubeClient.Apply(ctx, o.resource, o.unstruct, kube.KubeClientApplyOptions{}); err != nil { 52 | o.status = StatusFailed 53 | return fmt.Errorf("error applying resource: %w", err) 54 | } 55 | o.status = StatusCompleted 56 | 57 | return nil 58 | } 59 | 60 | func (o *ApplyResourceOperation) ID() string { 61 | if o.extraPost { 62 | return TypeExtraPostApplyResourceOperation + "/" + o.resource.ID() 63 | } 64 | 65 | return TypeApplyResourceOperation + "/" + o.resource.ID() 66 | } 67 | 68 | func (o *ApplyResourceOperation) HumanID() string { 69 | return "apply resource: " + o.resource.HumanID() 70 | } 71 | 72 | func (o *ApplyResourceOperation) Status() Status { 73 | return o.status 74 | } 75 | 76 | func (o *ApplyResourceOperation) Type() Type { 77 | if o.extraPost { 78 | return TypeExtraPostApplyResourceOperation 79 | } 80 | 81 | return TypeApplyResourceOperation 82 | } 83 | 84 | func (o *ApplyResourceOperation) Empty() bool { 85 | return false 86 | } 87 | -------------------------------------------------------------------------------- /internal/plan/operation/create_pending_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/common" 8 | "github.com/werf/nelm/internal/release" 9 | ) 10 | 11 | var _ Operation = (*CreatePendingReleaseOperation)(nil) 12 | 13 | const TypeCreatePendingReleaseOperation = "create-pending-release" 14 | 15 | func NewCreatePendingReleaseOperation( 16 | rel *release.Release, 17 | deployType common.DeployType, 18 | history release.Historier, 19 | ) *CreatePendingReleaseOperation { 20 | return &CreatePendingReleaseOperation{ 21 | deployType: deployType, 22 | release: rel, 23 | history: history, 24 | } 25 | } 26 | 27 | type CreatePendingReleaseOperation struct { 28 | deployType common.DeployType 29 | release *release.Release 30 | history release.Historier 31 | status Status 32 | } 33 | 34 | func (o *CreatePendingReleaseOperation) Execute(ctx context.Context) error { 35 | o.release.Pend(o.deployType) 36 | 37 | if err := o.history.CreateRelease(ctx, o.release); err != nil { 38 | o.status = StatusFailed 39 | return fmt.Errorf("error creating release: %w", err) 40 | } 41 | o.status = StatusCompleted 42 | 43 | return nil 44 | } 45 | 46 | func (o *CreatePendingReleaseOperation) ID() string { 47 | return TypeCreatePendingReleaseOperation + "/" + o.release.ID() 48 | } 49 | 50 | func (o *CreatePendingReleaseOperation) HumanID() string { 51 | return "create pending release: " + o.release.HumanID() 52 | } 53 | 54 | func (o *CreatePendingReleaseOperation) Status() Status { 55 | return o.status 56 | } 57 | 58 | func (o *CreatePendingReleaseOperation) Type() Type { 59 | return TypeCreatePendingReleaseOperation 60 | } 61 | 62 | func (o *CreatePendingReleaseOperation) Empty() bool { 63 | return false 64 | } 65 | -------------------------------------------------------------------------------- /internal/plan/operation/create_resource_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/api/errors" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | 10 | "github.com/werf/nelm/internal/kube" 11 | "github.com/werf/nelm/internal/resource" 12 | "github.com/werf/nelm/internal/resource/id" 13 | ) 14 | 15 | var _ Operation = (*CreateResourceOperation)(nil) 16 | 17 | const ( 18 | TypeCreateResourceOperation = "create" 19 | TypeExtraPostCreateResourceOperation = "extra-post-create" 20 | ) 21 | 22 | func NewCreateResourceOperation( 23 | resource *id.ResourceID, 24 | unstruct *unstructured.Unstructured, 25 | kubeClient kube.KubeClienter, 26 | opts CreateResourceOperationOptions, 27 | ) *CreateResourceOperation { 28 | return &CreateResourceOperation{ 29 | resource: resource, 30 | unstruct: unstruct, 31 | kubeClient: kubeClient, 32 | manageableBy: opts.ManageableBy, 33 | extraPost: opts.ExtraPost, 34 | forceReplicas: opts.ForceReplicas, 35 | } 36 | } 37 | 38 | type CreateResourceOperationOptions struct { 39 | ManageableBy resource.ManageableBy 40 | ForceReplicas *int 41 | ExtraPost bool 42 | } 43 | 44 | type CreateResourceOperation struct { 45 | resource *id.ResourceID 46 | unstruct *unstructured.Unstructured 47 | kubeClient kube.KubeClienter 48 | manageableBy resource.ManageableBy 49 | forceReplicas *int 50 | extraPost bool 51 | status Status 52 | } 53 | 54 | func (o *CreateResourceOperation) Execute(ctx context.Context) error { 55 | if _, err := o.kubeClient.Create(ctx, o.resource, o.unstruct, kube.KubeClientCreateOptions{ 56 | ForceReplicas: o.forceReplicas, 57 | }); err != nil { 58 | if errors.IsAlreadyExists(err) { 59 | if _, err := o.kubeClient.Apply(ctx, o.resource, o.unstruct, kube.KubeClientApplyOptions{}); err != nil { 60 | o.status = StatusFailed 61 | return fmt.Errorf("error applying resource: %w", err) 62 | } 63 | } 64 | 65 | o.status = StatusFailed 66 | return fmt.Errorf("error creating resource: %w", err) 67 | } 68 | 69 | o.status = StatusCompleted 70 | 71 | return nil 72 | } 73 | 74 | func (o *CreateResourceOperation) ID() string { 75 | if o.extraPost { 76 | return TypeExtraPostCreateResourceOperation + "/" + o.resource.ID() 77 | } 78 | 79 | return TypeCreateResourceOperation + "/" + o.resource.ID() 80 | } 81 | 82 | func (o *CreateResourceOperation) HumanID() string { 83 | return "create resource: " + o.resource.HumanID() 84 | } 85 | 86 | func (o *CreateResourceOperation) Status() Status { 87 | return o.status 88 | } 89 | 90 | func (o *CreateResourceOperation) Type() Type { 91 | if o.extraPost { 92 | return TypeExtraPostCreateResourceOperation 93 | } 94 | 95 | return TypeCreateResourceOperation 96 | } 97 | 98 | func (o *CreateResourceOperation) Empty() bool { 99 | return false 100 | } 101 | -------------------------------------------------------------------------------- /internal/plan/operation/delete_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/release" 8 | ) 9 | 10 | var _ Operation = (*DeleteReleaseOperation)(nil) 11 | 12 | const TypeDeleteReleaseOperation = "delete-release" 13 | 14 | func NewDeleteReleaseOperation( 15 | rel *release.Release, 16 | history release.Historier, 17 | ) *DeleteReleaseOperation { 18 | return &DeleteReleaseOperation{ 19 | release: rel, 20 | history: history, 21 | } 22 | } 23 | 24 | type DeleteReleaseOperation struct { 25 | release *release.Release 26 | history release.Historier 27 | status Status 28 | } 29 | 30 | func (o *DeleteReleaseOperation) Execute(ctx context.Context) error { 31 | if err := o.history.DeleteRelease(ctx, o.release); err != nil { 32 | o.status = StatusFailed 33 | return fmt.Errorf("error deleting release: %w", err) 34 | } 35 | 36 | o.status = StatusCompleted 37 | 38 | return nil 39 | } 40 | 41 | func (o *DeleteReleaseOperation) ID() string { 42 | return TypeDeleteReleaseOperation + "/" + o.release.ID() 43 | } 44 | 45 | func (o *DeleteReleaseOperation) HumanID() string { 46 | return "delete release: " + o.release.HumanID() 47 | } 48 | 49 | func (o *DeleteReleaseOperation) Status() Status { 50 | return o.status 51 | } 52 | 53 | func (o *DeleteReleaseOperation) Type() Type { 54 | return TypeDeleteReleaseOperation 55 | } 56 | 57 | func (o *DeleteReleaseOperation) Empty() bool { 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /internal/plan/operation/delete_resource_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/kube" 8 | "github.com/werf/nelm/internal/resource/id" 9 | ) 10 | 11 | var _ Operation = (*DeleteResourceOperation)(nil) 12 | 13 | const ( 14 | TypeDeleteResourceOperation = "delete" 15 | TypeExtraPostDeleteResourceOperation = "extra-post-delete" 16 | ) 17 | 18 | func NewDeleteResourceOperation( 19 | resource *id.ResourceID, 20 | kubeClient kube.KubeClienter, 21 | opts DeleteResourceOperationOptions, 22 | ) *DeleteResourceOperation { 23 | return &DeleteResourceOperation{ 24 | resource: resource, 25 | kubeClient: kubeClient, 26 | extraPost: opts.ExtraPost, 27 | } 28 | } 29 | 30 | type DeleteResourceOperationOptions struct { 31 | ExtraPost bool 32 | } 33 | 34 | type DeleteResourceOperation struct { 35 | resource *id.ResourceID 36 | kubeClient kube.KubeClienter 37 | extraPost bool 38 | status Status 39 | } 40 | 41 | func (o *DeleteResourceOperation) Execute(ctx context.Context) error { 42 | if err := o.kubeClient.Delete(ctx, o.resource, kube.KubeClientDeleteOptions{}); err != nil { 43 | o.status = StatusFailed 44 | return fmt.Errorf("error deleting resource: %w", err) 45 | } 46 | 47 | o.status = StatusCompleted 48 | 49 | return nil 50 | } 51 | 52 | func (o *DeleteResourceOperation) ID() string { 53 | if o.extraPost { 54 | return TypeExtraPostDeleteResourceOperation + "/" + o.resource.ID() 55 | } 56 | 57 | return TypeDeleteResourceOperation + "/" + o.resource.ID() 58 | } 59 | 60 | func (o *DeleteResourceOperation) HumanID() string { 61 | return "delete resource: " + o.resource.HumanID() 62 | } 63 | 64 | func (o *DeleteResourceOperation) Status() Status { 65 | return o.status 66 | } 67 | 68 | func (o *DeleteResourceOperation) Type() Type { 69 | if o.extraPost { 70 | return TypeExtraPostDeleteResourceOperation 71 | } 72 | 73 | return TypeDeleteResourceOperation 74 | } 75 | 76 | func (o *DeleteResourceOperation) Empty() bool { 77 | return false 78 | } 79 | -------------------------------------------------------------------------------- /internal/plan/operation/fail_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/release" 8 | ) 9 | 10 | var _ Operation = (*FailReleaseOperation)(nil) 11 | 12 | const TypeFailReleaseOperation = "fail-release" 13 | 14 | func NewFailReleaseOperation( 15 | rel *release.Release, 16 | history release.Historier, 17 | ) *FailReleaseOperation { 18 | return &FailReleaseOperation{ 19 | release: rel, 20 | history: history, 21 | } 22 | } 23 | 24 | type FailReleaseOperation struct { 25 | release *release.Release 26 | history release.Historier 27 | status Status 28 | } 29 | 30 | func (o *FailReleaseOperation) Execute(ctx context.Context) error { 31 | o.release.Fail() 32 | 33 | if err := o.history.UpdateRelease(ctx, o.release); err != nil { 34 | o.status = StatusFailed 35 | return fmt.Errorf("error updating release: %w", err) 36 | } 37 | o.status = StatusCompleted 38 | 39 | return nil 40 | } 41 | 42 | func (o *FailReleaseOperation) ID() string { 43 | return TypeFailReleaseOperation + "/" + o.release.ID() 44 | } 45 | 46 | func (o *FailReleaseOperation) HumanID() string { 47 | return "fail release: " + o.release.HumanID() 48 | } 49 | 50 | func (o *FailReleaseOperation) Status() Status { 51 | return o.status 52 | } 53 | 54 | func (o *FailReleaseOperation) Type() Type { 55 | return TypeFailReleaseOperation 56 | } 57 | 58 | func (o *FailReleaseOperation) Empty() bool { 59 | return false 60 | } 61 | -------------------------------------------------------------------------------- /internal/plan/operation/interface.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import "context" 4 | 5 | type Operation interface { 6 | Execute(ctx context.Context) error 7 | ID() string 8 | HumanID() string 9 | Status() Status 10 | Type() Type 11 | Empty() bool 12 | } 13 | 14 | type Status string 15 | 16 | const ( 17 | StatusUnknown Status = "" 18 | StatusCompleted Status = "completed" 19 | StatusFailed Status = "failed" 20 | ) 21 | 22 | type Type string 23 | -------------------------------------------------------------------------------- /internal/plan/operation/pending_uninstall_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/common" 8 | "github.com/werf/nelm/internal/release" 9 | ) 10 | 11 | var _ Operation = (*PendingUninstallReleaseOperation)(nil) 12 | 13 | const TypePendingUninstallReleaseOperation = "pending-uninstall-release" 14 | 15 | func NewPendingUninstallReleaseOperation( 16 | rel *release.Release, 17 | history release.Historier, 18 | ) *PendingUninstallReleaseOperation { 19 | return &PendingUninstallReleaseOperation{ 20 | release: rel, 21 | history: history, 22 | } 23 | } 24 | 25 | type PendingUninstallReleaseOperation struct { 26 | release *release.Release 27 | history release.Historier 28 | status Status 29 | } 30 | 31 | func (o *PendingUninstallReleaseOperation) Execute(ctx context.Context) error { 32 | o.release.Pend(common.DeployTypeUninstall) 33 | 34 | if err := o.history.UpdateRelease(ctx, o.release); err != nil { 35 | o.status = StatusFailed 36 | return fmt.Errorf("error updating release: %w", err) 37 | } 38 | 39 | o.status = StatusCompleted 40 | 41 | return nil 42 | } 43 | 44 | func (o *PendingUninstallReleaseOperation) ID() string { 45 | return TypePendingUninstallReleaseOperation + "/" + o.release.ID() 46 | } 47 | 48 | func (o *PendingUninstallReleaseOperation) HumanID() string { 49 | return "pending uninstall release: " + o.release.HumanID() 50 | } 51 | 52 | func (o *PendingUninstallReleaseOperation) Status() Status { 53 | return o.status 54 | } 55 | 56 | func (o *PendingUninstallReleaseOperation) Type() Type { 57 | return TypePendingUninstallReleaseOperation 58 | } 59 | 60 | func (o *PendingUninstallReleaseOperation) Empty() bool { 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /internal/plan/operation/stage_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import "context" 4 | 5 | var _ Operation = (*StageOperation)(nil) 6 | 7 | const TypeStageOperation = "stage" 8 | 9 | func NewStageOperation(name string) *StageOperation { 10 | return &StageOperation{ 11 | name: name, 12 | } 13 | } 14 | 15 | type StageOperation struct { 16 | name string 17 | status Status 18 | } 19 | 20 | func (o *StageOperation) Execute(ctx context.Context) error { 21 | o.status = StatusCompleted 22 | return nil 23 | } 24 | 25 | func (o *StageOperation) ID() string { 26 | return o.name 27 | } 28 | 29 | func (o *StageOperation) HumanID() string { 30 | return o.name 31 | } 32 | 33 | func (o *StageOperation) Status() Status { 34 | return o.status 35 | } 36 | 37 | func (o *StageOperation) Type() Type { 38 | return TypeStageOperation 39 | } 40 | 41 | func (o *StageOperation) Empty() bool { 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /internal/plan/operation/succeed_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/release" 8 | ) 9 | 10 | var _ Operation = (*SucceedReleaseOperation)(nil) 11 | 12 | const TypeSucceedReleaseOperation = "succeed-release" 13 | 14 | func NewSucceedReleaseOperation( 15 | rel *release.Release, 16 | history release.Historier, 17 | ) *SucceedReleaseOperation { 18 | return &SucceedReleaseOperation{ 19 | release: rel, 20 | history: history, 21 | } 22 | } 23 | 24 | type SucceedReleaseOperation struct { 25 | release *release.Release 26 | history release.Historier 27 | status Status 28 | } 29 | 30 | func (o *SucceedReleaseOperation) Execute(ctx context.Context) error { 31 | o.release.Succeed() 32 | 33 | if err := o.history.UpdateRelease(ctx, o.release); err != nil { 34 | o.status = StatusFailed 35 | return fmt.Errorf("error updating release: %w", err) 36 | } 37 | 38 | o.status = StatusCompleted 39 | 40 | return nil 41 | } 42 | 43 | func (o *SucceedReleaseOperation) ID() string { 44 | return TypeSucceedReleaseOperation + "/" + o.release.ID() 45 | } 46 | 47 | func (o *SucceedReleaseOperation) HumanID() string { 48 | return "succeed release: " + o.release.HumanID() 49 | } 50 | 51 | func (o *SucceedReleaseOperation) Status() Status { 52 | return o.status 53 | } 54 | 55 | func (o *SucceedReleaseOperation) Type() Type { 56 | return TypeSucceedReleaseOperation 57 | } 58 | 59 | func (o *SucceedReleaseOperation) Empty() bool { 60 | return false 61 | } 62 | -------------------------------------------------------------------------------- /internal/plan/operation/supersede_release_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/werf/nelm/internal/release" 8 | ) 9 | 10 | var _ Operation = (*SupersedeReleaseOperation)(nil) 11 | 12 | const TypeSupersedeReleaseOperation = "supersede-release" 13 | 14 | func NewSupersedeReleaseOperation( 15 | rel *release.Release, 16 | history release.Historier, 17 | ) *SupersedeReleaseOperation { 18 | return &SupersedeReleaseOperation{ 19 | release: rel, 20 | history: history, 21 | } 22 | } 23 | 24 | type SupersedeReleaseOperation struct { 25 | release *release.Release 26 | history release.Historier 27 | status Status 28 | } 29 | 30 | func (o *SupersedeReleaseOperation) Execute(ctx context.Context) error { 31 | o.release.Supersede() 32 | 33 | if err := o.history.UpdateRelease(ctx, o.release); err != nil { 34 | o.status = StatusFailed 35 | return fmt.Errorf("error updating release: %w", err) 36 | } 37 | 38 | o.status = StatusCompleted 39 | 40 | return nil 41 | } 42 | 43 | func (o *SupersedeReleaseOperation) ID() string { 44 | return TypeSupersedeReleaseOperation + "/" + o.release.ID() 45 | } 46 | 47 | func (o *SupersedeReleaseOperation) HumanID() string { 48 | return "supersede release: " + o.release.HumanID() 49 | } 50 | 51 | func (o *SupersedeReleaseOperation) Status() Status { 52 | return o.status 53 | } 54 | 55 | func (o *SupersedeReleaseOperation) Type() Type { 56 | return TypeSupersedeReleaseOperation 57 | } 58 | 59 | func (o *SupersedeReleaseOperation) Empty() bool { 60 | return false 61 | } 62 | -------------------------------------------------------------------------------- /internal/plan/operation/track_resource_absence.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/client-go/dynamic" 10 | 11 | "github.com/werf/kubedog/pkg/trackers/dyntracker" 12 | "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" 13 | "github.com/werf/kubedog/pkg/trackers/dyntracker/util" 14 | "github.com/werf/nelm/internal/resource/id" 15 | ) 16 | 17 | var _ Operation = (*TrackResourceAbsenceOperation)(nil) 18 | 19 | const TypeTrackResourceAbsenceOperation = "track-resource-absence" 20 | 21 | func NewTrackResourceAbsenceOperation( 22 | resource *id.ResourceID, 23 | taskState *util.Concurrent[*statestore.AbsenceTaskState], 24 | dynamicClient dynamic.Interface, 25 | mapper meta.ResettableRESTMapper, 26 | opts TrackResourceAbsenceOperationOptions, 27 | ) *TrackResourceAbsenceOperation { 28 | return &TrackResourceAbsenceOperation{ 29 | resource: resource, 30 | taskState: taskState, 31 | dynamicClient: dynamicClient, 32 | mapper: mapper, 33 | timeout: opts.Timeout, 34 | pollPeriod: opts.PollPeriod, 35 | } 36 | } 37 | 38 | type TrackResourceAbsenceOperationOptions struct { 39 | Timeout time.Duration 40 | PollPeriod time.Duration 41 | } 42 | 43 | type TrackResourceAbsenceOperation struct { 44 | resource *id.ResourceID 45 | taskState *util.Concurrent[*statestore.AbsenceTaskState] 46 | dynamicClient dynamic.Interface 47 | mapper meta.ResettableRESTMapper 48 | timeout time.Duration 49 | pollPeriod time.Duration 50 | 51 | status Status 52 | } 53 | 54 | func (o *TrackResourceAbsenceOperation) Execute(ctx context.Context) error { 55 | tracker := dyntracker.NewDynamicAbsenceTracker(o.taskState, o.dynamicClient, o.mapper, dyntracker.DynamicAbsenceTrackerOptions{ 56 | Timeout: o.timeout, 57 | PollPeriod: o.pollPeriod, 58 | }) 59 | 60 | if err := tracker.Track(ctx); err != nil { 61 | o.status = StatusFailed 62 | return fmt.Errorf("track resource absence: %w", err) 63 | } 64 | 65 | o.status = StatusCompleted 66 | return nil 67 | } 68 | 69 | func (o *TrackResourceAbsenceOperation) ID() string { 70 | return TypeTrackResourceAbsenceOperation + "/" + o.resource.ID() 71 | } 72 | 73 | func (o *TrackResourceAbsenceOperation) HumanID() string { 74 | return "track resource absence: " + o.resource.HumanID() 75 | } 76 | 77 | func (o *TrackResourceAbsenceOperation) Status() Status { 78 | return o.status 79 | } 80 | 81 | func (o *TrackResourceAbsenceOperation) Type() Type { 82 | return TypeTrackResourceAbsenceOperation 83 | } 84 | 85 | func (o *TrackResourceAbsenceOperation) Empty() bool { 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /internal/plan/operation/track_resource_presence.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/client-go/dynamic" 10 | 11 | "github.com/werf/kubedog/pkg/trackers/dyntracker" 12 | "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" 13 | "github.com/werf/kubedog/pkg/trackers/dyntracker/util" 14 | "github.com/werf/nelm/internal/resource/id" 15 | ) 16 | 17 | var _ Operation = (*TrackResourcePresenceOperation)(nil) 18 | 19 | const TypeTrackResourcePresenceOperation = "track-resource-presence" 20 | 21 | func NewTrackResourcePresenceOperation( 22 | resource *id.ResourceID, 23 | taskState *util.Concurrent[*statestore.PresenceTaskState], 24 | dynamicClient dynamic.Interface, 25 | mapper meta.ResettableRESTMapper, 26 | opts TrackResourcePresenceOperationOptions, 27 | ) *TrackResourcePresenceOperation { 28 | return &TrackResourcePresenceOperation{ 29 | resource: resource, 30 | taskState: taskState, 31 | dynamicClient: dynamicClient, 32 | mapper: mapper, 33 | timeout: opts.Timeout, 34 | pollPeriod: opts.PollPeriod, 35 | } 36 | } 37 | 38 | type TrackResourcePresenceOperationOptions struct { 39 | Timeout time.Duration 40 | PollPeriod time.Duration 41 | } 42 | 43 | type TrackResourcePresenceOperation struct { 44 | resource *id.ResourceID 45 | taskState *util.Concurrent[*statestore.PresenceTaskState] 46 | dynamicClient dynamic.Interface 47 | mapper meta.ResettableRESTMapper 48 | timeout time.Duration 49 | pollPeriod time.Duration 50 | 51 | status Status 52 | } 53 | 54 | func (o *TrackResourcePresenceOperation) Execute(ctx context.Context) error { 55 | tracker := dyntracker.NewDynamicPresenceTracker(o.taskState, o.dynamicClient, o.mapper, dyntracker.DynamicPresenceTrackerOptions{ 56 | Timeout: o.timeout, 57 | PollPeriod: o.pollPeriod, 58 | }) 59 | 60 | if err := tracker.Track(ctx); err != nil { 61 | o.status = StatusFailed 62 | return fmt.Errorf("track resource presence: %w", err) 63 | } 64 | 65 | o.status = StatusCompleted 66 | return nil 67 | } 68 | 69 | func (o *TrackResourcePresenceOperation) ID() string { 70 | return TypeTrackResourcePresenceOperation + "/" + o.resource.ID() 71 | } 72 | 73 | func (o *TrackResourcePresenceOperation) HumanID() string { 74 | return "track resource presence: " + o.resource.HumanID() 75 | } 76 | 77 | func (o *TrackResourcePresenceOperation) Status() Status { 78 | return o.status 79 | } 80 | 81 | func (o *TrackResourcePresenceOperation) Type() Type { 82 | return TypeTrackResourcePresenceOperation 83 | } 84 | 85 | func (o *TrackResourcePresenceOperation) Empty() bool { 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /internal/plan/operation/update_resource_operation.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | 9 | "github.com/werf/nelm/internal/kube" 10 | "github.com/werf/nelm/internal/resource" 11 | "github.com/werf/nelm/internal/resource/id" 12 | ) 13 | 14 | var _ Operation = (*UpdateResourceOperation)(nil) 15 | 16 | const ( 17 | TypeUpdateResourceOperation = "update" 18 | TypeExtraPostUpdateResourceOperation = "extra-post-update" 19 | ) 20 | 21 | func NewUpdateResourceOperation( 22 | resource *id.ResourceID, 23 | unstruct *unstructured.Unstructured, 24 | kubeClient kube.KubeClienter, 25 | opts UpdateResourceOperationOptions, 26 | ) (*UpdateResourceOperation, error) { 27 | return &UpdateResourceOperation{ 28 | resource: resource, 29 | unstruct: unstruct, 30 | kubeClient: kubeClient, 31 | manageableBy: opts.ManageableBy, 32 | extraPost: opts.ExtraPost, 33 | }, nil 34 | } 35 | 36 | type UpdateResourceOperationOptions struct { 37 | ManageableBy resource.ManageableBy 38 | ExtraPost bool 39 | } 40 | 41 | type UpdateResourceOperation struct { 42 | resource *id.ResourceID 43 | unstruct *unstructured.Unstructured 44 | kubeClient kube.KubeClienter 45 | manageableBy resource.ManageableBy 46 | extraPost bool 47 | status Status 48 | } 49 | 50 | func (o *UpdateResourceOperation) Execute(ctx context.Context) error { 51 | if _, err := o.kubeClient.Apply(ctx, o.resource, o.unstruct, kube.KubeClientApplyOptions{}); err != nil { 52 | o.status = StatusFailed 53 | return fmt.Errorf("error applying resource: %w", err) 54 | } 55 | o.status = StatusCompleted 56 | 57 | return nil 58 | } 59 | 60 | func (o *UpdateResourceOperation) ID() string { 61 | if o.extraPost { 62 | return TypeExtraPostUpdateResourceOperation + "/" + o.resource.ID() 63 | } 64 | 65 | return TypeUpdateResourceOperation + "/" + o.resource.ID() 66 | } 67 | 68 | func (o *UpdateResourceOperation) HumanID() string { 69 | return "update resource: " + o.resource.HumanID() 70 | } 71 | 72 | func (o *UpdateResourceOperation) Status() Status { 73 | return o.status 74 | } 75 | 76 | func (o *UpdateResourceOperation) Type() Type { 77 | if o.extraPost { 78 | return TypeExtraPostUpdateResourceOperation 79 | } 80 | 81 | return TypeUpdateResourceOperation 82 | } 83 | 84 | func (o *UpdateResourceOperation) Empty() bool { 85 | return false 86 | } 87 | -------------------------------------------------------------------------------- /internal/plan/plan_executor.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/dominikbraun/graph" 9 | "github.com/samber/lo" 10 | "github.com/sourcegraph/conc/pool" 11 | 12 | "github.com/werf/nelm/internal/plan/operation" 13 | "github.com/werf/nelm/internal/util" 14 | "github.com/werf/nelm/pkg/log" 15 | ) 16 | 17 | func NewPlanExecutor(plan *Plan, opts PlanExecutorOptions) *PlanExecutor { 18 | return &PlanExecutor{ 19 | plan: plan, 20 | networkParallelism: lo.Max([]int{opts.NetworkParallelism, 1}), 21 | } 22 | } 23 | 24 | type PlanExecutorOptions struct { 25 | NetworkParallelism int 26 | } 27 | 28 | type PlanExecutor struct { 29 | plan *Plan 30 | networkParallelism int 31 | } 32 | 33 | func (e *PlanExecutor) Execute(parentCtx context.Context) error { 34 | ctx, ctxCancelFn := context.WithCancel(parentCtx) 35 | 36 | opsMap, err := e.plan.PredecessorMap() 37 | if err != nil { 38 | return fmt.Errorf("error getting plan predecessor map: %w", err) 39 | } 40 | 41 | workerPool := pool.New().WithContext(ctx).WithMaxGoroutines(e.networkParallelism).WithCancelOnError().WithFirstError() 42 | completedOpsIDsCh := make(chan string, 100000) 43 | 44 | for i := 0; len(opsMap) > 0; i++ { 45 | if i > 0 { 46 | if ctx.Err() != nil { 47 | break 48 | } 49 | 50 | var gotCompletedOpID bool 51 | for len(completedOpsIDsCh) > 0 { 52 | completedOpID := <-completedOpsIDsCh 53 | gotCompletedOpID = true 54 | for _, edgeMap := range opsMap { 55 | delete(edgeMap, completedOpID) 56 | } 57 | } 58 | if !gotCompletedOpID { 59 | time.Sleep(100 * time.Millisecond) 60 | continue 61 | } 62 | } 63 | 64 | executableOpsIDs := e.findExecutableOpsIDs(opsMap) 65 | for _, opID := range executableOpsIDs { 66 | opID := opID 67 | delete(opsMap, opID) 68 | e.execOperation(opID, completedOpsIDsCh, workerPool, ctxCancelFn) 69 | } 70 | } 71 | 72 | if err := workerPool.Wait(); err != nil { 73 | return fmt.Errorf("error waiting for operations completion: %w", err) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (e *PlanExecutor) execOperation(opID string, completedOpsIDsCh chan string, workerPool *pool.ContextPool, ctxCancelFn context.CancelFunc) { 80 | workerPool.Go(func(ctx context.Context) error { 81 | failed := true 82 | defer func() { 83 | if failed { 84 | ctxCancelFn() 85 | } 86 | }() 87 | 88 | op := lo.Must(e.plan.Operation(opID)) 89 | 90 | switch op.Type() { 91 | case operation.TypeCreateResourceOperation, 92 | operation.TypeRecreateResourceOperation, 93 | operation.TypeUpdateResourceOperation, 94 | operation.TypeApplyResourceOperation, 95 | operation.TypeDeleteResourceOperation, 96 | operation.TypeExtraPostCreateResourceOperation, 97 | operation.TypeExtraPostRecreateResourceOperation, 98 | operation.TypeExtraPostApplyResourceOperation, 99 | operation.TypeExtraPostUpdateResourceOperation, 100 | operation.TypeExtraPostDeleteResourceOperation: 101 | log.Default.Debug(ctx, util.Capitalize(op.HumanID())) 102 | } 103 | 104 | if err := op.Execute(ctx); err != nil { 105 | return fmt.Errorf("error executing operation: %w", err) 106 | } 107 | 108 | completedOpsIDsCh <- opID 109 | 110 | failed = false 111 | return nil 112 | }) 113 | } 114 | 115 | func (e *PlanExecutor) findExecutableOpsIDs(opsMap map[string]map[string]graph.Edge[string]) []string { 116 | var executableOpsIDs []string 117 | for opID, edgeMap := range opsMap { 118 | if len(edgeMap) == 0 { 119 | executableOpsIDs = append(executableOpsIDs, opID) 120 | } 121 | } 122 | 123 | return executableOpsIDs 124 | } 125 | -------------------------------------------------------------------------------- /internal/plan/resourceinfo/deployable_prev_release_general_resource_info.go: -------------------------------------------------------------------------------- 1 | package resourceinfo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/samber/lo" 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/types" 10 | 11 | "github.com/werf/nelm/internal/common" 12 | "github.com/werf/nelm/internal/kube" 13 | "github.com/werf/nelm/internal/resource" 14 | "github.com/werf/nelm/internal/resource/id" 15 | ) 16 | 17 | func NewDeployablePrevReleaseGeneralResourceInfo(ctx context.Context, res *resource.GeneralResource, releaseNamespace string, kubeClient kube.KubeClienter, mapper meta.ResettableRESTMapper) (*DeployablePrevReleaseGeneralResourceInfo, error) { 18 | getObj, getErr := kubeClient.Get(ctx, res.ResourceID, kube.KubeClientGetOptions{ 19 | TryCache: true, 20 | }) 21 | if getErr != nil { 22 | if isNotFoundErr(getErr) || isNoSuchKindErr(getErr) { 23 | return &DeployablePrevReleaseGeneralResourceInfo{ 24 | ResourceID: res.ResourceID, 25 | resource: res, 26 | }, nil 27 | } else { 28 | return nil, fmt.Errorf("error getting previous release general resource: %w", getErr) 29 | } 30 | } 31 | getResource := resource.NewRemoteResource(getObj, resource.RemoteResourceOptions{ 32 | FallbackNamespace: releaseNamespace, 33 | Mapper: mapper, 34 | }) 35 | 36 | return &DeployablePrevReleaseGeneralResourceInfo{ 37 | ResourceID: res.ResourceID, 38 | resource: res, 39 | getResource: getResource, 40 | exists: getResource != nil, 41 | }, nil 42 | } 43 | 44 | type DeployablePrevReleaseGeneralResourceInfo struct { 45 | *id.ResourceID 46 | resource *resource.GeneralResource 47 | 48 | getResource *resource.RemoteResource 49 | 50 | exists bool 51 | } 52 | 53 | func (i *DeployablePrevReleaseGeneralResourceInfo) Resource() *resource.GeneralResource { 54 | return i.resource 55 | } 56 | 57 | func (i *DeployablePrevReleaseGeneralResourceInfo) LiveResource() *resource.RemoteResource { 58 | return i.getResource 59 | } 60 | 61 | func (i *DeployablePrevReleaseGeneralResourceInfo) ShouldKeepOnDelete(releaseName, releaseNamespace string) bool { 62 | return i.resource.KeepOnDelete() || (i.exists && i.getResource.KeepOnDelete(releaseName, releaseNamespace)) 63 | } 64 | 65 | func (i *DeployablePrevReleaseGeneralResourceInfo) ShouldDelete(curReleaseExistingResourcesUIDs []types.UID, releaseName, releaseNamespace string, deployType common.DeployType) bool { 66 | if !i.exists { 67 | return false 68 | } 69 | 70 | if i.ShouldKeepOnDelete(releaseName, releaseNamespace) { 71 | return false 72 | } 73 | 74 | if deployType == common.DeployTypeUninstall { 75 | return true 76 | } 77 | 78 | return !lo.Contains(curReleaseExistingResourcesUIDs, i.getResource.Unstructured().GetUID()) 79 | } 80 | -------------------------------------------------------------------------------- /internal/plan/resourceinfo/util.go: -------------------------------------------------------------------------------- 1 | package resourceinfo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/apimachinery/pkg/api/validation" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/util/json" 13 | 14 | "github.com/werf/nelm/internal/kube" 15 | "github.com/werf/nelm/internal/resource" 16 | "github.com/werf/nelm/pkg/log" 17 | ) 18 | 19 | func isImmutableErr(err error) bool { 20 | return err != nil && errors.IsInvalid(err) && strings.Contains(err.Error(), validation.FieldImmutableErrorMsg) 21 | } 22 | 23 | func isNoSuchKindErr(err error) bool { 24 | return err != nil && meta.IsNoMatchError(err) 25 | } 26 | 27 | func isNotFoundErr(err error) bool { 28 | return err != nil && errors.IsNotFound(err) 29 | } 30 | 31 | func fixManagedFieldsInCluster(ctx context.Context, namespace string, getObj *unstructured.Unstructured, getResource *resource.RemoteResource, kubeClient kube.KubeClienter, mapper meta.ResettableRESTMapper) error { 32 | if changed, err := getResource.FixManagedFields(); err != nil { 33 | return fmt.Errorf("error fixing managed fields: %w", err) 34 | } else if !changed { 35 | return nil 36 | } 37 | 38 | unstruct := unstructured.Unstructured{Object: map[string]interface{}{}} 39 | unstruct.SetManagedFields(getResource.Unstructured().GetManagedFields()) 40 | 41 | patch, err := json.Marshal(unstruct.UnstructuredContent()) 42 | if err != nil { 43 | return fmt.Errorf("error marshaling fixed managed fields: %w", err) 44 | } 45 | 46 | log.Default.Debug(ctx, "Fixing managed fields for resource %q", getResource.HumanID()) 47 | getObj, err = kubeClient.MergePatch(ctx, getResource.ResourceID, patch) 48 | if err != nil { 49 | return fmt.Errorf("error patching managed fields: %w", err) 50 | } 51 | 52 | getResource = resource.NewRemoteResource(getObj, resource.RemoteResourceOptions{ 53 | FallbackNamespace: namespace, 54 | Mapper: mapper, 55 | }) 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/plan/util.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/types" 5 | 6 | info "github.com/werf/nelm/internal/plan/resourceinfo" 7 | ) 8 | 9 | func CurrentReleaseExistingResourcesUIDs( 10 | standaloneCRDsInfos []*info.DeployableStandaloneCRDInfo, 11 | hookResourcesInfos []*info.DeployableHookResourceInfo, 12 | generalResourcesInfos []*info.DeployableGeneralResourceInfo, 13 | ) (existingUIDs []types.UID, present bool) { 14 | for _, info := range standaloneCRDsInfos { 15 | if uid, found := info.LiveUID(); found { 16 | existingUIDs = append(existingUIDs, uid) 17 | } 18 | } 19 | 20 | for _, info := range hookResourcesInfos { 21 | if uid, found := info.LiveUID(); found { 22 | existingUIDs = append(existingUIDs, uid) 23 | } 24 | } 25 | 26 | for _, info := range generalResourcesInfos { 27 | if uid, found := info.LiveUID(); found { 28 | existingUIDs = append(existingUIDs, uid) 29 | } 30 | } 31 | 32 | return existingUIDs, len(existingUIDs) > 0 33 | } 34 | -------------------------------------------------------------------------------- /internal/release/histories.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/samber/lo" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/client-go/discovery" 9 | 10 | helmrelease "github.com/werf/3p-helm/pkg/release" 11 | "github.com/werf/3p-helm/pkg/releaseutil" 12 | "github.com/werf/3p-helm/pkg/storage/driver" 13 | ) 14 | 15 | type BuildHistoriesOptions struct { 16 | DiscoveryClient discovery.CachedDiscoveryInterface 17 | Mapper meta.ResettableRESTMapper 18 | } 19 | 20 | func BuildHistories(historyStorage LegacyStorage, opts BuildHistoriesOptions) ([]*History, error) { 21 | legacyRels, err := historyStorage.Query(map[string]string{"owner": "helm"}) 22 | if err != nil && err != driver.ErrReleaseNotFound { 23 | return nil, fmt.Errorf("query releases: %w", err) 24 | } 25 | 26 | histories := make(map[string]*History) 27 | for _, legacyRelease := range legacyRels { 28 | id := legacyRelease.Namespace + "/" + legacyRelease.Name 29 | 30 | if _, ok := histories[id]; ok { 31 | histories[id].legacyReleases = append(histories[id].legacyReleases, legacyRelease) 32 | } else { 33 | histories[id] = &History{ 34 | releaseName: legacyRelease.Name, 35 | releaseNamespace: legacyRelease.Namespace, 36 | legacyReleases: []*helmrelease.Release{legacyRelease}, 37 | storage: historyStorage, 38 | mapper: opts.Mapper, 39 | discoveryClient: opts.DiscoveryClient, 40 | } 41 | } 42 | } 43 | 44 | for _, history := range histories { 45 | releaseutil.SortByRevision(history.legacyReleases) 46 | } 47 | 48 | return lo.Values(histories), nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/release/release_differ.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "fmt" 5 | "hash" 6 | "hash/fnv" 7 | "strings" 8 | 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | 11 | helmrelease "github.com/werf/3p-helm/pkg/release" 12 | ) 13 | 14 | func ReleaseUpToDate(oldRel, newRel *Release) (bool, error) { 15 | if oldRel.Status() != helmrelease.StatusDeployed || 16 | oldRel.Notes() != newRel.Notes() { 17 | return false, nil 18 | } 19 | 20 | oldHookResourcesHash := fnv.New32a() 21 | for _, oldHook := range oldRel.HookResources() { 22 | unstruct := cleanUnstruct(oldHook.Unstructured()) 23 | 24 | if err := writeUnstructHash(unstruct, oldHookResourcesHash); err != nil { 25 | return false, fmt.Errorf("write old hook resource %q hash: %w", oldHook.ResourceID.HumanID(), err) 26 | } 27 | } 28 | 29 | newHookResourcesHash := fnv.New32a() 30 | for _, newHook := range newRel.HookResources() { 31 | unstruct := cleanUnstruct(newHook.Unstructured()) 32 | 33 | if err := writeUnstructHash(unstruct, newHookResourcesHash); err != nil { 34 | return false, fmt.Errorf("write new hook resource %q hash: %w", newHook.ResourceID.HumanID(), err) 35 | } 36 | 37 | } 38 | 39 | if oldHookResourcesHash.Sum32() != newHookResourcesHash.Sum32() { 40 | return false, nil 41 | } 42 | 43 | oldGeneralResourcesHash := fnv.New32a() 44 | for _, oldRes := range oldRel.GeneralResources() { 45 | unstruct := cleanUnstruct(oldRes.Unstructured()) 46 | 47 | if err := writeUnstructHash(unstruct, oldGeneralResourcesHash); err != nil { 48 | return false, fmt.Errorf("write old general resource %q hash: %w", oldRes.ResourceID.HumanID(), err) 49 | } 50 | } 51 | 52 | newGeneralResourcesHash := fnv.New32a() 53 | for _, newRes := range newRel.GeneralResources() { 54 | unstruct := cleanUnstruct(newRes.Unstructured()) 55 | 56 | if err := writeUnstructHash(unstruct, newGeneralResourcesHash); err != nil { 57 | return false, fmt.Errorf("write new general resource %q hash: %w", newRes.ResourceID.HumanID(), err) 58 | } 59 | } 60 | 61 | if oldGeneralResourcesHash.Sum32() != newGeneralResourcesHash.Sum32() { 62 | return false, nil 63 | } 64 | 65 | return true, nil 66 | } 67 | 68 | func writeUnstructHash(unstruct *unstructured.Unstructured, hash hash.Hash32) error { 69 | if b, err := unstruct.MarshalJSON(); err != nil { 70 | return fmt.Errorf("unmarshal resource: %w", err) 71 | } else { 72 | hash.Write(b) 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func cleanUnstruct(unstruct *unstructured.Unstructured) *unstructured.Unstructured { 79 | unstr := unstruct.DeepCopy() 80 | 81 | if annos := unstr.GetAnnotations(); len(annos) > 0 { 82 | for key := range annos { 83 | if strings.HasPrefix(key, "project.werf.io/") || 84 | strings.Contains(key, "ci.werf.io/") || 85 | key == "werf.io/version" || 86 | key == "werf.io/release-channel" { 87 | delete(annos, key) 88 | } 89 | } 90 | 91 | unstr.SetAnnotations(annos) 92 | } 93 | 94 | return unstr 95 | } 96 | -------------------------------------------------------------------------------- /internal/release/storage.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/client-go/kubernetes" 8 | 9 | helmaction "github.com/werf/3p-helm/pkg/action" 10 | helmstorage "github.com/werf/3p-helm/pkg/storage" 11 | helmdriver "github.com/werf/3p-helm/pkg/storage/driver" 12 | "github.com/werf/nelm/pkg/log" 13 | ) 14 | 15 | type ReleaseStorageOptions struct { 16 | HistoryLimit int 17 | StaticClient *kubernetes.Clientset 18 | SQLConnectionString string 19 | } 20 | 21 | func NewReleaseStorage(ctx context.Context, namespace, storageDriver string, opts ReleaseStorageOptions) (*helmstorage.Storage, error) { 22 | var storage *helmstorage.Storage 23 | 24 | lazyClient := helmaction.NewLazyClient(namespace, func() (*kubernetes.Clientset, error) { 25 | return opts.StaticClient, nil 26 | }) 27 | 28 | logFn := func(format string, a ...interface{}) { 29 | log.Default.Debug(ctx, format, a...) 30 | } 31 | 32 | switch storageDriver { 33 | case "secret", "secrets", "": 34 | driver := helmdriver.NewSecrets(helmaction.NewSecretClient(lazyClient)) 35 | driver.Log = logFn 36 | 37 | storage = helmstorage.Init(driver) 38 | case "configmap", "configmaps": 39 | driver := helmdriver.NewConfigMaps(helmaction.NewConfigMapClient(lazyClient)) 40 | driver.Log = logFn 41 | 42 | storage = helmstorage.Init(driver) 43 | case "memory": 44 | driver := helmdriver.NewMemory() 45 | driver.SetNamespace(namespace) 46 | 47 | storage = helmstorage.Init(driver) 48 | case "sql": 49 | driver, err := helmdriver.NewSQL(opts.SQLConnectionString, logFn, namespace) 50 | if err != nil { 51 | return nil, fmt.Errorf("construct sql driver: %w", err) 52 | } 53 | 54 | storage = helmstorage.Init(driver) 55 | default: 56 | panic(fmt.Sprintf("Unknown storage driver: %s", storageDriver)) 57 | } 58 | 59 | storage.MaxHistory = opts.HistoryLimit 60 | 61 | return storage, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/resource/drop_invalid_annotations_and_labels_transformer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | "github.com/werf/nelm/pkg/log" 9 | ) 10 | 11 | var _ ResourceTransformer = (*DropInvalidAnnotationsAndLabelsTransformer)(nil) 12 | 13 | const TypeDropInvalidAnnotationsAndLabelsTransformer ResourceTransformerType = "drop-invalid-annotations-and-labels-transformer" 14 | 15 | func NewDropInvalidAnnotationsAndLabelsTransformer() *DropInvalidAnnotationsAndLabelsTransformer { 16 | return &DropInvalidAnnotationsAndLabelsTransformer{} 17 | } 18 | 19 | // TODO(3.0): remove this transformer. Replace it with proper early validation of resource Heads. 20 | type DropInvalidAnnotationsAndLabelsTransformer struct{} 21 | 22 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Match(ctx context.Context, info *ResourceTransformerResourceInfo) (matched bool, err error) { 23 | return true, nil 24 | } 25 | 26 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Transform(ctx context.Context, info *ResourceTransformerResourceInfo) ([]*unstructured.Unstructured, error) { 27 | annotations, _, _ := unstructured.NestedMap(info.Obj.Object, "metadata", "annotations") 28 | 29 | resultAnnotations := make(map[string]string) 30 | for annoKey, rawAnnoValue := range annotations { 31 | annoValue, valIsString := rawAnnoValue.(string) 32 | if !valIsString { 33 | log.Default.Warn(ctx, "Dropped invalid annotation %q in resource %q (%s): key is not a string", annoKey, info.Obj.GetName(), info.Obj.GroupVersionKind().String()) 34 | continue 35 | } 36 | 37 | resultAnnotations[annoKey] = annoValue 38 | } 39 | 40 | labels, _, _ := unstructured.NestedMap(info.Obj.Object, "metadata", "labels") 41 | 42 | resultLabels := make(map[string]string) 43 | for labelKey, rawLabelValue := range labels { 44 | labelValue, valIsString := rawLabelValue.(string) 45 | if !valIsString { 46 | log.Default.Warn(ctx, "Dropped invalid label %q in resource %q (%s): key is not a string", labelKey, info.Obj.GetName(), info.Obj.GroupVersionKind().String()) 47 | continue 48 | } 49 | 50 | resultLabels[labelKey] = labelValue 51 | } 52 | 53 | info.Obj.SetAnnotations(resultAnnotations) 54 | info.Obj.SetLabels(resultLabels) 55 | 56 | return []*unstructured.Unstructured{info.Obj}, nil 57 | } 58 | 59 | func (t *DropInvalidAnnotationsAndLabelsTransformer) Type() ResourceTransformerType { 60 | return TypeDropInvalidAnnotationsAndLabelsTransformer 61 | } 62 | -------------------------------------------------------------------------------- /internal/resource/extra_metadata_patcher.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | var _ ResourcePatcher = (*ExtraMetadataPatcher)(nil) 10 | 11 | const TypeExtraMetadataPatcher ResourcePatcherType = "extra-metadata-patcher" 12 | 13 | func NewExtraMetadataPatcher(annotations, labels map[string]string) *ExtraMetadataPatcher { 14 | return &ExtraMetadataPatcher{ 15 | annotations: annotations, 16 | labels: labels, 17 | } 18 | } 19 | 20 | type ExtraMetadataPatcher struct { 21 | annotations map[string]string 22 | labels map[string]string 23 | } 24 | 25 | func (p *ExtraMetadataPatcher) Match(ctx context.Context, info *ResourcePatcherResourceInfo) (bool, error) { 26 | return true, nil 27 | } 28 | 29 | func (p *ExtraMetadataPatcher) Patch(ctx context.Context, info *ResourcePatcherResourceInfo) (*unstructured.Unstructured, error) { 30 | setAnnotationsAndLabels(info.Obj, p.annotations, p.labels) 31 | return info.Obj, nil 32 | } 33 | 34 | func (p *ExtraMetadataPatcher) Type() ResourcePatcherType { 35 | return TypeExtraMetadataPatcher 36 | } 37 | -------------------------------------------------------------------------------- /internal/resource/id/resource_id.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/samber/lo" 8 | "k8s.io/apimachinery/pkg/api/meta" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | 12 | "github.com/werf/nelm/internal/util" 13 | ) 14 | 15 | func NewResourceID(name, namespace string, gvk schema.GroupVersionKind, opts ResourceIDOptions) *ResourceID { 16 | return &ResourceID{ 17 | name: name, 18 | namespace: namespace, 19 | gvk: gvk, 20 | defaultNamespace: opts.DefaultNamespace, 21 | filePath: opts.FilePath, 22 | mapper: opts.Mapper, 23 | } 24 | } 25 | 26 | func NewResourceIDFromUnstruct(unstruct *unstructured.Unstructured, opts ResourceIDOptions) *ResourceID { 27 | return NewResourceID(unstruct.GetName(), unstruct.GetNamespace(), unstruct.GroupVersionKind(), opts) 28 | } 29 | 30 | type ResourceIDOptions struct { 31 | DefaultNamespace string 32 | FilePath string 33 | Mapper meta.ResettableRESTMapper 34 | } 35 | 36 | func NewResourceIDFromID(id string, opts ResourceIDOptions) *ResourceID { 37 | split := strings.SplitN(id, ":", 4) 38 | lo.Must0(len(split) == 4) 39 | 40 | return NewResourceID(split[3], split[0], schema.GroupVersionKind{ 41 | Group: split[1], 42 | Kind: split[2], 43 | }, opts) 44 | } 45 | 46 | type ResourceID struct { 47 | name string 48 | namespace string 49 | gvk schema.GroupVersionKind 50 | defaultNamespace string 51 | filePath string 52 | mapper meta.ResettableRESTMapper 53 | } 54 | 55 | func (i *ResourceID) Name() string { 56 | return i.name 57 | } 58 | 59 | func (i *ResourceID) Namespace() string { 60 | return util.FallbackNamespace(i.namespace, i.defaultNamespace) 61 | } 62 | 63 | func (i *ResourceID) Namespaced() (namespaced bool, err error) { 64 | if i.mapper == nil { 65 | panic("don't call Namespaced() without mapper") 66 | } 67 | 68 | mapping, err := i.mapper.RESTMapping(i.gvk.GroupKind(), i.gvk.Version) 69 | if err != nil { 70 | return false, fmt.Errorf("error getting resource mapping for %q: %w", i.HumanID(), err) 71 | } 72 | 73 | return mapping.Scope == meta.RESTScopeNamespace, nil 74 | } 75 | 76 | func (i *ResourceID) GroupVersionKind() schema.GroupVersionKind { 77 | return i.gvk 78 | } 79 | 80 | func (i *ResourceID) GroupVersionResource() (schema.GroupVersionResource, error) { 81 | gvk := i.GroupVersionKind() 82 | mapping, err := i.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 83 | if err != nil { 84 | return schema.GroupVersionResource{}, fmt.Errorf("error getting resource mapping for %q: %w", i.HumanID(), err) 85 | } 86 | 87 | return mapping.Resource, nil 88 | } 89 | 90 | func (i *ResourceID) FilePath() string { 91 | return i.filePath 92 | } 93 | 94 | func (i *ResourceID) VersionID() string { 95 | return fmt.Sprintf("%s:%s:%s:%s:%s", i.Namespace(), i.gvk.Group, i.gvk.Version, i.gvk.Kind, i.name) 96 | } 97 | 98 | func (i *ResourceID) ID() string { 99 | return fmt.Sprintf("%s:%s:%s:%s", i.Namespace(), i.gvk.Group, i.gvk.Kind, i.name) 100 | } 101 | 102 | func (i *ResourceID) HumanID() string { 103 | if i.namespace != i.defaultNamespace && i.namespace != "" { 104 | return fmt.Sprintf("%s/%s/%s", i.namespace, i.gvk.Kind, i.Name()) 105 | } 106 | 107 | return fmt.Sprintf("%s/%s", i.gvk.Kind, i.Name()) 108 | } 109 | -------------------------------------------------------------------------------- /internal/resource/matcher/resource_matcher.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | import ( 4 | "github.com/werf/nelm/internal/resource/id" 5 | "github.com/werf/nelm/internal/util" 6 | ) 7 | 8 | func NewResourceMatcher(names, namespaces, groups, versions, kinds []string, opts ResourceMatcherOptions) *ResourceMatcher { 9 | var nses []string 10 | for _, ns := range namespaces { 11 | nses = append(nses, util.FallbackNamespace(ns, opts.DefaultNamespace)) 12 | } 13 | 14 | return &ResourceMatcher{ 15 | names: names, 16 | namespaces: nses, 17 | groups: groups, 18 | versions: versions, 19 | kinds: kinds, 20 | } 21 | } 22 | 23 | type ResourceMatcherOptions struct { 24 | DefaultNamespace string 25 | } 26 | 27 | type ResourceMatcher struct { 28 | names []string 29 | namespaces []string 30 | groups []string 31 | versions []string 32 | kinds []string 33 | } 34 | 35 | func (s *ResourceMatcher) Match(resource *id.ResourceID) bool { 36 | var nameMatch bool 37 | if len(s.names) == 0 { 38 | nameMatch = true 39 | } else { 40 | for _, name := range s.names { 41 | if resource.Name() == name { 42 | nameMatch = true 43 | break 44 | } 45 | } 46 | } 47 | if !nameMatch { 48 | return false 49 | } 50 | 51 | var namespaceMatch bool 52 | if len(s.namespaces) == 0 { 53 | namespaceMatch = true 54 | } else { 55 | for _, namespace := range s.namespaces { 56 | if resource.Namespace() == namespace { 57 | namespaceMatch = true 58 | break 59 | } 60 | } 61 | } 62 | if !namespaceMatch { 63 | return false 64 | } 65 | 66 | var groupMatch bool 67 | if len(s.groups) == 0 { 68 | groupMatch = true 69 | } else { 70 | for _, group := range s.groups { 71 | if resource.GroupVersionKind().Group == group { 72 | groupMatch = true 73 | break 74 | } 75 | } 76 | } 77 | if !groupMatch { 78 | return false 79 | } 80 | 81 | var versionMatch bool 82 | if len(s.versions) == 0 { 83 | versionMatch = true 84 | } else { 85 | for _, version := range s.versions { 86 | if resource.GroupVersionKind().Version == version { 87 | versionMatch = true 88 | break 89 | } 90 | } 91 | } 92 | if !versionMatch { 93 | return false 94 | } 95 | 96 | var kindMatch bool 97 | if len(s.kinds) == 0 { 98 | kindMatch = true 99 | } else { 100 | for _, kind := range s.kinds { 101 | if resource.GroupVersionKind().Kind == kind { 102 | kindMatch = true 103 | break 104 | } 105 | } 106 | } 107 | if !kindMatch { 108 | return false 109 | } 110 | 111 | return true 112 | } 113 | -------------------------------------------------------------------------------- /internal/resource/release_metadata_patcher.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | var _ ResourcePatcher = (*ReleaseMetadataPatcher)(nil) 10 | 11 | const TypeReleaseMetadataPatcher ResourcePatcherType = "release-metadata-patcher" 12 | 13 | func NewReleaseMetadataPatcher(releaseName, releaseNamespace string) *ReleaseMetadataPatcher { 14 | return &ReleaseMetadataPatcher{ 15 | releaseName: releaseName, 16 | releaseNamespace: releaseNamespace, 17 | } 18 | } 19 | 20 | type ReleaseMetadataPatcher struct { 21 | releaseName string 22 | releaseNamespace string 23 | } 24 | 25 | func (p *ReleaseMetadataPatcher) Match(ctx context.Context, info *ResourcePatcherResourceInfo) (bool, error) { 26 | return info.ManageableBy == ManageableBySingleRelease, nil 27 | } 28 | 29 | func (p *ReleaseMetadataPatcher) Patch(ctx context.Context, info *ResourcePatcherResourceInfo) (*unstructured.Unstructured, error) { 30 | annos := map[string]string{} 31 | annos["meta.helm.sh/release-name"] = p.releaseName 32 | annos["meta.helm.sh/release-namespace"] = p.releaseNamespace 33 | 34 | labels := map[string]string{} 35 | labels["app.kubernetes.io/managed-by"] = "Helm" 36 | 37 | setAnnotationsAndLabels(info.Obj, annos, labels) 38 | 39 | return info.Obj, nil 40 | } 41 | 42 | func (p *ReleaseMetadataPatcher) Type() ResourcePatcherType { 43 | return TypeReleaseMetadataPatcher 44 | } 45 | -------------------------------------------------------------------------------- /internal/resource/release_namespace.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | 7 | "github.com/werf/nelm/internal/resource/id" 8 | ) 9 | 10 | const TypeReleaseNamespace Type = "release-namespace" 11 | 12 | func NewReleaseNamespace(unstruct *unstructured.Unstructured, opts ReleaseNamespaceOptions) *ReleaseNamespace { 13 | resID := id.NewResourceIDFromUnstruct(unstruct, id.ResourceIDOptions{ 14 | FilePath: opts.FilePath, 15 | Mapper: opts.Mapper, 16 | }) 17 | 18 | return &ReleaseNamespace{ 19 | ResourceID: resID, 20 | unstruct: unstruct, 21 | mapper: opts.Mapper, 22 | } 23 | } 24 | 25 | type ReleaseNamespaceOptions struct { 26 | FilePath string 27 | Mapper meta.ResettableRESTMapper 28 | } 29 | 30 | type ReleaseNamespace struct { 31 | *id.ResourceID 32 | 33 | unstruct *unstructured.Unstructured 34 | mapper meta.ResettableRESTMapper 35 | } 36 | 37 | func (r *ReleaseNamespace) Validate() error { 38 | return nil 39 | } 40 | 41 | func (r *ReleaseNamespace) Unstructured() *unstructured.Unstructured { 42 | return r.unstruct 43 | } 44 | 45 | func (r *ReleaseNamespace) ManageableBy() ManageableBy { 46 | return ManageableByAnyone 47 | } 48 | 49 | func (r *ReleaseNamespace) Type() Type { 50 | return TypeReleaseNamespace 51 | } 52 | -------------------------------------------------------------------------------- /internal/resource/remote_resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | 7 | "github.com/werf/nelm/internal/resource/id" 8 | ) 9 | 10 | const TypeRemoteResource Type = "remote-resource" 11 | 12 | func NewRemoteResource(unstruct *unstructured.Unstructured, opts RemoteResourceOptions) *RemoteResource { 13 | resID := id.NewResourceIDFromUnstruct(unstruct, id.ResourceIDOptions{ 14 | DefaultNamespace: opts.FallbackNamespace, 15 | Mapper: opts.Mapper, 16 | }) 17 | 18 | return &RemoteResource{ 19 | ResourceID: resID, 20 | unstruct: unstruct, 21 | mapper: opts.Mapper, 22 | } 23 | } 24 | 25 | type RemoteResourceOptions struct { 26 | FallbackNamespace string 27 | Mapper meta.ResettableRESTMapper 28 | } 29 | 30 | type RemoteResource struct { 31 | *id.ResourceID 32 | 33 | unstruct *unstructured.Unstructured 34 | mapper meta.ResettableRESTMapper 35 | } 36 | 37 | func (r *RemoteResource) Unstructured() *unstructured.Unstructured { 38 | return r.unstruct 39 | } 40 | 41 | func (r *RemoteResource) Type() Type { 42 | return TypeRemoteResource 43 | } 44 | 45 | func (r *RemoteResource) FixManagedFields() (changed bool, err error) { 46 | return fixManagedFields(r.unstruct) 47 | } 48 | 49 | func (r *RemoteResource) AdoptableBy(releaseName, releaseNamespace string) (adoptable bool, nonAdoptableReason string) { 50 | return adoptableBy(r.unstruct, releaseName, releaseNamespace) 51 | } 52 | 53 | func (r *RemoteResource) KeepOnDelete(releaseName, releaseNamespace string) bool { 54 | if err := validateResourcePolicy(r.unstruct); err != nil { 55 | return true 56 | } 57 | 58 | return keepOnDelete(r.unstruct) || orphaned(r.unstruct, releaseName, releaseNamespace) 59 | } 60 | -------------------------------------------------------------------------------- /internal/resource/resource_lists_transformer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | var _ ResourceTransformer = (*ResourceListsTransformer)(nil) 12 | 13 | const TypeResourceListsTransformer ResourceTransformerType = "resource-lists-transformer" 14 | 15 | func NewResourceListsTransformer() *ResourceListsTransformer { 16 | return &ResourceListsTransformer{} 17 | } 18 | 19 | type ResourceListsTransformer struct{} 20 | 21 | func (t *ResourceListsTransformer) Match(ctx context.Context, info *ResourceTransformerResourceInfo) (matched bool, err error) { 22 | switch info.Type { 23 | case TypeHookResource, TypeGeneralResource: 24 | default: 25 | return false, nil 26 | } 27 | 28 | return info.Obj.IsList(), nil 29 | } 30 | 31 | func (t *ResourceListsTransformer) Transform(ctx context.Context, info *ResourceTransformerResourceInfo) ([]*unstructured.Unstructured, error) { 32 | var result []*unstructured.Unstructured 33 | 34 | if err := info.Obj.EachListItem( 35 | func(obj runtime.Object) error { 36 | result = append(result, obj.(*unstructured.Unstructured)) 37 | return nil 38 | }, 39 | ); err != nil { 40 | return nil, fmt.Errorf("error iterating over list items: %w", err) 41 | } 42 | 43 | return result, nil 44 | } 45 | 46 | func (t *ResourceListsTransformer) Type() ResourceTransformerType { 47 | return TypeResourceListsTransformer 48 | } 49 | -------------------------------------------------------------------------------- /internal/resource/resource_patcher.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | type ResourcePatcher interface { 10 | Match(ctx context.Context, resourceInfo *ResourcePatcherResourceInfo) (matched bool, err error) 11 | Patch(ctx context.Context, matchedResourceInfo *ResourcePatcherResourceInfo) (output *unstructured.Unstructured, err error) 12 | Type() ResourcePatcherType 13 | } 14 | 15 | type ResourcePatcherResourceInfo struct { 16 | Obj *unstructured.Unstructured 17 | Type Type 18 | ManageableBy ManageableBy 19 | } 20 | 21 | type ResourcePatcherType string 22 | -------------------------------------------------------------------------------- /internal/resource/resource_transformer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | type ResourceTransformerType string 10 | 11 | type ResourceTransformer interface { 12 | Match(ctx context.Context, resourceInfo *ResourceTransformerResourceInfo) (matched bool, err error) 13 | Transform(ctx context.Context, matchedResourceInfo *ResourceTransformerResourceInfo) (output []*unstructured.Unstructured, err error) 14 | Type() ResourceTransformerType 15 | } 16 | 17 | type ResourceTransformerResourceInfo struct { 18 | Obj *unstructured.Unstructured 19 | Type Type 20 | ManageableBy ManageableBy 21 | } 22 | -------------------------------------------------------------------------------- /internal/resource/sort.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/werf/nelm/internal/resource/id" 5 | ) 6 | 7 | func ResourceIDsSortHandler(id1, id2 *id.ResourceID) bool { 8 | kind1 := id1.GroupVersionKind().Kind 9 | kind2 := id2.GroupVersionKind().Kind 10 | if kind1 != kind2 { 11 | return kind1 < kind2 12 | } 13 | 14 | group1 := id1.GroupVersionKind().Group 15 | group2 := id2.GroupVersionKind().Group 16 | if group1 != group2 { 17 | return group1 < group2 18 | } 19 | 20 | version1 := id1.GroupVersionKind().Version 21 | version2 := id2.GroupVersionKind().Version 22 | if version1 != version2 { 23 | return version1 < version2 24 | } 25 | 26 | namespace1 := id1.Namespace() 27 | namespace2 := id2.Namespace() 28 | if namespace1 != namespace2 { 29 | return namespace1 < namespace2 30 | } 31 | 32 | return id1.Name() < id2.Name() 33 | } 34 | -------------------------------------------------------------------------------- /internal/resource/standalone_crd.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/client-go/kubernetes/scheme" 10 | 11 | "github.com/werf/nelm/internal/resource/id" 12 | ) 13 | 14 | const TypeStandaloneCRD Type = "standalone-crd" 15 | 16 | func NewStandaloneCRD(unstruct *unstructured.Unstructured, opts StandaloneCRDOptions) *StandaloneCRD { 17 | resID := id.NewResourceIDFromUnstruct(unstruct, id.ResourceIDOptions{ 18 | FilePath: opts.FilePath, 19 | DefaultNamespace: opts.DefaultNamespace, 20 | Mapper: opts.Mapper, 21 | }) 22 | 23 | return &StandaloneCRD{ 24 | ResourceID: resID, 25 | unstruct: unstruct, 26 | mapper: opts.Mapper, 27 | } 28 | } 29 | 30 | type StandaloneCRDOptions struct { 31 | FilePath string 32 | DefaultNamespace string 33 | Mapper meta.ResettableRESTMapper 34 | } 35 | 36 | func NewStandaloneCRDFromManifest(manifest string, opts StandaloneCRDFromManifestOptions) (*StandaloneCRD, error) { 37 | var filepath string 38 | if opts.FilePath != "" { 39 | filepath = opts.FilePath 40 | } else if strings.HasPrefix(manifest, "# Source: ") { 41 | firstLine := strings.TrimSpace(strings.Split(manifest, "\n")[0]) 42 | filepath = strings.TrimPrefix(firstLine, "# Source: ") 43 | } 44 | 45 | obj, _, err := scheme.Codecs.UniversalDecoder().Decode([]byte(manifest), nil, &unstructured.Unstructured{}) 46 | if err != nil { 47 | return nil, fmt.Errorf("error decoding CRD from file %q: %w", filepath, err) 48 | } 49 | 50 | unstructObj := obj.(*unstructured.Unstructured) 51 | 52 | crd := NewStandaloneCRD(unstructObj, StandaloneCRDOptions{ 53 | FilePath: filepath, 54 | DefaultNamespace: opts.DefaultNamespace, 55 | Mapper: opts.Mapper, 56 | }) 57 | 58 | return crd, nil 59 | } 60 | 61 | type StandaloneCRDFromManifestOptions struct { 62 | FilePath string 63 | DefaultNamespace string 64 | Mapper meta.ResettableRESTMapper 65 | } 66 | 67 | type StandaloneCRD struct { 68 | *id.ResourceID 69 | 70 | unstruct *unstructured.Unstructured 71 | mapper meta.ResettableRESTMapper 72 | } 73 | 74 | func (r *StandaloneCRD) Validate() error { 75 | return nil 76 | } 77 | 78 | func (r *StandaloneCRD) Unstructured() *unstructured.Unstructured { 79 | return r.unstruct 80 | } 81 | 82 | func (r *StandaloneCRD) ManageableBy() ManageableBy { 83 | return ManageableByAnyone 84 | } 85 | 86 | func (r *StandaloneCRD) Type() Type { 87 | return TypeStandaloneCRD 88 | } 89 | -------------------------------------------------------------------------------- /internal/resource/util.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/samber/lo" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ) 12 | 13 | func IsSensitive(groupKind schema.GroupKind, annotations map[string]string) bool { 14 | if _, value, found := FindAnnotationOrLabelByKeyPattern(annotations, annotationKeyPatternSensitive); found { 15 | sensitive := lo.Must(strconv.ParseBool(value)) 16 | 17 | if sensitive { 18 | return true 19 | } 20 | } 21 | 22 | if groupKind == (schema.GroupKind{Group: "", Kind: "Secret"}) { 23 | return true 24 | } 25 | 26 | return false 27 | } 28 | 29 | func IsHook(annotations map[string]string) bool { 30 | _, _, found := FindAnnotationOrLabelByKeyPattern(annotations, annotationKeyPatternHook) 31 | return found 32 | } 33 | 34 | func FindAnnotationOrLabelByKeyPattern(annotationsOrLabels map[string]string, pattern *regexp.Regexp) (key, value string, found bool) { 35 | key, found = lo.FindKeyBy(annotationsOrLabels, func(k, _ string) bool { 36 | return pattern.MatchString(k) 37 | }) 38 | if found { 39 | value = strings.TrimSpace(annotationsOrLabels[key]) 40 | } 41 | 42 | return key, value, found 43 | } 44 | 45 | func FindAnnotationsOrLabelsByKeyPattern(annotationsOrLabels map[string]string, pattern *regexp.Regexp) (result map[string]string, found bool) { 46 | result = map[string]string{} 47 | 48 | for key, value := range annotationsOrLabels { 49 | if pattern.MatchString(key) { 50 | result[key] = strings.TrimSpace(value) 51 | } 52 | } 53 | 54 | return result, len(result) > 0 55 | } 56 | 57 | func setAnnotationsAndLabels(res *unstructured.Unstructured, annotations, labels map[string]string) { 58 | if len(annotations) > 0 { 59 | annos := res.GetAnnotations() 60 | if annos == nil { 61 | annos = map[string]string{} 62 | } 63 | for k, v := range annotations { 64 | annos[k] = v 65 | } 66 | res.SetAnnotations(annos) 67 | } 68 | 69 | if len(labels) > 0 { 70 | lbls := res.GetLabels() 71 | if lbls == nil { 72 | lbls = map[string]string{} 73 | } 74 | for k, v := range labels { 75 | lbls[k] = v 76 | } 77 | res.SetLabels(lbls) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/util/diff.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/aymanbagabas/go-udiff" 9 | "github.com/aymanbagabas/go-udiff/myers" 10 | "github.com/gookit/color" 11 | "github.com/samber/lo" 12 | "github.com/wI2L/jsondiff" 13 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | "k8s.io/apimachinery/pkg/util/json" 15 | ) 16 | 17 | func ColoredUnifiedDiff(from, to string) (uDiff string, present bool) { 18 | edits := myers.ComputeEdits(from, to) 19 | if len(edits) == 0 { 20 | return "", false 21 | } 22 | 23 | uncoloredUDiff := lo.Must1(udiff.ToUnified("", "", from, edits, udiff.DefaultContextLines)) 24 | 25 | var uDiffLines []string 26 | var firstHunkHeaderStripped bool 27 | lines := strings.Split(uncoloredUDiff, "\n") 28 | for i, line := range lines { 29 | if strings.HasPrefix(line, "--- ") || strings.HasPrefix(line, "+++ ") || (i == len(lines)-1 && strings.TrimSpace(line) == "") { 30 | continue 31 | } 32 | 33 | if strings.HasPrefix(line, "@@ ") && strings.HasSuffix(line, " @@") { 34 | if !firstHunkHeaderStripped { 35 | firstHunkHeaderStripped = true 36 | continue 37 | } 38 | uDiffLines = append(uDiffLines, color.Gray.Renderln(" ...")) 39 | } else if strings.HasPrefix(line, "+") { 40 | uDiffLines = append(uDiffLines, color.Green.Renderln(line[:1]+" "+line[1:])) 41 | } else if strings.HasPrefix(line, "-") { 42 | uDiffLines = append(uDiffLines, color.Red.Renderln(line[:1]+" "+line[1:])) 43 | } else if strings.TrimSpace(line) == "" { 44 | uDiffLines = append(uDiffLines, color.Gray.Renderln(line)) 45 | } else { 46 | uDiffLines = append(uDiffLines, color.Gray.Renderln(" "+line)) 47 | } 48 | } 49 | 50 | if len(uDiffLines) == 0 { 51 | return "", false 52 | } 53 | 54 | return strings.Trim(strings.Join(uDiffLines, "\n"), "\n"), true 55 | } 56 | 57 | func ResourcesReallyDiffer(first, second *unstructured.Unstructured) (differ bool, err error) { 58 | firstJson, err := json.Marshal(first.UnstructuredContent()) 59 | if err != nil { 60 | return false, fmt.Errorf("error marshaling live object: %w", err) 61 | } 62 | 63 | secondJson, err := json.Marshal(second.UnstructuredContent()) 64 | if err != nil { 65 | return false, fmt.Errorf("error marshaling desired object: %w", err) 66 | } 67 | 68 | diffOps, err := jsondiff.CompareJSON(firstJson, secondJson) 69 | if err != nil { 70 | return false, fmt.Errorf("error comparing json: %w", err) 71 | } 72 | 73 | significantDiffOps := lo.Filter(diffOps, func(op jsondiff.Operation, _ int) bool { 74 | return !strings.HasPrefix(op.Path, "/metadata/creationTimestamp") && 75 | !strings.HasPrefix(op.Path, "/metadata/generation") && 76 | !strings.HasPrefix(op.Path, "/metadata/resourceVersion") && 77 | !strings.HasPrefix(op.Path, "/metadata/uid") && 78 | !strings.HasPrefix(op.Path, "/status") && 79 | !lo.Must(regexp.MatchString(`^/metadata/managedFields/[0-9]+/time$`, op.Path)) && 80 | !lo.Must(regexp.MatchString(`^/metadata/annotations/.*werf.io.*`, op.Path)) && 81 | !lo.Must(regexp.MatchString(`^/metadata/annotations/helm.sh~1hook.*`, op.Path)) && 82 | !lo.Must(regexp.MatchString(`^/metadata/labels/.*werf.io.*`, op.Path)) 83 | }) 84 | 85 | return len(significantDiffOps) > 0, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/util/groupversion.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/client-go/discovery" 9 | ) 10 | 11 | func ParseKubectlResourceStringtoGVK(resource string, restMapper meta.RESTMapper, discClient discovery.CachedDiscoveryInterface) (schema.GroupVersionKind, error) { 12 | var gvk schema.GroupVersionKind 13 | 14 | gvr := ParseKubectlResourceStringToGVR(resource) 15 | 16 | gvk, err := ConvertGVRtoGVK(gvr, restMapper) 17 | if err != nil { 18 | return gvk, fmt.Errorf("error converting group/version/resource to group/version/kind: %w", err) 19 | } 20 | 21 | return gvk, nil 22 | } 23 | 24 | func ConvertGVRtoGVK(gvr schema.GroupVersionResource, restMapper meta.RESTMapper) (schema.GroupVersionKind, error) { 25 | var gvk schema.GroupVersionKind 26 | if preferredKinds, err := restMapper.KindsFor(gvr); err != nil { 27 | return gvk, fmt.Errorf("error matching a group/version/resource %q: %w", gvr.String(), err) 28 | } else if len(preferredKinds) == 0 { 29 | return gvk, fmt.Errorf("no matches for group/version/resource %q", gvr.String()) 30 | } else { 31 | gvk = preferredKinds[0] 32 | } 33 | 34 | return gvk, nil 35 | } 36 | 37 | func ConvertGVKtoGVR(gvk schema.GroupVersionKind, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, namespaced bool, err error) { 38 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 39 | if err != nil { 40 | return schema.GroupVersionResource{}, false, fmt.Errorf("error getting resource mapping for group/version/kind %q: %w", gvk.String(), err) 41 | } 42 | 43 | return mapping.Resource, mapping.Scope == meta.RESTScopeNamespace, nil 44 | } 45 | 46 | func ParseKubectlResourceStringToGVR(resource string) schema.GroupVersionResource { 47 | var result schema.GroupVersionResource 48 | if gvr, gr := schema.ParseResourceArg(resource); gvr != nil { 49 | result = *gvr 50 | } else { 51 | result = gr.WithVersion("") 52 | } 53 | 54 | return result 55 | } 56 | 57 | func IsCRDFromGK(groupKind schema.GroupKind) bool { 58 | return groupKind == schema.GroupKind{ 59 | Group: "apiextensions.k8s.io", 60 | Kind: "CustomResourceDefinition", 61 | } 62 | } 63 | 64 | func IsCRDFromGR(groupKind schema.GroupResource) bool { 65 | return groupKind == schema.GroupResource{ 66 | Group: "apiextensions.k8s.io", 67 | Resource: "customresourcedefinitions", 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | jsonpatch "github.com/evanphx/json-patch" 8 | "github.com/tidwall/sjson" 9 | "github.com/wI2L/jsondiff" 10 | ) 11 | 12 | func MergeJson(mergeA, toB []byte) (result []byte, changed bool, err error) { 13 | ops, err := jsondiff.CompareJSON(toB, mergeA) 14 | if err != nil { 15 | return nil, false, fmt.Errorf("error comparing json: %w", err) 16 | } 17 | 18 | var addOps []jsondiff.Operation 19 | for _, op := range ops { 20 | switch t := op.Type; t { 21 | case jsondiff.OperationAdd: 22 | addOps = append(addOps, op) 23 | case jsondiff.OperationRemove, jsondiff.OperationReplace: 24 | continue 25 | default: 26 | panic(fmt.Sprintf("unexpected operation type: %s", t)) 27 | } 28 | } 29 | 30 | if len(addOps) == 0 { 31 | return toB, false, nil 32 | } 33 | 34 | var opStrings []string 35 | for _, op := range addOps { 36 | opStrings = append(opStrings, op.String()) 37 | } 38 | 39 | patchString := "[" + strings.Join(opStrings, ",") + "]" 40 | 41 | jpatch, err := jsonpatch.DecodePatch([]byte(patchString)) 42 | if err != nil { 43 | return nil, false, fmt.Errorf("error decoding patch: %w", err) 44 | } 45 | 46 | result, err = jpatch.Apply(toB) 47 | if err != nil { 48 | return nil, false, fmt.Errorf("error applying patch: %w", err) 49 | } 50 | 51 | return result, true, nil 52 | } 53 | 54 | func SubtractJson(fromA, subtractB []byte) (result []byte, changed bool, err error) { 55 | ops, err := jsondiff.CompareJSON(subtractB, fromA) 56 | if err != nil { 57 | return nil, false, fmt.Errorf("error comparing json: %w", err) 58 | } 59 | 60 | var addOps []jsondiff.Operation 61 | for _, op := range ops { 62 | switch t := op.Type; t { 63 | case jsondiff.OperationAdd, jsondiff.OperationReplace: 64 | addOps = append(addOps, op) 65 | case jsondiff.OperationRemove: 66 | continue 67 | default: 68 | panic(fmt.Sprintf("unexpected operation type: %s", t)) 69 | } 70 | } 71 | 72 | res := "{}" 73 | for _, op := range addOps { 74 | jsonPath := JsonPatchPathToJsonPath(op.Path) 75 | var err error 76 | res, err = sjson.Set(res, jsonPath, op.Value) 77 | if err != nil { 78 | return nil, false, fmt.Errorf("error setting value by jsonpath: %w", err) 79 | } 80 | } 81 | 82 | return []byte(res), string(fromA) != res, nil 83 | } 84 | 85 | func JsonPatchPathToJsonPath(path string) string { 86 | if strings.HasPrefix(path, "/") { 87 | path = path[1:] 88 | } 89 | path = strings.ReplaceAll(path, ".", `\.`) 90 | path = strings.ReplaceAll(path, ":", `\:`) 91 | path = strings.ReplaceAll(path, "/", ".") 92 | path = strings.ReplaceAll(path, "~1", "/") 93 | return strings.ReplaceAll(path, "~0", "~") 94 | } 95 | -------------------------------------------------------------------------------- /internal/util/multierror.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/go-multierror" 7 | ) 8 | 9 | func Multierrorf(format string, errs []error, a ...any) error { 10 | if len(errs) == 0 { 11 | return nil 12 | } 13 | 14 | if len(errs) == 1 { 15 | return fmt.Errorf(fmt.Sprintf(format, a...)+": %w", errs[0]) 16 | } 17 | 18 | return fmt.Errorf(fmt.Sprintf(format, a...)+": %w", multierror.Append(nil, errs...)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/util/namespace.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | func FallbackNamespace(namespace string, fallbackNamespaces ...string) string { 6 | if namespace != "" { 7 | return namespace 8 | } 9 | 10 | if len(fallbackNamespaces) > 0 { 11 | for _, ns := range fallbackNamespaces { 12 | if ns != "" { 13 | return ns 14 | } 15 | } 16 | } 17 | 18 | return v1.NamespaceDefault 19 | } 20 | -------------------------------------------------------------------------------- /internal/util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | func Capitalize(s string) string { 6 | if len(s) == 0 { 7 | return s 8 | } 9 | 10 | return strings.ToUpper(s[:1]) + s[1:] 11 | } 12 | -------------------------------------------------------------------------------- /nelm.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | xsFNBGfbI00BEADSeeYyWaeRZ5nmnhwHime19f8P6liuNqwAk8DTRYp+Zei08wod 4 | 6UIXZJFmHDefPqF8heqfG15p2ydV/U+ve4K+zRhaKP1sO7ByFB+N9KpfJkKcE55Y 5 | u4B2B8rEMoit9DNlf7kb3UmnrqL3P1nYnkgjW/uKpJsqzNoLtKd26dw3G5cWDprz 6 | yGM2MW1Ged4zCcghYCPaWTdumwd3aK23PbteiJKh1gtbQ3zTyKSLt8jbdGUPQ1iC 7 | IvgiBl0057wJTaey4zQSYxCZRGp9DCYbbMGLxt5VsCD438tR+qPjFVKPySOmr8iZ 8 | ECZqR7f9bw37dqIk81R/lMHVJ6ySp9yhTEglsiuE6E3b/tU3edRnkEK2GuMjoBhm 9 | Zs06Ki6S0g1n7p+64HeAzaPGoOzcOk8sPndjyiBYXQF1iUKDG0lsjSOWZxcxr2ng 10 | W03hrqfnkEJikLi6aNHMf65uosq/4Qxz7qw1JyZqclsf/0CLCym5lX/7I0faqi2W 11 | wIU0lBdEGHz7EibrZSwK1XwL1ARgocCiaC+CfP5tXYEBDiCeRwXKmevYQse1jIpv 12 | yp+WxIABcCdGobTNr7qEn15DtechvNtJQpZcrIb8OyDECWzCEjXOUbmHKzccQ6qn 13 | zKNp4em9FfRAVIfr/j2GTbWYSlBQtdcMgkzpTD91wNPTdxIYx+VlBuWm8QARAQAB 14 | zR50cmRsICh0cmRsIHNlcnZlciBhdXRvIHNpZ25lcinCwWgEEwEIABwFAmfbI00J 15 | ECAtJdHqfOtHAhsDAhkBAgsHAhUIAABEfBAAq3xyKEv2Z2I0H/1IJ9Hn/Y89p2AO 16 | m4+HDhMQpS5qtecrOjt84UIA79n+O5a4q/dyXHL/v3RKDMSAa6rA/9WwQZD7NzLB 17 | sT2DGs451LOqPSFkkCaui3lxEWDqwtluvFKBCRuym9iPl6d67QeZpaZybBr4Q6fO 18 | xCCwyUP8lS8FrN5GUBWpQL3NKhl3obsJE0ycWV8qDGKw/FprX6lnb2OJy1+LPMy8 19 | VJ90cz1M0p/tFXIeFsMvSgHP4QAuSSxYmhACdBM6Iz2D0zdGfuS5IQHHeCv5DR20 20 | WkJBNLyVQBorO3+/fq2VzOxC3clmJMZ2ejFnHfrWCC1IH+NzKb4PGpdi19S7NOpF 21 | JrrVxOC2XBCqyepAXCUWB67mSHuQt75FnHz5wnMpUdNCMp69X4V0XZ9ANuxcvBfo 22 | lLA/3gIo/yCL+NPe+9gD9scGsgAsKW1dhhFsvTfr5NG5q9wStWfMnRGGLrbWNoUB 23 | Sq9m8dWdeNk3ZQEI7H1E6jCxG7ejIN0qMk7rbzUDQApK9hBIyUPtMGp2vHr0w4do 24 | Wi2UEfNeBzhQWaog2STm/kUAB7axjmWpqdcE9JnhiXgMvIDC1uVRRT2kIbxrDj7g 25 | OAqyzlpX4swvkp58YbMSJ1/myamWgNV7irq+dMrCV/SEDs6Nko4YVJrzmfOeg0xX 26 | YRdhi/+sZH3gfujOwU0EZ9sjTQEQANs8CHRTKlGQ/pctdfC0QqUHUDYewigAO19B 27 | ng/E2pxQ7LckfRLG/AwzUizKk9KueTpJ0VDkZtWY/ysbL0NaJu7WY8df6HILC2Vp 28 | WmFowDMocX4PJQGu0/V5GVZEhpq569iut0s2HJ9p9xKDobDPYbXC02+sJYCJlOPf 29 | ISVDkhTLqoG2+2P4HJ8UuVbqpSDw4oUlee9E/gkwT0v32LKKN8N7Yk8nHWwhYA0Q 30 | eBFVa3XMhLbLqh65FyEus/Tx3UT044X1X8Lt+SptADYxEMecNqHeAOQDiHZyAY/A 31 | YbCNYqXgB3BcrvTZy+Iuh7y2geOSF1lC5VKrKlMrZg3gcmtQd9wYgcdQblzl4zks 32 | uFkyCHjupq1aL9MraVTN+CHvC2DBr+xFK0LB2RZocUYeYHUjfdsDPKaBKzqh+kk0 33 | b7eZXrzIbvO9DzVXnr+B59uJ3tIQL4e/I8L5B/gfSheOjUsswS7DyKvkiY1bNzZK 34 | YREjq5C+KRb2wgUwCIR20KpTWC85r/M/vltsWiJBc1f1KLKoKIFWy/5sdEKIVN12 35 | dI6Khio+iHJTNzWV9zVx8Xp4tjiDjO8tJNX/yPROGHW30GrVosA9JBVwC4f0adDX 36 | NzRHpoei+ivJHR5mRJ3efEfA72InjuBdAFGfmEqUJHIjufsMwU2Ls/8YTTI1RsWz 37 | dIRBbnbbABEBAAHCwV8EGAEIABMFAmfbI00JECAtJdHqfOtHAhsMAACFNxAAMXWq 38 | a5DDc0ASLorEiVM265HjmmRUOTrnSqvdrlAfoKEmBGurJHA5ldJOi3iC90VhYhZE 39 | p5rNhFVqBGeujTISpMn7cQl3g3W2CiXgXis7DuccTRbuzCddZf7MVOuqD4Z8vWw6 40 | 8NSIw6em0cwnhsmDZ2naCZuAo1ktyBVBxpD2N0TlVK+jQ1bqsG3YCWpMDntJBGvz 41 | W05JIO/3Ebd3i4UoahFJQRCbXxdqbyee2iwe8dQp3q/zMQ0HRiYF+uo0MpAIqDOs 42 | wH3jAsvd+b5pcZby1aDFAGGdoC8KWSkB0A6pdpY4ZIcgcAntlpCUTVkY2jgDd2ow 43 | g4ipnbehEoGg2giSJSTxQwd7mMjCR2wIIZmrPd7Kq1xS2FmA/vLPrG8koeiLskLj 44 | zUvMTlS/N5SBj+BRDGRf5KIzs8mi7LFgqInF1hFYvBoOL8L+W5ylPsdySSD7iix7 45 | nFjjN5rw2nasuwTteaN70DX9YFDeIcT5nK4lF1JyCoJPEKZLAc8Rug3uN6Mb/2VZ 46 | LGmZTostkmaPRQ7sqRIRTmuX0Dd/tI03FnzrsLPzamDGR+YvS9VUyVr3AUJWhGVP 47 | Ycuqzn+dJk/6Pcm+y7JghFzqHjbk8e9NEZc+UzCah3zixnovedCFcpDMpH4iXEfX 48 | HZfIDWjHk3sYyolMoxdJL4/Z8Yq6bhKJpCPRbEU= 49 | =5aFS 50 | -----END PGP PUBLIC KEY BLOCK----- 51 | -------------------------------------------------------------------------------- /pkg/action/action_suite_test.go: -------------------------------------------------------------------------------- 1 | package action_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestAction(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Action Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/action/errors.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "fmt" 4 | 5 | type ReleaseNotFoundError struct { 6 | ReleaseName string 7 | ReleaseNamespace string 8 | } 9 | 10 | func (e *ReleaseNotFoundError) Error() string { 11 | return fmt.Sprintf("release %q (namespace %q) not found", e.ReleaseName, e.ReleaseNamespace) 12 | } 13 | 14 | type ReleaseRevisionNotFoundError struct { 15 | ReleaseName string 16 | ReleaseNamespace string 17 | Revision int 18 | } 19 | 20 | func (e *ReleaseRevisionNotFoundError) Error() string { 21 | return fmt.Sprintf("revision %d of release %q (namespace %q) not found", e.Revision, e.ReleaseName, e.ReleaseNamespace) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/action/secret_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretFileDecryptLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretFileDecryptOptions struct { 17 | OutputFilePath string 18 | SecretKey string 19 | SecretWorkDir string 20 | TempDirPath string 21 | } 22 | 23 | func SecretFileDecrypt(ctx context.Context, filePath string, opts SecretFileDecryptOptions) error { 24 | currentDir, err := os.Getwd() 25 | if err != nil { 26 | return fmt.Errorf("get current working directory: %w", err) 27 | } 28 | 29 | opts, err = applySecretFileDecryptOptionsDefaults(opts, currentDir) 30 | if err != nil { 31 | return fmt.Errorf("build secret file decrypt options: %w", err) 32 | } 33 | 34 | if opts.SecretKey != "" { 35 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 36 | } 37 | 38 | if err := secret.SecretFileDecrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, filePath, opts.OutputFilePath); err != nil { 39 | return fmt.Errorf("secret file decrypt: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func applySecretFileDecryptOptionsDefaults(opts SecretFileDecryptOptions, currentDir string) (SecretFileDecryptOptions, error) { 46 | var err error 47 | if opts.TempDirPath == "" { 48 | opts.TempDirPath, err = os.MkdirTemp("", "") 49 | if err != nil { 50 | return SecretFileDecryptOptions{}, fmt.Errorf("create temp dir: %w", err) 51 | } 52 | } 53 | 54 | if opts.SecretWorkDir == "" { 55 | var err error 56 | opts.SecretWorkDir, err = os.Getwd() 57 | if err != nil { 58 | return SecretFileDecryptOptions{}, fmt.Errorf("get current working directory: %w", err) 59 | } 60 | } 61 | 62 | return opts, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/action/secret_file_edit.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretFileEditLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretFileEditOptions struct { 17 | SecretKey string 18 | SecretWorkDir string 19 | TempDirPath string 20 | } 21 | 22 | func SecretFileEdit(ctx context.Context, filePath string, opts SecretFileEditOptions) error { 23 | currentDir, err := os.Getwd() 24 | if err != nil { 25 | return fmt.Errorf("get current working directory: %w", err) 26 | } 27 | 28 | opts, err = applySecretFileEditOptionsDefaults(opts, currentDir) 29 | if err != nil { 30 | return fmt.Errorf("build secret file edit options: %w", err) 31 | } 32 | 33 | if opts.SecretKey != "" { 34 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 35 | } 36 | 37 | if err := secret.SecretEdit(ctx, secrets_manager.Manager, opts.SecretWorkDir, opts.TempDirPath, filePath, false); err != nil { 38 | return fmt.Errorf("secret edit: %w", err) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func applySecretFileEditOptionsDefaults(opts SecretFileEditOptions, currentDir string) (SecretFileEditOptions, error) { 45 | var err error 46 | if opts.TempDirPath == "" { 47 | opts.TempDirPath, err = os.MkdirTemp("", "") 48 | if err != nil { 49 | return SecretFileEditOptions{}, fmt.Errorf("create temp dir: %w", err) 50 | } 51 | } 52 | 53 | if opts.SecretWorkDir == "" { 54 | var err error 55 | opts.SecretWorkDir, err = os.Getwd() 56 | if err != nil { 57 | return SecretFileEditOptions{}, fmt.Errorf("get current working directory: %w", err) 58 | } 59 | } 60 | 61 | return opts, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/action/secret_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretFileEncryptLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretFileEncryptOptions struct { 17 | OutputFilePath string 18 | SecretKey string 19 | SecretWorkDir string 20 | TempDirPath string 21 | } 22 | 23 | func SecretFileEncrypt(ctx context.Context, filePath string, opts SecretFileEncryptOptions) error { 24 | currentDir, err := os.Getwd() 25 | if err != nil { 26 | return fmt.Errorf("get current working directory: %w", err) 27 | } 28 | 29 | opts, err = applySecretFileEncryptOptionsDefaults(opts, currentDir) 30 | if err != nil { 31 | return fmt.Errorf("build secret file encrypt options: %w", err) 32 | } 33 | 34 | if opts.SecretKey != "" { 35 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 36 | } 37 | 38 | if err := secret.SecretFileEncrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, filePath, opts.OutputFilePath); err != nil { 39 | return fmt.Errorf("secret file encrypt: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func applySecretFileEncryptOptionsDefaults(opts SecretFileEncryptOptions, currentDir string) (SecretFileEncryptOptions, error) { 46 | var err error 47 | if opts.TempDirPath == "" { 48 | opts.TempDirPath, err = os.MkdirTemp("", "") 49 | if err != nil { 50 | return SecretFileEncryptOptions{}, fmt.Errorf("create temp dir: %w", err) 51 | } 52 | } 53 | 54 | if opts.SecretWorkDir == "" { 55 | var err error 56 | opts.SecretWorkDir, err = os.Getwd() 57 | if err != nil { 58 | return SecretFileEncryptOptions{}, fmt.Errorf("get current working directory: %w", err) 59 | } 60 | } 61 | 62 | return opts, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/action/secret_key_create.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | ) 10 | 11 | const ( 12 | DefaultSecretKeyCreateLogLevel = ErrorLogLevel 13 | ) 14 | 15 | type SecretKeyCreateOptions struct { 16 | OutputNoPrint bool 17 | TempDirPath string 18 | } 19 | 20 | func SecretKeyCreate(ctx context.Context, opts SecretKeyCreateOptions) (string, error) { 21 | opts, err := applySecretKeyCreateOptionsDefaults(opts) 22 | if err != nil { 23 | return "", fmt.Errorf("build secret key create options: %w", err) 24 | } 25 | 26 | var result string 27 | if !opts.OutputNoPrint { 28 | if keyByte, err := secrets_manager.GenerateSecretKey(); err != nil { 29 | return "", fmt.Errorf("generate secret key: %w", err) 30 | } else { 31 | result = string(keyByte) 32 | } 33 | 34 | fmt.Println(result) 35 | } 36 | 37 | return result, nil 38 | } 39 | 40 | func applySecretKeyCreateOptionsDefaults(opts SecretKeyCreateOptions) (SecretKeyCreateOptions, error) { 41 | var err error 42 | if opts.TempDirPath == "" { 43 | opts.TempDirPath, err = os.MkdirTemp("", "") 44 | if err != nil { 45 | return SecretKeyCreateOptions{}, fmt.Errorf("create temp dir: %w", err) 46 | } 47 | } 48 | 49 | return opts, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/action/secret_key_rotate.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/nelm/pkg/secret" 9 | ) 10 | 11 | const ( 12 | DefaultSecretKeyRotateLogLevel = InfoLogLevel 13 | ) 14 | 15 | type SecretKeyRotateOptions struct { 16 | ChartDirPath string 17 | NewSecretKey string 18 | OldSecretKey string 19 | SecretValuesPaths []string 20 | SecretWorkDir string 21 | TempDirPath string 22 | } 23 | 24 | func SecretKeyRotate(ctx context.Context, opts SecretKeyRotateOptions) error { 25 | currentDir, err := os.Getwd() 26 | if err != nil { 27 | return fmt.Errorf("get current working directory: %w", err) 28 | } 29 | 30 | opts, err = applySecretKeyRotateOptionsDefaults(opts, currentDir) 31 | if err != nil { 32 | return fmt.Errorf("build secret key rotate options: %w", err) 33 | } 34 | 35 | if opts.OldSecretKey != "" { 36 | os.Setenv("WERF_OLD_SECRET_KEY", opts.OldSecretKey) 37 | } 38 | 39 | if opts.NewSecretKey != "" { 40 | os.Setenv("WERF_SECRET_KEY", opts.NewSecretKey) 41 | } 42 | 43 | if err := secret.RotateSecretKey(ctx, opts.ChartDirPath, opts.SecretWorkDir, opts.SecretValuesPaths...); err != nil { 44 | return fmt.Errorf("rotate secret key: %w", err) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func applySecretKeyRotateOptionsDefaults(opts SecretKeyRotateOptions, currentDir string) (SecretKeyRotateOptions, error) { 51 | var err error 52 | if opts.TempDirPath == "" { 53 | opts.TempDirPath, err = os.MkdirTemp("", "") 54 | if err != nil { 55 | return SecretKeyRotateOptions{}, fmt.Errorf("create temp dir: %w", err) 56 | } 57 | } 58 | 59 | if opts.ChartDirPath == "" { 60 | opts.ChartDirPath = currentDir 61 | } 62 | 63 | if opts.SecretWorkDir == "" { 64 | var err error 65 | opts.SecretWorkDir, err = os.Getwd() 66 | if err != nil { 67 | return SecretKeyRotateOptions{}, fmt.Errorf("get current working directory: %w", err) 68 | } 69 | } 70 | 71 | return opts, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_decrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretValuesFileDecryptLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretValuesFileDecryptOptions struct { 17 | OutputFilePath string 18 | SecretKey string 19 | SecretWorkDir string 20 | TempDirPath string 21 | } 22 | 23 | func SecretValuesFileDecrypt(ctx context.Context, valuesFilePath string, opts SecretValuesFileDecryptOptions) error { 24 | currentDir, err := os.Getwd() 25 | if err != nil { 26 | return fmt.Errorf("get current working directory: %w", err) 27 | } 28 | 29 | opts, err = applySecretValuesFileDecryptOptionsDefaults(opts, currentDir) 30 | if err != nil { 31 | return fmt.Errorf("build secret values file decrypt options: %w", err) 32 | } 33 | 34 | if opts.SecretKey != "" { 35 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 36 | } 37 | 38 | if err := secret.SecretValuesDecrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, valuesFilePath, opts.OutputFilePath); err != nil { 39 | return fmt.Errorf("secret values decrypt: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func applySecretValuesFileDecryptOptionsDefaults(opts SecretValuesFileDecryptOptions, currentDir string) (SecretValuesFileDecryptOptions, error) { 46 | var err error 47 | if opts.TempDirPath == "" { 48 | opts.TempDirPath, err = os.MkdirTemp("", "") 49 | if err != nil { 50 | return SecretValuesFileDecryptOptions{}, fmt.Errorf("create temp dir: %w", err) 51 | } 52 | } 53 | 54 | if opts.SecretWorkDir == "" { 55 | var err error 56 | opts.SecretWorkDir, err = os.Getwd() 57 | if err != nil { 58 | return SecretValuesFileDecryptOptions{}, fmt.Errorf("get current working directory: %w", err) 59 | } 60 | } 61 | 62 | return opts, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_edit.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretValuesFileEditLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretValuesFileEditOptions struct { 17 | SecretKey string 18 | SecretWorkDir string 19 | TempDirPath string 20 | } 21 | 22 | func SecretValuesFileEdit(ctx context.Context, valuesFilePath string, opts SecretValuesFileEditOptions) error { 23 | currentDir, err := os.Getwd() 24 | if err != nil { 25 | return fmt.Errorf("get current working directory: %w", err) 26 | } 27 | 28 | opts, err = applySecretValuesFileEditOptionsDefaults(opts, currentDir) 29 | if err != nil { 30 | return fmt.Errorf("build secret values file edit options: %w", err) 31 | } 32 | 33 | if opts.SecretKey != "" { 34 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 35 | } 36 | 37 | if err := secret.SecretEdit(ctx, secrets_manager.Manager, opts.SecretWorkDir, opts.TempDirPath, valuesFilePath, true); err != nil { 38 | return fmt.Errorf("secret edit: %w", err) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func applySecretValuesFileEditOptionsDefaults(opts SecretValuesFileEditOptions, currentDir string) (SecretValuesFileEditOptions, error) { 45 | var err error 46 | if opts.TempDirPath == "" { 47 | opts.TempDirPath, err = os.MkdirTemp("", "") 48 | if err != nil { 49 | return SecretValuesFileEditOptions{}, fmt.Errorf("create temp dir: %w", err) 50 | } 51 | } 52 | 53 | if opts.SecretWorkDir == "" { 54 | var err error 55 | opts.SecretWorkDir, err = os.Getwd() 56 | if err != nil { 57 | return SecretValuesFileEditOptions{}, fmt.Errorf("get current working directory: %w", err) 58 | } 59 | } 60 | 61 | return opts, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/action/secret_values_file_encrypt.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/werf/common-go/pkg/secrets_manager" 9 | "github.com/werf/nelm/pkg/secret" 10 | ) 11 | 12 | const ( 13 | DefaultSecretValuesFileEncryptLogLevel = ErrorLogLevel 14 | ) 15 | 16 | type SecretValuesFileEncryptOptions struct { 17 | OutputFilePath string 18 | SecretKey string 19 | SecretWorkDir string 20 | TempDirPath string 21 | } 22 | 23 | func SecretValuesFileEncrypt(ctx context.Context, valuesFilePath string, opts SecretValuesFileEncryptOptions) error { 24 | currentDir, err := os.Getwd() 25 | if err != nil { 26 | return fmt.Errorf("get current working directory: %w", err) 27 | } 28 | 29 | opts, err = applySecretValuesFileEncryptOptionsDefaults(opts, currentDir) 30 | if err != nil { 31 | return fmt.Errorf("build secret values file encrypt options: %w", err) 32 | } 33 | 34 | if opts.SecretKey != "" { 35 | os.Setenv("WERF_SECRET_KEY", opts.SecretKey) 36 | } 37 | 38 | if err := secret.SecretValuesEncrypt(ctx, secrets_manager.Manager, opts.SecretWorkDir, valuesFilePath, opts.OutputFilePath); err != nil { 39 | return fmt.Errorf("secret values encrypt: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func applySecretValuesFileEncryptOptionsDefaults(opts SecretValuesFileEncryptOptions, currentDir string) (SecretValuesFileEncryptOptions, error) { 46 | var err error 47 | if opts.TempDirPath == "" { 48 | opts.TempDirPath, err = os.MkdirTemp("", "") 49 | if err != nil { 50 | return SecretValuesFileEncryptOptions{}, fmt.Errorf("create temp dir: %w", err) 51 | } 52 | } 53 | 54 | if opts.SecretWorkDir == "" { 55 | var err error 56 | opts.SecretWorkDir, err = os.Getwd() 57 | if err != nil { 58 | return SecretValuesFileEncryptOptions{}, fmt.Errorf("get current working directory: %w", err) 59 | } 60 | } 61 | 62 | return opts, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/action/version.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/Masterminds/semver/v3" 11 | "github.com/goccy/go-yaml" 12 | "github.com/gookit/color" 13 | 14 | "github.com/werf/3p-helm/pkg/chart/loader" 15 | "github.com/werf/nelm/internal/common" 16 | ) 17 | 18 | const ( 19 | DefaultVersionOutputFormat = YamlOutputFormat 20 | DefaultVersionLogLevel = ErrorLogLevel 21 | ) 22 | 23 | type VersionOptions struct { 24 | OutputFormat string 25 | OutputNoPrint bool 26 | TempDirPath string 27 | } 28 | 29 | func Version(ctx context.Context, opts VersionOptions) (*VersionResult, error) { 30 | opts, err := applyVersionOptionsDefaults(opts) 31 | if err != nil { 32 | return nil, fmt.Errorf("build version options: %w", err) 33 | } 34 | 35 | loader.NoChartLockWarning = "" 36 | 37 | result := &VersionResult{ 38 | FullVersion: common.Version, 39 | } 40 | 41 | if semVer, err := semver.StrictNewVersion(common.Version); err == nil { 42 | result.MajorVersion = int(semVer.Major()) 43 | result.MinorVersion = int(semVer.Minor()) 44 | result.PatchVersion = int(semVer.Patch()) 45 | } 46 | 47 | if !opts.OutputNoPrint { 48 | var resultMessage string 49 | 50 | switch opts.OutputFormat { 51 | case JsonOutputFormat: 52 | b, err := json.MarshalIndent(result, "", strings.Repeat(" ", 2)) 53 | if err != nil { 54 | return nil, fmt.Errorf("marshal result to json: %w", err) 55 | } 56 | 57 | resultMessage = string(b) 58 | case YamlOutputFormat: 59 | b, err := yaml.MarshalContext(ctx, result) 60 | if err != nil { 61 | return nil, fmt.Errorf("marshal result to yaml: %w", err) 62 | } 63 | 64 | resultMessage = string(b) 65 | default: 66 | return nil, fmt.Errorf("unknown output format %q", opts.OutputFormat) 67 | } 68 | 69 | var colorLevel color.Level 70 | if color.Enable { 71 | colorLevel = color.TermColorLevel() 72 | } 73 | 74 | if err := writeWithSyntaxHighlight(os.Stdout, resultMessage, string(opts.OutputFormat), colorLevel); err != nil { 75 | return nil, fmt.Errorf("write result to output: %w", err) 76 | } 77 | } 78 | 79 | return result, nil 80 | } 81 | 82 | func applyVersionOptionsDefaults(opts VersionOptions) (VersionOptions, error) { 83 | var err error 84 | if opts.TempDirPath == "" { 85 | opts.TempDirPath, err = os.MkdirTemp("", "") 86 | if err != nil { 87 | return VersionOptions{}, fmt.Errorf("create temp dir: %w", err) 88 | } 89 | } 90 | 91 | if opts.OutputFormat == "" { 92 | opts.OutputFormat = DefaultVersionOutputFormat 93 | } 94 | 95 | return opts, nil 96 | } 97 | 98 | type VersionResult struct { 99 | FullVersion string `json:"full"` 100 | MajorVersion int `json:"major"` 101 | MinorVersion int `json:"minor"` 102 | PatchVersion int `json:"patch"` 103 | } 104 | -------------------------------------------------------------------------------- /pkg/featgate/feat.go: -------------------------------------------------------------------------------- 1 | package featgate 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/chanced/caps" 7 | 8 | "github.com/werf/nelm/internal/common" 9 | ) 10 | 11 | var ( 12 | FeatGateEnvVarsPrefix = caps.ToScreamingSnake(common.Brand) + "_FEAT_" 13 | FeatGates = []*FeatGate{} 14 | 15 | FeatGateRemoteCharts = NewFeatGate( 16 | "remote-charts", 17 | `Allow not only local, but also remote charts as an argument to cli commands. Also adds the "--chart-version" option`, 18 | ) 19 | 20 | FeatGateNativeReleaseList = NewFeatGate( 21 | "native-release-list", 22 | `Use the native "release list" command instead of "helm list" exposed as "release list"`, 23 | ) 24 | 25 | FeatGatePeriodicStackTraces = NewFeatGate( 26 | "periodic-stack-traces", 27 | `Print stack traces periodically to help with debugging deadlocks and other issues`, 28 | ) 29 | 30 | FeatGateNativeReleaseUninstall = NewFeatGate( 31 | "native-release-uninstall", 32 | `Use the new "release uninstall" command implementation (not fully backwards compatible)`, 33 | ) 34 | 35 | FeatGatePreviewV2 = NewFeatGate( 36 | "preview-v2", 37 | `Active all feature gates that will be enabled by default in Nelm v2`, 38 | ) 39 | ) 40 | 41 | func NewFeatGate(name, help string) *FeatGate { 42 | fg := &FeatGate{ 43 | Name: name, 44 | Help: help, 45 | } 46 | 47 | FeatGates = append(FeatGates, fg) 48 | 49 | return fg 50 | } 51 | 52 | type FeatGate struct { 53 | Name string 54 | Help string 55 | } 56 | 57 | func (g *FeatGate) EnvVarName() string { 58 | return FeatGateEnvVarsPrefix + caps.ToScreamingSnake(g.Name) 59 | } 60 | 61 | func (g *FeatGate) Default() bool { 62 | return false 63 | } 64 | 65 | func (g *FeatGate) Enabled() bool { 66 | return os.Getenv(g.EnvVarName()) == "true" 67 | } 68 | -------------------------------------------------------------------------------- /pkg/log/common.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | var ( 4 | Default Logger = DefaultLogboek 5 | DefaultLogboek = NewLogboekLogger() 6 | DefaultNull = NewNullLogger() 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/log/interface.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Logger interface { 8 | Trace(ctx context.Context, format string, a ...interface{}) 9 | TraceStruct(ctx context.Context, obj interface{}, format string, a ...interface{}) 10 | TracePush(ctx context.Context, group, format string, a ...interface{}) 11 | TracePop(ctx context.Context, group string) 12 | Debug(ctx context.Context, format string, a ...interface{}) 13 | DebugPush(ctx context.Context, group, format string, a ...interface{}) 14 | DebugPop(ctx context.Context, group string) 15 | Info(ctx context.Context, format string, a ...interface{}) 16 | InfoPush(ctx context.Context, group, format string, a ...interface{}) 17 | InfoPop(ctx context.Context, group string) 18 | Warn(ctx context.Context, format string, a ...interface{}) 19 | WarnPush(ctx context.Context, group, format string, a ...interface{}) 20 | WarnPop(ctx context.Context, group string) 21 | Error(ctx context.Context, format string, a ...interface{}) 22 | ErrorPush(ctx context.Context, group, format string, a ...interface{}) 23 | ErrorPop(ctx context.Context, group string) 24 | InfoBlock(ctx context.Context, opts BlockOptions, fn func()) 25 | InfoBlockErr(ctx context.Context, opts BlockOptions, fn func() error) error 26 | SetLevel(ctx context.Context, lvl Level) 27 | Level(ctx context.Context) Level 28 | AcceptLevel(ctx context.Context, lvl Level) bool 29 | } 30 | 31 | type Level string 32 | 33 | const ( 34 | SilentLevel Level = "silent" 35 | ErrorLevel Level = "error" 36 | WarningLevel Level = "warning" 37 | InfoLevel Level = "info" 38 | DebugLevel Level = "debug" 39 | TraceLevel Level = "trace" 40 | ) 41 | 42 | var Levels = []Level{SilentLevel, ErrorLevel, WarningLevel, InfoLevel, DebugLevel, TraceLevel} 43 | 44 | type BlockOptions struct { 45 | BlockTitle string 46 | } 47 | -------------------------------------------------------------------------------- /pkg/log/null_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var _ Logger = (*NullLogger)(nil) 8 | 9 | func NewNullLogger() *NullLogger { 10 | return &NullLogger{} 11 | } 12 | 13 | type NullLogger struct{} 14 | 15 | func (l *NullLogger) Trace(ctx context.Context, format string, a ...interface{}) {} 16 | 17 | func (l *NullLogger) TraceStruct(ctx context.Context, obj interface{}, format string, a ...interface{}) { 18 | } 19 | 20 | func (l *NullLogger) TracePush(ctx context.Context, group, format string, a ...interface{}) {} 21 | 22 | func (l *NullLogger) TracePop(ctx context.Context, group string) {} 23 | 24 | func (l *NullLogger) Debug(ctx context.Context, format string, a ...interface{}) {} 25 | 26 | func (l *NullLogger) DebugPush(ctx context.Context, group, format string, a ...interface{}) {} 27 | 28 | func (l *NullLogger) DebugPop(ctx context.Context, group string) {} 29 | 30 | func (l *NullLogger) Info(ctx context.Context, format string, a ...interface{}) {} 31 | 32 | func (l *NullLogger) InfoPush(ctx context.Context, group, format string, a ...interface{}) {} 33 | 34 | func (l *NullLogger) InfoPop(ctx context.Context, group string) {} 35 | 36 | func (l *NullLogger) Warn(ctx context.Context, format string, a ...interface{}) {} 37 | 38 | func (l *NullLogger) WarnPush(ctx context.Context, group, format string, a ...interface{}) {} 39 | 40 | func (l *NullLogger) WarnPop(ctx context.Context, group string) {} 41 | 42 | func (l *NullLogger) Error(ctx context.Context, format string, a ...interface{}) {} 43 | 44 | func (l *NullLogger) ErrorPush(ctx context.Context, group, format string, a ...interface{}) {} 45 | 46 | func (l *NullLogger) ErrorPop(ctx context.Context, group string) {} 47 | 48 | func (l *NullLogger) InfoBlock(ctx context.Context, opts BlockOptions, fn func()) { 49 | return 50 | } 51 | 52 | func (l *NullLogger) InfoBlockErr(ctx context.Context, opts BlockOptions, fn func() error) error { 53 | return nil 54 | } 55 | 56 | func (l *NullLogger) SetLevel(ctx context.Context, lvl Level) {} 57 | 58 | func (l *NullLogger) Level(context.Context) Level { 59 | return InfoLevel 60 | } 61 | 62 | func (l *NullLogger) AcceptLevel(ctx context.Context, lvl Level) bool { 63 | return false 64 | } 65 | -------------------------------------------------------------------------------- /pkg/secret/common.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gookit/color" 11 | "github.com/moby/term" 12 | "golang.org/x/crypto/ssh/terminal" 13 | 14 | "github.com/werf/common-go/pkg/util" 15 | ) 16 | 17 | type GenerateOptions struct { 18 | FilePath string 19 | OutputFilePath string 20 | Values bool 21 | } 22 | 23 | func ExpectedFilePathOrPipeError() error { 24 | return errors.New("expected FILE_PATH or pipe") 25 | } 26 | 27 | func InputFromInteractiveStdin(prompt string) ([]byte, error) { 28 | var data []byte 29 | var err error 30 | 31 | isStdoutTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) 32 | if isStdoutTerminal { 33 | fmt.Printf(color.New(color.Bold).Sprintf(prompt)) 34 | } 35 | 36 | prepareTerminal := func() (func() error, error) { 37 | state, err := term.SetRawTerminal(os.Stdin.Fd()) 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to put terminal into raw mode: %w", err) 40 | } 41 | 42 | restored := false 43 | 44 | return func() error { 45 | if restored { 46 | return nil 47 | } 48 | if err := term.RestoreTerminal(os.Stdin.Fd(), state); err != nil { 49 | return err 50 | } 51 | restored = true 52 | return nil 53 | }, nil 54 | } 55 | 56 | restoreTerminal, err := prepareTerminal() 57 | if err != nil { 58 | return nil, err 59 | } 60 | defer restoreTerminal() 61 | 62 | data, err = terminal.ReadPassword(int(os.Stdin.Fd())) 63 | 64 | if err := restoreTerminal(); err != nil { 65 | return nil, fmt.Errorf("unable to restore terminal: %w", err) 66 | } 67 | 68 | if isStdoutTerminal { 69 | fmt.Println() 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return data, nil 76 | } 77 | 78 | func InputFromStdin() ([]byte, error) { 79 | var data []byte 80 | var err error 81 | 82 | data, err = ioutil.ReadAll(os.Stdin) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return data, nil 88 | } 89 | 90 | func SaveGeneratedData(filePath string, data []byte) error { 91 | if err := os.MkdirAll(filepath.Dir(filePath), 0o777); err != nil { 92 | return err 93 | } 94 | 95 | if err := ioutil.WriteFile(filePath, data, 0o644); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func readFileData(filePath string) ([]byte, error) { 103 | if exist, err := util.FileExists(filePath); err != nil { 104 | return nil, err 105 | } else if !exist { 106 | absFilePath, err := filepath.Abs(filePath) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return nil, fmt.Errorf("secret file %q not found", absFilePath) 112 | } 113 | 114 | fileData, err := ioutil.ReadFile(filePath) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | return fileData, err 120 | } 121 | -------------------------------------------------------------------------------- /pkg/secret/decrypt.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh/terminal" 10 | 11 | "github.com/werf/common-go/pkg/secret" 12 | "github.com/werf/common-go/pkg/secrets_manager" 13 | ) 14 | 15 | func SecretFileDecrypt( 16 | ctx context.Context, 17 | m *secrets_manager.SecretsManager, 18 | workingDir, filePath, outputFilePath string, 19 | ) error { 20 | options := &GenerateOptions{ 21 | FilePath: filePath, 22 | OutputFilePath: outputFilePath, 23 | Values: false, 24 | } 25 | 26 | return secretDecrypt(ctx, m, workingDir, options) 27 | } 28 | 29 | func SecretValuesDecrypt( 30 | ctx context.Context, 31 | m *secrets_manager.SecretsManager, 32 | workingDir, filePath, outputFilePath string, 33 | ) error { 34 | options := &GenerateOptions{ 35 | FilePath: filePath, 36 | OutputFilePath: outputFilePath, 37 | Values: true, 38 | } 39 | 40 | return secretDecrypt(ctx, m, workingDir, options) 41 | } 42 | 43 | func secretDecrypt( 44 | ctx context.Context, 45 | m *secrets_manager.SecretsManager, 46 | workingDir string, 47 | options *GenerateOptions, 48 | ) error { 49 | var encodedData []byte 50 | var data []byte 51 | var err error 52 | 53 | var encoder *secret.YamlEncoder 54 | if enc, err := m.GetYamlEncoder(ctx, workingDir, false); err != nil { 55 | return err 56 | } else { 57 | encoder = enc 58 | } 59 | 60 | if options.FilePath != "" { 61 | encodedData, err = readFileData(options.FilePath) 62 | if err != nil { 63 | return err 64 | } 65 | } else { 66 | if !terminal.IsTerminal(int(os.Stdin.Fd())) { 67 | encodedData, err = InputFromStdin() 68 | if err != nil { 69 | return err 70 | } 71 | } else { 72 | return ExpectedFilePathOrPipeError() 73 | } 74 | 75 | if len(encodedData) == 0 { 76 | return nil 77 | } 78 | } 79 | 80 | encodedData = bytes.TrimSpace(encodedData) 81 | 82 | if options.Values { 83 | data, err = encoder.DecryptYamlData(encodedData) 84 | if err != nil { 85 | return err 86 | } 87 | } else { 88 | data, err = encoder.Decrypt(encodedData) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | if options.OutputFilePath != "" { 95 | if err := SaveGeneratedData(options.OutputFilePath, data); err != nil { 96 | return err 97 | } 98 | } else { 99 | if terminal.IsTerminal(int(os.Stdout.Fd())) { 100 | if !bytes.HasSuffix(data, []byte("\n")) { 101 | data = append(data, []byte("\n")...) 102 | } 103 | } 104 | 105 | fmt.Printf("%s", string(data)) 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /pkg/secret/encrypt.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh/terminal" 10 | 11 | "github.com/werf/common-go/pkg/secret" 12 | "github.com/werf/common-go/pkg/secrets_manager" 13 | ) 14 | 15 | func SecretFileEncrypt( 16 | ctx context.Context, 17 | m *secrets_manager.SecretsManager, 18 | workingDir, filePath, outputFilePath string, 19 | ) error { 20 | options := &GenerateOptions{ 21 | FilePath: filePath, 22 | OutputFilePath: outputFilePath, 23 | Values: false, 24 | } 25 | 26 | return secretEncrypt(ctx, m, workingDir, options) 27 | } 28 | 29 | func SecretValuesEncrypt( 30 | ctx context.Context, 31 | m *secrets_manager.SecretsManager, 32 | workingDir, filePath, outputFilePath string, 33 | ) error { 34 | options := &GenerateOptions{ 35 | FilePath: filePath, 36 | OutputFilePath: outputFilePath, 37 | Values: true, 38 | } 39 | 40 | return secretEncrypt(ctx, m, workingDir, options) 41 | } 42 | 43 | func secretEncrypt( 44 | ctx context.Context, 45 | m *secrets_manager.SecretsManager, 46 | workingDir string, 47 | options *GenerateOptions, 48 | ) error { 49 | var data []byte 50 | var encodedData []byte 51 | var err error 52 | 53 | var encoder *secret.YamlEncoder 54 | if enc, err := m.GetYamlEncoder(ctx, workingDir, false); err != nil { 55 | return err 56 | } else { 57 | encoder = enc 58 | } 59 | 60 | switch { 61 | case options.FilePath != "": 62 | data, err = readFileData(options.FilePath) 63 | if err != nil { 64 | return err 65 | } 66 | case !terminal.IsTerminal(int(os.Stdin.Fd())): 67 | data, err = InputFromStdin() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if len(data) == 0 { 73 | return nil 74 | } 75 | default: 76 | return ExpectedFilePathOrPipeError() 77 | } 78 | 79 | if options.Values { 80 | encodedData, err = encoder.EncryptYamlData(data) 81 | if err != nil { 82 | return err 83 | } 84 | } else { 85 | encodedData, err = encoder.Encrypt(data) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | 91 | if !bytes.HasSuffix(encodedData, []byte("\n")) { 92 | encodedData = append(encodedData, []byte("\n")...) 93 | } 94 | 95 | if options.OutputFilePath != "" { 96 | if err := SaveGeneratedData(options.OutputFilePath, encodedData); err != nil { 97 | return err 98 | } 99 | } else { 100 | fmt.Printf("%s", string(encodedData)) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /resources/images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/f1a561f81a1731e710bd6ccdaefc750963d8b791/resources/images/graph.png -------------------------------------------------------------------------------- /resources/images/nelm-release-install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/f1a561f81a1731e710bd6ccdaefc750963d8b791/resources/images/nelm-release-install.gif -------------------------------------------------------------------------------- /resources/images/nelm-release-plan-install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/werf/nelm/f1a561f81a1731e710bd6ccdaefc750963d8b791/resources/images/nelm-release-plan-install.gif -------------------------------------------------------------------------------- /scripts/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.9-bookworm@sha256:6a3aa4fd2c3e15bc8cb450e4a0ae353fb73b5f593bcbb5b25ffeee860cc2ec2a 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | 4 | ARG TARGETPLATFORM 5 | # linux/amd64 -> linux_amd64 6 | ENV PLATFORM=${TARGETPLATFORM/\//_} 7 | 8 | RUN apt-get -y update && \ 9 | apt-get -y install apt-utils gcc-aarch64-linux-gnu file && \ 10 | curl -sSLO https://github.com/go-task/task/releases/download/v3.43.3/task_${PLATFORM}.deb && \ 11 | apt-get -y install ./task_${PLATFORM}.deb && \ 12 | rm -rf ./task_${PLATFORM}.deb /var/cache/apt/* /var/lib/apt/lists/* /var/log/* 13 | 14 | ADD cmd /.nelm-deps/cmd 15 | ADD pkg /.nelm-deps/pkg 16 | ADD internal /.nelm-deps/internal 17 | COPY go.mod go.sum Taskfile.dist.yaml /.nelm-deps/ 18 | ADD scripts /.nelm-deps/scripts 19 | 20 | RUN cd /.nelm-deps && \ 21 | task build:dist:all version=base && \ 22 | task verify:binaries:dist:all version=base && \ 23 | rm -rf /.nelm-deps 24 | 25 | RUN git config --global --add safe.directory /git 26 | -------------------------------------------------------------------------------- /scripts/verify-dist-binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | script_dir="$(cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 5 | project_dir="$script_dir/.." 6 | 7 | version="${1:?Version should be set}" 8 | 9 | declare -A regexps 10 | regexps["$project_dir/dist/$version/linux-amd64/bin/nelm"]="x86-64.*statically linked" 11 | regexps["$project_dir/dist/$version/linux-arm64/bin/nelm"]="ARM aarch64.*statically linked" 12 | regexps["$project_dir/dist/$version/darwin-amd64/bin/nelm"]="Mach-O.*x86_64" 13 | regexps["$project_dir/dist/$version/darwin-arm64/bin/nelm"]="Mach-O.*arm64" 14 | regexps["$project_dir/dist/$version/windows-amd64/bin/nelm.exe"]="x86-64.*Windows" 15 | 16 | for filename in "${!regexps[@]}"; do 17 | if ! [[ -f "$filename" ]]; then 18 | echo Binary at "$filename" does not exist. 19 | exit 1 20 | fi 21 | 22 | file "$filename" | awk -v regexp="${regexps[$filename]}" '{print $0; if ($0 ~ regexp) { exit } else { print "Unexpected binary info ^^"; exit 1 }}' 23 | done 24 | -------------------------------------------------------------------------------- /trdl.yaml: -------------------------------------------------------------------------------- 1 | docker_image: registry.werf.io/nelm/builder:6aba21f40eb88676822303425f67049243119282@sha256:947036751c83977f2ff5c95d122c8714075450d122bd6a4e9c153b68bcc9cd4c 2 | commands: 3 | - export VERSION="$(echo {{ .Tag }} | cut -c2-)" 4 | - task -o group -p build:dist:all version=$VERSION 5 | - task -p verify:binaries:dist:all version=$VERSION 6 | - cp -a ./dist/$VERSION/* /result 7 | -------------------------------------------------------------------------------- /trdl_channels.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: "1" 3 | channels: 4 | - name: alpha 5 | version: 1.5.0 6 | - name: beta 7 | version: 1.4.1 8 | - name: ea 9 | version: 1.4.1 10 | - name: stable 11 | version: 1.3.0 12 | - name: rock-solid 13 | version: 1.2.2 14 | --------------------------------------------------------------------------------