├── kcleaner.rb
├── _config.yml
├── .DS_Store
├── requirements.txt
├── render1557878856917.gif
├── codecov.yml
├── dockerfile
├── tests
├── kcleaner_main_test.py
├── kcleaner_cli_test.py
├── kcleaner_ask_yn_test.py
├── kcleaner_file_exists_test.py
├── kcleaner_check_and_cleanup_backups_test.py
├── kcleaner_test.py
├── sampleConfig
├── kcleaner_update_file_test.py
└── kcleaner_remove_resource_test.py
├── .gitignore
├── setup.py
├── .github
├── dependabot.yml
└── workflows
│ └── pythonapp.yml
├── LICENSE
├── README.md
└── kcleaner.py
/kcleaner.rb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-tactile
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gcarrarom/kubeconfig-cleaner-cli/HEAD/.DS_Store
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | click
2 | mock
3 | pathlib
4 | iterfzf
5 | datetime
6 | PyYAML
7 | testfixtures
--------------------------------------------------------------------------------
/render1557878856917.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gcarrarom/kubeconfig-cleaner-cli/HEAD/render1557878856917.gif
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "setup.py" # ignore folders and all its contents
3 | - "tests/*" # wildcards accepted
4 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY requirements.txt ./
6 | RUN pip install --no-cache-dir -r requirements.txt
7 |
8 | COPY . .
--------------------------------------------------------------------------------
/tests/kcleaner_main_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import click
3 | from click.testing import CliRunner
4 | from kcleaner import cli
5 |
6 | runner = CliRunner()
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config
2 | .vscode/**
3 | kubeconfig_cleaner.egg-info/**
4 | venv/**
5 | __pycache__/**
6 | .pytest_cache/**
7 | test
8 | .coverage
9 | pytestdebug.log
10 | tests/__pycache__
11 | coverage.xml
12 | htmlcov
13 | demo.yml
14 | tests/*.bak
15 |
--------------------------------------------------------------------------------
/tests/kcleaner_cli_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import cli
3 | from testfixtures import log_capture
4 | import click
5 | from click.testing import CliRunner
6 | import pytest
7 | import yaml
8 |
9 | #cli(resource, name, kubeconfig, undo, debug)
10 |
11 | runner = CliRunner()
12 | sample_yaml = """
13 | something:
14 | alist:
15 | - item1: test
16 | something: test
17 | - item2: test
18 | something: test
19 | """
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='kcleaner',
5 | version='0.3.3',
6 | author='Gui Martins',
7 | url='https://fancywhale.ca/',
8 | author_email='gui.martins.94@outlook.com',
9 | packages=find_packages(),
10 | include_package_data=True,
11 | py_modules=['kcleaner'],
12 | install_requires=[
13 | 'Click',
14 | 'iterfzf',
15 | 'pyyaml'
16 | ],
17 | entry_points='''
18 | [console_scripts]
19 | kcleaner=kcleaner:cli
20 | ''',
21 | )
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/tests/kcleaner_ask_yn_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import ask_yn
3 | import kcleaner
4 | from testfixtures import log_capture
5 | import click
6 | from click.testing import CliRunner
7 | import pytest
8 | import yaml
9 | from io import StringIO
10 | import json
11 |
12 | runner = CliRunner()
13 |
14 | @log_capture()
15 | def test_ask_yn_more_than_2_wrong_answers(capture, monkeypatch):
16 | with runner.isolated_filesystem():
17 | number_inputs = StringIO('a\na\na\na\n')
18 | monkeypatch.setattr('sys.stdin', number_inputs)
19 |
20 | response = ask_yn("some question")
21 |
22 | assert response == 'n'
23 |
24 |
25 | @log_capture()
26 | def test_ask_yn_1_wrong_and_1_right_answer(capture, monkeypatch):
27 | with runner.isolated_filesystem():
28 | number_inputs = StringIO('a\ny\n')
29 | monkeypatch.setattr('sys.stdin', number_inputs)
30 |
31 | response = ask_yn("some question")
32 |
33 | assert response == "y"
34 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 gcarrarom
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/kcleaner_file_exists_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import file_exists
3 | from testfixtures import log_capture
4 | import click
5 | from click.testing import CliRunner
6 | import pytest
7 |
8 | runner = CliRunner()
9 |
10 | @log_capture()
11 | def test_no_parameters(capture):
12 |
13 | with pytest.raises(SystemExit) as pytest_wrapped_e:
14 | file_exists(None)
15 |
16 | assert pytest_wrapped_e.value.code == 20
17 | capture.check_present(
18 | ('root', 'ERROR', "Filename cannot be 'None'")
19 | )
20 |
21 | @log_capture()
22 | def test_empty_string(capture):
23 |
24 | with pytest.raises(SystemExit) as pytest_wrapped_e:
25 | file_exists("")
26 |
27 | assert pytest_wrapped_e.value.code == 21
28 | capture.check_present(
29 | ('root', 'ERROR', "Filename cannot be empty!")
30 | )
31 |
32 | @log_capture()
33 | def test_existing_file(capture):
34 | with runner.isolated_filesystem():
35 | with open('./config', 'w') as f:
36 | f.write('lololol')
37 |
38 | exists = file_exists("./config")
39 |
40 | assert exists == True
41 | capture.check_present(
42 | ('root', 'DEBUG', 'File exists!')
43 | )
44 |
45 | @log_capture()
46 | def test_non_existing_file(capture):
47 | with runner.isolated_filesystem():
48 |
49 | exists = file_exists("./config")
50 |
51 | assert exists == False
52 | capture.check_present(
53 | ('root', 'INFO', 'Config File Not found!')
54 | )
55 |
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://codecov.io/gh/gcarrarom/kubeconfig-cleaner-cli)
3 | 
4 | # Table of contents
5 | - [Table of contents](#table-of-contents)
6 | - [Demo](#demo)
7 | - [Usage](#usage)
8 | - [Installation](#installation)
9 |
10 |
11 | # Demo
12 | I want to clean my Kube config file without having to open my config file ever again :)
13 |
14 |
15 |
16 |
17 |
18 |
19 | # Usage
20 |
21 | To use this CLI simply type:
22 | `kcleaner`
23 | This will prompt you to remove the context by using Fuzzy Search.
24 | If you want to clean another Kube config file, you should use the option `-k` or `--kube-config` with the path for your config file.
25 | If you want to remove clusters, you can too! Just call `kcleaner clusters` and voilá!
26 | What about users? Sure can! `kcleaner users` is here to help!
27 | To select more than one entry, just press tab. All the selected entries will be removed!
28 |
29 | If you know the name of the config entry you're going to remove, you can always use the `-n` or `--name` option to remove it.
30 |
31 | Here's the output of the help command `kcleaner --help`:
32 | ```
33 | Usage: kcleaner.py [OPTIONS] [[users|clusters|contexts|token]]
34 |
35 | A little CLI tool to help keeping Config Files clean :)
36 |
37 | Options:
38 | -k, --kubeconfig TEXT path to the config file to clean
39 | -n, --name TEXT Name of the entry to remove
40 | -u, --undo Use this to roll back latest changes
41 | -d, --debug Use this to see debug level messages
42 | --help Show this message and exit.
43 | ```
44 |
45 | # Installation
46 |
47 | To install using pip, simply run this command:
48 |
49 | `pip install kcleaner`
50 |
51 | ## Requirements
52 | Python 3.x
53 |
--------------------------------------------------------------------------------
/tests/kcleaner_check_and_cleanup_backups_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import check_and_cleanup_backups, backup_limit
3 | from testfixtures import log_capture
4 | import click
5 | from click.testing import CliRunner
6 | import pytest
7 | import yaml
8 |
9 | runner = CliRunner()
10 | sample_yaml = """
11 | something:
12 | alist:
13 | - item1: test
14 | something: test
15 | - item2: test
16 | something: test
17 | """
18 |
19 | @log_capture()
20 | def test_none_parameters(capture):
21 |
22 | with pytest.raises(SystemExit) as pytest_wrapped_e:
23 | check_and_cleanup_backups(None)
24 |
25 | assert pytest_wrapped_e.value.code == 40
26 | capture.check_present(
27 | ('root', 'ERROR', "Filename cannot be 'None'!")
28 | )
29 |
30 | @log_capture()
31 | def test_bellow_backup_limit(capture):
32 | with runner.isolated_filesystem():
33 |
34 | for i in range(backup_limit-1):
35 | with open(f'./something_{i}_kcleaner.bak', 'w') as f:
36 | f.write('lololol')
37 |
38 | check_and_cleanup_backups("./something")
39 |
40 | capture.check_present(
41 | ('root', 'DEBUG', 'We are bellow the backup limit, nothing to do here.')
42 | )
43 |
44 | @log_capture()
45 | def test_1_over_backup_limit(capture):
46 | with runner.isolated_filesystem():
47 |
48 | for i in range(backup_limit+1):
49 | with open(f'./something_{i}_kcleaner.bak', 'w') as f:
50 | f.write('lololol')
51 |
52 | check_and_cleanup_backups("./something")
53 |
54 | capture.check_present(
55 | ('root', 'DEBUG', 'Removing File something_0_kcleaner.bak')
56 | )
57 |
58 | @log_capture()
59 | def test_2_over_backup_limit(capture):
60 | with runner.isolated_filesystem():
61 |
62 | for i in range(backup_limit+2):
63 | with open(f'./something_{i:03}_kcleaner.bak', 'w') as f:
64 | f.write('lololol')
65 |
66 | check_and_cleanup_backups("./something")
67 |
68 | capture.check_present(
69 | ('root', 'DEBUG', 'Removing File something_000_kcleaner.bak'),
70 | ('root', 'DEBUG', 'Removing File something_001_kcleaner.bak')
71 | )
--------------------------------------------------------------------------------
/tests/kcleaner_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import click
3 | from click.testing import CliRunner
4 | from kcleaner import cli
5 | from testfixtures import log_capture
6 |
7 |
8 | runner = CliRunner()
9 |
10 | @log_capture()
11 | def test_clean_non_existant_file(capture):
12 |
13 | results = runner.invoke(cli, ['-k', './non_existent_file'])
14 | assert results.exit_code == 10
15 | capture.check_present(
16 | ('root', 'DEBUG', 'Trying to retrieve contents of file ./non_existent_file'),
17 | ('root', 'DEBUG', 'checking if file ./non_existent_file exists...'),
18 | ('root', 'INFO', 'Config File Not found!'),
19 | ('root', 'ERROR', 'Cannot work with an empty file!, please check the path of your config file.'),
20 | )
21 |
22 | @log_capture()
23 | def test_clean_empty_file(capture):
24 | with runner.isolated_filesystem():
25 | with open('./config', 'w') as f:
26 | f.write('')
27 |
28 | result = runner.invoke(cli, ['-k', './config'])
29 | assert result.exit_code == 11
30 | capture.check_present(
31 | ('root', 'DEBUG', 'Trying to retrieve contents of file ./config'),
32 | ('root', 'DEBUG', 'checking if file ./config exists...'),
33 | ('root', 'DEBUG', 'File exists!'),
34 | ('root', 'DEBUG', "Type of the file contents: "),
35 | ('root', 'ERROR', "Config File is empty! Can't use it.")
36 | )
37 |
38 | @log_capture()
39 | def test_clean_empty_file_debug(capture):
40 | with runner.isolated_filesystem():
41 | with open('./config', 'w') as f:
42 | f.write('')
43 |
44 | result = runner.invoke(cli, ['-k', './config', '-d'])
45 | assert result.exit_code == 11
46 | capture.check_present(
47 | ('root', 'DEBUG', 'Running with Debug flag'),
48 | ('root', 'DEBUG', 'Trying to retrieve contents of file ./config'),
49 | ('root', 'DEBUG', 'checking if file ./config exists...'),
50 | ('root', 'DEBUG', 'File exists!'),
51 | ('root', 'DEBUG', "Type of the file contents: "),
52 | ('root', 'ERROR', "Config File is empty! Can't use it.")
53 | )
54 |
55 | @log_capture()
56 | def test_clean_empty_file_undo(capture):
57 | with runner.isolated_filesystem():
58 | with open('./config', 'w') as f:
59 | f.write('')
60 |
61 | result = runner.invoke(cli, ['-k', './config', '-u'])
62 | #assert result.exit_code == 11
63 | capture.check_present(
64 | ('root', 'INFO', 'Undo flag was set! checking for the backup file...')
65 | )
66 |
67 | @log_capture()
68 | def test_non_valid_yaml(capture):
69 | with runner.isolated_filesystem():
70 | with open('./config', 'w') as f:
71 | f.write('lololol')
72 |
73 | result = runner.invoke(cli, ['-k', './config'])
74 | assert result.exit_code == 12
75 | capture.check_present(
76 | ('root', 'DEBUG', 'checking if file ./config exists...'),
77 | ('root', 'DEBUG', 'File exists!'),
78 | ('root', 'ERROR', 'Config File is not a valid yaml file!'),
79 | )
--------------------------------------------------------------------------------
/.github/workflows/pythonapp.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python application
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 | if: "!contains(github.event.head_commit.message, 'skip ci')"
15 | strategy:
16 | matrix:
17 | python_version: [3.7,3.8]
18 |
19 | fail-fast: true
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Set up Python ${{ matrix.python_version }}
25 | uses: actions/setup-python@v1
26 | with:
27 | python-version: ${{ matrix.python_version }}
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip
31 | pip install -r requirements.txt
32 | - name: Lint with flake8
33 | run: |
34 | pip install flake8
35 | # stop the build if there are Python syntax errors or undefined names
36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39 | - name: Test with pytest
40 | run: |
41 | pip install pytest pytest-cov
42 | python -m pytest --cov=./
43 | - name: Upload Code coverage
44 | run: |
45 | pip install codecov
46 | codecov --token=$CODECOV_TOKEN
47 | env:
48 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
49 |
50 | release:
51 | needs: [build]
52 | if: "!contains(github.event.head_commit.message, 'skip ci') && github.event_name == 'push' && github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, 'skip cd')"
53 | runs-on: ubuntu-latest
54 | steps:
55 | - uses: actions/checkout@v2
56 | - name: Retrieving the Version
57 | run: |
58 | echo "Getting version from the setup.py file..."
59 | ApplicationVersion=$(cat setup.py | grep version | cut -d '=' -f 2 | cut -d "'" -f 2)
60 | echo "setup.py file version: $ApplicationVersion"
61 | echo "Testing if this version already exists in GitHub..."
62 | echo "Get the tags first..."
63 | tags=$(curl https://api.github.com/repos/gcarrarom/kubeconfig-cleaner-cli/tags)
64 | echo "check if there's a match on the version.."
65 | Match=$(echo $tags | jq -r ".[] | select(.name == \"v$ApplicationVersion\")")
66 |
67 | if [[ -z "$Match" ]]; then
68 | echo "All good, this doesn't match any old versions"
69 | else
70 | echo "Nope, we have this already... try choosing another one ;)"
71 | exit 100
72 | fi
73 | echo "Version to be used: $ApplicationVersion"
74 | echo "::set-env name=RELEASE_VERSION::$ApplicationVersion"
75 | - name: Create Release
76 | id: create_release
77 | uses: actions/create-release@latest
78 | env:
79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80 | with:
81 | tag_name: v${{ env.RELEASE_VERSION }}
82 | release_name: Release v${{ env.RELEASE_VERSION }}
83 | draft: false
84 | prerelease: false
85 | - name: Pip Upload
86 | uses: onichandame/pip-upload-action@0.0.1
87 | with:
88 | username: gui.martins
89 | password: ${{secrets.PYPI_PASS}}
90 |
--------------------------------------------------------------------------------
/tests/sampleConfig:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | clusters:
3 | - cluster:
4 | server: https://super.coolcluster.fancywhale.ca
5 | name: SuperCoolCluster
6 | - cluster:
7 | server: https://super.coolcluster.fancywhale.ca
8 | name: SuperCoolCluster2
9 | - cluster:
10 | server: https://super.coolcluster.fancywhale.ca
11 | name: SuperCoolCluster3
12 | - cluster:
13 | server: https://super.coolcluster.fancywhale.ca
14 | name: SuperCoolCluster4
15 | - cluster:
16 | server: https://super.coolcluster.fancywhale.ca
17 | name: SuperCoolCluster5
18 | - cluster:
19 | server: https://super.coolcluster.fancywhale.ca
20 | name: SuperCoolCluster6
21 | - cluster:
22 | server: https://super.coolcluster.fancywhale.ca
23 | name: SuperCoolCluster7
24 | - cluster:
25 | server: https://super.coolcluster.fancywhale.ca
26 | name: SuperCoolCluster8
27 | contexts:
28 | - context:
29 | cluster: SuperCoolCluster
30 | user: SuperCoolUserName
31 | name: SuperCoolContext1
32 | - context:
33 | cluster: SuperCoolCluster
34 | user: SuperCoolUserName
35 | name: SuperCoolContext2
36 | - context:
37 | cluster: SuperCoolCluster
38 | user: SuperCoolUserName
39 | name: SuperCoolContext3
40 | - context:
41 | cluster: SuperCoolCluster
42 | user: SuperCoolUserName
43 | name: SuperCoolContext4
44 | - context:
45 | cluster: SuperCoolCluster
46 | user: SuperCoolUserName
47 | name: SuperCoolContext5
48 | - context:
49 | cluster: SuperCoolCluster
50 | user: SuperCoolUserName
51 | name: SuperCoolContext6
52 | - context:
53 | cluster: SuperCoolCluster
54 | user: SuperCoolUserName
55 | name: SuperCoolContext7
56 | current-context: SuperCoolContext
57 | kind: Config
58 | preferences: {}
59 | users:
60 | - name: SuperCoolUserName1
61 | user:
62 | auth-provider:
63 | config:
64 | apiserver-id: some-id-that-makes-sense
65 | client-id: some-id-that-makes-sense
66 | tenant-id: some-id-that-makes-sense
67 | name: some-auth-provider
68 | - name: SuperCoolUserName2
69 | user:
70 | auth-provider:
71 | config:
72 | apiserver-id: some-id-that-makes-sense
73 | client-id: some-id-that-makes-sense
74 | tenant-id: some-id-that-makes-sense
75 | name: some-auth-provider
76 | - name: SuperCoolUserName3
77 | user:
78 | auth-provider:
79 | config:
80 | apiserver-id: some-id-that-makes-sense
81 | client-id: some-id-that-makes-sense
82 | tenant-id: some-id-that-makes-sense
83 | name: some-auth-provider
84 | - name: SuperCoolUserName4
85 | user:
86 | auth-provider:
87 | config:
88 | apiserver-id: some-id-that-makes-sense
89 | client-id: some-id-that-makes-sense
90 | tenant-id: some-id-that-makes-sense
91 | name: some-auth-provider
92 | - name: SuperCoolUserName5
93 | user:
94 | auth-provider:
95 | config:
96 | apiserver-id: some-id-that-makes-sense
97 | client-id: some-id-that-makes-sense
98 | tenant-id: some-id-that-makes-sense
99 | name: some-auth-provider
100 | - name: SuperCoolUserName6
101 | user:
102 | auth-provider:
103 | config:
104 | apiserver-id: some-id-that-makes-sense
105 | client-id: some-id-that-makes-sense
106 | tenant-id: some-id-that-makes-sense
107 | name: some-auth-provider
108 | - name: SuperCoolUserName7
109 | user:
110 | auth-provider:
111 | config:
112 | access-token: SomeRandomToken
113 | apiserver-id: some-id-that-makes-sense
114 | client-id: some-id-that-makes-sense
115 | expires-in: '3600'
116 | expires-on: '1559593884'
117 | refresh-token: SomeRandomRefreshToken
118 | tenant-id: some-id-that-makes-sense
119 | name: some-auth-provider
120 | - name: SuperCoolUserName8
121 | user:
122 | auth-provider:
123 | config:
124 | apiserver-id: some-id-that-makes-sense
125 | client-id: some-id-that-makes-sense
126 | tenant-id: some-id-that-makes-sense
127 | name: some-auth-provider
128 |
--------------------------------------------------------------------------------
/tests/kcleaner_update_file_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import update_file
3 | from testfixtures import log_capture
4 | import click
5 | from click.testing import CliRunner
6 | import pytest
7 | import yaml
8 |
9 | runner = CliRunner()
10 | sample_yaml = """
11 | something:
12 | alist:
13 | - item1: test
14 | something: test
15 | - item2: test
16 | something: test
17 | """
18 | sample_broken_yaml = """
19 | text foobar
20 | number: 2
21 | """
22 |
23 | @log_capture()
24 | def test_none_parameters(capture):
25 |
26 | with pytest.raises(SystemExit) as pytest_wrapped_e:
27 | update_file(None, None)
28 |
29 | assert pytest_wrapped_e.value.code == 20
30 | capture.check_present(
31 | ('root', 'ERROR', "Filename cannot be 'None'")
32 | )
33 |
34 | @log_capture()
35 | def test_none_file_empty_content(capture):
36 |
37 | with pytest.raises(SystemExit) as pytest_wrapped_e:
38 | update_file(None, "")
39 |
40 | assert pytest_wrapped_e.value.code == 20
41 | capture.check_present(
42 | ('root', 'ERROR', "Filename cannot be 'None'")
43 | )
44 |
45 | @log_capture()
46 | def test_empty_file_empty_content(capture):
47 |
48 | with pytest.raises(SystemExit) as pytest_wrapped_e:
49 | update_file("", "")
50 |
51 | assert pytest_wrapped_e.value.code == 21
52 | capture.check_present(
53 | ('root', 'ERROR', 'Filename cannot be empty!')
54 | )
55 |
56 | @log_capture()
57 | def test_non_existant_file_none_content(capture):
58 | with runner.isolated_filesystem():
59 | with pytest.raises(SystemExit) as pytest_wrapped_e:
60 | update_file("./config", None)
61 |
62 | assert pytest_wrapped_e.value.code == 30
63 | capture.check_present(
64 | ('root', 'ERROR', "Yaml Value cannot be 'None'!")
65 | )
66 |
67 | @log_capture()
68 | def test_non_existant_file_empty_content(capture):
69 | with runner.isolated_filesystem():
70 | with pytest.raises(SystemExit) as pytest_wrapped_e:
71 | update_file("./config", "")
72 |
73 | assert pytest_wrapped_e.value.code == 31
74 | capture.check_present(
75 | ('root', 'ERROR', 'Yaml Value cannot be empty!')
76 | )
77 |
78 | @log_capture()
79 | def test_non_existant_file_not_yaml_content(capture):
80 | with runner.isolated_filesystem():
81 | with pytest.raises(SystemExit) as pytest_wrapped_e:
82 | update_file("./config", "{{123lololol123}}")
83 |
84 | assert pytest_wrapped_e.value.code == 32
85 | capture.check_present(
86 | ('root', 'ERROR', 'Yaml value is not valid!')
87 | )
88 |
89 | @log_capture()
90 | def test_existant_file_yaml_content(capture):
91 | with runner.isolated_filesystem():
92 | with open('./config', 'w') as f:
93 | f.write('lololol')
94 | update_file("./config", sample_yaml)
95 |
96 | with open('./config', 'r') as f:
97 | result = yaml.safe_load(f)
98 |
99 | assert sample_yaml in result
100 | capture.check_present(
101 | ('root', 'DEBUG', 'Writing new yaml doc into the config file')
102 | )
103 |
104 | @log_capture()
105 | def test_existant_file_dict_content(capture):
106 | with runner.isolated_filesystem():
107 | with open('./config', 'w') as f:
108 | f.write('lololol')
109 | dictyaml = yaml.safe_load(sample_yaml)
110 | update_file("./config", dictyaml)
111 |
112 | with open('./config', 'r') as f:
113 | result = yaml.safe_load(f)
114 |
115 | capture.check_present(
116 | ('root', 'DEBUG', f'This is a dict yaml doc object. Should be fine to convert as yaml. Content:\n{dictyaml}'),
117 | ('root', 'DEBUG', 'Writing new yaml doc into the config file')
118 | )
119 |
120 | @log_capture()
121 | def test_backup_file_yaml_content(capture):
122 | with runner.isolated_filesystem():
123 | with open('./something_kcleaner.bak', 'w') as f:
124 | f.write('lololol')
125 | update_file("./something_kcleaner.bak", sample_yaml)
126 |
127 | with open('./something_kcleaner.bak', 'r') as f:
128 | result = yaml.safe_load(f)
129 |
130 | assert sample_yaml in result
131 | capture.check_present(
132 | ('root', 'DEBUG', 'Checking for all kcleaner backup files')
133 | )
--------------------------------------------------------------------------------
/tests/kcleaner_remove_resource_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from kcleaner import remove_resource, get_file
3 | import kcleaner
4 | from testfixtures import log_capture
5 | import click
6 | from click.testing import CliRunner
7 | import pytest
8 | import yaml
9 |
10 | runner = CliRunner()
11 | sample_yaml_no_token = """
12 | apiVersion: v1
13 | clusters:
14 | - cluster:
15 | server: https://super.coolcluster.fancywhale.ca
16 | name: SuperCoolCluster
17 | contexts:
18 | - context:
19 | cluster: SuperCoolCluster
20 | user: SuperCoolUserName
21 | name: SuperCoolContext1
22 | current-context: SuperCoolContext
23 | kind: Config
24 | preferences: {}
25 | users:
26 | - name: SuperCoolUserName1
27 | user:
28 | auth-provider:
29 | config:
30 | apiserver-id: some-id-that-makes-sense
31 | client-id: some-id-that-makes-sense
32 | tenant-id: some-id-that-makes-sense
33 | name: some-auth-provider
34 | """
35 | sample_yaml_token = """
36 | apiVersion: v1
37 | clusters:
38 | - cluster:
39 | server: https://super.coolcluster.fancywhale.ca
40 | name: SuperCoolCluster
41 | contexts:
42 | - context:
43 | cluster: SuperCoolCluster
44 | user: SuperCoolUserName
45 | name: SuperCoolContext1
46 | current-context: SuperCoolContext
47 | kind: Config
48 | preferences: {}
49 | users:
50 | - name: SuperCoolUserName1
51 | user:
52 | auth-provider:
53 | config:
54 | access-token: SomeRandomToken
55 | apiserver-id: some-id-that-makes-sense
56 | client-id: some-id-that-makes-sense
57 | expires-in: '3600'
58 | expires-on: '1559593884'
59 | refresh-token: SomeRandomRefreshToken
60 | tenant-id: some-id-that-makes-sense
61 | name: some-auth-provider
62 | """
63 |
64 | @log_capture()
65 | def test_none_parameters(capture):
66 |
67 | with pytest.raises(SystemExit) as pytest_wrapped_e:
68 | remove_resource(None, None)
69 |
70 | assert pytest_wrapped_e.value.code == 50
71 | capture.check_present(
72 | ('root', 'ERROR', 'Config File cannot be "None"!'),
73 | )
74 |
75 | @log_capture()
76 | def test_removing_type_none(capture):
77 |
78 | with pytest.raises(SystemExit) as pytest_wrapped_e:
79 | remove_resource("", None)
80 |
81 | assert pytest_wrapped_e.value.code == 50
82 | capture.check_present(
83 | ('root', 'ERROR', 'Removing type cannot be "None"!'),
84 | )
85 |
86 | @log_capture()
87 | def test_config_file_empty(capture):
88 |
89 | with pytest.raises(SystemExit) as pytest_wrapped_e:
90 | remove_resource("", "")
91 |
92 | assert pytest_wrapped_e.value.code == 51
93 | capture.check_present(
94 | ('root', 'ERROR', 'Parameters cannot be empty!'),
95 | )
96 |
97 | @log_capture()
98 | def test_remove_token_not_available(capture, monkeypatch):
99 | with runner.isolated_filesystem():
100 |
101 | with open(f'./SampleConfigFile', 'w') as f:
102 | f.write(sample_yaml_no_token)
103 |
104 | config_file = get_file("./SampleConfigFile")
105 |
106 | monkeypatch.setattr("kcleaner.iterfzf", lambda listOfResources,multi: "")
107 |
108 | with pytest.raises(SystemExit) as pytest_wrapped_e:
109 | new_config = remove_resource(config_file, "token")
110 |
111 | assert pytest_wrapped_e.value.code == 52
112 | capture.check_present(
113 | ('root', 'ERROR', 'No resources to remove selected!'),
114 | )
115 |
116 |
117 | @log_capture()
118 | def test_remove_token(capture, monkeypatch):
119 | name_to_remove = "SuperCoolUserName1"
120 | with runner.isolated_filesystem():
121 |
122 | with open(f'./SampleConfigFile', 'w') as f:
123 | f.write(sample_yaml_token)
124 | with open(f'./SampleConfigFileToTest', 'w') as f:
125 | f.write(sample_yaml_no_token)
126 |
127 | config_file = get_file("./SampleConfigFile")
128 | config_to_test = get_file("./SampleConfigFileToTest")
129 |
130 | monkeypatch.setattr("kcleaner.iterfzf", lambda listOfResources,multi: f"{name_to_remove}")
131 |
132 | new_config = remove_resource(config_file, "token")
133 |
134 | assert new_config == config_to_test
135 | capture.check_present(
136 | ('root', 'DEBUG', f'Removing token information from the user(s) {name_to_remove}'),
137 | ('root', 'DEBUG', f'removing tokens from user {name_to_remove}'),
138 | ('root', 'DEBUG', 'Token Removed successfully!'),
139 | )
--------------------------------------------------------------------------------
/kcleaner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import print_function
3 | import os
4 | import logging
5 | import click
6 | import yaml
7 | from pathlib import Path
8 | from iterfzf import iterfzf
9 | import datetime
10 |
11 | backup_limit = 10
12 | backup_date_format = '%Y-%m-%d_%H-%M-%S'
13 |
14 | def ask_yn(yn_question, default='n'):
15 | tries = 0
16 | while True:
17 | response = input("%s (y/n)" % (yn_question))
18 | tries = tries + 1
19 | if response in ['y', 'n']:
20 | break
21 | elif tries > 2:
22 | response = default
23 | break
24 | return response
25 |
26 | def check_and_cleanup_backups(filename):
27 | if filename == None:
28 | logging.error("Filename cannot be 'None'!")
29 | exit(40)
30 | logging.debug(f"Checking if there's not more than {backup_limit} backup files in this directory")
31 | dirpath = os.path.dirname(os.path.abspath(filename))
32 | logging.debug(f"Getting all the files in {dirpath}")
33 | files = os.listdir(dirpath)
34 | logging.debug(f"These are all the files in the directory:\n{files}")
35 | logging.debug(f"Checking for all kcleaner backup files")
36 | files = [item for item in files if "kcleaner.bak" in item]
37 | logging.debug(f"These are the backup files in this folder:\n{files}")
38 | if len(files) > backup_limit:
39 | logging.info(f"Cleaning up excess of backup files - we have {len(files)} already... - Removing the {len(files) - backup_limit} oldest files")
40 | files.sort()
41 | for file in files[0:(len(files)-backup_limit)]:
42 | logging.debug(f"Removing File {file}")
43 | os.remove(f"{dirpath}/{file}")
44 | else:
45 | logging.debug(f'We are bellow the backup limit, nothing to do here.')
46 |
47 | def update_file(filename, yamldoc):
48 | if not file_exists(filename) and not "bak" in filename:
49 | logging.error("Cannot work with an empty file!, please check the path of your config file.")
50 | if "bak" in filename:
51 | check_and_cleanup_backups(filename)
52 | if yamldoc == None:
53 | logging.error("Yaml Value cannot be 'None'!")
54 | exit(30)
55 | elif yamldoc == "":
56 | logging.error("Yaml Value cannot be empty!")
57 | exit(31)
58 | logging.debug(f'Checking the type of the yamldoc: {type(yamldoc)}')
59 | if type(yamldoc) is not dict:
60 | try:
61 | yaml.safe_load(yamldoc)
62 | except:
63 | logging.exception("Yaml value is not valid!")
64 | exit(32)
65 | else:
66 | logging.debug(f"This is a dict yaml doc object. Should be fine to convert as yaml. Content:\n{yamldoc}")
67 | logging.debug(f"Opening write stream for file {filename}")
68 | with open(filename, 'w') as stream:
69 | try:
70 | logging.debug("Writing new yaml doc into the config file")
71 | yaml.dump(yamldoc, stream)
72 | except yaml.YAMLError as exc:
73 | logging.exception(f"Exception occured while trying to write Yaml file: {exc}")
74 |
75 | def get_file(filename):
76 | logging.debug(f'Trying to retrieve contents of file {filename}')
77 | if not file_exists(filename):
78 | logging.error("Cannot work with an empty file!, please check the path of your config file.")
79 | exit(10)
80 | with open(filename, 'r') as stream:
81 | try:
82 | config_file = yaml.safe_load(stream)
83 | except yaml.YAMLError as exc:
84 | logging.exception(f"Exception occured while trying to load Yaml file: {exc}")
85 | exit(13)
86 | logging.debug(f'File Contents\n{config_file}')
87 | logging.debug(f'Type of the file contents: {type(config_file)}')
88 | if config_file == None:
89 | logging.error("Config File is empty! Can't use it.")
90 | exit(11)
91 | elif type(config_file) == str:
92 | logging.error("Config File is not a valid yaml file!")
93 | exit(12)
94 | return config_file
95 |
96 | def file_exists(filename):
97 | logging.debug(f"checking if file {filename} exists...")
98 | if filename == None:
99 | logging.error("Filename cannot be 'None'")
100 | exit(20)
101 | elif filename == "":
102 | logging.error("Filename cannot be empty!")
103 | exit(21)
104 | exists = os.path.isfile(filename)
105 | if exists:
106 | logging.debug("File exists!")
107 | else:
108 | logging.info('Config File Not found!')
109 | return exists
110 |
111 | def get_backup(backup_path):
112 | logging.debug(f"Checking all backups available in the directory {backup_path}")
113 | files = os.listdir(backup_path)
114 | logging.debug(f"These are all the files in the directory:\n{files}")
115 | logging.debug(f"Checking for all kcleaner backup files")
116 | files = [item for item in files if "kcleaner.bak" in item]
117 | logging.debug(f"These are the backup files in this folder:\n{files}")
118 | files.sort(reverse=True)
119 | dates = []
120 | for file in files:
121 | dates.append((datetime.datetime.strptime("_".join(file.split('_')[0:2]), backup_date_format).strftime("%c")))
122 | logging.debug(dates)
123 | backup_to_use = iterfzf(dates)
124 | logging.debug(f'Backup chosen: {backup_to_use}')
125 | backup_file_to_use = f"{datetime.datetime.strptime(backup_to_use, '%c').strftime(backup_date_format)}_kcleaner.bak"
126 | logging.debug(f'Backup file: {backup_file_to_use}')
127 | return get_file(f"{backup_path}/{backup_file_to_use}")
128 |
129 |
130 | def remove_resource(config_file, removing_type):
131 | if config_file == None:
132 | logging.error(f'Config File cannot be "None"!')
133 | exit(50)
134 | if removing_type == None:
135 | logging.error(f'Removing type cannot be "None"!')
136 | exit(50)
137 |
138 | if removing_type == "" or config_file == "":
139 | logging.error(f'Parameters cannot be empty!')
140 | exit(51)
141 |
142 | if removing_type == 'token':
143 | removing_type = 'users'
144 | removing_token = True
145 | else:
146 | removing_token = False
147 | logging.debug(f"Started removal of {removing_type}")
148 | resources_name_list = []
149 | logging.debug('gathering list of objects for the this resource type')
150 | if removing_token:
151 | for resource in config_file[removing_type]:
152 | try:
153 | logging.debug(f"{resource['user']['auth-provider']['config']['access-token']}")
154 | resources_name_list.append(resource['name'])
155 | except:
156 | continue
157 | else:
158 | for resource in config_file[removing_type]:
159 | resources_name_list.append(resource['name'])
160 |
161 | resources_to_remove = []
162 | logging.debug('Prompting for selection')
163 | resources_to_remove = (iterfzf(resources_name_list, multi=True))
164 | logging.debug('List of resources selected: {resources_to_remove}')
165 | if resources_to_remove == None or resources_to_remove == "":
166 | logging.error("No resources to remove selected!")
167 | exit(52)
168 |
169 | logging.debug(f"{len(config_file[removing_type])} {removing_type} before the removal")
170 |
171 | #TODO: Implement cross resource finding
172 | #response = ask_yn("Remove Related Resources?")
173 | #print(f"Your response = {response}")
174 |
175 | if removing_token:
176 | logging.debug(f"Removing token information from the user(s) {resources_to_remove}")
177 | for item in config_file[removing_type]:
178 | if item['name'] in resources_to_remove:
179 | logging.debug(f"removing tokens from user {item['name']}")
180 | item['user']['auth-provider']['config'].pop('access-token', None)
181 | item['user']['auth-provider']['config'].pop('expires-in', None)
182 | item['user']['auth-provider']['config'].pop('expires-on', None)
183 | item['user']['auth-provider']['config'].pop('refresh-token', None)
184 | logging.debug(f'Token Removed successfully!')
185 |
186 | else:
187 | try:
188 | logging.debug('Removing resources...')
189 | config_file[removing_type] = [item for item in config_file[removing_type] if item['name'] not in resources_to_remove]
190 | except KeyError:
191 | logging.exception(f"Something went wrong!!")
192 |
193 | logging.debug(f"{len(config_file[removing_type])} {removing_type} in the end")
194 |
195 | return config_file
196 |
197 | @click.command()
198 | @click.argument(
199 | "resource",
200 | type=click.Choice(
201 | [
202 | 'users',
203 | 'clusters',
204 | 'contexts',
205 | 'token'
206 | ]
207 | ),
208 | default='contexts'
209 | )
210 | @click.option(
211 | '--kubeconfig', '-k', default=f'{Path.home()}/.kube/config',
212 | help="path to the config file to clean"
213 | )
214 | @click.option(
215 | '--name', '-n',
216 | help='Name of the entry to remove',
217 | )
218 | @click.option(
219 | '--undo', '-u',
220 | help='Use this to roll back latest changes',
221 | is_flag=True
222 | )
223 | @click.option(
224 | '--debug', '-d',
225 | help='Use this to see debug level messages',
226 | is_flag=True
227 | )
228 | def cli(resource, name, kubeconfig, undo, debug):
229 | """
230 | A little CLI tool to help keeping Config Files clean :)
231 | """
232 | if debug:
233 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
234 | logging.debug('Running with Debug flag')
235 | else:
236 | logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s')
237 |
238 | kubeconfig_dir = os.path.dirname(os.path.abspath(kubeconfig))
239 | kubeconfig_backup = f"{kubeconfig_dir}/{datetime.datetime.now().strftime(backup_date_format)}_kcleaner.bak"
240 |
241 | if undo:
242 | logging.info(f"Undo flag was set! checking for the backup file...")
243 | logging.debug(f'Searching for backup config file {kubeconfig_backup}')
244 | config_file_after = get_backup(kubeconfig_dir)
245 | else:
246 | config_file_before = get_file(kubeconfig)
247 | logging.debug(f'Backing up config file at {kubeconfig_backup} before doing anything')
248 | update_file(kubeconfig_backup, config_file_before)
249 | logging.info(f'Using resource {resource}')
250 | logging.debug(f'Config file to use: {kubeconfig}')
251 | if name == None:
252 | logging.debug(f'Name is empty, using fzf to search for the resource to remove')
253 | else:
254 | logging.debug(f'Name of the resource requested to remove: {name}')
255 | config_file_after = remove_resource(config_file_before, resource)
256 |
257 | logging.debug(f"New Config file content: \n{config_file_after}")
258 | update_file(kubeconfig, config_file_after)
259 |
260 | if __name__ == '__main__':
261 | cli(obj={})
262 |
--------------------------------------------------------------------------------