├── .coveragerc ├── .editorconfig ├── .editorconfig-checker.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── workflows │ ├── code_checks.yml │ ├── lint_pr_titles.yml │ ├── release_please.yml │ └── workflows_checks.yml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .pylintrc ├── .python-version ├── .taplo.toml ├── .tool-versions ├── CONTRIBUTING.md ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── poe_tasks.toml ├── pyproject.toml ├── pytest.ini ├── renovate.json5 ├── ruff.toml ├── ruff_defaults.toml ├── src └── kubesplit │ ├── __init__.py │ ├── __main__.py │ ├── args.py │ ├── config.py │ ├── convert.py │ ├── errors.py │ ├── helpers.py │ ├── k8s_descriptor.py │ ├── kubesplit.py │ ├── namespaces.py │ └── output.py ├── test-assets ├── expected │ ├── all-in-one--no-quotes-preserved--no-resource-prefix │ │ ├── apps-demo │ │ │ └── rolebinding--example-ns-demo-developer-binding.yml │ │ ├── apps-integration │ │ │ └── rolebinding--example-ns-integration-developer-binding.yml │ │ ├── clusterrole--example-node-viewer.yml │ │ ├── clusterrole--example-traefik-ingress-controller.yml │ │ ├── clusterrolebinding--example-node-viewer-developer.yml │ │ ├── clusterrolebinding--example-traefik-ingress-controller.yml │ │ ├── ingress-controllers │ │ │ ├── configmap--traefik-conf.yml │ │ │ ├── deployment--traefik-ingress-controller.yml │ │ │ ├── ingress--traefik-web-ui.yml │ │ │ ├── persistentvolumeclaim--traefik-acme.yml │ │ │ ├── service--traefik-ingress-endpoint.yml │ │ │ ├── service--traefik-web-ui.yml │ │ │ └── serviceaccount--traefik-ingress-controller.yml │ │ ├── namespace--apps-demo.yml │ │ ├── namespace--apps-integration.yml │ │ └── namespace--ingress-controllers.yml │ ├── all-in-one--no-quotes-preserved │ │ ├── 00--namespace--apps-demo.yml │ │ ├── 00--namespace--apps-integration.yml │ │ ├── 00--namespace--ingress-controllers.yml │ │ ├── 01--clusterrole--example-node-viewer.yml │ │ ├── 01--clusterrole--example-traefik-ingress-controller.yml │ │ ├── 02--clusterrolebinding--example-node-viewer-developer.yml │ │ ├── 02--clusterrolebinding--example-traefik-ingress-controller.yml │ │ ├── apps-demo │ │ │ └── 05--rolebinding--example-ns-demo-developer-binding.yml │ │ ├── apps-integration │ │ │ └── 05--rolebinding--example-ns-integration-developer-binding.yml │ │ └── ingress-controllers │ │ │ ├── 03--serviceaccount--traefik-ingress-controller.yml │ │ │ ├── 11--configmap--traefik-conf.yml │ │ │ ├── 12--persistentvolumeclaim--traefik-acme.yml │ │ │ ├── 20--deployment--traefik-ingress-controller.yml │ │ │ ├── 30--service--traefik-ingress-endpoint.yml │ │ │ ├── 30--service--traefik-web-ui.yml │ │ │ └── 31--ingress--traefik-web-ui.yml │ ├── k8s-deployment-with-comments-1--no-quotes-preserved--no-resource-prefix--spaces-before-comment_1 │ │ └── deployment--crashing-for-tests-because-command.yml │ ├── k8s-deployment-with-comments-1--no-quotes-preserved │ │ └── 20--deployment--crashing-for-tests-because-command.yml │ ├── mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved │ │ ├── akira │ │ │ └── 25--replicaset--bididididi.yml │ │ └── yolo │ │ │ └── 25--replicaset--frontend.yml │ └── mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved │ │ ├── 99--configmaplist--list_0.yml │ │ ├── 99--rolelist--list_1.yml │ │ ├── 99--rolelist--list_2.yml │ │ ├── akira │ │ └── 25--replicaset--bididididi.yml │ │ └── yolo │ │ └── 25--replicaset--frontend.yml ├── source │ ├── all-in-one.yml │ ├── k8s-deployment-with-comments-1.yml │ ├── mixed-content-valid-invalid-and-empty-resources.yml │ └── mixed-content-valid-invalid-empty-and-list-resources.yml └── test_kubesplit.bash ├── tests.bats ├── tests ├── test_convert.py ├── test_k8s_descriptor.py ├── test_namespaces.py └── test_output.py ├── toolbox └── mk │ ├── common.mk │ ├── mdlint.mk │ ├── pre-commit.mk │ ├── python-base-app.mk │ ├── python-base-venv.mk │ ├── python-uv-app.mk │ ├── python-uv-extras.mk │ ├── python-uv-venv.mk │ └── remote-mk.mk ├── tox.ini ├── uv.lock ├── version.txt └── wait-for-pypi.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = True 3 | branch = True 4 | source = 5 | kubesplit 6 | omit = 7 | .tox 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | max_line_length = 120 14 | trim_trailing_whitespace = true 15 | 16 | # 4 space indentation 17 | # don't check for max_line_length, rely on pylint 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | max_line_length = unset 22 | 23 | [Makefile] 24 | indent_style = tab 25 | max_line_length = unset 26 | 27 | [*.mk] 28 | indent_style = tab 29 | max_line_length = unset 30 | 31 | [setup.cfg] 32 | indent_style = tab 33 | 34 | [*.{tf,tfvars}] 35 | indent_size = 2 36 | indent_style = space 37 | max_line_length = unset 38 | 39 | [*.{yml,yaml}] 40 | indent_size = 2 41 | indent_style = space 42 | max_line_length = unset 43 | 44 | # Fix for json files 45 | [*.json] 46 | max_line_length = unset 47 | 48 | [*.{md,rst}] 49 | trim_trailing_whitespace = false 50 | max_line_length = unset 51 | indent_size = unset 52 | 53 | [*.{sh,bats}] 54 | max_line_length = unset 55 | 56 | [LICENSE] 57 | max_line_length = unset 58 | indent_style = unset 59 | indent_size = unset 60 | 61 | # Fix for tox.ini 62 | [**/tox.ini] 63 | max_line_length = unset 64 | 65 | [*.txt] 66 | max_line_length = unset 67 | 68 | [.pylintrc] 69 | max_line_length = unset 70 | 71 | [poetry.lock] 72 | max_line_length = unset 73 | 74 | [uv.lock] 75 | max_line_length = unset 76 | -------------------------------------------------------------------------------- /.editorconfig-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Exclude": [ 3 | "experiments" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @looztra 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Declare a new kubesplit issue 2 | 3 | ## Metadata 4 | 5 | * kubesplit version: 6 | * Python version: 7 | * Operating System: 8 | 9 | ## Description 10 | 11 | Describe what you were trying to get done. 12 | Tell us what happened, what went wrong, and what you expected to happen. 13 | 14 | ## What I Did 15 | 16 | ```bash 17 | Paste the command(s) you ran and the output. 18 | If there was a crash, please include the traceback here. 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/workflows/code_checks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code checks 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | branches: 11 | - main 12 | types: [opened, synchronize, reopened, ready_for_review] 13 | workflow_dispatch: 14 | inputs: 15 | force_release: 16 | description: Force release 17 | required: false 18 | type: boolean 19 | default: false 20 | 21 | concurrency: 22 | group: ${{ github.ref }}-${{ github.workflow }}-checks 23 | cancel-in-progress: true 24 | 25 | env: 26 | UV_LINK_MODE: copy 27 | UV_PYTHON_PREFERENCE: only-managed 28 | UV_CACHE_DIR: /tmp/.uv-cache 29 | 30 | jobs: 31 | pre-commit: 32 | name: Pre-commit 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 20 35 | 36 | steps: 37 | - name: Checkout current branch 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - uses: jdx/mise-action@v2 43 | with: 44 | install: true 45 | cache: true 46 | 47 | - name: Restore uv cache 48 | uses: actions/cache@v4 49 | with: 50 | path: ${{ env.UV_CACHE_DIR }} 51 | key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} 52 | restore-keys: | 53 | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} 54 | uv-${{ runner.os }} 55 | 56 | - name: Setup venv 57 | run: | 58 | uv python install 59 | make setup-venv 60 | 61 | - name: Check if uv lock file is up to date 62 | env: 63 | UV_KEYRING_PROVIDER: subprocess 64 | run: | 65 | echo "::group::Check if uv lock file is up to date" 66 | echo "Detect files that could affect the dynamic versioning if any (git dirty state)" 67 | git status --porcelain --untracked-files 68 | echo "Performing 'uv lock' in dry-run mode" 69 | uv lock --dry-run 70 | echo "Performing 'uv lock'" 71 | if ! uv lock --locked ; then 72 | echo "::error title=uv lock file::The lockfile at 'uv.lock' needs to be updated. To update the lockfile, run 'uv lock'" 73 | exit 1 74 | fi 75 | echo "::endgroup::" 76 | 77 | - name: Minimize uv cache 78 | run: uv cache prune --ci 79 | 80 | - name: Detect Python version 81 | id: detect-python-version 82 | run: | 83 | echo "python_version=$(python --version | cut -d " " -f2 | cut -d . -f1-2)" >> "${GITHUB_OUTPUT}" 84 | 85 | - name: Restore pre-commit cache 86 | uses: actions/cache@v4 87 | with: 88 | path: ~/.cache/pre-commit 89 | key: pre-commit|${{ steps.detect-python-version.outputs.python_version }}|${{ hashFiles('.pre-commit-config.yaml') }} 90 | 91 | - name: Run pre-commit checks 92 | run: | 93 | echo "::group::Run pre-commit checks" 94 | pre-commit run --all-files --show-diff-on-failure 95 | echo "::endgroup::" 96 | 97 | python-checks: 98 | name: Python checks 99 | runs-on: ubuntu-latest 100 | outputs: 101 | deploy_env: ${{ steps.detect-deploy-env.outputs.deploy_env }} 102 | needs: 103 | - pre-commit 104 | steps: 105 | - name: Checkout Code 106 | uses: actions/checkout@v4 107 | with: 108 | fetch-depth: 0 109 | 110 | - uses: jdx/mise-action@v2 111 | with: 112 | install: true 113 | cache: true 114 | 115 | - name: Restore uv cache 116 | uses: actions/cache@v4 117 | with: 118 | path: /tmp/.uv-cache 119 | key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} 120 | restore-keys: | 121 | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} 122 | uv-${{ runner.os }} 123 | 124 | - name: Setup venv 125 | run: | 126 | uv python install 127 | make setup-venv 128 | 129 | - name: Python checks 130 | run: | 131 | echo "::group::make lint" 132 | make lint 133 | echo "::endgroup::" 134 | echo "::group::make test" 135 | make test 136 | echo "::endgroup::" 137 | 138 | - name: Run Integration tests 139 | run: | 140 | echo "::group::make integration-tests" 141 | make integration-tests 142 | echo "::endgroup::" 143 | 144 | - name: Minimize uv cache 145 | run: uv cache prune --ci 146 | 147 | - name: Check we can build the distribution 148 | run: | 149 | echo "::group::make dist" 150 | make dist 151 | echo "::endgroup::" 152 | 153 | - name: Store the distribution packages 154 | uses: actions/upload-artifact@v4 155 | if: ${{ github.event_name == 'push' || inputs.force_release }} 156 | with: 157 | name: python-package-distributions 158 | path: dist/ 159 | 160 | - name: Detect environment 161 | if: ${{ github.event_name == 'push' || inputs.force_release }} 162 | id: detect-deploy-env 163 | run: | 164 | echo "deploy_env=${{ startsWith(github.ref, 'refs/tags') && 'pypi' || 'testpypi' }}" >> "${GITHUB_OUTPUT}" 165 | 166 | deploy-release: 167 | name: Deploy Release 168 | runs-on: ubuntu-latest 169 | if: ${{ github.event_name == 'push' || inputs.force_release }} 170 | environment: 171 | name: ${{ needs.python-checks.outputs.deploy_env }} 172 | url: ${{ needs.python-checks.outputs.deploy_env == 'pypi' && 'https://pypi.org/project/kubesplit/' || 'https://test.pypi.org/project/kubesplit/' }} 173 | permissions: 174 | id-token: write 175 | needs: 176 | - python-checks 177 | steps: 178 | - name: Download the distribution packages 179 | uses: actions/download-artifact@v4 180 | with: 181 | name: python-package-distributions 182 | path: dist/ 183 | 184 | - name: Publish package distributions to TestPyPI 185 | uses: pypa/gh-action-pypi-publish@release/v1 186 | with: 187 | repository-url: ${{ needs.python-checks.outputs.deploy_env == 'pypi' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }} 188 | -------------------------------------------------------------------------------- /.github/workflows/lint_pr_titles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint PR Title 3 | 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | lint-pr-title: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | id: lint_pr_title 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | requireScope: true 21 | 22 | - uses: marocchino/sticky-pull-request-comment@v2 23 | # When the previous steps fails, the workflow would stop. By adding this 24 | # condition you can continue the execution with the populated error message. 25 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 26 | with: 27 | header: pr-title-lint-error 28 | message: | 29 | Hey dear contributors ! 👋🏼 30 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 31 | Error details: 32 | ``` 33 | ${{ steps.lint_pr_title.outputs.error_message }} 34 | ``` 35 | 36 | # Delete a previous comment when the issue has been resolved 37 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 38 | uses: marocchino/sticky-pull-request-comment@v2 39 | with: 40 | header: pr-title-lint-error 41 | delete: true 42 | -------------------------------------------------------------------------------- /.github/workflows/release_please.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | name: Manage releases 12 | 13 | jobs: 14 | release-please: 15 | name: Release Please 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | with: 20 | token: ${{ secrets.RELEASE_PLEASE_TOKEN }} 21 | release-type: simple 22 | -------------------------------------------------------------------------------- /.github/workflows/workflows_checks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Workflows sanity checks 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | types: [opened, synchronize, reopened, ready_for_review] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | repo-checks: 15 | name: Check the repository github actions workflows 16 | timeout-minutes: 10 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout current branch 21 | uses: actions/checkout@v4 22 | 23 | - name: Run actionlint 24 | uses: reviewdog/action-actionlint@v1 25 | with: 26 | fail_level: error 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | samples/ 2 | build/ 3 | dist/ 4 | .venv*/ 5 | 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | .vscode/ 55 | 56 | generated/ 57 | -------------------------------------------------------------------------------- /.markdownlint-cli2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | globs: 3 | - '**/*.md' 4 | - '!.venv*/**/*.md' 5 | - '!**/.tox/**/*.md' 6 | - '!**/.pytest_cache' 7 | - '!CHANGELOG.md' 8 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # For a list of all rules, see here: https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md 3 | 4 | # Default state for all rules 5 | default: true 6 | 7 | # MD013/line-length - Line length 8 | MD013: false 9 | 10 | # MD024/Multiple headings with the same content 11 | MD024: false 12 | 13 | # MD026/no-trailing-punctuation - Trailing punctuation in heading 14 | MD026: false 15 | 16 | # MD033/no-inline-html - Inline HTML 17 | MD033: false 18 | 19 | # MD037/Spaces inside emphasis markers 20 | MD037: false 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: trailing-whitespace # Trims trailing whitespace 7 | - id: check-yaml # Validates YAML files 8 | args: [--allow-multiple-documents] 9 | - id: check-toml # Validates TOML files 10 | - id: check-json # Validates JSON files # .vscode/ json files are actually jsonc files 11 | exclude: ^.vscode/.* 12 | - id: check-xml # Validates XML files 13 | - id: check-added-large-files # Checks for files that are added to the repository that are larger than a threshold 14 | - id: check-case-conflict # Checks for files that would conflict in case-insensitive filesystems 15 | - id: check-merge-conflict # Checks for files that contain merge conflict strings 16 | - id: detect-private-key # Check for the existence of private keys 17 | - id: check-executables-have-shebangs # Checks that executables have shebangs 18 | - id: end-of-file-fixer # Makes sure files end in a newline and only a newline 19 | exclude: | 20 | (?x)^( 21 | .gitmodules| 22 | shared/bats-libs/.* 23 | )$ 24 | 25 | - repo: https://gitlab.com/bmares/check-json5 26 | rev: v1.0.0 27 | hooks: 28 | - id: check-json5 29 | 30 | - repo: https://github.com/DavidAnson/markdownlint-cli2 31 | rev: v0.18.1 32 | hooks: 33 | - id: markdownlint-cli2 34 | 35 | - repo: https://github.com/editorconfig-checker/editorconfig-checker.python 36 | rev: 3.2.1 37 | hooks: 38 | - id: editorconfig-checker-system # Check editorconfig compliance 39 | alias: ec 40 | 41 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 42 | rev: 3.0.0 43 | hooks: 44 | - id: shellcheck # Shell scripts conform to shellcheck 45 | exclude: ^shared/bats-libs/.* 46 | - id: shfmt # Check shell style with shfmt 47 | exclude: ^shared/bats-libs/.* 48 | 49 | - repo: https://github.com/adrienverge/yamllint.git 50 | rev: v1.37.1 51 | hooks: 52 | - id: yamllint 53 | args: [--config-data, relaxed, --no-warnings] 54 | 55 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 56 | rev: 0.2.3 57 | hooks: 58 | - id: yamlfmt 59 | args: [--mapping, '2', --sequence, '4', --offset, '2', --width, '250', --explicit_start] 60 | exclude: | 61 | (?x)^( 62 | test-assets/.* 63 | )$ 64 | 65 | - repo: local 66 | hooks: 67 | - id: taplo 68 | name: Check TOML files are formatted with 'taplo' 69 | language: docker_image 70 | entry: tamasfe/taplo:0.9.3 format --config .taplo.toml 71 | types: [toml] 72 | - id: ruff-format 73 | name: Check that files are formatted as expected with 'ruff format --check' 74 | entry: uv run --frozen ruff format --check 75 | language: system 76 | pass_filenames: false 77 | - id: ruff-check 78 | name: Lint files with 'ruff check' 79 | entry: uv run --frozen ruff check 80 | require_serial: true 81 | language: system 82 | pass_filenames: false 83 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | # A comma-separated list of package or module names from where C extensions may 3 | # be loaded. Extensions are loading into the active Python interpreter and may 4 | # run arbitrary code. 5 | extension-pkg-whitelist=pydantic 6 | 7 | # Add files or directories to the blacklist. They should be base names, not 8 | # paths. 9 | ignore=tests 10 | 11 | # Add files or directories matching the regex patterns to the blacklist. The 12 | # regex matches against base names, not paths. 13 | ignore-patterns= 14 | 15 | # Python code to execute, usually for sys.path manipulation such as 16 | # pygtk.require(). 17 | #init-hook= 18 | 19 | 20 | 21 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 22 | # number of processors available to use. 23 | jobs=1 24 | 25 | # Control the amount of potential inferred values when inferring a single 26 | # object. This can help the performance when dealing with large functions or 27 | # complex, nested conditions. 28 | limit-inference-results=100 29 | 30 | # List of plugins (as comma separated values of python modules names) to load, 31 | # usually to register additional checkers. 32 | load-plugins= 33 | 34 | # Pickle collected data for later comparisons. 35 | persistent=yes 36 | 37 | # Specify a configuration file. 38 | #rcfile= 39 | 40 | # When enabled, pylint would attempt to guess common misconfiguration and emit 41 | # user-friendly hints instead of false-positive error messages. 42 | suggestion-mode=yes 43 | 44 | # Allow loading of arbitrary C extensions. Extensions are imported into the 45 | # active Python interpreter and may run arbitrary code. 46 | unsafe-load-any-extension=no 47 | 48 | 49 | [REPORTS] 50 | 51 | # Python expression which should return a note less than 10 (10 is the highest 52 | # note). You have access to the variables errors warning, statement which 53 | # respectively contain the number of errors / warnings messages and the total 54 | # number of statements analyzed. This is used by the global evaluation report 55 | # (RP0004). 56 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 57 | 58 | # Template used to display messages. This is a python new-style format string 59 | # used to format the message information. See doc for all details. 60 | msg-template= 61 | 62 | # Set the output format. Available formats are text, parseable, colorized, json 63 | # and msvs (visual studio). You can also give a reporter class, e.g. 64 | # mypackage.mymodule.MyReporterClass. 65 | output-format=text 66 | 67 | # Tells whether to display a full report or only the messages. 68 | reports=no 69 | 70 | # Activate the evaluation score. 71 | score=yes 72 | 73 | 74 | [MESSAGES CONTROL] 75 | 76 | # Only show warnings with the listed confidence levels. Leave empty to show 77 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 78 | confidence= 79 | 80 | # Disable the message, report, category or checker with the given id(s). You 81 | # can either give multiple identifiers separated by comma (,) or put this 82 | # option multiple times (only on the command line, not in the configuration 83 | # file where it should appear only once). You can also use "--disable=all" to 84 | # disable everything first and then reenable specific checks. For example, if 85 | # you want to run only the similarities checker, you can use "--disable=all 86 | # --enable=similarities". If you want to run only the classes checker, but have 87 | # no Warning level messages displayed, use "--disable=all --enable=classes 88 | # --disable=W". 89 | disable=raw-checker-failed, 90 | bad-inline-option, 91 | locally-disabled, 92 | file-ignored, 93 | suppressed-message, 94 | useless-suppression, 95 | deprecated-pragma, 96 | use-symbolic-message-instead, 97 | too-few-public-methods, 98 | line-too-long 99 | 100 | # Enable the message, report, category or checker with the given id(s). You can 101 | # either give multiple identifier separated by comma (,) or put this option 102 | # multiple time (only on the command line, not in the configuration file where 103 | # it should appear only once). See also the "--disable" option for examples. 104 | enable=c-extension-no-member 105 | 106 | 107 | [STRING] 108 | 109 | # This flag controls whether inconsistent-quotes generates a warning when the 110 | # character used as a quote delimiter is used inconsistently within a module. 111 | check-quote-consistency=no 112 | 113 | # This flag controls whether the implicit-str-concat should generate a warning 114 | # on implicit string concatenation in sequences defined over several lines. 115 | check-str-concat-over-line-jumps=no 116 | 117 | [DESIGN] 118 | 119 | # List of regular expressions of class ancestor names to ignore when counting 120 | # public methods (see R0903) 121 | exclude-too-few-public-methods= 122 | 123 | # List of qualified class names to ignore when counting class parents (see 124 | # R0901) 125 | ignored-parents= 126 | 127 | # Maximum number of arguments for function / method. 128 | max-args=5 129 | 130 | # Maximum number of attributes for a class (see R0902). 131 | max-attributes=7 132 | 133 | # Maximum number of boolean expressions in an if statement. 134 | max-bool-expr=5 135 | 136 | # Maximum number of branch for function / method body. 137 | max-branches=12 138 | 139 | # Maximum number of locals for function / method body. 140 | max-locals=15 141 | 142 | # Maximum number of parents for a class (see R0901). 143 | max-parents=7 144 | 145 | # Maximum number of public methods for a class (see R0904). 146 | max-public-methods=20 147 | 148 | # Maximum number of return / yield for function / method body. 149 | max-returns=6 150 | 151 | # Maximum number of statements in function / method body. 152 | max-statements=50 153 | 154 | # Minimum number of public methods for a class (see R0903). 155 | min-public-methods=2 156 | 157 | [LOGGING] 158 | 159 | # Format style used to check logging format string. `old` means using % 160 | # formatting, while `new` is for `{}` formatting. 161 | logging-format-style=old 162 | 163 | # Logging modules to check that the string format arguments are in logging 164 | # function parameter format. 165 | logging-modules=logging 166 | 167 | 168 | [TYPECHECK] 169 | 170 | # List of decorators that produce context managers, such as 171 | # contextlib.contextmanager. Add to this list to register other decorators that 172 | # produce valid context managers. 173 | contextmanager-decorators=contextlib.contextmanager 174 | 175 | # List of members which are set dynamically and missed by pylint inference 176 | # system, and so shouldn't trigger E1101 when accessed. Python regular 177 | # expressions are accepted. 178 | generated-members=.*client.* 179 | 180 | # Tells whether to warn about missing members when the owner of the attribute 181 | # is inferred to be None. 182 | ignore-none=yes 183 | 184 | # This flag controls whether pylint should warn about no-member and similar 185 | # checks whenever an opaque object is returned when inferring. The inference 186 | # can return multiple potential results while evaluating a Python object, but 187 | # some branches might not be evaluated, which results in partial inference. In 188 | # that case, it might be useful to still emit no-member and other checks for 189 | # the rest of the inferred objects. 190 | ignore-on-opaque-inference=yes 191 | 192 | # List of symbolic message names to ignore for Mixin members. 193 | ignored-checks-for-mixins=no-member, 194 | not-async-context-manager, 195 | not-context-manager, 196 | attribute-defined-outside-init 197 | 198 | # List of class names for which member attributes should not be checked (useful 199 | # for classes with dynamically set attributes). This supports the use of 200 | # qualified names. 201 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 202 | 203 | # Show a hint with possible names when a member name was not found. The aspect 204 | # of finding the hint is based on edit distance. 205 | missing-member-hint=yes 206 | 207 | # The minimum edit distance a name should have in order to be considered a 208 | # similar match for a missing member name. 209 | missing-member-hint-distance=1 210 | 211 | # The total number of similar names that should be taken in consideration when 212 | # showing a hint for a missing member. 213 | missing-member-max-choices=1 214 | 215 | # Regex pattern to define which classes are considered mixins. 216 | mixin-class-rgx=.*[Mm]ixin 217 | 218 | # List of decorators that change the signature of a decorated function. 219 | signature-mutators= 220 | 221 | 222 | # Tells whether missing members accessed in mixin class should be ignored. A 223 | # mixin class is detected if its name ends with "mixin" (case insensitive). 224 | ignore-mixin-members=yes 225 | 226 | # List of module names for which member attributes should not be checked 227 | # (useful for modules/projects where namespaces are manipulated during runtime 228 | # and thus existing member attributes cannot be deduced by static analysis. It 229 | # supports qualified module names, as well as Unix pattern matching. 230 | ignored-modules= 231 | 232 | 233 | [EXCEPTIONS] 234 | 235 | # Exceptions that will emit a warning when being caught. Defaults to 236 | # "BaseException, Exception". 237 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 238 | 239 | 240 | [SIMILARITIES] 241 | 242 | # Ignore comments when computing similarities. 243 | ignore-comments=yes 244 | 245 | # Ignore docstrings when computing similarities. 246 | ignore-docstrings=yes 247 | 248 | # Ignore imports when computing similarities. 249 | ignore-imports=no 250 | 251 | # Signatures are removed from the similarity computation 252 | ignore-signatures=yes 253 | 254 | # Minimum lines number of a similarity. 255 | min-similarity-lines=15 256 | 257 | [BASIC] 258 | 259 | # Naming style matching correct argument names. 260 | argument-naming-style=snake_case 261 | 262 | # Regular expression matching correct argument names. Overrides argument- 263 | # naming-style. 264 | #argument-rgx= 265 | 266 | # Naming style matching correct attribute names. 267 | attr-naming-style=snake_case 268 | 269 | # Regular expression matching correct attribute names. Overrides attr-naming- 270 | # style. 271 | #attr-rgx= 272 | 273 | # Bad variable names which should always be refused, separated by a comma. 274 | bad-names=foo, 275 | bar, 276 | baz, 277 | toto, 278 | titi, 279 | tutu, 280 | tata 281 | 282 | # Bad variable names regexes, separated by a comma. If names match any regex, 283 | # they will always be refused 284 | bad-names-rgxs= 285 | 286 | # Naming style matching correct class attribute names. 287 | class-attribute-naming-style=any 288 | 289 | # Regular expression matching correct class attribute names. Overrides class- 290 | # attribute-naming-style. 291 | #class-attribute-rgx= 292 | 293 | # Naming style matching correct class constant names. 294 | class-const-naming-style=UPPER_CASE 295 | 296 | # Naming style matching correct class names. 297 | class-naming-style=PascalCase 298 | 299 | # Regular expression matching correct class names. Overrides class-naming- 300 | # style. 301 | #class-rgx= 302 | 303 | # Naming style matching correct constant names. 304 | const-naming-style=UPPER_CASE 305 | 306 | # Regular expression matching correct constant names. Overrides const-naming- 307 | # style. 308 | #const-rgx= 309 | 310 | # Minimum line length for functions/classes that require docstrings, shorter 311 | # ones are exempt. 312 | docstring-min-length=-1 313 | 314 | # Naming style matching correct function names. 315 | function-naming-style=snake_case 316 | 317 | # Regular expression matching correct function names. Overrides function- 318 | # naming-style. 319 | #function-rgx= 320 | 321 | # Good variable names which should always be accepted, separated by a comma. 322 | good-names=i,j,k,ex,Run,_ 323 | 324 | # Include a hint for the correct naming format with invalid-name. 325 | include-naming-hint=no 326 | 327 | # Naming style matching correct inline iteration names. 328 | inlinevar-naming-style=any 329 | 330 | # Regular expression matching correct inline iteration names. Overrides 331 | # inlinevar-naming-style. 332 | #inlinevar-rgx= 333 | 334 | # Naming style matching correct method names. 335 | method-naming-style=snake_case 336 | 337 | # Regular expression matching correct method names. Overrides method-naming- 338 | # style. 339 | #method-rgx= 340 | 341 | # Naming style matching correct module names. 342 | module-naming-style=snake_case 343 | 344 | # Regular expression matching correct module names. Overrides module-naming- 345 | # style. 346 | #module-rgx= 347 | 348 | # Colon-delimited sets of names that determine each other's naming style when 349 | # the name regexes allow several styles. 350 | name-group= 351 | 352 | # Regular expression which should only match function or class names that do 353 | # not require a docstring. 354 | no-docstring-rgx=^_ 355 | 356 | # List of decorators that produce properties, such as abc.abstractproperty. Add 357 | # to this list to register other decorators that produce valid properties. 358 | # These decorators are taken in consideration only for invalid-name. 359 | property-classes=abc.abstractproperty 360 | 361 | # Naming style matching correct variable names. 362 | variable-naming-style=snake_case 363 | 364 | # Regular expression matching correct variable names. Overrides variable- 365 | # naming-style. 366 | #variable-rgx= 367 | 368 | [FORMAT] 369 | 370 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 371 | expected-line-ending-format= 372 | 373 | # Regexp for a line that is allowed to be longer than the limit. 374 | ignore-long-lines=^\s*(# )??$ 375 | 376 | # Number of spaces of indent required inside a hanging or continued line. 377 | indent-after-paren=4 378 | 379 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 380 | # tab). 381 | indent-string=' ' 382 | 383 | # Maximum number of characters on a single line. 384 | max-line-length=100 385 | 386 | # Maximum number of lines in a module. 387 | max-module-lines=1000 388 | 389 | # Allow the body of a class to be on the same line as the declaration if body 390 | # contains single statement. 391 | single-line-class-stmt=no 392 | 393 | # Allow the body of an if to be on the same line as the test if there is no 394 | # else. 395 | single-line-if-stmt=no 396 | 397 | 398 | 399 | [IMPORTS] 400 | 401 | # List of modules that can be imported at any level, not just the top level 402 | # one. 403 | allow-any-import-level= 404 | 405 | # Allow wildcard imports from modules that define __all__. 406 | allow-wildcard-with-all=no 407 | 408 | # Deprecated modules which should not be used, separated by a comma. 409 | deprecated-modules=optparse,tkinter.tix 410 | 411 | # Create a graph of external dependencies in the given file (report RP0402 must 412 | # not be disabled). 413 | ext-import-graph= 414 | 415 | # Create a graph of every (i.e. internal and external) dependencies in the 416 | # given file (report RP0402 must not be disabled). 417 | import-graph= 418 | 419 | # Create a graph of internal dependencies in the given file (report RP0402 must 420 | # not be disabled). 421 | int-import-graph= 422 | 423 | # Force import order to recognize a module as part of the standard 424 | # compatibility libraries. 425 | known-standard-library= 426 | 427 | # Force import order to recognize a module as part of a third party library. 428 | known-third-party= 429 | 430 | # Analyse import fallback blocks. This can be used to support both Python 2 and 431 | # 3 compatible code, which means that the block might have code that exists 432 | # only in one or another interpreter, leading to false positives when analysed. 433 | analyse-fallback-blocks=no 434 | 435 | 436 | [MISCELLANEOUS] 437 | 438 | # List of note tags to take in consideration, separated by a comma. 439 | notes=FIXME, 440 | XXX, 441 | TODO 442 | 443 | # Regular expression of note tags to take in consideration. 444 | notes-rgx= 445 | 446 | 447 | [CLASSES] 448 | 449 | # Warn about protected attribute access inside special methods 450 | check-protected-access-in-special-methods=no 451 | 452 | # List of method names used to declare (i.e. assign) instance attributes. 453 | defining-attr-methods=__init__, 454 | __new__, 455 | setUp, 456 | __post_init__ 457 | 458 | # List of member names, which should be excluded from the protected access 459 | # warning. 460 | exclude-protected=_asdict, 461 | _fields, 462 | _replace, 463 | _source, 464 | _make 465 | 466 | # List of valid names for the first argument in a class method. 467 | valid-classmethod-first-arg=cls 468 | 469 | # List of valid names for the first argument in a metaclass class method. 470 | valid-metaclass-classmethod-first-arg=cls 471 | 472 | 473 | [REFACTORING] 474 | 475 | # Maximum number of nested blocks for function / method body 476 | max-nested-blocks=5 477 | 478 | # Complete name of functions that never returns. When checking for 479 | # inconsistent-return-statements if a never returning function is called then 480 | # it will be considered as an explicit return statement and no message will be 481 | # printed. 482 | never-returning-functions=sys.exit,argparse.parse_error 483 | 484 | 485 | 486 | [VARIABLES] 487 | 488 | # List of additional names supposed to be defined in builtins. Remember that 489 | # you should avoid defining new builtins when possible. 490 | additional-builtins= 491 | 492 | # Tells whether unused global variables should be treated as a violation. 493 | allow-global-unused-variables=yes 494 | 495 | # List of strings which can identify a callback function by name. A callback 496 | # name must start or end with one of those strings. 497 | callbacks=cb_, 498 | _cb 499 | 500 | # A regular expression matching the name of dummy variables (i.e. expected to 501 | # not be used). 502 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 503 | 504 | # Argument names that match this expression will be ignored. Default to name 505 | # with leading underscore. 506 | ignored-argument-names=_.*|^ignored_|^unused_ 507 | 508 | # Tells whether we should check for unused import in __init__ files. 509 | init-import=no 510 | 511 | # List of qualified module names which can have objects that can redefine 512 | # builtins. 513 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 514 | 515 | 516 | # Limits count of emitted suggestions for spelling mistakes. 517 | max-spelling-suggestions=4 518 | 519 | # Spelling dictionary name. Available dictionaries: en_GB (aspell), en_US 520 | # (hunspell), en_AU (aspell), en (aspell), en_CA (aspell). 521 | spelling-dict= 522 | 523 | # List of comma separated words that should be considered directives if they 524 | # appear at the beginning of a comment and should not be checked. 525 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 526 | 527 | # List of comma separated words that should not be checked. 528 | spelling-ignore-words= 529 | 530 | # A path to a file that contains the private dictionary; one word per line. 531 | spelling-private-dict-file= 532 | 533 | # Tells whether to store unknown words to the private dictionary (see the 534 | # --spelling-private-dict-file option) instead of raising a message. 535 | spelling-store-unknown-words=no 536 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | include = [ 2 | "**/*.toml", 3 | "**/*.toml.jinja", 4 | ] 5 | 6 | [formatting] 7 | 8 | align_comments = true 9 | allowed_blank_lines = 1 10 | array_auto_expand = true 11 | array_trailing_comma = true 12 | column_width = 1 13 | indent_string = " " 14 | reorder_arrays = false 15 | reorder_keys = true 16 | trailing_newline = true 17 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | editorconfig-checker 3.3.0 2 | shfmt 3.11.0 3 | uv 0.7.9 4 | shellcheck 0.10.0 5 | pre-commit 4.2.0 6 | bats 1.11.1 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Prerequisites 4 | 5 | ### Mandatory 6 | 7 | - [poetry](https://python-poetry.org/docs/#installation) 8 | 9 | ### Optional 10 | 11 | - [Pre-Commit](https://pre-commit.com/#install) 12 | - [asdf](https://asdf-vm.com/guide/getting-started.html) 13 | - [ActionLint](https://github.com/rhysd/actionlint) 14 | - [shfmt](https://github.com/mvdan/sh) 15 | - [editorconfig-checker](https://github.com/editorconfig-checker/editorconfig-checker) 16 | 17 | ### Install the optional prequisites 18 | 19 | - If you installed asdf, you can simply run: 20 | 21 | ```bash 22 | for plugin in $(cut -d " " -f 1 < .tool-versions); do 23 | asdf plugin add "${plugin}" 24 | done 25 | asdf install 26 | ``` 27 | 28 | - This will: 29 | - install asdf plugins for this repository 30 | - install the version of tool required for this repository 31 | 32 | ## Hack 33 | 34 | - run `make setup-venv` so that `poetry` inits a virtual environment with required dependencies (same as `poetry install --sync`) 35 | - run `eval $(make echo-venv-activate-cmd)` to activate the virtual environment if needed 36 | 37 | ### Update the dependencies 38 | 39 | Run `make update-requirements-file`, this will basically: 40 | 41 | - run `poetry lock` to update the dependencies and the lock file 42 | - update the `requirements.txt` file used by tox virutal environments (to save a call to poetry each time you run tox) 43 | 44 | ### Code 45 | 46 | - run `make tests` to launch unit tests (same as `tox -e py3`) 47 | - run `make lint` to launch linters (same as `tox -e linters`): 48 | - `black` (see the [pyproject.toml](pyproject.toml) for the black configuration) 49 | - `isort` (see the [pyproject.toml](pyproject.toml) for the isort configuration + the tox.ini because we configure isort with `--dont-order-by-type` that cannot be specified in a config file) 50 | - `flake8` 51 | - `pylint` 52 | 53 | ### Pre-Commit 54 | 55 | - If you want to run pre-commit before each commit, run once `make precommit-install` 56 | - If you don't want to configure a pre-commit hook (your choice, pre-commit is run by the CICD anyway), you can run it when you want, use `make precommit-run` 57 | 58 | ### Configure your editor 59 | 60 | #### VSCode 61 | 62 | - It's a good idea to install the [EditorConfig for VSCode extension](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 63 | - Your `settings.json` (user or workspace or project) should contain: 64 | 65 | ```json 66 | { 67 | "[python]": { 68 | "editor.formatOnSave": true, 69 | "editor.codeActionsOnSave": { 70 | "source.organizeImports": true, 71 | }, 72 | }, 73 | "python.formatting.provider": "black", 74 | "isort.args": [ 75 | "--dont-order-by-type" 76 | ], 77 | "files.trimTrailingWhitespace": true, 78 | "files.insertFinalNewline": true, 79 | "files.trimFinalNewlines": true 80 | } 81 | ``` 82 | 83 | #### Others 84 | 85 | - Feel free to contribute any other editor configuration 86 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim-bullseye 2 | 3 | LABEL org.label-schema.schema-version "1.0" \ 4 | org.label-schema.name "kubesplit" \ 5 | org.label-schema.description "kubesplit packaged as a docker image" \ 6 | org.label-schema.vcs-url "https://github.com/looztra/kubesplit" \ 7 | org.label-schema.vendor "looztra" \ 8 | org.label-schema.docker.cmd.help "docker run --rm -v $(pwd):/app/code looztra/kubesplit:TAG help" \ 9 | org.label-schema.docker.cmd "docker run --rm -v $(pwd):/app/code looztra/kubesplit:TAG -i input" 10 | ENV PIP_ROOT_USER_ACTION=ignore\ 11 | PIP_DISABLE_PIP_VERSION_CHECK=1 12 | 13 | WORKDIR /app/code 14 | COPY wait-for-pypi.sh /app/code 15 | ENTRYPOINT ["kubesplit"] 16 | CMD ["--help"] 17 | ARG GIT_SHA1 18 | ARG GIT_REF 19 | ARG APP_VERSION 20 | LABEL org.label-schema.version ${APP_VERSION} \ 21 | org.label-schema.vcs-ref ${GIT_SHA1} \ 22 | io.nodevops.git-ref=${GIT_REF} 23 | 24 | RUN chmod +x /app/code/wait-for-pypi.sh \ 25 | && /app/code/wait-for-pypi.sh ${APP_VERSION} kubesplit \ 26 | && pip install --no-cache-dir kubesplit==${APP_VERSION} 27 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2019-06-16) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | 10 | 0.2.0 (2019-06-16) 11 | ------------------ 12 | 13 | * Fix yaml formatting application 14 | * Allow unprefixed resource files 15 | * Support for ReplicaSets order 16 | 17 | 0.3.0 (2020-05-17) 18 | ------------------ 19 | 20 | * refactor : use the newly available yamkix module and rely on its configuration and writer helpers that make all the yaml writing options work as expected (some of them were not working) 21 | * now kubesplit can also deal with comments! 22 | * fix #5 (deal with Lists) 23 | * chore(CI): introduce some integration tests 24 | 25 | 0.3.1 (2020-05-17) 26 | ------------------ 27 | 28 | * dummy release so that the history is updated on PyPi 29 | 30 | 0.3.3 (2023-06-24) 31 | ------------------ 32 | 33 | * update requirements to deal with ruamel.yaml >= 0.17.27 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache Software License 2.0 2 | 3 | Copyright (c) 2019, Christophe Furmaniak 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME ?= kubesplit 2 | LOCAL_MK_ROOT ?= toolbox/mk 3 | 4 | 5 | include $(LOCAL_MK_ROOT)/common.mk 6 | include $(LOCAL_MK_ROOT)/mdlint.mk 7 | include $(LOCAL_MK_ROOT)/pre-commit.mk 8 | include $(LOCAL_MK_ROOT)/python-uv-extras.mk 9 | include $(LOCAL_MK_ROOT)/python-base-venv.mk 10 | include $(LOCAL_MK_ROOT)/python-uv-venv.mk 11 | include $(LOCAL_MK_ROOT)/python-base-app.mk 12 | include $(LOCAL_MK_ROOT)/python-uv-app.mk 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubesplit 2 | 3 | [![Pypi](https://img.shields.io/pypi/v/kubesplit.svg)](https://pypi.python.org/pypi/kubesplit) 4 | 5 | ## What? 6 | 7 | Split multidoc yaml formatted [kubernetes](https://kubernetes.io/) descriptors to a set of single resource files. 8 | 9 | If you just want an opinionated yaml formatter, you can have a look at [yamkix](https://github.com/looztra/yamkix). 10 | 11 | ## Installation 12 | 13 | ### The pip way 14 | 15 | ```bash 16 | # Install/Update 17 | pip3 install -U --user kubesplit 18 | # Enjoy 19 | kubesplit -i path/to/yaml_file.yml -o path/to/output/directory 20 | # or 21 | cat path/to/yaml_file.yml | kubesplit -o path/to/output/directory 22 | 23 | ``` 24 | 25 | ### The docker way 26 | 27 | ```bash 28 | # Use latest 29 | docker image pull looztra/kubesplit 30 | # Or one of the version+sha1 related tags 31 | docker image pull looztra/kubesplit:[version]-[sha1] 32 | # Enjoy 33 | docker container run \ 34 | -ti --rm \ 35 | -v $(pwd):/code \ 36 | -w /code looztra/kubesplit \ 37 | -i path/to/yaml_file.yml \ 38 | -o path/to/output/directory 39 | # or 40 | cat path/to/yaml_file.yml | \ 41 | docker container run \ 42 | -ti --rm \ 43 | -v $(pwd):/code \ 44 | -w /code looztra/kubesplit \ 45 | -o path/to/output/directory 46 | ``` 47 | 48 | All tags available at 49 | 50 | ## Usage 51 | 52 | ```bash 53 | ╰(.venv)─» kubesplit -h 54 | usage: kubesplit [-h] [-i INPUT] [-t TYP] -o OUTPUT_DIR [-n] [-e] [-q] [-f] 55 | [-d] [-c] [-p] 56 | 57 | Split a set of Kubernetes descriptors to a set of files. The yaml format of 58 | the generated files can be tuned using the same parameters as the one used by 59 | Yamkix. By default, explicit_start is `On`, explicit_end is `Off` and array 60 | elements are pushed inwards the start of the matching sequence. Comments are 61 | preserved thanks to default parsing mode `rt`. 62 | 63 | optional arguments: 64 | -h, --help show this help message and exit 65 | -i INPUT, --input INPUT 66 | the file to parse, or STDIN if not specified or if 67 | value is - 68 | -t TYP, --typ TYP the yaml parser mode. Can be `safe` or `rt` 69 | -o OUTPUT_DIR, --output-dir OUTPUT_DIR 70 | the name of the output target directory. The target 71 | directory will be created if it does not exist if it's 72 | possible 73 | -n, --no-explicit-start 74 | by default, explicit start (---) of the yaml doc is 75 | `On`, you can disable it with this option 76 | -e, --explicit-end by default, explicit end (...) of the yaml doc is 77 | `Off`, you can enable it with this option 78 | -q, --no-quotes-preserved 79 | by default, quotes are preserved you can disable this 80 | with this option 81 | -f, --default-flow-style 82 | enable the default flow style `Off` by default. In 83 | default flow style (with typ=`rt`), maps and lists are 84 | written like json 85 | -d, --no-dash-inwards 86 | by default, dash are pushed inwards use `--no-dash- 87 | inwards` to have the dash start at the sequence level 88 | -c, --clean-output-dir 89 | clean the output directory (rmtree) if set (default is 90 | False) 91 | -p, --no-resource-prefix 92 | by default, resource files are number prefixed, you 93 | can disable this behaviour with this flag 94 | 95 | ``` 96 | 97 | ## Features 98 | 99 | - Invalid Kubernetes resources are ignored 100 | - Empty resources are ignored 101 | - Each resource found in the input is stored in a file with a name reflecting the name of the resource and its _kubernetes_ kind 102 | - Cluster-wide resources (namespaces, clusterroles, clusterrolebindings) are stored in the root directory of the output, namespaced resources are stored in a subdirectory named like the namespace 103 | - By default, resources are prefixed, use `--no-resource-prefix` to disable order prefixes 104 | - By default, quotes are preserved, use `--no-quotes-preserved` to disable quotes unless needed (for boolean and numbers if they were provided in the input as for the moment Kubesplit is not aware of the fact that only kubernetes annotations and environment variables require string) 105 | - By default, dash elements in list are pushed inwards, you can disable this behaviour with the `-d`/`--no-dash-inwards` option 106 | - Comments are preserved 107 | - The output directory will be created if it doesn't exist (if the user running the command as sufficient rights) 108 | - You can clean (delete files and directories existing before running `kubesplit`) the output directory with the `-c`/`--clean-output-dir` (**use at your own risks**) 109 | 110 | ## Examples 111 | 112 | You can find some input and output examples in the [test-assets](https://github.com/looztra/kubesplit/tree/master/test-assets) directory 113 | 114 | ### Valid resources, no quotes preserved 115 | 116 | ```bash 117 | ╰(.venv)─» kubesplit --input test-assets/source/all-in-one.yml \ 118 | --output test-assets/expected/all-in-one--no-quotes-preserved \ 119 | --no-quotes-preserved \ 120 | --clean-output-dir 121 | Processing: input=test-assets/source/all-in-one.yml, output_dir=test-assets/expected/all-in-one--no-quotes-preserved, clean_output_dir=True, typ=rt, explicit_start=True, explicit_end=False, default_flow_style=False, quotes_preserved=False, dash_inwards=True, prefix_resource_files=True 122 | Found [16] valid / [0] invalid / [0] empty resources 123 | 124 | ╰(.venv)─» tree --dirsfirst test-assets/expected/all-in-one--no-quotes-preserved 125 | test-assets/expected/all-in-one--no-quotes-preserved 126 | ├── apps-demo 127 | │   └── 05--rolebinding--example-ns-demo-developer-binding.yml 128 | ├── apps-integration 129 | │   └── 05--rolebinding--example-ns-integration-developer-binding.yml 130 | ├── ingress-controllers 131 | │   ├── 03--serviceaccount--traefik-ingress-controller.yml 132 | │   ├── 11--configmap--traefik-conf.yml 133 | │   ├── 12--persistentvolumeclaim--traefik-acme.yml 134 | │   ├── 20--deployment--traefik-ingress-controller.yml 135 | │   ├── 30--service--traefik-ingress-endpoint.yml 136 | │   ├── 30--service--traefik-web-ui.yml 137 | │   └── 31--ingress--traefik-web-ui.yml 138 | ├── 00--namespace--apps-demo.yml 139 | ├── 00--namespace--apps-integration.yml 140 | ├── 00--namespace--ingress-controllers.yml 141 | ├── 01--clusterrole--example-node-viewer.yml 142 | ├── 01--clusterrole--example-traefik-ingress-controller.yml 143 | ├── 02--clusterrolebinding--example-node-viewer-developer.yml 144 | └── 02--clusterrolebinding--example-traefik-ingress-controller.yml 145 | 146 | 3 directories, 16 files 147 | ``` 148 | 149 | ### Valid resources, no prefix, no quotes preserved 150 | 151 | ```bash 152 | ╰(.venv)─» kubesplit --input test-assets/source/all-in-one.yml \ 153 | --output test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix \ 154 | --no-quotes-preserved \ 155 | --no-resource-prefix \ 156 | --clean-output-dir 157 | Processing: input=test-assets/source/all-in-one.yml, output_dir=test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix, clean_output_dir=True, typ=rt, explicit_start=True, explicit_end=False, default_flow_style=False, quotes_preserved=False, dash_inwards=True, prefix_resource_files=False 158 | Found [16] valid / [0] invalid / [0] empty resources 159 | 160 | ╰(.venv)─» tree --dirsfirst test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix 161 | test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix 162 | ├── apps-demo 163 | │   └── rolebinding--example-ns-demo-developer-binding.yml 164 | ├── apps-integration 165 | │   └── rolebinding--example-ns-integration-developer-binding.yml 166 | ├── ingress-controllers 167 | │   ├── configmap--traefik-conf.yml 168 | │   ├── deployment--traefik-ingress-controller.yml 169 | │   ├── ingress--traefik-web-ui.yml 170 | │   ├── persistentvolumeclaim--traefik-acme.yml 171 | │   ├── serviceaccount--traefik-ingress-controller.yml 172 | │   ├── service--traefik-ingress-endpoint.yml 173 | │   └── service--traefik-web-ui.yml 174 | ├── clusterrolebinding--example-node-viewer-developer.yml 175 | ├── clusterrolebinding--example-traefik-ingress-controller.yml 176 | ├── clusterrole--example-node-viewer.yml 177 | ├── clusterrole--example-traefik-ingress-controller.yml 178 | ├── namespace--apps-demo.yml 179 | ├── namespace--apps-integration.yml 180 | └── namespace--ingress-controllers.yml 181 | 182 | 3 directories, 16 files 183 | ``` 184 | 185 | ### Mixed content : valid, invalid and empty resources, no quotes preserved 186 | 187 | ```bash 188 | ╰(.venv)─» kubesplit --input test-assets/source/mixed-content-valid-invalid-and-empty-resources.yml \ 189 | --output test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved \ 190 | --no-quotes-preserved \ 191 | --clean-output-dir 192 | Processing: input=test-assets/source/mixed-content-valid-invalid-and-empty-resources.yml, output_dir=test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved, clean_output_dir=True, typ=rt, explicit_start=True, explicit_end=False, default_flow_style=False, quotes_preserved=False, dash_inwards=True, prefix_resource_files=True 193 | Found [2] valid / [1] invalid / [1] empty resources 194 | 195 | ╰(.venv)─» tree --dirsfirst test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved 196 | test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved 197 | ├── akira 198 | │   └── 25--replicaset--bididididi.yml 199 | └── yolo 200 | └── 25--replicaset--frontend.yml 201 | 202 | 2 directories, 2 files 203 | 204 | ``` 205 | 206 | ## Use cases 207 | 208 | ### With Kustomize 209 | 210 | ```bash 211 | kustomize build overlays/prod | kubesplit -q -i - -o generated/prod 212 | 213 | ``` 214 | 215 | ### With Helm 216 | 217 | ```bash 218 | helm template --namespace target-ns --values config.yml my-chart | kubesplit -q -i - -o generated/prod 219 | 220 | ``` 221 | 222 | ## To preserve or not to preserve quotes? 223 | 224 | - _Quotes preserved_ means : if there were quotes in the input, they will also be present in the output, and it will be the same type (single/double) of quotes 225 | - _Quotes not preserved_ means : 226 | - if quotes are not necessary (around _pure_ strings), they will be removed 227 | - if quotes are present around booleans and numbers, they will be converted to default (single quotes) 228 | - if quotes are not present around booleans and numbers, there will be no quotes in the output too 229 | 230 | **Note**: there is no option for the moment to force the usage of double quotes when `-q`/`--no-quotes-preserved` is used. 231 | 232 | ### Quotes preserved (default behaviour) 233 | 234 | With input : 235 | 236 | ```yaml 237 | --- 238 | apiVersion: extensions/v1beta1 # with comment 239 | kind: ReplicaSet 240 | metadata: 241 | name: tname 242 | namespace: tns 243 | annotations: 244 | string_no_quotes: frontend 245 | string_single_quotes: 'frontend' 246 | string_double_quotes: "frontend" 247 | boolean_no_quotes: true 248 | boolean_single_quotes: 'true' 249 | boolean_double_quotes: "true" 250 | number_no_quotes: 1 251 | number_single_quotes: '1' 252 | number_double_quotes: "1" 253 | ``` 254 | 255 | the ouput will be the same as the input : 256 | 257 | ```yaml 258 | --- 259 | apiVersion: extensions/v1beta1 # with comment 260 | kind: ReplicaSet 261 | metadata: 262 | name: tname 263 | namespace: tns 264 | annotations: 265 | string_no_quotes: frontend 266 | string_single_quotes: 'frontend' 267 | string_double_quotes: "frontend" 268 | boolean_no_quotes: true 269 | boolean_single_quotes: 'true' 270 | boolean_double_quotes: "true" 271 | number_no_quotes: 1 272 | number_single_quotes: '1' 273 | number_double_quotes: "1" 274 | 275 | ``` 276 | 277 | ### Quotes not preserved (using `-q/--no-quotes-preserved`) 278 | 279 | With input : 280 | 281 | ```yaml 282 | --- 283 | apiVersion: extensions/v1beta1 # with comment 284 | kind: ReplicaSet 285 | metadata: 286 | name: tname 287 | namespace: tns 288 | annotations: 289 | string_no_quotes: frontend 290 | string_single_quotes: 'frontend' 291 | string_double_quotes: "frontend" 292 | boolean_no_quotes: true 293 | boolean_single_quotes: 'true' 294 | boolean_double_quotes: "true" 295 | number_no_quotes: 1 296 | number_single_quotes: '1' 297 | number_double_quotes: "1" 298 | ``` 299 | 300 | the ouput will be : 301 | 302 | ```yaml 303 | --- 304 | apiVersion: extensions/v1beta1 # with comment 305 | kind: ReplicaSet 306 | metadata: 307 | name: tname 308 | namespace: tns 309 | annotations: 310 | string_no_quotes: frontend 311 | string_single_quotes: frontend 312 | string_double_quotes: frontend 313 | boolean_no_quotes: true 314 | boolean_single_quotes: 'true' 315 | boolean_double_quotes: 'true' 316 | number_no_quotes: 1 317 | number_single_quotes: '1' 318 | number_double_quotes: '1' 319 | 320 | ``` 321 | 322 | **Note** : `kubesplit` is not fully _Kubernetes_ aware for the moment, so it does not try to enforce this behaviour only on string sensible _kubernetes_ resource fields (`.metadata.annotations` and `.spec.containers.environment` values) 323 | 324 | ## TODO 325 | 326 | - Provide an option to enforce the quote type (by default, with `--no-quotes-preserved` boolean and integers are forced with single quotes) Hint => 327 | 328 | ## Contribute 329 | 330 | ```bash 331 | # Setup a local virtual env (needed once) 332 | python3 -m venv .venv 333 | # Activate 334 | source .venv/bin/activate.fish # <= adjust 335 | # Install requirements 336 | pip install -r requirements_dev.txt 337 | # Run locally 338 | python -m kubesplit -h 339 | # All make targets 340 | make 341 | # Tests anyone? 342 | make test 343 | # hack hack 344 | # push PR 345 | ``` 346 | 347 | ## Credits 348 | 349 | - This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) project template. 350 | - Kubesplit uses the awesome [ruamel.yaml](https://yaml.readthedocs.io/en/latest/pyyaml.html) python lib. 351 | - Dependencies scanned by [PyUp.io](https://pyup.io/) 352 | -------------------------------------------------------------------------------- /poe_tasks.toml: -------------------------------------------------------------------------------- 1 | [tasks] 2 | "lint:all" = [ 3 | "ruff:fmt:check", 4 | "ruff:lint", 5 | "pylint", 6 | "pyright", 7 | ] 8 | pylint = [ 9 | "pylint:run", 10 | ] 11 | "pylint:run" = "pylint kubesplit tests" 12 | pyright = [ 13 | "pyright:run", 14 | ] 15 | "pyright:run" = "pyright" 16 | "pytest:cov" = "pytest --cov src --cov-report=xml --cov-report=term-missing --cov-branch" 17 | "ruff:fmt:check" = "ruff format --check" 18 | "ruff:fmt:run" = "ruff format" 19 | "ruff:lint" = "ruff check" 20 | "ruff:lint:fix" = "ruff check --fix" 21 | style = [ 22 | "ruff:fmt:run", 23 | ] 24 | test = "pytest" 25 | "test:cov" = [ 26 | "pytest:cov", 27 | ] 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatchling", 5 | "versioningit>=3.1.2", 6 | ] 7 | 8 | [project] 9 | authors = [ 10 | { name = "Christophe Furmaniak", email = "christophe.furmaniak@gmail.com" }, 11 | ] 12 | classifiers = [ 13 | "Topic :: Utilities", 14 | # Specify the Python versions you support here. 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | ] 20 | dependencies = [ 21 | "yamkix>=0.10.0", 22 | ] 23 | description = "A CLI to split multidoc yaml formatted kubernetes descriptors to a set of single resource files" 24 | dynamic = [ 25 | "version", 26 | ] 27 | license = "Apache-2.0" 28 | name = "kubesplit" 29 | readme = "README.md" 30 | requires-python = ">=3.10" 31 | 32 | [project.urls] 33 | Repository = "https://github.com/looztra/kubesplit" 34 | 35 | [project.scripts] 36 | kubesplit = "kubesplit.__main__:main" 37 | 38 | [dependency-groups] 39 | dev = [ 40 | "tox>=4.2", 41 | "faker>=36.0", 42 | "poethepoet>=0.29.0", 43 | "pylint>=3.3.0", 44 | "pyright>=1.1.384", 45 | "pytest-cov>=6.0", 46 | "pytest-mock>=3.14.0", 47 | "pytest-unordered>=0.6.1", 48 | "pytest>=8.3.3", 49 | "ruff>=0.9.6", 50 | "tox-uv>=1.23.2", 51 | ] 52 | 53 | [tool.hatch.version] 54 | source = "versioningit" 55 | 56 | [tool.hatch.build.targets.wheel] 57 | include = [ 58 | "src/kubesplit", 59 | ] 60 | 61 | [tool.hatch.build.targets.wheel.sources] 62 | "src/kubesplit" = "kubesplit" 63 | 64 | [tool.versioningit] 65 | default-version = "0.0.0.dev0+0" 66 | 67 | [tool.versioningit.vcs] 68 | default-tag = "v0.0.0" 69 | 70 | [tool.versioningit.format] 71 | # Format used when there have been commits since the most recent tag: 72 | distance = "{next_version}.dev{distance}" 73 | # Example formatted version: 1.2.3.post42+ge174a1f 74 | 75 | # Format used when there are uncommitted changes: 76 | dirty = "{base_version}+dirty{build_date:%Y%m%d}" 77 | # Example formatted version: 1.2.3+d20230922 78 | 79 | # Format used when there are both commits and uncommitted changes: 80 | distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.d{build_date:%Y%m%d}" 81 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .git .tox *.egg* old docs dist build 3 | addopts = --strict-markers --strict-config -ra 4 | pythonpath = src 5 | testpaths = tests 6 | filterwarnings = 7 | # logdecorator module have deprecation warning 8 | #ignore:The default value of the `reraise` parameter will be changed to `True` in the future.*:DeprecationWarning 9 | 10 | markers = 11 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | automerge: true, 7 | "pre-commit": { 8 | enabled: true, 9 | automerge: true 10 | }, 11 | lockFileMaintenance: { 12 | enabled: true, 13 | automerge: true 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | extend = "ruff_defaults.toml" 2 | # you can change this file, you should NOT remove the extend directive 3 | 4 | [lint] 5 | 6 | ignore = [ 7 | ] 8 | -------------------------------------------------------------------------------- /ruff_defaults.toml: -------------------------------------------------------------------------------- 1 | # This file is the default configuration coming from the template, do not change it. 2 | # If you need to tune the configuration, use the `ruff.toml` file (but keep the "extend" directive inside it). 3 | line-length = 119 4 | target-version = "py310" 5 | 6 | [lint] 7 | 8 | select = [ 9 | "ALL", 10 | ] 11 | 12 | ignore = [ 13 | "B008", # B008: Do not perform function call {name} in argument defaults 14 | "COM812", # COM812: Trailing comma missing 15 | "FBT", # All flake8-boolean-trap rules, 16 | "ISC001", # The following rule may cause conflicts when used with the formatter 17 | ] 18 | 19 | per-file-ignores."**/tests/*" = [ 20 | "INP001", # INP001: File {filename} is part of an implicit namespace package. Add an __init__.py. 21 | "S101", # S101: Use of assert detected. Hey man, we do need asserts in pytest tests. 22 | ] 23 | 24 | # Use Google-style docstrings. 25 | [lint.pydocstyle] 26 | convention = "google" 27 | 28 | [lint.flake8-pytest-style] 29 | fixture-parentheses = false 30 | -------------------------------------------------------------------------------- /src/kubesplit/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for kubesplit.""" 2 | 3 | from importlib.metadata import version 4 | 5 | __author__ = """Christophe Furmaniak""" 6 | __email__ = "christophe.furmaniak@gmail.com" 7 | 8 | __version__ = version(distribution_name="kubesplit") # pragma: no cover 9 | -------------------------------------------------------------------------------- /src/kubesplit/__main__.py: -------------------------------------------------------------------------------- 1 | """Allow kubesplit to be executable through `python -m kubesplit`.""" 2 | 3 | from .kubesplit import main 4 | 5 | if __name__ == "__main__": # pragma: no cover 6 | main() 7 | -------------------------------------------------------------------------------- /src/kubesplit/args.py: -------------------------------------------------------------------------------- 1 | """Deal with args.""" 2 | 3 | import argparse 4 | 5 | from yamkix.args import add_yamkix_options_to_parser 6 | 7 | from kubesplit import __version__ 8 | from kubesplit.config import KubesplitConfig, get_config_from_args 9 | 10 | 11 | def build_parser() -> argparse.ArgumentParser: 12 | """Build the cli args parser.""" 13 | parser = argparse.ArgumentParser( 14 | description=f"""Kubesplit v{__version__} 15 | Split a set of Kubernetes descriptors to a set of files. 16 | The yaml format of the generated files can be tuned using the same\ 17 | parameters as the one used by Yamkix. 18 | By default, explicit_start is `On`, explicit_end is `Off`\ 19 | and array elements are pushed inwards the start of the \ 20 | matching sequence. Comments are preserved thanks to default \ 21 | parsing mode `rt`. 22 | """ 23 | ) 24 | parser.add_argument( 25 | "-i", 26 | "--input", 27 | required=False, 28 | help="the file to parse, or STDIN if not specified or if value is -", 29 | ) 30 | parser.add_argument( 31 | "-o", 32 | "--output-dir", 33 | required=False, 34 | help="the name of the output target directory.\ 35 | The target directory will be created if it does not\ 36 | exist if it's possible", 37 | ) 38 | parser.add_argument( 39 | "-c", 40 | "--clean-output-dir", 41 | action="store_true", 42 | help="clean the output directory (rmtree) if set (default is False)", 43 | ) 44 | parser.add_argument( 45 | "-p", 46 | "--no-resource-prefix", 47 | action="store_true", 48 | help="by default, resource files are number prefixed, you can disable \ 49 | this behavior with this flag", 50 | ) 51 | add_yamkix_options_to_parser(parser, short_opt_override={"--spaces-before-comment": "-s"}) 52 | parser.add_argument( 53 | "-v", 54 | "--version", 55 | action="store_true", 56 | help="show kubesplit version", 57 | ) 58 | return parser 59 | 60 | 61 | def parse_cli(args: list[str]) -> KubesplitConfig: 62 | """Parse the cli args.""" 63 | parser = build_parser() 64 | args_as_ns = parser.parse_args(args) 65 | return get_config_from_args(args_as_ns) 66 | -------------------------------------------------------------------------------- /src/kubesplit/config.py: -------------------------------------------------------------------------------- 1 | """Kubesplit configuration helpers.""" 2 | 3 | import sys 4 | from argparse import Namespace 5 | from dataclasses import dataclass 6 | 7 | from yamkix.config import YamkixConfig 8 | from yamkix.config import get_config_from_args as yamkix_get_config_from_args 9 | 10 | from kubesplit import __version__ 11 | from kubesplit.errors import MissingOutputDirError 12 | 13 | 14 | @dataclass 15 | class KubesplitIOConfig: 16 | """Represents Kubesplit input/output configuration.""" 17 | 18 | input: str | None 19 | input_display_name: str 20 | output_dir: str 21 | 22 | 23 | @dataclass 24 | class KubesplitConfig: 25 | """Represents Kubesplit configuration.""" 26 | 27 | clean_output_dir: bool 28 | prefix_resource_files: bool 29 | version: bool 30 | io_config: KubesplitIOConfig 31 | yamkix_config: YamkixConfig 32 | 33 | 34 | def should_we_show_version(args: Namespace) -> bool: 35 | """Should we show version or not?.""" 36 | return args.version if args.version is not None else False 37 | 38 | 39 | def get_io_config_from_args(args: Namespace, show_version: bool) -> KubesplitIOConfig: 40 | """Build a KubesplitIOConfig from parsed args.""" 41 | input_display_name = "STDIN" 42 | if args.input is None or args.input == "-": 43 | f_input = None 44 | else: 45 | f_input = args.input 46 | input_display_name = f_input 47 | if show_version: 48 | output_dir = "N/A" 49 | else: 50 | if args.output_dir is None: 51 | raise MissingOutputDirError 52 | output_dir = args.output_dir 53 | return KubesplitIOConfig( 54 | input=f_input, 55 | input_display_name=input_display_name, 56 | output_dir=output_dir, 57 | ) 58 | 59 | 60 | def get_config_from_args(args: Namespace) -> KubesplitConfig: 61 | """Build a KubesplitConfig from parsed args.""" 62 | show_version: bool = should_we_show_version(args) 63 | yamkix_config = yamkix_get_config_from_args(args, inc_io_config=False) 64 | io_config = get_io_config_from_args(args, show_version) 65 | return KubesplitConfig( 66 | clean_output_dir=args.clean_output_dir, 67 | prefix_resource_files=not args.no_resource_prefix, 68 | version=show_version, 69 | yamkix_config=yamkix_config, 70 | io_config=io_config, 71 | ) 72 | 73 | 74 | def print_config(kubesplit_config: KubesplitConfig) -> None: 75 | """Print a human readable Kubesplit config on stderr.""" 76 | io_config = kubesplit_config.io_config 77 | if io_config.output_dir is None: 78 | raise SystemExit 79 | print( # noqa: T201 80 | "[kubesplit(" 81 | + __version__ 82 | + ")] Processing: input=" 83 | + io_config.input_display_name 84 | + ", output_dir=" 85 | + io_config.output_dir 86 | + ", clean_output_dir=" 87 | + str(kubesplit_config.clean_output_dir) 88 | + ", prefix_resource_files=" 89 | + str(kubesplit_config.prefix_resource_files) 90 | + ", typ=" 91 | + kubesplit_config.yamkix_config.parsing_mode 92 | + ", explicit_start=" 93 | + str(kubesplit_config.yamkix_config.explicit_start) 94 | + ", explicit_end=" 95 | + str(kubesplit_config.yamkix_config.explicit_end) 96 | + ", default_flow_style=" 97 | + str(kubesplit_config.yamkix_config.default_flow_style) 98 | + ", quotes_preserved=" 99 | + str(kubesplit_config.yamkix_config.quotes_preserved) 100 | + ", dash_inwards=" 101 | + str(kubesplit_config.yamkix_config.dash_inwards) 102 | + ", spaces_before_comment=" 103 | + str(kubesplit_config.yamkix_config.spaces_before_comment) 104 | + ", show_version=" 105 | + str(kubesplit_config.version), 106 | file=sys.stderr, 107 | ) 108 | -------------------------------------------------------------------------------- /src/kubesplit/convert.py: -------------------------------------------------------------------------------- 1 | """From input to descriptors.""" 2 | 3 | import logging 4 | import sys 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from ruamel.yaml import YAML 9 | from ruamel.yaml.scanner import ScannerError 10 | from yamkix.config import YamkixConfig, get_default_yamkix_config 11 | from yamkix.yaml_writer import get_opinionated_yaml_writer 12 | 13 | from kubesplit.k8s_descriptor import K8SDescriptor 14 | from kubesplit.namespaces import get_all_namespaces, prepare_namespace_directories 15 | from kubesplit.output import save_descriptors_to_dir 16 | 17 | LOGGER = logging.getLogger(__name__) 18 | default_yamkix_config = get_default_yamkix_config() 19 | default_yaml = YAML(typ="rt") 20 | StreamTextType = Any 21 | 22 | 23 | def resource_is_list(resource: dict[str, Any]) -> bool: 24 | """Check if the resource is a list.""" 25 | if resource: 26 | return "kind" in resource and "apiVersion" in resource and resource["kind"].endswith("List") 27 | return False 28 | 29 | 30 | def resource_is_object(resource: dict[str, Any]) -> bool: 31 | """Check if the resource is a simple object.""" 32 | if resource: 33 | return "metadata" in resource and "kind" in resource and "name" in resource["metadata"] 34 | return False 35 | 36 | 37 | def deal_with_list( 38 | resource: dict[str, Any], list_index: int, use_order_prefix: bool, split_lists_to_items: bool = False 39 | ) -> dict[str, Any]: 40 | """Deal with lists.""" 41 | descriptors = {} 42 | if split_lists_to_items: 43 | print("Not supported yet") # noqa: T201 44 | else: 45 | list_name = "list_" + str(list_index) 46 | k8s_descriptor = K8SDescriptor( 47 | name=list_name, 48 | kind=resource["kind"], 49 | namespace=None, 50 | as_yaml=resource, 51 | use_order_prefix=use_order_prefix, 52 | ) 53 | descriptors[k8s_descriptor.id] = k8s_descriptor 54 | return descriptors 55 | 56 | 57 | # pylint: disable=too-many-locals 58 | def convert_input_to_descriptors( 59 | input_ref: StreamTextType, 60 | yaml_reader: YAML = default_yaml, 61 | prefix_resource_files: bool = True, 62 | split_lists_to_items: bool = False, 63 | ) -> dict[str, Any]: 64 | """Convert input_ref to a dict of descriptors.""" 65 | parsed = yaml_reader.load_all(input_ref.read()) 66 | descriptors = {} 67 | try: 68 | nb_empty_resources = 0 69 | nb_invalid_resources = 0 70 | nb_valid_resources = 0 71 | nb_lists = 0 72 | # Read the parsed content to force the scanner to issue errors if any 73 | for full_resource in parsed: 74 | if full_resource: 75 | if resource_is_object(full_resource): 76 | resource_name = full_resource["metadata"]["name"] 77 | resource_kind = full_resource["kind"] 78 | if "namespace" in full_resource["metadata"]: 79 | resource_namespace = full_resource["metadata"]["namespace"] 80 | else: 81 | resource_namespace = None 82 | k8s_descriptor = K8SDescriptor( 83 | name=resource_name, 84 | kind=resource_kind, 85 | namespace=resource_namespace, 86 | as_yaml=full_resource, 87 | use_order_prefix=prefix_resource_files, 88 | ) 89 | descriptors[k8s_descriptor.id] = k8s_descriptor 90 | nb_valid_resources = nb_valid_resources + 1 91 | elif resource_is_list(full_resource): 92 | descriptors_from_list = deal_with_list( 93 | full_resource, 94 | nb_lists, 95 | prefix_resource_files, 96 | split_lists_to_items=split_lists_to_items, 97 | ) 98 | descriptors.update(descriptors_from_list) 99 | nb_lists = nb_lists + 1 100 | else: 101 | nb_invalid_resources = nb_invalid_resources + 1 102 | else: 103 | nb_empty_resources = nb_empty_resources + 1 104 | print( # noqa: T201 105 | f"Found [{nb_valid_resources}] valid /" 106 | f" [{nb_lists}] lists /" 107 | f" [{nb_invalid_resources}] invalid /" 108 | f" [{nb_empty_resources}] empty resources" 109 | ) 110 | except ScannerError as scanner_error: 111 | print("Something is wrong in the input, got error from Scanner") # noqa: T201 112 | print(scanner_error) # noqa: T201 113 | return {} 114 | return descriptors 115 | 116 | 117 | def convert_input_to_files_in_directory( 118 | input_name: Path | None, 119 | root_directory: Path, 120 | prefix_resource_files: bool = True, 121 | yamkix_config: YamkixConfig = default_yamkix_config, 122 | ) -> None: 123 | """convert_input_to_files_in_directory.""" 124 | yaml = get_opinionated_yaml_writer(yamkix_config) 125 | if input_name is not None: 126 | with input_name.open(encoding="UTF-8") as f_input: 127 | descriptors = convert_input_to_descriptors(f_input, yaml, prefix_resource_files=prefix_resource_files) 128 | else: 129 | descriptors = convert_input_to_descriptors(sys.stdin, yaml, prefix_resource_files=prefix_resource_files) 130 | 131 | if len(descriptors) > 0: 132 | namespaces = get_all_namespaces(descriptors) 133 | prepare_namespace_directories(root_directory, namespaces) 134 | save_descriptors_to_dir( 135 | descriptors, 136 | root_directory, 137 | yaml_instance=yaml, 138 | yamkix_config=yamkix_config, 139 | ) 140 | else: 141 | LOGGER.error("Nothing found in provided input, check for previous errors") 142 | -------------------------------------------------------------------------------- /src/kubesplit/errors.py: -------------------------------------------------------------------------------- 1 | """Provide the custom Kubesplit errors.""" 2 | 3 | 4 | class MissingOutputDirError(ValueError): 5 | """Exception raised for invalid --typ option value.""" 6 | 7 | def __init__(self) -> None: 8 | """Create a new instance of InvalidTypValueError.""" 9 | super().__init__("The following arguments are required: -o/--output-dir") 10 | 11 | 12 | class K8SNamespaceError(ValueError): 13 | """Exception raised when Namespace is not set and an operation is performed on it.""" 14 | 15 | def __init__(self) -> None: 16 | """Create a new instance of InvalidTypValueError.""" 17 | super().__init__("Cannot perform requested operation on a resource without a namespace") 18 | -------------------------------------------------------------------------------- /src/kubesplit/helpers.py: -------------------------------------------------------------------------------- 1 | """Provide generic helpers.""" 2 | 3 | from kubesplit import __version__ 4 | 5 | 6 | def get_version_string() -> str: 7 | """Return the version string.""" 8 | return "kubesplit v" + __version__ 9 | 10 | 11 | def print_version() -> None: 12 | """Print version.""" 13 | print(get_version_string()) # noqa: T201 14 | -------------------------------------------------------------------------------- /src/kubesplit/k8s_descriptor.py: -------------------------------------------------------------------------------- 1 | """Provides a wrapper for a Kubernetes descriptor.""" 2 | 3 | from collections.abc import Mapping 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from types import MappingProxyType 7 | from typing import ClassVar 8 | 9 | from kubesplit.errors import K8SNamespaceError 10 | 11 | 12 | @dataclass 13 | class K8SDescriptor: # pylint: disable=too-many-instance-attributes 14 | """Kubernetes descriptor.""" 15 | 16 | name: str 17 | kind: str 18 | namespace: str | None 19 | as_yaml: dict 20 | use_order_prefix: bool = True 21 | extension: str = "yml" 22 | is_list: bool = False 23 | 24 | _cluster_wide_str_rep: ClassVar[str] = "__clusterwide__" 25 | _order_prefixes: ClassVar[Mapping[str, str]] = MappingProxyType( 26 | { 27 | "namespace": "00", 28 | "clusterrole": "01", 29 | "clusterrolebinding": "02", 30 | "serviceaccount": "03", 31 | "role": "04", 32 | "rolebinding": "05", 33 | "secret": "10", 34 | "configmap": "11", 35 | "persistentvolumeclaim": "12", 36 | "persistentvolume": "13", 37 | "deployment": "20", 38 | "daemonset": "21", 39 | "statefulset": "22", 40 | "job": "23", 41 | "cronjob": "24", 42 | "replicaset": "25", 43 | "service": "30", 44 | "ingress": "31", 45 | "networkpolicy": "40", 46 | "poddisruptionbudget": "41", 47 | "priorityclass": "42", 48 | "__unknown__": "99", 49 | } 50 | ) 51 | 52 | def __post_init__( 53 | self, 54 | ) -> None: 55 | """Init.""" 56 | ns_or_cluster_wide = self.namespace if self.namespace is not None else K8SDescriptor._cluster_wide_str_rep 57 | self.id = f"ns:{ns_or_cluster_wide}/kind:{self.kind}/name:{self.name}" 58 | 59 | def has_namespace(self) -> bool: 60 | """has_namespace.""" 61 | return self.namespace is not None 62 | 63 | def compute_namespace_dirname(self) -> str: 64 | """compute_namespace_dirname.""" 65 | if self.namespace is not None: 66 | return self.namespace.lower() 67 | raise K8SNamespaceError 68 | 69 | def compute_filename(self) -> str: 70 | """compute_filename.""" 71 | return f"{self.get_order_prefix()}{self.kind.lower()}--{self.name.lower().replace(':', '-')}.{self.extension}" 72 | 73 | def get_order_prefix(self) -> str: 74 | """get_order_prefix.""" 75 | if self.use_order_prefix: 76 | k = self.kind.lower() if self.kind.lower() in K8SDescriptor._order_prefixes else "__unknown__" 77 | return f"{K8SDescriptor._order_prefixes[k]}--" 78 | return "" 79 | 80 | def compute_filename_with_namespace(self, root_directory: Path) -> Path: 81 | """compute_filename_with_namespace.""" 82 | if self.has_namespace(): 83 | return root_directory / self.compute_namespace_dirname() / self.compute_filename() 84 | return root_directory / self.compute_filename() 85 | -------------------------------------------------------------------------------- /src/kubesplit/kubesplit.py: -------------------------------------------------------------------------------- 1 | """Main module.""" 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | from kubesplit.args import parse_cli 7 | from kubesplit.config import KubesplitConfig, print_config 8 | from kubesplit.convert import convert_input_to_files_in_directory 9 | from kubesplit.helpers import print_version 10 | from kubesplit.output import clean_root_dir, create_root_dir 11 | 12 | 13 | def split_input_to_files(kubesplit_config: KubesplitConfig) -> None: 14 | """Split input to files.""" 15 | root_directory = Path(kubesplit_config.io_config.output_dir) 16 | clean_output_dir = kubesplit_config.clean_output_dir 17 | prefix_resource_files = kubesplit_config.prefix_resource_files 18 | input_name = kubesplit_config.io_config.input 19 | yamkix_config = kubesplit_config.yamkix_config 20 | 21 | create_root_dir(root_directory) 22 | if clean_output_dir: 23 | clean_root_dir(root_directory) 24 | convert_input_to_files_in_directory( 25 | input_name=Path(input_name) if input_name is not None else None, 26 | root_directory=root_directory, 27 | prefix_resource_files=prefix_resource_files, 28 | yamkix_config=yamkix_config, 29 | ) 30 | 31 | 32 | def main() -> None: 33 | """Parse args and call the split mojo.""" 34 | kubesplit_config = parse_cli(sys.argv[1:]) 35 | if kubesplit_config.version: 36 | print_version() 37 | else: 38 | print_config(kubesplit_config) 39 | split_input_to_files(kubesplit_config) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /src/kubesplit/namespaces.py: -------------------------------------------------------------------------------- 1 | """Deal with namespaces.""" 2 | 3 | import logging 4 | from pathlib import Path 5 | 6 | from kubesplit.k8s_descriptor import K8SDescriptor 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | def get_all_namespaces(descriptors: dict[str, K8SDescriptor]) -> set[str]: 12 | """get_all_namespaces.""" 13 | all_namespaces = set() 14 | for descriptor in descriptors.values(): 15 | if descriptor.has_namespace(): 16 | all_namespaces.add(descriptor.compute_namespace_dirname()) 17 | return all_namespaces 18 | 19 | 20 | def prepare_namespace_directories(root_directory: Path, namespaces: set[str]) -> None: 21 | """prepare_namespace_directories.""" 22 | for namespace in namespaces: 23 | ns_dir = root_directory / namespace 24 | if not ns_dir.exists(): 25 | LOGGER.info("Creating directory [%s]", ns_dir) 26 | ns_dir.mkdir(parents=True) 27 | -------------------------------------------------------------------------------- /src/kubesplit/output.py: -------------------------------------------------------------------------------- 1 | """Some stuff need to get out.""" 2 | 3 | import shutil 4 | from pathlib import Path 5 | from typing import Any, TextIO 6 | 7 | from ruamel.yaml import YAML 8 | from yamkix.config import YamkixConfig, get_default_yamkix_config 9 | from yamkix.yamkix import yamkix_dump_one 10 | 11 | from kubesplit.k8s_descriptor import K8SDescriptor 12 | 13 | default_yaml = YAML(typ="rt") 14 | default_yamkix_config = get_default_yamkix_config() 15 | 16 | 17 | def create_root_dir(root_directory: Path) -> None: 18 | """create_root_dir.""" 19 | if not root_directory.exists(): 20 | root_directory.mkdir(parents=True) 21 | 22 | 23 | def clean_root_dir(root_directory: Path) -> None: 24 | """clean_root_dir.""" 25 | if root_directory.is_dir(): 26 | shutil.rmtree(root_directory) 27 | root_directory.mkdir(parents=True) 28 | 29 | 30 | def save_descriptor_to_stream( 31 | descriptor: K8SDescriptor, 32 | out: TextIO, 33 | yaml_instance: YAML, 34 | yamkix_config: YamkixConfig = default_yamkix_config, 35 | ) -> None: 36 | """save_descriptor_to_stream.""" 37 | yamkix_dump_one( 38 | descriptor.as_yaml, 39 | yaml_instance, 40 | yamkix_config.dash_inwards, 41 | out, 42 | yamkix_config.spaces_before_comment, 43 | ) 44 | 45 | 46 | def save_descriptors_to_dir( 47 | descriptors: dict[str, Any], 48 | root_directory: Path, 49 | yaml_instance: YAML, 50 | yamkix_config: YamkixConfig = default_yamkix_config, 51 | ) -> None: 52 | """Save input descriptors to files in dir.""" 53 | for desc in descriptors.values(): 54 | with desc.compute_filename_with_namespace(root_directory).open( 55 | mode="w", 56 | encoding="UTF-8", 57 | ) as out: 58 | save_descriptor_to_stream( 59 | descriptor=desc, 60 | out=out, 61 | yaml_instance=yaml_instance, 62 | yamkix_config=yamkix_config, 63 | ) 64 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/apps-demo/rolebinding--example-ns-demo-developer-binding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: RoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: namespaces/demo 9 | name: example:ns-demo-developer-binding 10 | namespace: apps-demo 11 | roleRef: 12 | apiGroup: '' 13 | kind: ClusterRole 14 | name: view 15 | subjects: 16 | - apiGroup: rbac.authorization.k8s.io 17 | kind: User 18 | name: peter.parker@example.com 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/apps-integration/rolebinding--example-ns-integration-developer-binding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: RoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: namespaces/integration 9 | name: example:ns-integration-developer-binding 10 | namespace: apps-integration 11 | roleRef: 12 | apiGroup: '' 13 | kind: ClusterRole 14 | name: edit 15 | subjects: 16 | - apiGroup: rbac.authorization.k8s.io 17 | kind: User 18 | name: peter.parker@example.com 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/clusterrole--example-node-viewer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: rbac/shared 8 | example.com/target: devel 9 | labels: 10 | owner: example 11 | name: example:node-viewer 12 | rules: 13 | - apiGroups: 14 | - '' 15 | resources: 16 | - nodes 17 | - namespaces 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/clusterrole--example-traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | owner: example 10 | name: example:traefik-ingress-controller 11 | rules: 12 | - apiGroups: 13 | - '' 14 | resources: 15 | - services 16 | - endpoints 17 | - secrets 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - extensions 24 | resources: 25 | - ingresses 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/clusterrolebinding--example-node-viewer-developer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: rbac/developers 9 | name: example:node-viewer-developer 10 | roleRef: 11 | apiGroup: '' 12 | kind: ClusterRole 13 | name: example:node-viewer 14 | subjects: 15 | - apiGroup: rbac.authorization.k8s.io 16 | kind: User 17 | name: peter.parker@example.com 18 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/clusterrolebinding--example-traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | owner: example 10 | name: example:traefik-ingress-controller 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: example:traefik-ingress-controller 15 | subjects: 16 | - kind: ServiceAccount 17 | name: traefik-ingress-controller 18 | namespace: ingress-controllers 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/configmap--traefik-conf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | data: 4 | traefik.toml: | 5 | insecureSkipVerify = true 6 | defaultEntryPoints = ["http","https"] 7 | [entryPoints] 8 | [entryPoints.http] 9 | address = ":80" 10 | [entryPoints.http.redirect] 11 | entryPoint = "https" 12 | [entryPoints.https] 13 | address = ":443" 14 | [entryPoints.https.tls] 15 | [acme] 16 | email = "ops@example.com" 17 | storageFile = "/acme/acme.json" 18 | entryPoint = "https" 19 | onDemand = true 20 | onHostRule = true 21 | [acme.httpChallenge] 22 | entryPoint = "http" 23 | kind: ConfigMap 24 | metadata: 25 | annotations: 26 | example.com/generated-by: kustomize 27 | example.com/kustomize-component: ingresses 28 | name: traefik-conf 29 | namespace: ingress-controllers 30 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/deployment--traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | app: traefik-ingress-lb 10 | name: traefik-ingress-controller 11 | namespace: ingress-controllers 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: traefik-ingress-lb 17 | template: 18 | metadata: 19 | annotations: 20 | example.com/generated-by: kustomize 21 | example.com/kustomize-component: ingresses 22 | example.com/owner: ops 23 | labels: 24 | app: traefik-ingress-lb 25 | spec: 26 | containers: 27 | - args: 28 | - --configfile=/config/traefik.toml 29 | - --web 30 | - --kubernetes 31 | image: traefik:v1.7.9 32 | imagePullPolicy: Always 33 | name: traefik 34 | ports: 35 | - containerPort: 80 36 | name: main-http 37 | - containerPort: 443 38 | name: main-https 39 | - containerPort: 8080 40 | name: admin 41 | resources: 42 | limits: 43 | cpu: 200m 44 | memory: 64Mi 45 | volumeMounts: 46 | - mountPath: /acme 47 | name: traefik-acme 48 | - mountPath: /config 49 | name: traefik-config 50 | serviceAccountName: traefik-ingress-controller 51 | terminationGracePeriodSeconds: 60 52 | volumes: 53 | - name: traefik-acme 54 | persistentVolumeClaim: 55 | claimName: traefik-acme 56 | - configMap: 57 | name: traefik-conf 58 | name: traefik-config 59 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/ingress--traefik-web-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | annotations: 6 | ingress.kubernetes.io/auth-secret: traefik-ui-auth 7 | ingress.kubernetes.io/auth-type: basic 8 | kubernetes.io/ingress.class: traefik 9 | example.com/generated-by: kustomize 10 | example.com/kustomize-component: ingresses 11 | name: traefik-web-ui 12 | namespace: ingress-controllers 13 | spec: 14 | rules: 15 | - host: traefik-ui.dev.example.com 16 | http: 17 | paths: 18 | - backend: 19 | serviceName: traefik-web-ui 20 | servicePort: admin 21 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/persistentvolumeclaim--traefik-acme.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | example.com/what: Store Let's Encrypt TLS certificates 9 | name: traefik-acme 10 | namespace: ingress-controllers 11 | spec: 12 | accessModes: 13 | - ReadWriteOnce 14 | resources: 15 | requests: 16 | storage: 500Mi 17 | storageClassName: standard 18 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/service--traefik-ingress-endpoint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-ingress-endpoint 9 | namespace: ingress-controllers 10 | spec: 11 | loadBalancerIP: 192.168.10.10 12 | ports: 13 | - name: main-http 14 | port: 80 15 | targetPort: main-http 16 | - name: main-https 17 | port: 443 18 | targetPort: main-https 19 | selector: 20 | app: traefik-ingress-lb 21 | type: LoadBalancer 22 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/service--traefik-web-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-web-ui 9 | namespace: ingress-controllers 10 | spec: 11 | ports: 12 | - name: admin 13 | port: 8080 14 | targetPort: admin 15 | selector: 16 | app: traefik-ingress-lb 17 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/ingress-controllers/serviceaccount--traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-ingress-controller 9 | namespace: ingress-controllers 10 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/namespace--apps-demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: namespaces/demo 8 | source-single: single 9 | source-double: double 10 | source-boolean-single: 'true' 11 | source-boolean-double: 'true' 12 | source-int-single: '1' 13 | source-int-double: '2' 14 | labels: 15 | example.com/editors: ci 16 | name: apps-demo 17 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/namespace--apps-integration.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: namespaces/integration 8 | labels: 9 | example.com/editors: developers 10 | name: apps-integration 11 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix/namespace--ingress-controllers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/access: restricted 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: ingresses 9 | labels: 10 | example.com/editors: ops 11 | example.com/ns-owner: example 12 | name: ingress-controllers 13 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/00--namespace--apps-demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: namespaces/demo 8 | source-single: single 9 | source-double: double 10 | source-boolean-single: 'true' 11 | source-boolean-double: 'true' 12 | source-int-single: '1' 13 | source-int-double: '2' 14 | labels: 15 | example.com/editors: ci 16 | name: apps-demo 17 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/00--namespace--apps-integration.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: namespaces/integration 8 | labels: 9 | example.com/editors: developers 10 | name: apps-integration 11 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/00--namespace--ingress-controllers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/access: restricted 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: ingresses 9 | labels: 10 | example.com/editors: ops 11 | example.com/ns-owner: example 12 | name: ingress-controllers 13 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/01--clusterrole--example-node-viewer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: rbac/shared 8 | example.com/target: devel 9 | labels: 10 | owner: example 11 | name: example:node-viewer 12 | rules: 13 | - apiGroups: 14 | - '' 15 | resources: 16 | - nodes 17 | - namespaces 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/01--clusterrole--example-traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | owner: example 10 | name: example:traefik-ingress-controller 11 | rules: 12 | - apiGroups: 13 | - '' 14 | resources: 15 | - services 16 | - endpoints 17 | - secrets 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - extensions 24 | resources: 25 | - ingresses 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/02--clusterrolebinding--example-node-viewer-developer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: rbac/developers 9 | name: example:node-viewer-developer 10 | roleRef: 11 | apiGroup: '' 12 | kind: ClusterRole 13 | name: example:node-viewer 14 | subjects: 15 | - apiGroup: rbac.authorization.k8s.io 16 | kind: User 17 | name: peter.parker@example.com 18 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/02--clusterrolebinding--example-traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | owner: example 10 | name: example:traefik-ingress-controller 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: example:traefik-ingress-controller 15 | subjects: 16 | - kind: ServiceAccount 17 | name: traefik-ingress-controller 18 | namespace: ingress-controllers 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/apps-demo/05--rolebinding--example-ns-demo-developer-binding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: RoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: namespaces/demo 9 | name: example:ns-demo-developer-binding 10 | namespace: apps-demo 11 | roleRef: 12 | apiGroup: '' 13 | kind: ClusterRole 14 | name: view 15 | subjects: 16 | - apiGroup: rbac.authorization.k8s.io 17 | kind: User 18 | name: peter.parker@example.com 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/apps-integration/05--rolebinding--example-ns-integration-developer-binding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: RoleBinding 4 | metadata: 5 | annotations: 6 | owner: example 7 | example.com/generated-by: kustomize 8 | example.com/kustomize-component: namespaces/integration 9 | name: example:ns-integration-developer-binding 10 | namespace: apps-integration 11 | roleRef: 12 | apiGroup: '' 13 | kind: ClusterRole 14 | name: edit 15 | subjects: 16 | - apiGroup: rbac.authorization.k8s.io 17 | kind: User 18 | name: peter.parker@example.com 19 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/03--serviceaccount--traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-ingress-controller 9 | namespace: ingress-controllers 10 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/11--configmap--traefik-conf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | data: 4 | traefik.toml: | 5 | insecureSkipVerify = true 6 | defaultEntryPoints = ["http","https"] 7 | [entryPoints] 8 | [entryPoints.http] 9 | address = ":80" 10 | [entryPoints.http.redirect] 11 | entryPoint = "https" 12 | [entryPoints.https] 13 | address = ":443" 14 | [entryPoints.https.tls] 15 | [acme] 16 | email = "ops@example.com" 17 | storageFile = "/acme/acme.json" 18 | entryPoint = "https" 19 | onDemand = true 20 | onHostRule = true 21 | [acme.httpChallenge] 22 | entryPoint = "http" 23 | kind: ConfigMap 24 | metadata: 25 | annotations: 26 | example.com/generated-by: kustomize 27 | example.com/kustomize-component: ingresses 28 | name: traefik-conf 29 | namespace: ingress-controllers 30 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/12--persistentvolumeclaim--traefik-acme.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | example.com/what: Store Let's Encrypt TLS certificates 9 | name: traefik-acme 10 | namespace: ingress-controllers 11 | spec: 12 | accessModes: 13 | - ReadWriteOnce 14 | resources: 15 | requests: 16 | storage: 500Mi 17 | storageClassName: standard 18 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/20--deployment--traefik-ingress-controller.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | labels: 9 | app: traefik-ingress-lb 10 | name: traefik-ingress-controller 11 | namespace: ingress-controllers 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: traefik-ingress-lb 17 | template: 18 | metadata: 19 | annotations: 20 | example.com/generated-by: kustomize 21 | example.com/kustomize-component: ingresses 22 | example.com/owner: ops 23 | labels: 24 | app: traefik-ingress-lb 25 | spec: 26 | containers: 27 | - args: 28 | - --configfile=/config/traefik.toml 29 | - --web 30 | - --kubernetes 31 | image: traefik:v1.7.9 32 | imagePullPolicy: Always 33 | name: traefik 34 | ports: 35 | - containerPort: 80 36 | name: main-http 37 | - containerPort: 443 38 | name: main-https 39 | - containerPort: 8080 40 | name: admin 41 | resources: 42 | limits: 43 | cpu: 200m 44 | memory: 64Mi 45 | volumeMounts: 46 | - mountPath: /acme 47 | name: traefik-acme 48 | - mountPath: /config 49 | name: traefik-config 50 | serviceAccountName: traefik-ingress-controller 51 | terminationGracePeriodSeconds: 60 52 | volumes: 53 | - name: traefik-acme 54 | persistentVolumeClaim: 55 | claimName: traefik-acme 56 | - configMap: 57 | name: traefik-conf 58 | name: traefik-config 59 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/30--service--traefik-ingress-endpoint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-ingress-endpoint 9 | namespace: ingress-controllers 10 | spec: 11 | loadBalancerIP: 192.168.10.10 12 | ports: 13 | - name: main-http 14 | port: 80 15 | targetPort: main-http 16 | - name: main-https 17 | port: 443 18 | targetPort: main-https 19 | selector: 20 | app: traefik-ingress-lb 21 | type: LoadBalancer 22 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/30--service--traefik-web-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: ingresses 8 | name: traefik-web-ui 9 | namespace: ingress-controllers 10 | spec: 11 | ports: 12 | - name: admin 13 | port: 8080 14 | targetPort: admin 15 | selector: 16 | app: traefik-ingress-lb 17 | -------------------------------------------------------------------------------- /test-assets/expected/all-in-one--no-quotes-preserved/ingress-controllers/31--ingress--traefik-web-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | annotations: 6 | ingress.kubernetes.io/auth-secret: traefik-ui-auth 7 | ingress.kubernetes.io/auth-type: basic 8 | kubernetes.io/ingress.class: traefik 9 | example.com/generated-by: kustomize 10 | example.com/kustomize-component: ingresses 11 | name: traefik-web-ui 12 | namespace: ingress-controllers 13 | spec: 14 | rules: 15 | - host: traefik-ui.dev.example.com 16 | http: 17 | paths: 18 | - backend: 19 | serviceName: traefik-web-ui 20 | servicePort: admin 21 | -------------------------------------------------------------------------------- /test-assets/expected/k8s-deployment-with-comments-1--no-quotes-preserved--no-resource-prefix--spaces-before-comment_1/deployment--crashing-for-tests-because-command.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Comment 1 3 | apiVersion: extensions/v1beta1 4 | kind: Deployment 5 | metadata: 6 | # Comment 2 7 | name: crashing-for-tests-because-command 8 | # Comment 3 9 | labels: 10 | name: I-am-a-failure-pod 11 | spec: 12 | strategy: 13 | type: Recreate 14 | # Comment 5 15 | template: # I am a comment that needs to be shifted 16 | metadata: 17 | labels: # Comment 4 18 | name: I-am-a-failure-pod 19 | spec: 20 | containers: 21 | - name: crashy 22 | image: alpine:3.10.3 23 | command: 24 | - /bin/false 25 | -------------------------------------------------------------------------------- /test-assets/expected/k8s-deployment-with-comments-1--no-quotes-preserved/20--deployment--crashing-for-tests-because-command.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Comment 1 3 | apiVersion: extensions/v1beta1 4 | kind: Deployment 5 | metadata: 6 | # Comment 2 7 | name: crashing-for-tests-because-command 8 | # Comment 3 9 | labels: 10 | name: I-am-a-failure-pod 11 | spec: 12 | strategy: 13 | type: Recreate 14 | # Comment 5 15 | template: # I am a comment that needs to be shifted 16 | metadata: 17 | labels: # Comment 4 18 | name: I-am-a-failure-pod 19 | spec: 20 | containers: 21 | - name: crashy 22 | image: alpine:3.10.3 23 | command: 24 | - /bin/false 25 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved/akira/25--replicaset--bididididi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # with comment 3 | kind: ReplicaSet 4 | metadata: 5 | name: bididididi 6 | namespace: akira 7 | annotations: 8 | boubou: frontend 9 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved/yolo/25--replicaset--frontend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # I am a valid resource 3 | kind: ReplicaSet 4 | metadata: 5 | name: frontend 6 | namespace: yolo 7 | annotations: 8 | boubou: frontend 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchExpressions: 13 | - key: env 14 | operator: In 15 | values: [int, rd] 16 | matchLabels: 17 | app: frontend 18 | template: 19 | metadata: 20 | name: frontend 21 | labels: 22 | app: frontend 23 | env: int 24 | spec: 25 | containers: 26 | - name: nginx-fe-4-rs 27 | image: nginx:stable-alpine 28 | ports: 29 | - containerPort: 80 30 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved/99--configmaplist--list_0.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # I am a list 3 | apiVersion: v1 4 | items: 5 | - apiVersion: v1 6 | data: 7 | envoy.json: some data 8 | kind: ConfigMap 9 | metadata: 10 | name: grafana-dashboard-statefulset 11 | namespace: monitoring 12 | - apiVersion: v1 13 | data: 14 | envoy.json: some data 15 | kind: ConfigMap 16 | metadata: 17 | name: grafana-dashboard-statefulset2 18 | namespace: monitoring 19 | kind: ConfigMapList 20 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved/99--rolelist--list_1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # I am a list without items 3 | apiVersion: v1 4 | kind: RoleList 5 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved/99--rolelist--list_2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # I am a list with an empty items array 3 | apiVersion: v1 4 | kind: RoleList 5 | items: [] 6 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved/akira/25--replicaset--bididididi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # with comment 3 | kind: ReplicaSet 4 | metadata: 5 | name: bididididi 6 | namespace: akira 7 | annotations: 8 | boubou: frontend 9 | -------------------------------------------------------------------------------- /test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved/yolo/25--replicaset--frontend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # I am a valid resource 3 | kind: ReplicaSet 4 | metadata: 5 | name: frontend 6 | namespace: yolo 7 | annotations: 8 | boubou: frontend 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchExpressions: 13 | - key: env 14 | operator: In 15 | values: [int, rd] 16 | matchLabels: 17 | app: frontend 18 | template: 19 | metadata: 20 | name: frontend 21 | labels: 22 | app: frontend 23 | env: int 24 | spec: 25 | containers: 26 | - name: nginx-fe-4-rs 27 | image: nginx:stable-alpine 28 | ports: 29 | - containerPort: 80 30 | -------------------------------------------------------------------------------- /test-assets/source/all-in-one.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | annotations: 6 | example.com/generated-by: kustomize 7 | example.com/kustomize-component: namespaces/demo 8 | source-single: 'single' 9 | source-double: "double" 10 | source-boolean-single: 'true' 11 | source-boolean-double: "true" 12 | source-int-single: '1' 13 | source-int-double: "2" 14 | labels: 15 | example.com/editors: ci 16 | name: apps-demo 17 | --- 18 | apiVersion: v1 19 | kind: Namespace 20 | metadata: 21 | annotations: 22 | example.com/generated-by: kustomize 23 | example.com/kustomize-component: namespaces/integration 24 | labels: 25 | example.com/editors: developers 26 | name: apps-integration 27 | --- 28 | apiVersion: v1 29 | kind: Namespace 30 | metadata: 31 | annotations: 32 | example.com/access: restricted 33 | example.com/generated-by: kustomize 34 | example.com/kustomize-component: ingresses 35 | labels: 36 | example.com/editors: ops 37 | example.com/ns-owner: example 38 | name: ingress-controllers 39 | --- 40 | apiVersion: v1 41 | kind: ServiceAccount 42 | metadata: 43 | annotations: 44 | example.com/generated-by: kustomize 45 | example.com/kustomize-component: ingresses 46 | name: traefik-ingress-controller 47 | namespace: ingress-controllers 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1beta1 50 | kind: ClusterRole 51 | metadata: 52 | annotations: 53 | example.com/generated-by: kustomize 54 | example.com/kustomize-component: rbac/shared 55 | example.com/target: devel 56 | labels: 57 | owner: example 58 | name: example:node-viewer 59 | rules: 60 | - apiGroups: 61 | - "" 62 | resources: 63 | - nodes 64 | - namespaces 65 | verbs: 66 | - get 67 | - list 68 | - watch 69 | --- 70 | apiVersion: rbac.authorization.k8s.io/v1beta1 71 | kind: ClusterRole 72 | metadata: 73 | annotations: 74 | example.com/generated-by: kustomize 75 | example.com/kustomize-component: ingresses 76 | labels: 77 | owner: example 78 | name: example:traefik-ingress-controller 79 | rules: 80 | - apiGroups: 81 | - "" 82 | resources: 83 | - services 84 | - endpoints 85 | - secrets 86 | verbs: 87 | - get 88 | - list 89 | - watch 90 | - apiGroups: 91 | - extensions 92 | resources: 93 | - ingresses 94 | verbs: 95 | - get 96 | - list 97 | - watch 98 | --- 99 | apiVersion: rbac.authorization.k8s.io/v1beta1 100 | kind: RoleBinding 101 | metadata: 102 | annotations: 103 | owner: example 104 | example.com/generated-by: kustomize 105 | example.com/kustomize-component: namespaces/demo 106 | name: example:ns-demo-developer-binding 107 | namespace: apps-demo 108 | roleRef: 109 | apiGroup: "" 110 | kind: ClusterRole 111 | name: view 112 | subjects: 113 | - apiGroup: rbac.authorization.k8s.io 114 | kind: User 115 | name: peter.parker@example.com 116 | --- 117 | apiVersion: rbac.authorization.k8s.io/v1beta1 118 | kind: RoleBinding 119 | metadata: 120 | annotations: 121 | owner: example 122 | example.com/generated-by: kustomize 123 | example.com/kustomize-component: namespaces/integration 124 | name: example:ns-integration-developer-binding 125 | namespace: apps-integration 126 | roleRef: 127 | apiGroup: "" 128 | kind: ClusterRole 129 | name: edit 130 | subjects: 131 | - apiGroup: rbac.authorization.k8s.io 132 | kind: User 133 | name: peter.parker@example.com 134 | --- 135 | apiVersion: rbac.authorization.k8s.io/v1beta1 136 | kind: ClusterRoleBinding 137 | metadata: 138 | annotations: 139 | owner: example 140 | example.com/generated-by: kustomize 141 | example.com/kustomize-component: rbac/developers 142 | name: example:node-viewer-developer 143 | roleRef: 144 | apiGroup: "" 145 | kind: ClusterRole 146 | name: example:node-viewer 147 | subjects: 148 | - apiGroup: rbac.authorization.k8s.io 149 | kind: User 150 | name: peter.parker@example.com 151 | --- 152 | apiVersion: rbac.authorization.k8s.io/v1beta1 153 | kind: ClusterRoleBinding 154 | metadata: 155 | annotations: 156 | example.com/generated-by: kustomize 157 | example.com/kustomize-component: ingresses 158 | labels: 159 | owner: example 160 | name: example:traefik-ingress-controller 161 | roleRef: 162 | apiGroup: rbac.authorization.k8s.io 163 | kind: ClusterRole 164 | name: example:traefik-ingress-controller 165 | subjects: 166 | - kind: ServiceAccount 167 | name: traefik-ingress-controller 168 | namespace: ingress-controllers 169 | --- 170 | apiVersion: v1 171 | data: 172 | traefik.toml: | 173 | insecureSkipVerify = true 174 | defaultEntryPoints = ["http","https"] 175 | [entryPoints] 176 | [entryPoints.http] 177 | address = ":80" 178 | [entryPoints.http.redirect] 179 | entryPoint = "https" 180 | [entryPoints.https] 181 | address = ":443" 182 | [entryPoints.https.tls] 183 | [acme] 184 | email = "ops@example.com" 185 | storageFile = "/acme/acme.json" 186 | entryPoint = "https" 187 | onDemand = true 188 | onHostRule = true 189 | [acme.httpChallenge] 190 | entryPoint = "http" 191 | kind: ConfigMap 192 | metadata: 193 | annotations: 194 | example.com/generated-by: kustomize 195 | example.com/kustomize-component: ingresses 196 | name: traefik-conf 197 | namespace: ingress-controllers 198 | --- 199 | apiVersion: v1 200 | kind: Service 201 | metadata: 202 | annotations: 203 | example.com/generated-by: kustomize 204 | example.com/kustomize-component: ingresses 205 | name: traefik-ingress-endpoint 206 | namespace: ingress-controllers 207 | spec: 208 | loadBalancerIP: 192.168.10.10 209 | ports: 210 | - name: main-http 211 | port: 80 212 | targetPort: main-http 213 | - name: main-https 214 | port: 443 215 | targetPort: main-https 216 | selector: 217 | app: traefik-ingress-lb 218 | type: LoadBalancer 219 | --- 220 | apiVersion: v1 221 | kind: Service 222 | metadata: 223 | annotations: 224 | example.com/generated-by: kustomize 225 | example.com/kustomize-component: ingresses 226 | name: traefik-web-ui 227 | namespace: ingress-controllers 228 | spec: 229 | ports: 230 | - name: admin 231 | port: 8080 232 | targetPort: admin 233 | selector: 234 | app: traefik-ingress-lb 235 | --- 236 | apiVersion: extensions/v1beta1 237 | kind: Deployment 238 | metadata: 239 | annotations: 240 | example.com/generated-by: kustomize 241 | example.com/kustomize-component: ingresses 242 | labels: 243 | app: traefik-ingress-lb 244 | name: traefik-ingress-controller 245 | namespace: ingress-controllers 246 | spec: 247 | replicas: 1 248 | selector: 249 | matchLabels: 250 | app: traefik-ingress-lb 251 | template: 252 | metadata: 253 | annotations: 254 | example.com/generated-by: kustomize 255 | example.com/kustomize-component: ingresses 256 | example.com/owner: ops 257 | labels: 258 | app: traefik-ingress-lb 259 | spec: 260 | containers: 261 | - args: 262 | - --configfile=/config/traefik.toml 263 | - --web 264 | - --kubernetes 265 | image: traefik:v1.7.9 266 | imagePullPolicy: Always 267 | name: traefik 268 | ports: 269 | - containerPort: 80 270 | name: main-http 271 | - containerPort: 443 272 | name: main-https 273 | - containerPort: 8080 274 | name: admin 275 | resources: 276 | limits: 277 | cpu: 200m 278 | memory: 64Mi 279 | volumeMounts: 280 | - mountPath: /acme 281 | name: traefik-acme 282 | - mountPath: /config 283 | name: traefik-config 284 | serviceAccountName: traefik-ingress-controller 285 | terminationGracePeriodSeconds: 60 286 | volumes: 287 | - name: traefik-acme 288 | persistentVolumeClaim: 289 | claimName: traefik-acme 290 | - configMap: 291 | name: traefik-conf 292 | name: traefik-config 293 | --- 294 | apiVersion: extensions/v1beta1 295 | kind: Ingress 296 | metadata: 297 | annotations: 298 | ingress.kubernetes.io/auth-secret: traefik-ui-auth 299 | ingress.kubernetes.io/auth-type: basic 300 | kubernetes.io/ingress.class: traefik 301 | example.com/generated-by: kustomize 302 | example.com/kustomize-component: ingresses 303 | name: traefik-web-ui 304 | namespace: ingress-controllers 305 | spec: 306 | rules: 307 | - host: traefik-ui.dev.example.com 308 | http: 309 | paths: 310 | - backend: 311 | serviceName: traefik-web-ui 312 | servicePort: admin 313 | --- 314 | apiVersion: v1 315 | kind: PersistentVolumeClaim 316 | metadata: 317 | annotations: 318 | example.com/generated-by: kustomize 319 | example.com/kustomize-component: ingresses 320 | example.com/what: Store Let's Encrypt TLS certificates 321 | name: traefik-acme 322 | namespace: ingress-controllers 323 | spec: 324 | accessModes: 325 | - ReadWriteOnce 326 | resources: 327 | requests: 328 | storage: 500Mi 329 | storageClassName: standard 330 | -------------------------------------------------------------------------------- /test-assets/source/k8s-deployment-with-comments-1.yml: -------------------------------------------------------------------------------- 1 | # Comment 1 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | # Comment 2 6 | name: crashing-for-tests-because-command 7 | # Comment 3 8 | labels: 9 | name: I-am-a-failure-pod 10 | spec: 11 | strategy: 12 | type: Recreate 13 | # Comment 5 14 | template: # I am a comment that needs to be shifted 15 | metadata: 16 | labels: # Comment 4 17 | name: I-am-a-failure-pod 18 | spec: 19 | containers: 20 | - name: crashy 21 | image: alpine:3.10.3 22 | command: 23 | - /bin/false 24 | -------------------------------------------------------------------------------- /test-assets/source/mixed-content-valid-invalid-and-empty-resources.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # I am a valid resource 3 | kind: ReplicaSet 4 | metadata: 5 | name: frontend 6 | namespace: yolo 7 | annotations: 8 | boubou: frontend 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchExpressions: 13 | - key: env 14 | operator: "In" 15 | values: ["int", "rd"] 16 | matchLabels: 17 | app: frontend 18 | template: 19 | metadata: 20 | name: frontend 21 | labels: 22 | app: frontend 23 | env: int 24 | spec: 25 | containers: 26 | - name: nginx-fe-4-rs 27 | image: nginx:stable-alpine 28 | ports: 29 | - containerPort: 80 30 | --- 31 | # I am a empty resource 32 | --- 33 | apiVersion: extensions/v1beta1 # with comment 34 | kind: ReplicaSet 35 | metadata: 36 | name: bididididi 37 | namespace: akira 38 | annotations: 39 | boubou: frontend 40 | --- 41 | apiVersion: extensions/v1beta1 # I am an invalid resource because I have no kind 42 | metadata: 43 | name: bididididi 44 | namespace: akira 45 | annotations: 46 | boubou: frontend 47 | -------------------------------------------------------------------------------- /test-assets/source/mixed-content-valid-invalid-empty-and-list-resources.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 # I am a valid resource 3 | kind: ReplicaSet 4 | metadata: 5 | name: frontend 6 | namespace: yolo 7 | annotations: 8 | boubou: frontend 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchExpressions: 13 | - key: env 14 | operator: "In" 15 | values: ["int", "rd"] 16 | matchLabels: 17 | app: frontend 18 | template: 19 | metadata: 20 | name: frontend 21 | labels: 22 | app: frontend 23 | env: int 24 | spec: 25 | containers: 26 | - name: nginx-fe-4-rs 27 | image: nginx:stable-alpine 28 | ports: 29 | - containerPort: 80 30 | --- 31 | # I am a empty resource 32 | --- 33 | apiVersion: extensions/v1beta1 # with comment 34 | kind: ReplicaSet 35 | metadata: 36 | name: bididididi 37 | namespace: akira 38 | annotations: 39 | boubou: frontend 40 | --- 41 | apiVersion: extensions/v1beta1 # I am an invalid resource because I have no kind 42 | metadata: 43 | name: bididididi 44 | namespace: akira 45 | annotations: 46 | boubou: frontend 47 | --- 48 | # I am a list 49 | apiVersion: v1 50 | items: 51 | - apiVersion: v1 52 | data: 53 | envoy.json: "some data" 54 | kind: ConfigMap 55 | metadata: 56 | name: grafana-dashboard-statefulset 57 | namespace: monitoring 58 | - apiVersion: v1 59 | data: 60 | envoy.json: "some data" 61 | kind: ConfigMap 62 | metadata: 63 | name: grafana-dashboard-statefulset2 64 | namespace: monitoring 65 | kind: ConfigMapList 66 | --- 67 | # I am a list without items 68 | apiVersion: v1 69 | kind: RoleList 70 | --- 71 | # I am a list with an empty items array 72 | apiVersion: v1 73 | kind: RoleList 74 | items: [] 75 | -------------------------------------------------------------------------------- /test-assets/test_kubesplit.bash: -------------------------------------------------------------------------------- 1 | function diff_result_vs_expected() { 2 | local f_input=$1 3 | local config=$2 4 | 5 | [ -d "$BATS_TMPDIR/result" ] 6 | [ -d "$BATS_TEST_DIRNAME/test-assets/expected/${f_input}--${config}" ] 7 | diff -q -r "$BATS_TEST_DIRNAME/test-assets/expected/${f_input}--${config}/" "$BATS_TMPDIR/result/" 8 | } 9 | 10 | function kubesplit_no_quotes_preserved() { 11 | local f_input=$1 12 | uv run kubesplit \ 13 | --input "$BATS_TEST_DIRNAME/test-assets/source/${f_input}.yml" \ 14 | --output "$BATS_TMPDIR/result" \ 15 | --no-quotes-preserved \ 16 | --clean-output-dir 17 | diff_result_vs_expected "${f_input}" no-quotes-preserved 18 | } 19 | 20 | function kubesplit_no_quotes_preserved_stdin_not_specified() { 21 | local f_input=$1 22 | uv run kubesplit \ 23 | --output "$BATS_TMPDIR/result" \ 24 | --no-quotes-preserved \ 25 | --clean-output-dir <"$BATS_TEST_DIRNAME/test-assets/source/${f_input}.yml" 26 | diff_result_vs_expected "${f_input}" no-quotes-preserved 27 | } 28 | 29 | function kubesplit_no_quotes_preserved_stdin_is_dash() { 30 | local f_input=$1 31 | uv run kubesplit \ 32 | --input - \ 33 | --output "$BATS_TMPDIR/result" \ 34 | --no-quotes-preserved \ 35 | --clean-output-dir <"$BATS_TEST_DIRNAME/test-assets/source/${f_input}.yml" 36 | diff_result_vs_expected "${f_input}" no-quotes-preserved 37 | } 38 | 39 | function kubesplit_no_quotes_preserved_no_resource_prefix() { 40 | local f_input=$1 41 | uv run kubesplit \ 42 | --input "$BATS_TEST_DIRNAME/test-assets/source/${f_input}.yml" \ 43 | --output "$BATS_TMPDIR/result" \ 44 | --no-quotes-preserved \ 45 | --no-resource-prefix \ 46 | --clean-output-dir 47 | diff_result_vs_expected "${f_input}" no-quotes-preserved--no-resource-prefix 48 | } 49 | 50 | function kubesplit_no_quotes_preserved_no_resource_prefix_spaces_before_comment_1() { 51 | local f_input=$1 52 | uv run kubesplit \ 53 | --input "$BATS_TEST_DIRNAME/test-assets/source/${f_input}.yml" \ 54 | --output "$BATS_TMPDIR/result" \ 55 | --no-quotes-preserved \ 56 | --no-resource-prefix \ 57 | --spaces-before-comment 1 \ 58 | --clean-output-dir 59 | diff_result_vs_expected "${f_input}" no-quotes-preserved--no-resource-prefix--spaces-before-comment_1 60 | } 61 | -------------------------------------------------------------------------------- /tests.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test-assets/test_kubesplit 4 | 5 | @test "--help" { 6 | uv run kubesplit --help 7 | } 8 | 9 | # kubesplit --input test-assets/source/all-in-one.yml \ 10 | # --output test-assets/expected/all-in-one--no-quotes-preserved \ 11 | # --no-quotes-preserved \ 12 | # --clean-output-dir 13 | @test "all-in-one.yml, --no-quotes-preserved, --clean-output-dir" { 14 | kubesplit_no_quotes_preserved all-in-one 15 | } 16 | 17 | # kubesplit --input test-assets/source/all-in-one.yml \ 18 | # --output test-assets/expected/all-in-one--no-quotes-preserved--no-resource-prefix \ 19 | # --no-quotes-preserved \ 20 | # --no-resource-prefix \ 21 | # --clean-output-dir 22 | @test "all-in-one.yml, --no-quotes-preserved, --no-resource-prefix, --clean-output-dir" { 23 | kubesplit_no_quotes_preserved_no_resource_prefix all-in-one 24 | } 25 | 26 | # kubesplit --input test-assets/source/mixed-content-valid-invalid-and-empty-resources.yml \ 27 | # --output test-assets/expected/mixed-content-valid-invalid-and-empty-resources--no-quotes-preserved \ 28 | # --no-quotes-preserved \ 29 | # --clean-output-dir 30 | @test "mixed-content-valid-invalid-and-empty-resources.yml, --no-quotes-preserved, --clean-output-dir" { 31 | kubesplit_no_quotes_preserved mixed-content-valid-invalid-and-empty-resources 32 | } 33 | 34 | # kubesplit --input test-assets/source/mixed-content-valid-invalid-empty-and-list-resources.yml \ 35 | # --output test-assets/expected/mixed-content-valid-invalid-empty-and-list-resources--no-quotes-preserved \ 36 | # --no-quotes-preserved \ 37 | # --clean-output-dir 38 | @test "mixed-content-valid-invalid-empty-and-list-resources.yml, --no-quotes-preserved, --clean-output-dir" { 39 | kubesplit_no_quotes_preserved mixed-content-valid-invalid-empty-and-list-resources 40 | } 41 | 42 | # kubesplit --input test-assets/source/k8s-deployment-with-comments-1.yml \ 43 | # --output test-assets/expected/k8s-deployment-with-comments-1--no-quotes-preserved \ 44 | # --no-quotes-preserved \ 45 | # --clean-output-dir 46 | @test "k8s-deployment-with-comments-1.yml, --no-quotes-preserved, --clean-output-dir" { 47 | kubesplit_no_quotes_preserved k8s-deployment-with-comments-1 48 | } 49 | 50 | # kubesplit --input test-assets/source/k8s-deployment-with-comments-1.yml \ 51 | # --output test-assets/expected/k8s-deployment-with-comments-1--no-quotes-preserved--no-resource-prefix--spaces-before-comment_1 \ 52 | # --no-quotes-preserved \ 53 | # --no-resource-prefix \ 54 | # --spaces-before-comment 1 \ 55 | # --clean-output-dir 56 | @test "k8s-deployment-with-comments-1.yml, --no-quotes-preserved, --no-resource-prefix, --spaces-before-comment 1, --clean-output-dir" { 57 | kubesplit_no_quotes_preserved_no_resource_prefix_spaces_before_comment_1 k8s-deployment-with-comments-1 58 | } 59 | 60 | # kubesplit \ 61 | # --output test-assets/expected/all-in-one--no-quotes-preserved \ 62 | # --no-quotes-preserved \ 63 | # --clean-output-dir < test-assets/source/all-in-one.yml 64 | @test "all-in-one.yml, --no-quotes-preserved, --clean-output-dir, input not specified" { 65 | kubesplit_no_quotes_preserved_stdin_not_specified all-in-one 66 | } 67 | 68 | # kubesplit \ 69 | # --input - \ 70 | # --output test-assets/expected/all-in-one--no-quotes-preserved \ 71 | # --no-quotes-preserved \ 72 | # --clean-output-dir < test-assets/source/all-in-one.yml 73 | @test "all-in-one.yml, --no-quotes-preserved, --clean-output-dir, input is -" { 74 | kubesplit_no_quotes_preserved_stdin_is_dash all-in-one 75 | } 76 | -------------------------------------------------------------------------------- /tests/test_convert.py: -------------------------------------------------------------------------------- 1 | """Test the convert package (editorconfig-checker-disable-file).""" 2 | 3 | from io import StringIO 4 | from textwrap import dedent 5 | 6 | from ruamel.yaml import YAML 7 | 8 | from kubesplit.convert import StreamTextType, convert_input_to_descriptors, resource_is_list, resource_is_object 9 | 10 | default_yaml = YAML(typ="rt") 11 | 12 | 13 | def string_to_single_resource(resource_as_string: StreamTextType): # noqa: ANN201 14 | """Helper to convert a string to a resource.""" 15 | resource_as_string.seek(0) 16 | return default_yaml.load(resource_as_string.read()) 17 | 18 | 19 | def test_resource_is_list_when_resource_is_empty() -> None: 20 | """Test resource_is_list when resource is empty.""" 21 | string = StringIO() 22 | string.write( 23 | """--- 24 | """ 25 | ) 26 | sut = string_to_single_resource(string) 27 | res = resource_is_list(sut) 28 | assert res is False 29 | 30 | 31 | def test_resource_is_list_when_resource_is_not_a_list() -> None: 32 | """Test resource_is_list when resource is not a list.""" 33 | string = StringIO() 34 | string.write( 35 | """--- 36 | apiVersion: extensions/v1beta1 # with comment 37 | kind: ReplicaSet 38 | """ 39 | ) 40 | sut = string_to_single_resource(string) 41 | res = resource_is_list(sut) 42 | assert res is False 43 | 44 | 45 | def test_resource_is_list_when_resource_is_a_list() -> None: 46 | """Test resource_is_list when resource is a list.""" 47 | string = StringIO() 48 | string.write( 49 | dedent("""--- 50 | apiVersion: v1 51 | items: 52 | - apiVersion: v1 53 | data: 54 | envoy.json: "some data" 55 | kind: ConfigMap 56 | metadata: 57 | name: grafana-dashboard-statefulset 58 | namespace: monitoring 59 | - apiVersion: v1 60 | data: 61 | envoy.json: "some data" 62 | kind: ConfigMap 63 | metadata: 64 | name: grafana-dashboard-statefulset2 65 | namespace: monitoring 66 | kind: ConfigMapList 67 | """) 68 | ) 69 | sut = string_to_single_resource(string) 70 | res = resource_is_list(sut) 71 | assert res is True 72 | 73 | 74 | def test_resource_is_list_when_resource_is_a_list_without_items() -> None: 75 | """Test resource_is_list when resource is a list without items.""" 76 | string = StringIO() 77 | string.write( 78 | """--- 79 | apiVersion: v1 80 | kind: ConfigMapList 81 | """ 82 | ) 83 | sut = string_to_single_resource(string) 84 | res = resource_is_list(sut) 85 | assert res is True 86 | 87 | 88 | def test_resource_is_object_when_resource_is_a_list() -> None: 89 | """Test resource_is_object when resource is a list.""" 90 | string = StringIO() 91 | string.write( 92 | """--- 93 | apiVersion: v1 94 | items: 95 | - apiVersion: v1 96 | data: 97 | envoy.json: "some data" 98 | kind: ConfigMap 99 | metadata: 100 | name: grafana-dashboard-statefulset 101 | namespace: monitoring 102 | - apiVersion: v1 103 | data: 104 | envoy.json: "some data" 105 | kind: ConfigMap 106 | metadata: 107 | name: grafana-dashboard-statefulset2 108 | namespace: monitoring 109 | kind: ConfigMapList 110 | """ 111 | ) 112 | sut = string_to_single_resource(string) 113 | res = resource_is_object(sut) 114 | assert res is False 115 | 116 | 117 | def test_resource_is_object_when_resource_is_object() -> None: 118 | """Test resource_is_object when resource is object.""" 119 | string = StringIO() 120 | string.write( 121 | """--- 122 | apiVersion: extensions/v1beta1 # with comment 123 | kind: ReplicaSet 124 | metadata: 125 | name: ze_super_replicaset 126 | annotations: 127 | nodevops.io/yamkix: rulez 128 | """ 129 | ) 130 | sut = string_to_single_resource(string) 131 | res = resource_is_object(sut) 132 | assert res is True 133 | 134 | 135 | def test_resource_is_object_when_resource_is_empty() -> None: 136 | """Test resource_is_object when resource is empty.""" 137 | string = StringIO() 138 | string.write( 139 | """--- 140 | """ 141 | ) 142 | sut = string_to_single_resource(string) 143 | res = resource_is_object(sut) 144 | assert res is False 145 | 146 | 147 | def test_convert_input_to_descriptors() -> None: 148 | """test_convert_input_to_descriptors.""" 149 | sut = StringIO() 150 | sut.write( 151 | """--- 152 | apiVersion: extensions/v1beta1 # with comment 153 | kind: ReplicaSet 154 | metadata: 155 | name: frontend 156 | namespace: yolo 157 | annotations: 158 | boubou: frontend 159 | spec: 160 | replicas: 2 161 | selector: 162 | matchExpressions: 163 | - key: env 164 | operator: "In" 165 | values: ["int", "rd"] 166 | matchLabels: 167 | app: frontend 168 | template: 169 | metadata: 170 | name: frontend 171 | labels: 172 | app: frontend 173 | env: int 174 | spec: 175 | containers: 176 | - name: nginx-fe-4-rs 177 | image: nginx:stable-alpine 178 | ports: 179 | - containerPort: 80 180 | """ 181 | ) 182 | sut.seek(0) 183 | res = convert_input_to_descriptors(sut) 184 | assert len(res) == 1 185 | k = next(iter(res)) 186 | assert res[k].name == "frontend" 187 | assert res[k].kind == "ReplicaSet" 188 | assert res[k].namespace == "yolo" 189 | 190 | 191 | def test_convert_input_to_descriptors_when_input_is_empty() -> None: 192 | """test_convert_input_to_descriptors_when_input_is_empty.""" 193 | sut = StringIO() 194 | sut.write( 195 | """--- 196 | """ 197 | ) 198 | sut.seek(0) 199 | res = convert_input_to_descriptors(sut) 200 | assert len(res) == 0 201 | 202 | 203 | def test_convert_input_to_descriptors_when_input_is_invalid_no_metadata() -> None: 204 | """test_convert_input_to_descriptors_when_input_is_invalid_no_metadata.""" 205 | sut = StringIO() 206 | sut.write( 207 | """--- 208 | apiVersion: extensions/v1beta1 # with comment 209 | kind: ReplicaSet 210 | """ 211 | ) 212 | sut.seek(0) 213 | res = convert_input_to_descriptors(sut) 214 | assert len(res) == 0 215 | 216 | 217 | def test_convert_input_to_descriptors_when_input_is_invalid_no_kind() -> None: 218 | """test_convert_input_to_descriptors_when_input_is_invalid_no_kind.""" 219 | sut = StringIO() 220 | sut.write( 221 | """--- 222 | apiVersion: extensions/v1beta1 # with comment 223 | metadata: 224 | name: frontend 225 | namespace: yolo 226 | annotations: 227 | boubou: frontend 228 | 229 | """ 230 | ) 231 | sut.seek(0) 232 | res = convert_input_to_descriptors(sut) 233 | assert len(res) == 0 234 | 235 | 236 | def test_convert_input_to_descriptors_when_input_is_invalid_no_name() -> None: 237 | """test_convert_input_to_descriptors_when_input_is_invalid_no_name.""" 238 | sut = StringIO() 239 | sut.write( 240 | """--- 241 | apiVersion: extensions/v1beta1 # with comment 242 | kind: ReplicaSet 243 | metadata: 244 | namespace: yolo 245 | annotations: 246 | boubou: frontend 247 | 248 | """ 249 | ) 250 | sut.seek(0) 251 | res = convert_input_to_descriptors(sut) 252 | assert len(res) == 0 253 | 254 | 255 | def test_convert_input_to_descriptors_when_content_is_mixed() -> None: 256 | """test_convert_input_to_descriptors_when_content_is_mixed.""" 257 | sut = StringIO() 258 | sut.write( 259 | """--- 260 | apiVersion: extensions/v1beta1 # with comment 261 | kind: ReplicaSet 262 | metadata: 263 | name: frontend 264 | namespace: yolo 265 | annotations: 266 | boubou: frontend 267 | spec: 268 | replicas: 2 269 | selector: 270 | matchExpressions: 271 | - key: env 272 | operator: "In" 273 | values: ["int", "rd"] 274 | matchLabels: 275 | app: frontend 276 | template: 277 | metadata: 278 | name: frontend 279 | labels: 280 | app: frontend 281 | env: int 282 | spec: 283 | containers: 284 | - name: nginx-fe-4-rs 285 | image: nginx:stable-alpine 286 | ports: 287 | - containerPort: 80 288 | --- 289 | --- 290 | apiVersion: extensions/v1beta1 # with comment 291 | kind: ReplicaSet 292 | metadata: 293 | name: bididididi 294 | namespace: akira 295 | annotations: 296 | boubou: frontend 297 | --- 298 | apiVersion: extensions/v1beta1 # with comment 299 | metadata: 300 | name: bididididi 301 | namespace: akira 302 | annotations: 303 | boubou: frontend 304 | 305 | """ 306 | ) 307 | sut.seek(0) 308 | res = convert_input_to_descriptors(sut) 309 | assert len(res) == 2 # noqa: PLR2004 310 | 311 | 312 | def test_convert_input_to_descriptors_when_content_has_no_namespace() -> None: 313 | """test_convert_input_to_descriptors_when_content_has_no_namespace.""" 314 | sut = StringIO() 315 | sut.write( 316 | """--- 317 | apiVersion: v1 318 | kind: Namespace 319 | metadata: 320 | annotations: 321 | qima.com/generated-by: kustomize 322 | qima.com/kustomize-component: namespaces/demo 323 | labels: 324 | qima.com/editors: ci 325 | name: apps-demo 326 | """ 327 | ) 328 | sut.seek(0) 329 | res = convert_input_to_descriptors(sut) 330 | k = next(iter(res)) 331 | assert len(res) == 1 332 | assert res[k].name == "apps-demo" 333 | assert res[k].kind == "Namespace" 334 | assert res[k].namespace is None 335 | 336 | 337 | def test_convert_input_to_descriptors_with_a_single_list() -> None: 338 | """test_convert_input_to_descriptors.""" 339 | sut = StringIO() 340 | sut.write( 341 | """--- 342 | apiVersion: v1 343 | items: 344 | - apiVersion: v1 345 | data: 346 | envoy.json: "some data" 347 | kind: ConfigMap 348 | metadata: 349 | name: grafana-dashboard-statefulset 350 | namespace: monitoring 351 | - apiVersion: v1 352 | data: 353 | envoy.json: "some data" 354 | kind: ConfigMap 355 | metadata: 356 | name: grafana-dashboard-statefulset2 357 | namespace: monitoring 358 | kind: ConfigMapList 359 | """ 360 | ) 361 | sut.seek(0) 362 | res = convert_input_to_descriptors(sut) 363 | assert len(res) == 1 364 | k = next(iter(res)) 365 | assert res[k].kind == "ConfigMapList" 366 | assert res[k].namespace is None 367 | -------------------------------------------------------------------------------- /tests/test_k8s_descriptor.py: -------------------------------------------------------------------------------- 1 | """Test the K8SDescriptor wrapper.""" 2 | 3 | import pytest 4 | 5 | from kubesplit.errors import K8SNamespaceError 6 | from kubesplit.k8s_descriptor import K8SDescriptor 7 | 8 | data_for_test_get_order_prefix = [ 9 | ("Namespace"), 10 | ("ServiceAccount"), 11 | ("ClusterRole"), 12 | ("Role"), 13 | ("ClusterRoleBinding"), 14 | ("RoleBinding"), 15 | ("Deployment"), 16 | ("Service"), 17 | ("Ingress"), 18 | ("StatefulSet"), 19 | ] 20 | 21 | 22 | def test_has_namespace_when_true() -> None: 23 | """test_has_namespace_when_true.""" 24 | k8s_default_svc_dummy = K8SDescriptor(name="dummy", kind="Service", namespace="default", as_yaml={}) 25 | assert k8s_default_svc_dummy.has_namespace() 26 | 27 | 28 | def test_has_namespace_when_false() -> None: 29 | """test_has_namespace_when_false.""" 30 | k8s_default_svc_dummy = K8SDescriptor(name="dummy", kind="Service", namespace=None, as_yaml={}) 31 | assert not k8s_default_svc_dummy.has_namespace() 32 | 33 | 34 | def test_compute_namespace_dirname_when_has_namespace() -> None: 35 | """test_compute_namespace_dirname_when_has_namespace.""" 36 | k8s_default_svc_dummy = K8SDescriptor(name="dummy", kind="Service", namespace="default", as_yaml={}) 37 | assert k8s_default_svc_dummy.compute_namespace_dirname() == "default" 38 | 39 | 40 | def test_compute_namespace_dirname_when_no_namespace() -> None: 41 | """test_compute_namespace_dirname_when_no_namespace.""" 42 | k8s_default_svc_dummy = K8SDescriptor(name="dummy", kind="Service", namespace=None, as_yaml={}) 43 | with pytest.raises(K8SNamespaceError): 44 | k8s_default_svc_dummy.compute_namespace_dirname() 45 | 46 | 47 | @pytest.mark.parametrize("kind", data_for_test_get_order_prefix) 48 | def test_get_order_prefix(kind: str) -> None: 49 | """test_get_order_prefix.""" 50 | sut = K8SDescriptor( 51 | name="for_test", 52 | kind=kind, 53 | namespace=None, 54 | as_yaml={}, 55 | use_order_prefix=True, 56 | ) 57 | assert (sut.get_order_prefix() == "") is False 58 | 59 | 60 | @pytest.mark.parametrize("kind", data_for_test_get_order_prefix) 61 | def test_get_order_prefix_when_disabled(kind: str) -> None: 62 | """test_get_order_prefix_when_disabled.""" 63 | sut = K8SDescriptor( 64 | name="for_test", 65 | kind=kind, 66 | namespace=None, 67 | as_yaml={}, 68 | use_order_prefix=False, 69 | ) 70 | assert (sut.get_order_prefix() == "") is True 71 | 72 | 73 | def test_is_list_with_default() -> None: 74 | """Test is_list with default.""" 75 | sut = K8SDescriptor( 76 | name="for_test", 77 | kind="ConfigMap", 78 | namespace=None, 79 | as_yaml={}, 80 | use_order_prefix=False, 81 | ) 82 | assert sut.is_list is False 83 | 84 | 85 | def test_is_list_can_be_set() -> None: 86 | """Test is_list with default.""" 87 | sut = K8SDescriptor( 88 | name="for_test", 89 | kind="ConfigMapList", 90 | namespace=None, 91 | as_yaml={}, 92 | use_order_prefix=False, 93 | ) 94 | sut.is_list = True 95 | assert sut.is_list is True 96 | -------------------------------------------------------------------------------- /tests/test_namespaces.py: -------------------------------------------------------------------------------- 1 | """Tests for `kubesplit` package.""" 2 | 3 | from kubesplit.k8s_descriptor import K8SDescriptor 4 | from kubesplit.namespaces import get_all_namespaces 5 | 6 | 7 | def test_get_all_namespace() -> None: 8 | """test_get_all_namespace.""" 9 | descriptors = {} 10 | k8s_default_svc_dummy = K8SDescriptor(name="dummy", kind="Service", namespace="default", as_yaml={}) 11 | k8s_default_svc_dummy2 = K8SDescriptor(name="dummy2", kind="Service", namespace="default", as_yaml={}) 12 | k8s_ns1_deploy_foo = K8SDescriptor(name="foo", kind="Deployment", namespace="ns1", as_yaml={}) 13 | k8s_ns2_deploy_foo = K8SDescriptor(name="foo", kind="Deployment", namespace="ns2", as_yaml={}) 14 | descriptors[k8s_default_svc_dummy.id] = k8s_default_svc_dummy 15 | descriptors[k8s_default_svc_dummy2.id] = k8s_default_svc_dummy2 16 | descriptors[k8s_ns1_deploy_foo.id] = k8s_ns1_deploy_foo 17 | descriptors[k8s_ns2_deploy_foo.id] = k8s_ns2_deploy_foo 18 | res = get_all_namespaces(descriptors) 19 | assert len(res) == 3 # noqa: PLR2004 20 | assert "default" in res 21 | assert "ns1" in res 22 | assert "ns2" in res 23 | 24 | 25 | def test_get_all_namespace_when_no_descriptors() -> None: 26 | """test_get_all_namespace_when_no_descriptors.""" 27 | descriptors = {} 28 | res = get_all_namespaces(descriptors) 29 | assert len(res) == 0 30 | -------------------------------------------------------------------------------- /tests/test_output.py: -------------------------------------------------------------------------------- 1 | """Test the output package (editorconfig-checker-disable-file).""" 2 | 3 | from io import StringIO 4 | 5 | from yamkix.config import get_yamkix_config_from_default 6 | from yamkix.yaml_writer import get_opinionated_yaml_writer 7 | 8 | from kubesplit.k8s_descriptor import K8SDescriptor 9 | from kubesplit.output import save_descriptor_to_stream 10 | 11 | 12 | def test_roundtrip_when_preserve_quotes_true() -> None: 13 | """test_roundtrip_when_preserve_quotes_true.""" 14 | s_input = """--- 15 | apiVersion: extensions/v1beta1 # with comment 16 | kind: ReplicaSet 17 | metadata: 18 | name: tname 19 | namespace: tns 20 | annotations: 21 | string_no_quotes: frontend 22 | string_single_quotes: 'frontend' 23 | string_double_quotes: "frontend" 24 | boolean_no_quotes: true 25 | boolean_single_quotes: 'true' 26 | boolean_double_quotes: "true" 27 | number_no_quotes: 1 28 | number_single_quotes: '1' 29 | number_double_quotes: "1" 30 | """ 31 | yamkix_config = get_yamkix_config_from_default(quotes_preserved=True) 32 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 33 | parsed = yaml_instance.load_all(s_input) 34 | as_yaml = None 35 | for yaml_resource in parsed: 36 | as_yaml = yaml_resource 37 | assert as_yaml is not None 38 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 39 | output = StringIO() 40 | save_descriptor_to_stream( 41 | descriptor, 42 | output, 43 | yaml_instance=yaml_instance, 44 | yamkix_config=yamkix_config, 45 | ) 46 | s_output = output.getvalue() 47 | assert s_output == s_input 48 | 49 | 50 | def test_roundtrip_when_preserve_quotes_false() -> None: 51 | """test_roundtrip_when_preserve_quotes_false.""" 52 | s_input = """--- 53 | apiVersion: extensions/v1beta1 # with comment 54 | kind: ReplicaSet 55 | metadata: 56 | name: tname 57 | namespace: tns 58 | annotations: 59 | string_no_quotes: frontend 60 | string_single_quotes: 'frontend' 61 | string_double_quotes: "frontend" 62 | boolean_no_quotes: true 63 | boolean_single_quotes: 'true' 64 | boolean_double_quotes: "true" 65 | number_no_quotes: 1 66 | number_single_quotes: '1' 67 | number_double_quotes: "1" 68 | """ 69 | s_expected = """--- 70 | apiVersion: extensions/v1beta1 # with comment 71 | kind: ReplicaSet 72 | metadata: 73 | name: tname 74 | namespace: tns 75 | annotations: 76 | string_no_quotes: frontend 77 | string_single_quotes: frontend 78 | string_double_quotes: frontend 79 | boolean_no_quotes: true 80 | boolean_single_quotes: 'true' 81 | boolean_double_quotes: 'true' 82 | number_no_quotes: 1 83 | number_single_quotes: '1' 84 | number_double_quotes: '1' 85 | """ 86 | yamkix_config = get_yamkix_config_from_default(quotes_preserved=False) 87 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 88 | parsed = yaml_instance.load_all(s_input) 89 | as_yaml = None 90 | for yaml_resource in parsed: 91 | as_yaml = yaml_resource 92 | assert as_yaml is not None 93 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 94 | output = StringIO() 95 | save_descriptor_to_stream(descriptor, output, yaml_instance, yamkix_config=yamkix_config) 96 | s_output = output.getvalue() 97 | assert s_output == s_expected 98 | 99 | 100 | def test_roundtrip_when_dash_inwards_false() -> None: 101 | """test_roundtrip_when_dash_inwards_false.""" 102 | s_input = """--- 103 | apiVersion: v1 # with comment 104 | kind: Pod 105 | metadata: 106 | name: yan_solo 107 | namespace: tatouine 108 | spec: 109 | containers: 110 | - name : first 111 | image: nginx 112 | ports: 113 | - name: http 114 | port: 80 115 | - name: https 116 | port: 443 117 | """ 118 | s_expected = """--- 119 | apiVersion: v1 # with comment 120 | kind: Pod 121 | metadata: 122 | name: yan_solo 123 | namespace: tatouine 124 | spec: 125 | containers: 126 | - name: first 127 | image: nginx 128 | ports: 129 | - name: http 130 | port: 80 131 | - name: https 132 | port: 443 133 | """ 134 | yamkix_config = get_yamkix_config_from_default(dash_inwards=False) 135 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 136 | parsed = yaml_instance.load_all(s_input) 137 | as_yaml = None 138 | for yaml_resource in parsed: 139 | as_yaml = yaml_resource 140 | assert as_yaml 141 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 142 | output = StringIO() 143 | save_descriptor_to_stream(descriptor, output, yaml_instance, yamkix_config=yamkix_config) 144 | s_output = output.getvalue() 145 | assert s_output == s_expected 146 | 147 | 148 | def test_roundtrip_when_dash_inwards_true() -> None: 149 | """test_roundtrip_when_dash_inwards_true.""" 150 | s_input = """--- 151 | apiVersion: v1 # with comment 152 | kind: Pod 153 | metadata: 154 | name: yan_solo 155 | namespace: tatouine 156 | spec: 157 | containers: 158 | - name : first 159 | image: nginx 160 | ports: 161 | - name: http 162 | port: 80 163 | - name: https 164 | port: 443 165 | """ 166 | s_expected = """--- 167 | apiVersion: v1 # with comment 168 | kind: Pod 169 | metadata: 170 | name: yan_solo 171 | namespace: tatouine 172 | spec: 173 | containers: 174 | - name: first 175 | image: nginx 176 | ports: 177 | - name: http 178 | port: 80 179 | - name: https 180 | port: 443 181 | """ 182 | yamkix_config = get_yamkix_config_from_default(dash_inwards=True) 183 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 184 | parsed = yaml_instance.load_all(s_input) 185 | as_yaml = None 186 | for yaml_resource in parsed: 187 | as_yaml = yaml_resource 188 | assert as_yaml 189 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 190 | output = StringIO() 191 | save_descriptor_to_stream(descriptor, output, yaml_instance, yamkix_config=yamkix_config) 192 | s_output = output.getvalue() 193 | assert s_output == s_expected 194 | 195 | 196 | def test_roundtrip_with_unconsistent_comments() -> None: 197 | """test_roundtrip_with_unconsistent_comments. 198 | 199 | Comments badly placed should be pushed to 1 char after content 200 | """ 201 | s_input = """--- 202 | apiVersion: v1 # with comment 203 | kind: Pod 204 | metadata: 205 | name: yan_solo 206 | namespace: tatouine 207 | spec: 208 | containers: 209 | - name : first 210 | image: nginx 211 | ports: 212 | - name: http 213 | port: 80 214 | - name: https 215 | port: 443 216 | """ 217 | s_expected = """--- 218 | apiVersion: v1 # with comment 219 | kind: Pod 220 | metadata: 221 | name: yan_solo 222 | namespace: tatouine 223 | spec: 224 | containers: 225 | - name: first 226 | image: nginx 227 | ports: 228 | - name: http 229 | port: 80 230 | - name: https 231 | port: 443 232 | """ 233 | yamkix_config = get_yamkix_config_from_default(spaces_before_comment=1) 234 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 235 | parsed = yaml_instance.load_all(s_input) 236 | as_yaml = None 237 | for yaml_resource in parsed: 238 | as_yaml = yaml_resource 239 | assert as_yaml 240 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 241 | output = StringIO() 242 | save_descriptor_to_stream(descriptor, output, yaml_instance, yamkix_config=yamkix_config) 243 | s_output = output.getvalue() 244 | assert s_output == s_expected 245 | 246 | 247 | def test_roundtrip_with_weird_comments_config() -> None: 248 | """test_roundtrip_with_weird_comments_config.""" 249 | s_input = """--- 250 | apiVersion: v1 # with comment 251 | kind: Pod 252 | metadata: 253 | name: yan_solo 254 | namespace: tatouine 255 | spec: 256 | containers: 257 | - name : first 258 | image: nginx 259 | ports: 260 | - name: http 261 | port: 80 262 | - name: https 263 | port: 443 264 | """ 265 | s_expected = """--- 266 | apiVersion: v1 # with comment 267 | kind: Pod 268 | metadata: 269 | name: yan_solo 270 | namespace: tatouine 271 | spec: 272 | containers: 273 | - name: first 274 | image: nginx 275 | ports: 276 | - name: http 277 | port: 80 278 | - name: https 279 | port: 443 280 | """ 281 | yamkix_config = get_yamkix_config_from_default(spaces_before_comment=7) 282 | yaml_instance = get_opinionated_yaml_writer(yamkix_config) 283 | parsed = yaml_instance.load_all(s_input) 284 | as_yaml = None 285 | for yaml_resource in parsed: 286 | as_yaml = yaml_resource 287 | assert as_yaml 288 | descriptor = K8SDescriptor(name="tname", kind="ReplicaSet", namespace="tns", as_yaml=as_yaml) 289 | output = StringIO() 290 | save_descriptor_to_stream(descriptor, output, yaml_instance, yamkix_config=yamkix_config) 291 | s_output = output.getvalue() 292 | assert s_output == s_expected 293 | -------------------------------------------------------------------------------- /toolbox/mk/common.mk: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | # 3 | IS_GIT_CONTEXT := $(shell git rev-parse --short HEAD > /dev/null 2>&1 || echo "not-git") 4 | 5 | ifneq ($(IS_GIT_CONTEXT),not-git) 6 | ifdef GITHUB_SHA_SHORT 7 | GIT_SHA1 := $(GITHUB_SHA_SHORT) 8 | else 9 | GIT_SHA1 := $(shell git rev-parse --short=7 HEAD || echo "not.git") 10 | endif 11 | ifdef GITHUB_REF 12 | GIT_REF := $(GITHUB_REF) 13 | else 14 | GIT_REF := $(shell git describe --tags --exact-match 2>/dev/null || git symbolic-ref -q --short HEAD || echo "not.git") 15 | endif 16 | ifdef GITHUB_HEAD_REF_SLUG 17 | GIT_REF_SAFE_NAME = $(GITHUB_HEAD_REF_SLUG) 18 | else 19 | ifdef GITHUB_EVENT_REF_SLUG 20 | GIT_REF_SAFE_NAME = $(GITHUB_EVENT_REF_SLUG) 21 | else 22 | GIT_REF_SAFE_NAME = $(shell echo $(GIT_REF) | tr "/" "-") 23 | endif 24 | endif 25 | GIT_STATUS_LINES_COUNT := $(shell git status --porcelain --untracked-files | wc -l || echo "not.git") 26 | else 27 | GIT_SHA1 := not.git 28 | GIT_REF := not.git 29 | GIT_REF_SAFE_NAME := not.git 30 | GIT_STATUS_LINES_COUNT := 0 31 | endif 32 | ifeq ($(GIT_STATUS_LINES_COUNT),0) 33 | GIT_IS_DIRTY := 34 | else 35 | GIT_IS_DIRTY := dirty 36 | endif 37 | ifdef GIT_IS_DIRTY 38 | GIT_SHA1_DIRTY_MAYBE := $(GIT_SHA1)-$(GIT_IS_DIRTY) 39 | GIT_SHA1_DIRTY_MAYBE_DOT := $(GIT_SHA1).$(GIT_IS_DIRTY) 40 | else 41 | GIT_SHA1_DIRTY_MAYBE := $(GIT_SHA1) 42 | GIT_SHA1_DIRTY_MAYBE_DOT := $(GIT_SHA1) 43 | endif 44 | ifeq ($(OS),Windows_NT) 45 | detected_OS := Windows 46 | else 47 | detected_OS := $(shell sh -c 'uname 2>/dev/null || echo Unknown') 48 | endif 49 | # Check Find Command 50 | # See https://stackoverflow.com/a/14777895/295716 51 | ifeq ($(detected_OS),Darwin) # Mac OS X 52 | FIND_CMD := gfind 53 | else 54 | FIND_CMD := find 55 | endif 56 | GIT_REF_SAFE_NAME_STRIPPED ?= $(shell echo "$(GIT_REF_SAFE_NAME)" | sed -e 's/$(APP_NAME)-//' -e 's/refs-tags-//' -e 's/refs-heads-//') 57 | # 58 | DOCKER_BINARY ?= docker 59 | # 60 | PWD := $(shell pwd) 61 | DOCKER_GUARD := $(shell command -v ${DOCKER_BINARY} 2> /dev/null) 62 | FZF_GUARD := $(shell command -v fzf 2> /dev/null) 63 | WHICH_BASH ?= $(shell which bash) 64 | CURRENT_SHELL ?= $(shell echo $$SHELL | rev | cut -d "/" -f 1 | rev) 65 | FIND_GUARD := $(shell command -v ${FIND_CMD} 2> /dev/null) 66 | BYPASS_GAR ?= 67 | 68 | .PHONY: git-status-extended 69 | git-status-extended: 70 | @echo "+ $@" 71 | @git status --porcelain --untracked-files 72 | 73 | .PHONY: is-git-dirty 74 | is-git-dirty: ## Echo dirty if git repo is dirty 🚽 75 | @echo ${GIT_IS_DIRTY} 76 | 77 | .PHONY: check-docker 78 | check-docker: ## Check if docker is installed 🐳 79 | @echo "+ $@" 80 | ifndef DOCKER_GUARD 81 | $(error "docker (binary=${DOCKER_BINARY}) is not available please install it") 82 | endif 83 | @echo "Found docker (binary=${DOCKER_BINARY}) (and that's a good news) 🐳" 84 | 85 | .PHONY: check-fzf 86 | check-fzf: ## Check if fzf is installed 87 | @echo "+ $@" 88 | ifndef FZF_GUARD 89 | $(error "fzf is not available please install it") 90 | endif 91 | @echo "Found fzf 👌" 92 | 93 | .PHONY: check-find 94 | check-find: ## Check if find is installed 95 | @echo "+ $@" 96 | ifndef FIND_GUARD 97 | $(error "$(FIND_CMD) is not available please install it (brew install findutils on macOS).") 98 | endif 99 | @echo "Found $(FIND_CMD) 👌" 100 | 101 | print-%: ## Print the current value of a variable 102 | @echo '$($*)' 103 | 104 | .PHONY: help 105 | help: ## ▶ Print MAIN targets 🆘 106 | @echo "+ $@" 107 | @grep -E '^[%a-zA-Z0-9_-]+:.*?## ▶ .*$$' $(MAKEFILE_LIST) \ 108 | | cut -d ":" -f2- \ 109 | | sort \ 110 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-36s\033[0m %s\n", $$1, $$2}' 111 | 112 | .PHONY: help-all 113 | help-all: ## ▶ Print ALL targets 🧻 114 | @echo "+ $@" 115 | @printf "\033[1m\033[5m\033[38;5;208mTop level targets flagged with \033[93m▶\033[25m\033[0m\n" 116 | @grep -E '^[%a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 117 | | cut -d ":" -f2- \ 118 | | sort \ 119 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-36s\033[0m %s\n", $$1, $$2}' 120 | 121 | menu: SHELL := $(WHICH_BASH) 122 | .PHONY: menu 123 | menu: check-fzf ## ▶ Display the interactive menu with top-level targets 🌶️ 124 | @top_level_targets=$$( grep -E '^[%a-zA-Z0-9_-]+:.*?## ▶ .*$$' $(MAKEFILE_LIST) | grep -v menu | cut -d ":" -f2 | tr '\n' ' ') ; \ 125 | selected_item=$$( echo $$top_level_targets | tr ' ' '\n' | sort | fzf) ; \ 126 | $(MAKE) $$selected_item 127 | 128 | menu-all: SHELL := $(WHICH_BASH) 129 | .PHONY: menu-all 130 | menu-all: check-fzf ## ▶ Display the interactive menu with all targets 🍍 131 | @top_level_targets=$$( grep -E '^[%a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | grep -v menu | cut -d ":" -f2 | tr '\n' ' ') ; \ 132 | selected_item=$$( echo $$top_level_targets | tr ' ' '\n' | sort | fzf) ; \ 133 | $(MAKE) $$selected_item 134 | -------------------------------------------------------------------------------- /toolbox/mk/mdlint.mk: -------------------------------------------------------------------------------- 1 | MDLINT_CLI_DOCKER_IMAGE ?= davidanson/markdownlint-cli2:v0.6.0 2 | 3 | .PHONY: mdlint 4 | mdlint: check-docker ## ▶ Run markdownlint-cli2 on current directory and sub-directories 5 | @echo "+ $@" 6 | @echo "Running markdownlint-cli2 in a docker container" 7 | @if ! test -f .markdownlint-cli2.yaml; then \ 8 | echo "No '.markdownlint-cli2.yaml' file found, so default globs will be used, see "; \ 9 | fi 10 | @docker container run -ti \ 11 | --rm \ 12 | -w /app/code \ 13 | -v $(PWD):/app/code \ 14 | $(MDLINT_CLI_DOCKER_IMAGE) 15 | 16 | .PHONY: mdlint-fix 17 | mdlint-fix: check-docker ## ▶ Run markdownlint-cli2 fix on current directory and sub-directories 18 | @echo "+ $@" 19 | @echo "Running markdownlint-cli2 fix in a docker container" 20 | @if ! test -f .markdownlint-cli2.yaml; then \ 21 | echo "No '.markdownlint-cli2.yaml' file found, so default globs will be used, see "; \ 22 | fi 23 | @docker container run -ti \ 24 | --rm \ 25 | -w /app/code \ 26 | -v $(PWD):/app/code \ 27 | --entrypoint="markdownlint-cli2-fix" \ 28 | $(MDLINT_CLI_DOCKER_IMAGE) 29 | -------------------------------------------------------------------------------- /toolbox/mk/pre-commit.mk: -------------------------------------------------------------------------------- 1 | PRECOMMIT_GUARD := $(shell command -v pre-commit 2> /dev/null) 2 | 3 | .PHONY: check-precommit 4 | check-precommit: ## Check if pre-commit is installed 🙉 5 | @echo "+ $@" 6 | ifndef PRECOMMIT_GUARD 7 | $(error "pre-commit is not available please install it (https://pre-commit.com/#install)") 8 | endif 9 | @echo "Found pre-commit 👌" 10 | 11 | .PHONY: precommit-install 12 | precommit-install: check-precommit ## ▶ Install pre-commit hooks 13 | @echo "+ $@" 14 | pre-commit install 15 | 16 | .PHONY: precommit-run 17 | precommit-run: check-precommit ## ▶ Run pre-commit hooks 18 | @echo "+ $@" 19 | pre-commit run --show-diff-on-failure --color=always --all-files 20 | 21 | .PHONY: pre-commit-run 22 | pre-commit-run: precommit-run ## Alias for 'precommit-run' 23 | 24 | .PHONY: pre-commit-install 25 | pre-commit-install: precommit-install ## Alias for 'precommit-install' 26 | -------------------------------------------------------------------------------- /toolbox/mk/python-base-app.mk: -------------------------------------------------------------------------------- 1 | # APP_MODULE is APP_NAME unless set elsewhere 2 | APP_MODULE ?= $(shell echo $(APP_NAME)| tr - _ ) 3 | IT_TESTS_TARGET ?= . 4 | 5 | .PHONY: clean 6 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 7 | @echo "+ $@" 8 | 9 | .PHONY: clean-build 10 | clean-build: check-find ## Remove build artifacts 11 | @echo "+ $@" 12 | rm -fr build/ 13 | rm -fr dist/ 14 | rm -fr .eggs/ 15 | $(FIND_CMD) . -name '*.egg-info' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -fr {} + 16 | $(FIND_CMD) . -name '*.egg' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -f {} + 17 | 18 | .PHONY: clean-pyc 19 | clean-pyc: check-find ## Remove Python file artifacts 20 | @echo "+ $@" 21 | $(FIND_CMD) . -name '*.pyc' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -f {} + 22 | $(FIND_CMD) . -name '*.pyo' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -f {} + 23 | $(FIND_CMD) . -name '*~' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -f {} + 24 | $(FIND_CMD) . -name '__pycache__' -not -path "**/.venv/*" -not -path "**/.tox/*" -exec rm -fr {} + 25 | 26 | .PHONY: clean-test 27 | clean-test: ## Remove test and coverage artifacts 28 | @echo "+ $@" 29 | rm -fr .tox/ 30 | rm -f .coverage 31 | rm -fr htmlcov/ 32 | rm -fr .pytest_cache 33 | 34 | .PHONY: test 35 | test: tests ## Wrapper, same as the 'tests' target 36 | 37 | .PHONY: unit-tests 38 | unit-tests: tests ## Wrapper, same as the 'tests' target 39 | 40 | .PHONY: integration-tests 41 | integration-tests: ## ▶ Run integration tests (if any) 42 | @echo "+ $@" 43 | cd $(IT_TESTS_TARGET); bats . 44 | 45 | .PHONY: integration-test 46 | integration-test: integration-tests ## Wrapper, same as the 'integration-tests' target 47 | 48 | .PHONY: package 49 | package: dist 50 | 51 | .PHONY: build 52 | build: lint tests ## ▶ lint and test all in one 53 | 54 | .PHONY: echo-app-name 55 | echo-app-name: ## Echo APP_NAME value (used in ci) 56 | @echo $(APP_NAME) 57 | 58 | .PHONY: check-preflight 59 | check-preflight:: ## Preflight/prerequisites checks 60 | @echo "+ $@" 61 | 62 | .PHONY: check-all 63 | check-all: clean lint test integration-test ## Reset cache and test everything 64 | @echo "+ $@" 65 | 66 | .PHONY: show-deps 67 | show-deps: list-installed-dependencies ## ▶ Show dependencies 68 | @echo "+ $@" 69 | 70 | .PHONY: style 71 | style: ## ▶ Run all formatters calling target $(STYLE_TARGETS) 72 | @echo "+ $@" 73 | @$(MAKE) --no-print-directory $(STYLE_TARGETS) 74 | 75 | .PHONY: lint 76 | lint: ## ▶ Run all linters calling target $(LINT_TARGETS) 77 | @echo "+ $@" 78 | @$(MAKE) --no-print-directory $(LINT_TARGETS) 79 | 80 | .PHONY: tests 81 | tests: ## ▶ Run tests calling target $(TESTS_TARGETS) 82 | @echo "+ $@" 83 | @$(MAKE) --no-print-directory $(TESTS_TARGETS) 84 | 85 | .PHONY: dist 86 | dist: ## ▶ Generate packages calling target $(DIST_TARGETS) 87 | @echo "+ $@" 88 | @$(MAKE) --no-print-directory $(DIST_TARGETS) 89 | 90 | .PHONY: dist-upload 91 | dist-upload: ## ▶ Upload packages calling target $(DIST_UPLOAD_TARGETS) 92 | @echo "+ $@" 93 | @$(MAKE) --no-print-directory $(DIST_UPLOAD_TARGETS) 94 | -------------------------------------------------------------------------------- /toolbox/mk/python-base-venv.mk: -------------------------------------------------------------------------------- 1 | VENV_PYTHON3 := python3 2 | PYTHON3_GUARD := $(shell command -v ${VENV_PYTHON3} 2> /dev/null) 3 | VENV_DIR ?= .venv 4 | ifneq ($(VENV_DIR),) 5 | VENV_EXISTS := $(shell ls -d $(VENV_DIR) 2> /dev/null) 6 | else 7 | VENV_EXISTS := 8 | endif 9 | VENV_ACTIVATED := $(shell echo $(VIRTUAL_ENV) 2> /dev/null) 10 | VENV_ACTIVATE_FISH_CMD := source $(VENV_DIR)/bin/activate.fish 11 | VENV_ACTIVATE_OTHER_CMD := source $(VENV_DIR)/bin/activate 12 | 13 | .PHONY: check-python3 14 | check-python3: ## Check if python3 is installed 🐍 15 | @echo "+ $@" 16 | ifndef PYTHON3_GUARD 17 | $(error "$(VENV_PYTHON3) is not available please install it") 18 | endif 19 | @echo "Found $(VENV_PYTHON3) (and that's a good news)" 20 | 21 | .PHONY: check-venv-exists 22 | check-venv-exists: ## Check if venv is created 🙉 23 | @echo "+ $@" 24 | ifneq ($(VENV_EXISTS),) 25 | @echo "Found venv at path '$(VENV_DIR)' (and that's a good news)" 26 | else 27 | $(error "no venv dir found, please create it first with 'make setup-venv'") 28 | endif 29 | 30 | .PHONY: delete-venv 31 | delete-venv: ## ▶ Delete venv 32 | @echo "+ $@" 33 | @if [ -d $(VENV_DIR) ]; then \ 34 | echo "Deleting directory [$(VENV_DIR)]"; \ 35 | rm -rf $(VENV_DIR); \ 36 | else \ 37 | echo "Nothing to do, directory [$(VENV_DIR)] does not exist"; \ 38 | fi 39 | 40 | .PHONY: venv 41 | venv: setup-venv 42 | 43 | .PHONY: activate-raw-venv 44 | activate-raw-venv: SHELL := $(WHICH_BASH) 45 | activate-raw-venv: check-python3 check-venv-exists ## Activate venv for the current shell ✨ 46 | @echo "+ $@" 47 | @echo "Activating venv for shell [$(CURRENT_SHELL)]" 48 | @echo "please exec the current command: " 49 | @echo "------------>" 50 | @if [[ "$(CURRENT_SHELL)" == "fish" ]]; then \ 51 | echo $(VENV_ACTIVATE_FISH_CMD); \ 52 | else \ 53 | echo $(VENV_ACTIVATE_OTHER_CMD); \ 54 | fi 55 | @echo "<------------" 56 | 57 | .PHONY: echo-venv-activate-cmd 58 | echo-venv-activate-cmd: SHELL := $(WHICH_BASH) 59 | echo-venv-activate-cmd: ## ▶ Echo the command to use to activate the venv 60 | @if [[ "$(CURRENT_SHELL)" == "fish" ]]; then \ 61 | echo $(VENV_ACTIVATE_FISH_CMD); \ 62 | else \ 63 | echo $(VENV_ACTIVATE_OTHER_CMD); \ 64 | fi 65 | 66 | .PHONY: check-venv-is-ready 67 | check-venv-is-ready: check-venv-is-activated ## Check if venv is ready 68 | echo "+ $@" 69 | 70 | .PHONY: check-venv-is-activated 71 | check-venv-is-activated: ## Check if venv is activated 👻 72 | @echo "+ $@" 73 | ifndef VENV_ACTIVATED 74 | $(error "venv does not seem to be activated, please activate it with 'make activate-venv'") 75 | endif 76 | @echo "venv activated (and that's a good news)" 77 | @echo "Running venv from [${VIRTUAL_ENV}]" 78 | 79 | .PHONY: exit-venv 80 | exit-venv: check-venv-is-activated ## Exit venv (deactivate) 👋 81 | @echo "+ $@" 82 | @echo "Please exec the command:" 83 | @echo "deactivate" 84 | 85 | .PHONY: recreate-venv 86 | recreate-venv: delete-venv setup-venv ## ▶ Recreate the virtual environment 🔄 87 | @echo "+ $@" 88 | 89 | .PHONY: reset-venv 90 | reset-venv: recreate-venv ## Alias for 'recreate-venv' 91 | @echo "+ $@" 92 | -------------------------------------------------------------------------------- /toolbox/mk/python-uv-app.mk: -------------------------------------------------------------------------------- 1 | UV_TESTS_WITH_COVERAGE_SCRIPT_NAME ?= pytest:cov 2 | UV_LINT_ALL_SCRIPT_NAME ?= lint:all 3 | UV_STYLE_SCRIPT_NAME ?= style 4 | STYLE_TARGETS ?= style-uv-default 5 | LINT_TARGETS ?= lint-uv-default 6 | TESTS_TARGETS ?= tests-uv-default 7 | DIST_TARGETS ?= dist-uv-default 8 | DIST_UPLOAD_TARGETS ?= dist-upload-uv-default 9 | TWINE_UPLOAD_TARGET ?= dist/*.whl 10 | UV_PACKAGE_LIST ?= all 11 | UV_TASK_RUNNER ?= poe 12 | UV_RUN_OPTIONS ?= --frozen 13 | 14 | .PHONY: style-uv-default 15 | style-uv-default: check-uv ## Enforce style with task '$(UV_STYLE_SCRIPT_NAME)' 16 | @echo "+ $@" 17 | @uv run $(UV_RUN_OPTIONS) $(UV_TASK_RUNNER) $(UV_STYLE_SCRIPT_NAME) 18 | @echo "🦾 Done!" 19 | 20 | .PHONY: fast-lint 21 | fast-lint: check-uv ## ▶ Run ruff check 22 | @echo "+ $@" 23 | @uv run $(UV_RUN_OPTIONS) ruff check 24 | @echo "🦾 Done!" 25 | 26 | .PHONY: lint-uv-default 27 | lint-uv-default: check-uv ## Run all linters with task '$(UV_LINT_ALL_SCRIPT_NAME)' 28 | @echo "+ $@" 29 | @uv run $(UV_RUN_OPTIONS) $(UV_TASK_RUNNER) $(UV_LINT_ALL_SCRIPT_NAME) 30 | 31 | .PHONY: tests-no-coverage 32 | tests-no-coverage: check-uv check-prerequisites-pytest ## ▶ Run tests quickly (no coverage) 🚀 33 | @echo "+ $@" 34 | @uv run $(UV_RUN_OPTIONS) pytest 35 | 36 | .PHONY: tests-with-coverage 37 | tests-with-coverage: check-uv check-prerequisites-pytest ## ▶ Run tests with coverage 38 | @echo "+ $@" 39 | @uv run $(UV_RUN_OPTIONS) $(UV_TASK_RUNNER) $(UV_TESTS_WITH_COVERAGE_SCRIPT_NAME) 40 | 41 | .PHONY: tests-uv-default 42 | tests-uv-default: tests-with-coverage ## Run tests (defaults to tests-with-coverage) 43 | @echo "+ $@" 44 | 45 | .PHONY: dist-uv-default 46 | dist-uv-default: SHELL := $(WHICH_BASH) 47 | dist-uv-default: check-uv ## Build python package(s) default target (called by "make dist") if not overriden 48 | @echo "+ $@" 49 | @if [[ "$(UV_PACKAGE_LIST)" == "all" ]] ; then \ 50 | uv build --wheel --all; \ 51 | else \ 52 | for package in $(UV_PACKAGE_LIST) ; \ 53 | do \ 54 | uv build --wheel --package $$package ; \ 55 | done \ 56 | fi 57 | 58 | .PHONY: dist-upload-uv-default 59 | dist-upload-uv-default: ## Upload the python3 package 60 | @echo "+ $@" 61 | # @uv publish FIXME 62 | 63 | .PHONY: list-installed-dependencies 64 | list-installed-dependencies: check-uv ## ▶ List installed dependencies 65 | @echo "+ $@" 66 | @uv tree --frozen 67 | 68 | .PHONY: check-prerequisites-pytest 69 | check-prerequisites-pytest: SHELL := $(WHICH_BASH) 70 | check-prerequisites-pytest: ## Check if pytest is in the .venv 71 | @echo "+ $@" 72 | @if uv run $(UV_RUN_OPTIONS) pytest --version > /dev/null 2>&1; then \ 73 | echo "'pytest' found in venv, everything seems ok"; \ 74 | else \ 75 | echo "No 'pytest' found in the venv, did you run 'make setup-venv'?"; \ 76 | exit 1; \ 77 | fi 78 | -------------------------------------------------------------------------------- /toolbox/mk/python-uv-extras.mk: -------------------------------------------------------------------------------- 1 | UV_BINARY := uv 2 | UV_GUARD := $(shell command -v $(UV_BINARY) 2> /dev/null) 3 | 4 | .PHONY: check-uv 5 | check-uv: ## Check if uv is installed 🐍 6 | @echo "+ $@" 7 | ifndef UV_GUARD 8 | error "$(UV_BINARY) is not available please install it" 9 | endif 10 | @echo "Using $(UV_BINARY) at '${UV_GUARD}'" 11 | -------------------------------------------------------------------------------- /toolbox/mk/python-uv-venv.mk: -------------------------------------------------------------------------------- 1 | UV_VERSION := $(shell $(UV_BINARY) --version | awk '{print $$2}' 2> /dev/null) 2 | UV_MIN_VERSION := 0.4.30 3 | VENV_DIR ?= .venv 4 | 5 | .PHONY: check-uv-version 6 | check-uv-version: check-uv ## Check if uv is installed and version is greater or equal than $(UV_MIN_VERSION) ✂️ 7 | @echo "+ $@" 8 | @if ! printf '%s\n' "$(UV_MIN_VERSION)" "$(UV_VERSION)" | sort --check=quiet --version-sort; then \ 9 | echo "uv version $(UV_VERSION) is less than $(UV_MIN_VERSION)"; \ 10 | exit 1; \ 11 | else \ 12 | echo "Using uv version $(UV_VERSION) >= $(UV_MIN_VERSION)"; \ 13 | fi 14 | 15 | .PHONY: setup-venv 16 | setup-venv: check-uv-version ## ▶ Setup a virtual env for running our python goodness 🎃 17 | @echo "+ $@" 18 | @uv sync --frozen --all-packages --keyring-provider subprocess 19 | 20 | .PHONY: generate-lock-file 21 | generate-lock-file: check-uv ## ▶ Refresh the lock file 🔒 22 | @echo "+ $@" 23 | @uv lock --keyring-provider subprocess 24 | 25 | .PHONY: upgrade-dependencies 26 | upgrade-dependencies: check-uv-version ## ▶ Upgrade dependencies (uv.lock and venv) ♨️ 27 | @echo "+ $@" 28 | @uv sync --upgrade --all-packages --keyring-provider subprocess 29 | 30 | .PHONY: update-requirements-file 31 | update-requirements-file: upgrade-dependencies ## ▶ Deprecated, use upgrade-dependencies instead 32 | @echo "+ $@" 33 | @echo "This target is deprecated, please use 'upgrade-dependencies' instead" 34 | 35 | .PHONY: install-requirements 36 | install-requirements: setup-venv ## ▶ Install requirements in a single command 37 | @echo "+ $@" 38 | @echo "This target is not required in a uv context, you can just use 'make setup-venv' instead" 39 | 40 | .PHONY: install-all-requirements 41 | install-all-requirements: setup-venv ## ▶ Install all requirements in a single command 🏎️ 42 | @echo "+ $@" 43 | @echo "This target is not required in a uv context, you can just use 'make setup-venv' instead" 44 | 45 | .PHONY: activate-venv 46 | activate-venv: activate-raw-venv ## Activate venv for the current shell ✨ 47 | @echo "+ $@" 48 | 49 | .PHONY: generate-requirements-file 50 | generate-requirements-file: generate-lock-file ## Generate the lock file (alias for 'generate-lock-file') 51 | 52 | .PHONY: generate-requirements-files 53 | generate-requirements-files: generate-lock-file ## ▶ Generate the lock file (alias for 'generate-lock-file') 54 | @echo "+ $@" 55 | 56 | .PHONY: refresh-workspace 57 | refresh-workspace: ## ▶ Refresh the workspace (use this after adding new members to the uv workspace) 58 | @echo "+ $@" 59 | @uv sync --all-packages --keyring-provider subprocess 60 | -------------------------------------------------------------------------------- /toolbox/mk/remote-mk.mk: -------------------------------------------------------------------------------- 1 | WHICH_BASH_ ?= $(shell which bash) 2 | SHA256SUM_GUARD := $(shell command -v sha256sum 2> /dev/null) 3 | LOCAL_MK_CACHE ?= generated/mk 4 | REMOTE_MK_REPO ?= looztra/toolbox 5 | REMOTE_MK_DIR ?= toolbox/mk 6 | 7 | # $1: mk file without .mk extension 8 | # $2: mk file sha256 signature 9 | # $3: git ref 10 | define get_remote_mk 11 | @echo "+ retreiving $@" 12 | @mkdir -p $(LOCAL_MK_CACHE) 13 | @GITHUB_API_TOKEN=$${GITHUB_API_TOKEN:-$$GITHUB_TOKEN}; \ 14 | if [ -z "$${GITHUB_API_TOKEN}" ]; then \ 15 | echo -e "\e[0;31m**ERROR** please set either GITHUB_API_TOKEN or GITHUB_TOKEN\e[m"; \ 16 | echo -e "\e[0;33mThis is the next error message (Makefile:xx: $(LOCAL_MK_CACHE)/$(1).mk: No such file or directory) root cause!\e[m"; \ 17 | exit 1; \ 18 | fi; \ 19 | curl \ 20 | -o $(LOCAL_MK_CACHE)/$(1).fetched.mk \ 21 | -L "https://whatever:$(GITHUB_API_TOKEN)@raw.githubusercontent.com/$(REMOTE_MK_REPO)/$(3)/$(REMOTE_MK_DIR)/$(1).mk" 22 | @echo "$(2) *$(LOCAL_MK_CACHE)/$(1).fetched.mk" \ 23 | | sha256sum --check - && \ 24 | mv $(LOCAL_MK_CACHE)/$(1).fetched.mk $(LOCAL_MK_CACHE)/$(1).mk 25 | endef 26 | 27 | .PHONY: check-sha256sum 28 | check-sha256sum: ## Check if sha256sum is installed 🙉 29 | @echo "+ $@" 30 | ifndef SHA256SUM_GUARD 31 | $(error "sha256sum is not available please install it") 32 | endif 33 | 34 | 35 | .PHONY: init-mk 36 | init-mk: check-sha256sum ## ▶ no-op target to make sure all remote targets are downloaded 37 | @echo "+ $@" 38 | 39 | .PHONY: clear-mk-cache 40 | clear-mk-cache: ## ▶ Clear make target cache 41 | @echo "+ $@" 42 | @mkdir -p $(LOCAL_MK_CACHE) 43 | @rm -rf $(LOCAL_MK_CACHE)/*.mk 44 | 45 | $(LOCAL_MK_CACHE)/%.mk: SHELL := $(WHICH_BASH_) 46 | $(LOCAL_MK_CACHE)/%.mk: Makefile 47 | $(eval MK_NAME = $(shell echo $@ | cut -d "/" -f3 | cut -d "." -f1)) 48 | @echo "Working on remote mk [$(MK_NAME)]" 49 | $(eval MK_NAME_UPPER = $(shell echo $(MK_NAME) | tr '[:lower:]' '[:upper:]' | tr '-' '_')) 50 | $(eval MK_SHA256_VAR_NAME= MK_$(MK_NAME_UPPER)_SHA256) 51 | $(eval MK_SHA256_VALUE = $($(MK_SHA256_VAR_NAME))) 52 | @if [ -z "$(MK_SHA256_VALUE)" ]; then \ 53 | echo -e "\e[31m**ERROR** Cannot find the SHA256 signature for remote mk [$(MK_NAME)], please define variable [$(MK_SHA256_VAR_NAME)]\e[0m"; \ 54 | echo; \ 55 | exit 1; \ 56 | fi 57 | $(call get_remote_mk,$(MK_NAME),$(MK_SHA256_VALUE),$(MK_GIT_REF)) 58 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | skipsdist = True 8 | envlist = py313,py312,py311,py310 9 | 10 | 11 | [testenv] 12 | runner = uv-venv-lock-runner 13 | description = Run tests 14 | commands = 15 | pytest 16 | python --version 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | uv_python_preference = only-managed 20 | allowlist_externals = bats, python 21 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looztra/kubesplit/22e161569fa135e878e428667621b7fdfe0e0903/version.txt -------------------------------------------------------------------------------- /wait-for-pypi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_VERSION=$1 4 | PACKAGE_NAME=$2 5 | while true; do 6 | date 7 | pip install "${PACKAGE_NAME}"=="${PACKAGE_VERSION}" || true 8 | if hash "${PACKAGE_NAME}"; then 9 | echo "Found expected version, let's go on" 10 | break 11 | else 12 | echo "Did not find the expected version [${PACKAGE_VERSION}] for package [${PACKAGE_NAME}], sleeping 15s" 13 | sleep 15s 14 | fi 15 | done 16 | --------------------------------------------------------------------------------