Compare commits

..

No commits in common. "main" and "v1.1.9" have entirely different histories.
main ... v1.1.9

41 changed files with 2610 additions and 1556 deletions

23
.chglog/CHANGELOG.tpl.md Executable file
View File

@ -0,0 +1,23 @@
# Changelog
{{ range .Versions -}}
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }})
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ (regexReplaceAll "(.*)/issues/(.*)" (regexReplaceAll "(Co-\\w*-by.*)" .Subject "") "${1}/pull/${2}") | trim }}
{{ end }}
{{- end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}

25
.chglog/config.yml Executable file
View File

@ -0,0 +1,25 @@
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://github.com/thegeeklab/docker-tidy
options:
commit_groups:
title_maps:
feat: Features
fix: Bug Fixes
perf: Performance Improvements
refactor: Code Refactoring
chore: Others
test: Testing
ci: CI Pipeline
docs: Documentation
header:
pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
pattern_maps:
- Type
- Scope
- Subject
notes:
keywords:
- BREAKING CHANGE

512
.drone.jsonnet Normal file
View File

@ -0,0 +1,512 @@
local PythonVersion(pyversion='3.7') = {
name: 'python' + std.strReplace(pyversion, '.', '') + '-pytest',
image: 'python:' + pyversion,
environment: {
PY_COLORS: 1,
},
commands: [
'pip install poetry poetry-dynamic-versioning -qq',
'poetry config experimental.new-installer false',
'poetry install',
'poetry run pytest dockertidy --cov=dockertidy --cov-append --no-cov-on-fail',
'poetry version',
'poetry run docker-tidy --help',
],
depends_on: [
'fetch',
],
};
local PipelineLint = {
kind: 'pipeline',
name: 'lint',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'yapf',
image: 'python:3.10',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry config experimental.new-installer false',
'poetry install',
'poetry run yapf -dr ./dockertidy',
],
},
{
name: 'flake8',
image: 'python:3.10',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry config experimental.new-installer false',
'poetry install',
'poetry run flake8 ./dockertidy',
],
},
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineTest = {
kind: 'pipeline',
name: 'test',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'fetch',
image: 'python:3.10',
commands: [
'git fetch -tq',
],
},
PythonVersion(pyversion='3.7'),
PythonVersion(pyversion='3.8'),
PythonVersion(pyversion='3.9'),
PythonVersion(pyversion='3.10'),
{
name: 'codecov',
image: 'python:3.10',
environment: {
PY_COLORS: 1,
CODECOV_TOKEN: { from_secret: 'codecov_token' },
},
commands: [
'pip install codecov -qq',
'codecov --required -X gcov',
],
depends_on: [
'python37-pytest',
'python38-pytest',
'python39-pytest',
'python310-pytest',
],
},
],
depends_on: [
'lint',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineSecurity = {
kind: 'pipeline',
name: 'security',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'bandit',
image: 'python:3.10',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry config experimental.new-installer false',
'poetry install',
'poetry run bandit -r ./dockertidy -x ./dockertidy/test',
],
},
],
depends_on: [
'test',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuildPackage = {
kind: 'pipeline',
name: 'build-package',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
name: 'build',
image: 'python:3.10',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry build',
],
},
{
name: 'checksum',
image: 'alpine',
commands: [
'cd dist/ && sha256sum * > ../sha256sum.txt',
],
},
{
name: 'changelog-generate',
image: 'thegeeklab/git-chglog',
commands: [
'git fetch -tq',
'git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased}',
],
},
{
name: 'changelog-format',
image: 'thegeeklab/alpine-tools',
commands: [
'prettier CHANGELOG.md',
'prettier -w CHANGELOG.md',
],
},
{
name: 'publish-github',
image: 'plugins/github-release',
settings: {
overwrite: true,
api_key: { from_secret: 'github_token' },
files: ['dist/*', 'sha256sum.txt'],
title: '${DRONE_TAG}',
note: 'CHANGELOG.md',
},
when: {
ref: ['refs/tags/**'],
},
},
{
name: 'publish-pypi',
image: 'python:3.10',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry publish -n',
],
environment: {
POETRY_HTTP_BASIC_PYPI_USERNAME: { from_secret: 'pypi_username' },
POETRY_HTTP_BASIC_PYPI_PASSWORD: { from_secret: 'pypi_password' },
},
when: {
ref: ['refs/tags/**'],
},
},
],
depends_on: [
'security',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuildContainer(arch='amd64') = {
local build = if arch == 'arm' then [{
name: 'build',
image: 'python:3.10-alpine',
commands: [
'apk add -Uq --no-cache build-base openssl-dev libffi-dev musl-dev python3-dev git cargo',
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry build',
],
environment: {
CARGO_NET_GIT_FETCH_WITH_CLI: true,
},
}] else [{
name: 'build',
image: 'python:3.10',
commands: [
'git fetch -tq',
'pip install poetry poetry-dynamic-versioning -qq',
'poetry build',
],
}],
kind: 'pipeline',
name: 'build-container-' + arch,
platform: {
os: 'linux',
arch: arch,
},
steps: build + [
{
name: 'dryrun',
image: 'thegeeklab/drone-docker:19',
settings: {
dry_run: true,
dockerfile: 'docker/Dockerfile.' + arch,
repo: 'thegeeklab/${DRONE_REPO_NAME}',
username: { from_secret: 'docker_username' },
password: { from_secret: 'docker_password' },
},
depends_on: ['build'],
when: {
ref: ['refs/pull/**'],
},
},
{
name: 'publish-dockerhub',
image: 'thegeeklab/drone-docker:19',
settings: {
auto_tag: true,
auto_tag_suffix: arch,
dockerfile: 'docker/Dockerfile.' + arch,
repo: 'thegeeklab/${DRONE_REPO_NAME}',
username: { from_secret: 'docker_username' },
password: { from_secret: 'docker_password' },
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
depends_on: ['dryrun'],
},
{
name: 'publish-quay',
image: 'thegeeklab/drone-docker:19',
settings: {
auto_tag: true,
auto_tag_suffix: arch,
dockerfile: 'docker/Dockerfile.' + arch,
registry: 'quay.io',
repo: 'quay.io/thegeeklab/${DRONE_REPO_NAME}',
username: { from_secret: 'quay_username' },
password: { from_secret: 'quay_password' },
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
depends_on: ['dryrun'],
},
],
depends_on: [
'security',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineDocs = {
kind: 'pipeline',
name: 'docs',
platform: {
os: 'linux',
arch: 'amd64',
},
concurrency: {
limit: 1,
},
steps: [
{
name: 'assets',
image: 'thegeeklab/alpine-tools',
commands: [
'make doc',
],
},
{
name: 'markdownlint',
image: 'thegeeklab/markdownlint-cli',
commands: [
"markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md'",
],
},
{
name: 'spellcheck',
image: 'node:lts-alpine',
commands: [
'npm install -g spellchecker-cli',
"spellchecker --files 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions",
],
environment: {
FORCE_COLOR: true,
NPM_CONFIG_LOGLEVEL: 'error',
},
},
{
name: 'testbuild',
image: 'thegeeklab/hugo:0.91.0',
commands: [
'hugo -s docs/ -b http://localhost/',
],
},
{
name: 'link-validation',
image: 'thegeeklab/link-validator',
commands: [
'link-validator -ro',
],
environment: {
LINK_VALIDATOR_BASE_DIR: 'docs/public',
},
},
{
name: 'build',
image: 'thegeeklab/hugo:0.91.0',
commands: [
'hugo -s docs/',
],
},
{
name: 'beautify',
image: 'node:lts-alpine',
commands: [
'npm install -g js-beautify',
"html-beautify -r -f 'docs/public/**/*.html'",
],
environment: {
FORCE_COLOR: true,
NPM_CONFIG_LOGLEVEL: 'error',
},
},
{
name: 'publish',
image: 'plugins/s3-sync',
settings: {
access_key: { from_secret: 's3_access_key' },
bucket: 'geekdocs',
delete: true,
endpoint: 'https://sp.rknet.org',
path_style: true,
secret_key: { from_secret: 's3_secret_access_key' },
source: 'docs/public/',
strip_prefix: 'docs/public/',
target: '/${DRONE_REPO_NAME}',
},
when: {
ref: ['refs/heads/main', 'refs/tags/**'],
},
},
],
depends_on: [
'build-package',
'build-container-amd64',
'build-container-arm64',
'build-container-arm',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineNotifications = {
kind: 'pipeline',
name: 'notifications',
platform: {
os: 'linux',
arch: 'amd64',
},
steps: [
{
image: 'plugins/manifest',
name: 'manifest-dockerhub',
settings: {
ignore_missing: true,
auto_tag: true,
username: { from_secret: 'docker_username' },
password: { from_secret: 'docker_password' },
spec: 'docker/manifest.tmpl',
},
when: {
status: ['success'],
},
},
{
image: 'plugins/manifest',
name: 'manifest-quay',
settings: {
ignore_missing: true,
auto_tag: true,
username: { from_secret: 'quay_username' },
password: { from_secret: 'quay_password' },
spec: 'docker/manifest-quay.tmpl',
},
when: {
status: ['success'],
},
},
{
name: 'pushrm-dockerhub',
pull: 'always',
image: 'chko/docker-pushrm:1',
environment: {
DOCKER_PASS: {
from_secret: 'docker_password',
},
DOCKER_USER: {
from_secret: 'docker_username',
},
PUSHRM_FILE: 'README.md',
PUSHRM_SHORT: 'Keep docker hosts tidy',
PUSHRM_TARGET: 'thegeeklab/${DRONE_REPO_NAME}',
},
when: {
status: ['success'],
},
},
{
name: 'pushrm-quay',
pull: 'always',
image: 'chko/docker-pushrm:1',
environment: {
APIKEY__QUAY_IO: {
from_secret: 'quay_token',
},
PUSHRM_FILE: 'README.md',
PUSHRM_TARGET: 'quay.io/thegeeklab/${DRONE_REPO_NAME}',
},
when: {
status: ['success'],
},
},
{
name: 'matrix',
image: 'thegeeklab/drone-matrix',
settings: {
homeserver: { from_secret: 'matrix_homeserver' },
roomid: { from_secret: 'matrix_roomid' },
template: 'Status: **{{ build.Status }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.Link }}){{#if build.Branch}} ({{ build.Branch }}){{/if}} by {{ commit.Author }}<br/> Message: {{ commit.Message.Title }}',
username: { from_secret: 'matrix_username' },
password: { from_secret: 'matrix_password' },
},
when: {
status: ['success', 'failure'],
},
},
],
depends_on: [
'docs',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**'],
status: ['success', 'failure'],
},
};
[
PipelineLint,
PipelineTest,
PipelineSecurity,
PipelineBuildPackage,
PipelineBuildContainer(arch='amd64'),
PipelineBuildContainer(arch='arm64'),
PipelineBuildContainer(arch='arm'),
PipelineDocs,
PipelineNotifications,
]

654
.drone.yml Normal file
View File

@ -0,0 +1,654 @@
---
kind: pipeline
name: lint
platform:
os: linux
arch: amd64
steps:
- name: yapf
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run yapf -dr ./dockertidy
environment:
PY_COLORS: 1
- name: flake8
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run flake8 ./dockertidy
environment:
PY_COLORS: 1
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
---
kind: pipeline
name: test
platform:
os: linux
arch: amd64
steps:
- name: fetch
image: python:3.10
commands:
- git fetch -tq
- name: python37-pytest
image: python:3.7
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pytest dockertidy --cov=dockertidy --cov-append --no-cov-on-fail
- poetry version
- poetry run docker-tidy --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: python38-pytest
image: python:3.8
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pytest dockertidy --cov=dockertidy --cov-append --no-cov-on-fail
- poetry version
- poetry run docker-tidy --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: python39-pytest
image: python:3.9
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pytest dockertidy --cov=dockertidy --cov-append --no-cov-on-fail
- poetry version
- poetry run docker-tidy --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: python310-pytest
image: python:3.10
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pytest dockertidy --cov=dockertidy --cov-append --no-cov-on-fail
- poetry version
- poetry run docker-tidy --help
environment:
PY_COLORS: 1
depends_on:
- fetch
- name: codecov
image: python:3.10
commands:
- pip install codecov -qq
- codecov --required -X gcov
environment:
CODECOV_TOKEN:
from_secret: codecov_token
PY_COLORS: 1
depends_on:
- python37-pytest
- python38-pytest
- python39-pytest
- python310-pytest
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- lint
---
kind: pipeline
name: security
platform:
os: linux
arch: amd64
steps:
- name: bandit
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run bandit -r ./dockertidy -x ./dockertidy/test
environment:
PY_COLORS: 1
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- test
---
kind: pipeline
name: build-package
platform:
os: linux
arch: amd64
steps:
- name: build
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: checksum
image: alpine
commands:
- cd dist/ && sha256sum * > ../sha256sum.txt
- name: changelog-generate
image: thegeeklab/git-chglog
commands:
- git fetch -tq
- git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased}
- name: changelog-format
image: thegeeklab/alpine-tools
commands:
- prettier CHANGELOG.md
- prettier -w CHANGELOG.md
- name: publish-github
image: plugins/github-release
settings:
api_key:
from_secret: github_token
files:
- dist/*
- sha256sum.txt
note: CHANGELOG.md
overwrite: true
title: ${DRONE_TAG}
when:
ref:
- refs/tags/**
- name: publish-pypi
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry publish -n
environment:
POETRY_HTTP_BASIC_PYPI_PASSWORD:
from_secret: pypi_password
POETRY_HTTP_BASIC_PYPI_USERNAME:
from_secret: pypi_username
when:
ref:
- refs/tags/**
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- security
---
kind: pipeline
name: build-container-amd64
platform:
os: linux
arch: amd64
steps:
- name: build
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: dryrun
image: thegeeklab/drone-docker:19
settings:
dockerfile: docker/Dockerfile.amd64
dry_run: true
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/pull/**
depends_on:
- build
- name: publish-dockerhub
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: amd64
dockerfile: docker/Dockerfile.amd64
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
- name: publish-quay
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: amd64
dockerfile: docker/Dockerfile.amd64
password:
from_secret: quay_password
registry: quay.io
repo: quay.io/thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: quay_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- security
---
kind: pipeline
name: build-container-arm64
platform:
os: linux
arch: arm64
steps:
- name: build
image: python:3.10
commands:
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: dryrun
image: thegeeklab/drone-docker:19
settings:
dockerfile: docker/Dockerfile.arm64
dry_run: true
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/pull/**
depends_on:
- build
- name: publish-dockerhub
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: arm64
dockerfile: docker/Dockerfile.arm64
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
- name: publish-quay
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: arm64
dockerfile: docker/Dockerfile.arm64
password:
from_secret: quay_password
registry: quay.io
repo: quay.io/thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: quay_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- security
---
kind: pipeline
name: build-container-arm
platform:
os: linux
arch: arm
steps:
- name: build
image: python:3.10-alpine
commands:
- apk add -Uq --no-cache build-base openssl-dev libffi-dev musl-dev python3-dev git cargo
- git fetch -tq
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
environment:
CARGO_NET_GIT_FETCH_WITH_CLI: true
- name: dryrun
image: thegeeklab/drone-docker:19
settings:
dockerfile: docker/Dockerfile.arm
dry_run: true
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/pull/**
depends_on:
- build
- name: publish-dockerhub
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: arm
dockerfile: docker/Dockerfile.arm
password:
from_secret: docker_password
repo: thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: docker_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
- name: publish-quay
image: thegeeklab/drone-docker:19
settings:
auto_tag: true
auto_tag_suffix: arm
dockerfile: docker/Dockerfile.arm
password:
from_secret: quay_password
registry: quay.io
repo: quay.io/thegeeklab/${DRONE_REPO_NAME}
username:
from_secret: quay_username
when:
ref:
- refs/heads/main
- refs/tags/**
depends_on:
- dryrun
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- security
---
kind: pipeline
name: docs
platform:
os: linux
arch: amd64
concurrency:
limit: 1
steps:
- name: assets
image: thegeeklab/alpine-tools
commands:
- make doc
- name: markdownlint
image: thegeeklab/markdownlint-cli
commands:
- markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md'
- name: spellcheck
image: node:lts-alpine
commands:
- npm install -g spellchecker-cli
- spellchecker --files 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions
environment:
FORCE_COLOR: true
NPM_CONFIG_LOGLEVEL: error
- name: testbuild
image: thegeeklab/hugo:0.91.0
commands:
- hugo -s docs/ -b http://localhost/
- name: link-validation
image: thegeeklab/link-validator
commands:
- link-validator -ro
environment:
LINK_VALIDATOR_BASE_DIR: docs/public
- name: build
image: thegeeklab/hugo:0.91.0
commands:
- hugo -s docs/
- name: beautify
image: node:lts-alpine
commands:
- npm install -g js-beautify
- html-beautify -r -f 'docs/public/**/*.html'
environment:
FORCE_COLOR: true
NPM_CONFIG_LOGLEVEL: error
- name: publish
image: plugins/s3-sync
settings:
access_key:
from_secret: s3_access_key
bucket: geekdocs
delete: true
endpoint: https://sp.rknet.org
path_style: true
secret_key:
from_secret: s3_secret_access_key
source: docs/public/
strip_prefix: docs/public/
target: /${DRONE_REPO_NAME}
when:
ref:
- refs/heads/main
- refs/tags/**
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- build-package
- build-container-amd64
- build-container-arm64
- build-container-arm
---
kind: pipeline
name: notifications
platform:
os: linux
arch: amd64
steps:
- name: manifest-dockerhub
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
password:
from_secret: docker_password
spec: docker/manifest.tmpl
username:
from_secret: docker_username
when:
status:
- success
- name: manifest-quay
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
password:
from_secret: quay_password
spec: docker/manifest-quay.tmpl
username:
from_secret: quay_username
when:
status:
- success
- name: pushrm-dockerhub
pull: always
image: chko/docker-pushrm:1
environment:
DOCKER_PASS:
from_secret: docker_password
DOCKER_USER:
from_secret: docker_username
PUSHRM_FILE: README.md
PUSHRM_SHORT: Keep docker hosts tidy
PUSHRM_TARGET: thegeeklab/${DRONE_REPO_NAME}
when:
status:
- success
- name: pushrm-quay
pull: always
image: chko/docker-pushrm:1
environment:
APIKEY__QUAY_IO:
from_secret: quay_token
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/thegeeklab/${DRONE_REPO_NAME}
when:
status:
- success
- name: matrix
image: thegeeklab/drone-matrix
settings:
homeserver:
from_secret: matrix_homeserver
password:
from_secret: matrix_password
roomid:
from_secret: matrix_roomid
template: "Status: **{{ build.Status }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.Link }}){{#if build.Branch}} ({{ build.Branch }}){{/if}} by {{ commit.Author }}<br/> Message: {{ commit.Message.Title }}"
username:
from_secret: matrix_username
when:
status:
- success
- failure
trigger:
ref:
- refs/heads/main
- refs/tags/**
status:
- success
- failure
depends_on:
- docs
---
kind: signature
hmac: 1841bc8bcbf056bae7577aaa9a11fb3f1627777745155ce86f48ff87d6d812de
...

View File

@ -52,11 +52,6 @@ branches:
required_status_checks: required_status_checks:
strict: false strict: false
contexts: contexts:
- ci/woodpecker/pr/lint - continuous-integration/drone/pr
- ci/woodpecker/pr/test enforce_admins: null
- ci/woodpecker/pr/build-package
- ci/woodpecker/pr/build-container
- ci/woodpecker/pr/docs
enforce_admins: false
required_linear_history: true
restrictions: null restrictions: null

2
.gitignore vendored
View File

@ -106,9 +106,7 @@ pip-wheel-metadata
docs/themes/ docs/themes/
docs/public/ docs/public/
resources/_gen/ resources/_gen/
.hugo_build.lock
# Misc # Misc
.dockertidy* .dockertidy*
CHANGELOG.md CHANGELOG.md
.ruff_cache

View File

@ -1,47 +0,0 @@
---
version: "1.1"
versioning:
update-major: []
update-minor: [feat]
update-patch: [fix, perf, refactor, chore, test, ci, docs]
tag:
pattern: "v%d.%d.%d"
release-notes:
sections:
- name: Features
commit-types: [feat]
section-type: commits
- name: Bug Fixes
commit-types: [fix]
section-type: commits
- name: Performance Improvements
commit-types: [perf]
section-type: commits
- name: Code Refactoring
commit-types: [refactor]
section-type: commits
- name: Others
commit-types: [chore]
section-type: commits
- name: Testing
commit-types: [test]
section-type: commits
- name: CI Pipeline
commit-types: [ci]
section-type: commits
- name: Documentation
commit-types: [docs]
section-type: commits
- name: Breaking Changes
section-type: breaking-changes
commit-message:
footer:
issue:
key: issue
add-value-prefix: "#"
issue:
regex: "#?[0-9]+"

View File

@ -1 +0,0 @@
https://hub.docker.com/r/thegeeklab/*

View File

@ -1,82 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: build
image: docker.io/library/python:3.13
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: security-build
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: [build]
settings:
containerfile: Containerfile.multiarch
output: type=oci,dest=oci/${CI_REPO_NAME},tar=false
repo: ${CI_REPO}
- name: security-scan
image: docker.io/aquasec/trivy
depends_on: [security-build]
commands:
- trivy -v
- trivy image --input oci/${CI_REPO_NAME}
environment:
TRIVY_EXIT_CODE: "1"
TRIVY_IGNORE_UNFIXED: "true"
TRIVY_NO_PROGRESS: "true"
TRIVY_SEVERITY: HIGH,CRITICAL
TRIVY_TIMEOUT: 1m
TRIVY_DB_REPOSITORY: docker.io/aquasec/trivy-db:2
- name: publish-dockerhub
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: [security-scan]
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: docker_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
repo: ${CI_REPO}
username:
from_secret: docker_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
- name: publish-quay
image: quay.io/thegeeklab/wp-docker-buildx:5
depends_on: security-scan
settings:
auto_tag: true
containerfile: Containerfile.multiarch
password:
from_secret: quay_password
platforms:
- linux/amd64
- linux/arm64
provenance: false
registry: quay.io
repo: quay.io/${CI_REPO}
username:
from_secret: quay_username
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
depends_on:
- lint
- test

View File

@ -1,56 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: build
image: docker.io/library/python:3.13
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry build
- name: checksum
image: quay.io/thegeeklab/alpine-tools
commands:
- cd dist/ && sha256sum * > ../sha256sum.txt
- name: changelog
image: quay.io/thegeeklab/git-sv
commands:
- git sv current-version
- git sv release-notes -t ${CI_COMMIT_TAG:-next} -o CHANGELOG.md
- cat CHANGELOG.md
- name: publish-github
image: docker.io/plugins/github-release
settings:
api_key:
from_secret: github_token
files:
- dist/*
- sha256sum.txt
note: CHANGELOG.md
overwrite: true
title: ${CI_COMMIT_TAG}
when:
- event: [tag]
- name: publish-pypi
image: docker.io/library/python:3.13
environment:
POETRY_HTTP_BASIC_PYPI_PASSWORD:
from_secret: pypi_password
POETRY_HTTP_BASIC_PYPI_USERNAME:
from_secret: pypi_username
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry publish -n
when:
- event: [tag]
depends_on:
- lint
- test

View File

@ -1,101 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: assets
image: quay.io/thegeeklab/alpine-tools
commands:
- make doc
- name: markdownlint
image: quay.io/thegeeklab/markdownlint-cli
depends_on: [assets]
commands:
- markdownlint 'README.md' 'CONTRIBUTING.md'
- name: spellcheck
image: quay.io/thegeeklab/alpine-tools
depends_on: [assets]
commands:
- spellchecker --files 'docs/**/*.md' 'README.md' 'CONTRIBUTING.md' -d .dictionary -p spell indefinite-article syntax-urls
environment:
FORCE_COLOR: "true"
- name: link-validation
image: docker.io/lycheeverse/lychee
depends_on: [assets]
commands:
- lychee --no-progress --format detailed docs/content README.md
- name: build
image: quay.io/thegeeklab/hugo:0.136.5
depends_on: [link-validation]
commands:
- hugo --panicOnWarning -s docs/
- name: beautify
image: quay.io/thegeeklab/alpine-tools
depends_on: [build]
commands:
- html-beautify -r -f 'docs/public/**/*.html'
- name: publish
image: quay.io/thegeeklab/wp-s3-action
depends_on: [beautify]
settings:
access_key:
from_secret: s3_access_key
bucket: geekdocs
delete: true
endpoint:
from_secret: s3_endpoint
path_style: true
secret_key:
from_secret: s3_secret_access_key
source: docs/public/
strip_prefix: docs/public/
target: /${CI_REPO_NAME}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success, failure]
- name: pushrm-dockerhub
image: docker.io/chko/docker-pushrm:1
depends_on: [publish]
environment:
DOCKER_PASS:
from_secret: docker_password
DOCKER_USER:
from_secret: docker_username
PUSHRM_FILE: README.md
PUSHRM_SHORT: Keep docker hosts tidy
PUSHRM_TARGET: ${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
- name: pushrm-quay
image: docker.io/chko/docker-pushrm:1
depends_on: [publish]
environment:
APIKEY__QUAY_IO:
from_secret: quay_token
PUSHRM_FILE: README.md
PUSHRM_TARGET: quay.io/${CI_REPO}
when:
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
status: [success]
depends_on:
- build-package
- build-container

View File

@ -1,27 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
- name: check-format
image: docker.io/library/python:3.13
depends_on: []
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
- poetry run ruff format --check --diff ./${CI_REPO_NAME//-/}
environment:
PY_COLORS: "1"
- name: check-coding
image: docker.io/library/python:3.13
depends_on: []
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
- poetry run ruff check ./${CI_REPO_NAME//-/}
environment:
PY_COLORS: "1"

View File

@ -1,26 +0,0 @@
---
when:
- event: [tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
runs_on: [success, failure]
steps:
- name: matrix
image: quay.io/thegeeklab/wp-matrix
settings:
homeserver:
from_secret: matrix_homeserver
room_id:
from_secret: matrix_room_id
user_id:
from_secret: matrix_user_id
access_token:
from_secret: matrix_access_token
when:
- status: [success, failure]
depends_on:
- docs

View File

@ -1,35 +0,0 @@
---
when:
- event: [pull_request, tag]
- event: [push, manual]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
variables:
- &pytest_base
depends_on: []
commands:
- pip install poetry poetry-dynamic-versioning -qq
- poetry install
- poetry run pytest --cov-append
- poetry version
- poetry run ${CI_REPO_NAME} --help
environment:
PY_COLORS: "1"
steps:
- name: python-313
image: docker.io/library/python:3.13
<<: *pytest_base
- name: python-312
image: docker.io/library/python:3.12
<<: *pytest_base
- name: python-311
image: docker.io/library/python:3.11
<<: *pytest_base
- name: python-310
image: docker.io/library/python:3.10
<<: *pytest_base

View File

@ -3,7 +3,7 @@
## Security ## Security
If you think you have found a **security issue**, please do not mention it in this repository. If you think you have found a **security issue**, please do not mention it in this repository.
Instead, send an email to `security@thegeeklab.de` with as many details as possible so it can be handled confidential. Instead, send an email to security@thegeeklab.de with as many details as possible so it can be handled confidential.
## Bug Reports and Feature Requests ## Bug Reports and Feature Requests

View File

@ -1,5 +1,5 @@
# renovate: datasource=github-releases depName=thegeeklab/hugo-geekdoc # renovate: datasource=github-releases depName=thegeeklab/hugo-geekdoc
THEME_VERSION := v1.2.1 THEME_VERSION := v0.25.1
THEME := hugo-geekdoc THEME := hugo-geekdoc
BASEDIR := docs BASEDIR := docs
THEMEDIR := $(BASEDIR)/themes THEMEDIR := $(BASEDIR)/themes

View File

@ -2,11 +2,12 @@
Keep docker hosts tidy Keep docker hosts tidy
[![Build Status](https://ci.thegeeklab.de/api/badges/thegeeklab/docker-tidy/status.svg)](https://ci.thegeeklab.de/repos/thegeeklab/docker-tidy) [![Build Status](https://img.shields.io/drone/build/thegeeklab/docker-tidy?logo=drone&server=https%3A%2F%2Fdrone.thegeeklab.de)](https://drone.thegeeklab.de/thegeeklab/docker-tidy)
[![Docker Hub](https://img.shields.io/badge/docker-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/docker-tidy) [![Docker Hub](https://img.shields.io/badge/docker-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/docker-tidy)
[![Python Version](https://img.shields.io/pypi/pyversions/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![Python Version](https://img.shields.io/pypi/pyversions/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![PyPI Status](https://img.shields.io/pypi/status/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![PyPI Status](https://img.shields.io/pypi/status/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![PyPI Release](https://img.shields.io/pypi/v/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![PyPI Release](https://img.shields.io/pypi/v/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![Codecov](https://img.shields.io/codecov/c/github/thegeeklab/docker-tidy)](https://codecov.io/gh/thegeeklab/docker-tidy)
[![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/graphs/contributors)
[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/docker-tidy) [![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/docker-tidy)
[![License: Apache-2.0](https://img.shields.io/github/license/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/blob/main/LICENSE) [![License: Apache-2.0](https://img.shields.io/github/license/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/blob/main/LICENSE)
@ -17,7 +18,7 @@ You can find the full documentation at [https://docker-tidy.geekdocs.de](https:/
## Contributors ## Contributors
Special thanks to all [contributors](https://github.com/thegeeklab/docker-tidy/graphs/contributors). If you would like to contribute, Special thanks goes to all [contributors](https://github.com/thegeeklab/docker-tidy/graphs/contributors). If you would like to contribute,
please see the [instructions](https://github.com/thegeeklab/docker-tidy/blob/main/CONTRIBUTING.md). please see the [instructions](https://github.com/thegeeklab/docker-tidy/blob/main/CONTRIBUTING.md).
## License ## License

View File

@ -1,4 +1,4 @@
FROM python:3.13-alpine@sha256:ee60f1fbb72e1844edce9ea169e0342b477822a3b5ec7a32637803bdca5c7362 FROM python:3.10-alpine@sha256:dce56d40d885d2c8847aa2a278a29d50450c8e3d10f9d7ffeb2f38dcc1eb0ea4
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>" LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>" LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>"

26
docker/Dockerfile.arm Normal file
View File

@ -0,0 +1,26 @@
FROM arm32v7/python:3.10-alpine@sha256:12675b471e7e8cff36cf9124b72bc99e8b56c326fb0a53dabac537532e634e1f
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.title="docker-tidy"
LABEL org.opencontainers.image.url="https://docker-tidy.geekdocs.de/"
LABEL org.opencontainers.image.source="https://github.com/thegeeklab/docker-tidy"
LABEL org.opencontainers.image.documentation="https://docker-tidy.geekdocs.de/"
ENV PY_COLORS=1
ENV TZ=UTC
ADD dist/docker_tidy-*.whl /
RUN apk --update add --virtual .build-deps build-base libffi-dev openssl-dev && \
pip install --upgrade --no-cache-dir pip && \
pip install --no-cache-dir $(find / -name "docker_tidy-*.whl") && \
apk del .build-deps && \
rm -f docker_tidy-*.whl && \
rm -rf /var/cache/apk/* && \
rm -rf /root/.cache/ && \
rm -rf /tmp/*
USER root
CMD []
ENTRYPOINT ["/usr/local/bin/docker-tidy", "gc"]

26
docker/Dockerfile.arm64 Normal file
View File

@ -0,0 +1,26 @@
FROM arm64v8/python:3.10-alpine@sha256:696a82b99de314cb5407a6ca3029df2514e4ecd09f63f115e8fb6988edc2914d
LABEL maintainer="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.authors="Robert Kaussow <mail@thegeeklab.de>"
LABEL org.opencontainers.image.title="docker-tidy"
LABEL org.opencontainers.image.url="https://docker-tidy.geekdocs.de/"
LABEL org.opencontainers.image.source="https://github.com/thegeeklab/docker-tidy"
LABEL org.opencontainers.image.documentation="https://docker-tidy.geekdocs.de/"
ENV PY_COLORS=1
ENV TZ=UTC
ADD dist/docker_tidy-*.whl /
RUN apk --update add --virtual .build-deps build-base libffi-dev openssl-dev && \
pip install --upgrade --no-cache-dir pip && \
pip install --no-cache-dir $(find / -name "docker_tidy-*.whl") && \
apk del .build-deps && \
rm -f docker_tidy-*.whl && \
rm -rf /var/cache/apk/* && \
rm -rf /root/.cache/ && \
rm -rf /tmp/*
USER root
CMD []
ENTRYPOINT ["/usr/local/bin/docker-tidy", "gc"]

24
docker/manifest-quay.tmpl Normal file
View File

@ -0,0 +1,24 @@
image: quay.io/thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: quay.io/thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}amd64
platform:
architecture: amd64
os: linux
- image: quay.io/thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: quay.io/thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm
platform:
architecture: arm
os: linux
variant: v7

24
docker/manifest.tmpl Normal file
View File

@ -0,0 +1,24 @@
image: thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}amd64
platform:
architecture: amd64
os: linux
- image: thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: thegeeklab/docker-tidy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm
platform:
architecture: arm
os: linux
variant: v7

View File

@ -2,10 +2,10 @@
"""Stop long running docker iamges.""" """Stop long running docker iamges."""
import dateutil.parser import dateutil.parser
import docker
import docker.errors import docker.errors
import requests.exceptions import requests.exceptions
import docker
from dockertidy.config import SingleConfig from dockertidy.config import SingleConfig
from dockertidy.logger import SingleLog from dockertidy.logger import SingleLog
from dockertidy.parser import timedelta from dockertidy.parser import timedelta
@ -45,7 +45,9 @@ class AutoStop:
) or (not prefix and self._has_been_running_since(container, max_run_time)): ) or (not prefix and self._has_been_running_since(container, max_run_time)):
self.logger.info( self.logger.info(
"Stopping container {id} {name}: running since {started}".format( "Stopping container {id} {name}: running since {started}".format(
id=container["Id"][:16], name=name, started=container["State"]["StartedAt"] id=container["Id"][:16],
name=name,
started=container["State"]["StartedAt"]
) )
) )
@ -56,11 +58,12 @@ class AutoStop:
try: try:
client.stop(cid) client.stop(cid)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
self.logger.warning(f"Failed to stop container {cid}: {e!s}") self.logger.warn("Failed to stop container {id}: {msg}".format(id=cid, msg=str(e)))
except docker.errors.APIError as e: except docker.errors.APIError as e:
self.logger.warning(f"Error stopping {cid}: {e!s}") self.logger.warn("Error stopping {id}: {msg}".format(id=cid, msg=str(e)))
def _build_container_matcher(self, prefixes): def _build_container_matcher(self, prefixes):
def matcher(name): def matcher(name):
return any(name.startswith(prefix) for prefix in prefixes) return any(name.startswith(prefix) for prefix in prefixes)
@ -86,4 +89,4 @@ class AutoStop:
self.stop_containers() self.stop_containers()
if not config["stop"]["max_run_time"]: if not config["stop"]["max_run_time"]:
self.logger.warning("Skipped, no arguments given") self.logger.warn("Skipped, no arguments given")

View File

@ -36,7 +36,7 @@ class DockerTidy:
action="store_true", action="store_true",
default=None, default=None,
dest="dry_run", dest="dry_run",
help="only log actions, don't stop anything", help="only log actions, don't stop anything"
) )
parser.add_argument( parser.add_argument(
"-t", "-t",
@ -44,7 +44,7 @@ class DockerTidy:
type=int, type=int,
dest="http_timeout", dest="http_timeout",
metavar="HTTP_TIMEOUT", metavar="HTTP_TIMEOUT",
help="HTTP timeout in seconds for making docker API calls", help="HTTP timeout in seconds for making docker API calls"
) )
parser.add_argument( parser.add_argument(
"-v", dest="logging.level", action="append_const", const=-1, help="increase log level" "-v", dest="logging.level", action="append_const", const=-1, help="increase log level"
@ -52,7 +52,9 @@ class DockerTidy:
parser.add_argument( parser.add_argument(
"-q", dest="logging.level", action="append_const", const=1, help="decrease log level" "-q", dest="logging.level", action="append_const", const=1, help="decrease log level"
) )
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") parser.add_argument(
"--version", action="version", version="%(prog)s {}".format(__version__)
)
subparsers = parser.add_subparsers(dest="command", help="sub-command help") subparsers = parser.add_subparsers(dest="command", help="sub-command help")
subparsers.required = True subparsers.required = True
@ -64,7 +66,7 @@ class DockerTidy:
dest="gc.max_container_age", dest="gc.max_container_age",
metavar="MAX_CONTAINER_AGE", metavar="MAX_CONTAINER_AGE",
help="maximum age for a container, containers older than this age " help="maximum age for a container, containers older than this age "
"will be removed (dateparser value)", "will be removed (dateparser value)"
) )
parser_gc.add_argument( parser_gc.add_argument(
"--max-image-age", "--max-image-age",
@ -72,13 +74,13 @@ class DockerTidy:
dest="gc.max_image_age", dest="gc.max_image_age",
metavar="MAX_IMAGE_AGE", metavar="MAX_IMAGE_AGE",
help="maxium age for an image, images older than this age will be " help="maxium age for an image, images older than this age will be "
"removed (dateparser value)", "removed (dateparser value)"
) )
parser_gc.add_argument( parser_gc.add_argument(
"--dangling-volumes", "--dangling-volumes",
action="store_true", action="store_true",
dest="gc.dangling_volumes", dest="gc.dangling_volumes",
help="dangling volumes will be removed", help="dangling volumes will be removed"
) )
parser_gc.add_argument( parser_gc.add_argument(
"--exclude-image", "--exclude-image",
@ -86,7 +88,7 @@ class DockerTidy:
type=str, type=str,
dest="gc.exclude_images", dest="gc.exclude_images",
metavar="EXCLUDE_IMAGE", metavar="EXCLUDE_IMAGE",
help="never remove images with this tag", help="never remove images with this tag"
) )
parser_gc.add_argument( parser_gc.add_argument(
"--exclude-container-label", "--exclude-container-label",
@ -94,7 +96,8 @@ class DockerTidy:
type=str, type=str,
dest="gc.exclude_container_labels", dest="gc.exclude_container_labels",
metavar="EXCLUDE_CONTAINER_LABEL", metavar="EXCLUDE_CONTAINER_LABEL",
help="never remove containers with this label key or label key=value", help="never remove containers with this label key "
"or label key=value"
) )
parser_stop = subparsers.add_parser( parser_stop = subparsers.add_parser(
@ -105,7 +108,7 @@ class DockerTidy:
type=timedelta_validator, type=timedelta_validator,
dest="stop.max_run_time", dest="stop.max_run_time",
metavar="MAX_RUN_TIME", metavar="MAX_RUN_TIME",
help="maximum time a container is allows to run (dateparser value)", help="maximum time a container is allows to run (dateparser value)"
) )
parser_stop.add_argument( parser_stop.add_argument(
"--prefix", "--prefix",
@ -113,7 +116,7 @@ class DockerTidy:
type=str, type=str,
dest="stop.prefix", dest="stop.prefix",
metavar="PREFIX", metavar="PREFIX",
help="only stop containers which match one of the prefix", help="only stop containers which match one of the prefix"
) )
return parser.parse_args().__dict__ return parser.parse_args().__dict__
@ -127,10 +130,10 @@ class DockerTidy:
try: try:
self.log.set_level(config.config["logging"]["level"]) self.log.set_level(config.config["logging"]["level"])
except ValueError as e: except ValueError as e:
self.log.sysexit_with_message(f"Can not set log level.\n{e!s}") self.log.sysexit_with_message("Can not set log level.\n{}".format(str(e)))
self.logger.info(f"Using config file {config.config_file}") self.logger.info("Using config file {}".format(config.config_file))
self.logger.debug(f"Config dump: {config.config}") self.logger.debug("Config dump: {}".format(config.config))
return config return config

View File

@ -13,15 +13,15 @@ from jsonschema._utils import format_as_index
import dockertidy.exception import dockertidy.exception
import dockertidy.parser import dockertidy.parser
from dockertidy.parser import env from dockertidy.parser import env
from dockertidy.utils import Singleton, dict_intersect from dockertidy.utils import Singleton
from dockertidy.utils import dict_intersect
config_dir = AppDirs("docker-tidy").user_config_dir config_dir = AppDirs("docker-tidy").user_config_dir
default_config_file = os.path.join(config_dir, "config.yml") default_config_file = os.path.join(config_dir, "config.yml")
class Config: class Config():
""" """Create an object with all necessary settings.
Create an object with all necessary settings.
Settings are loade from multiple locations in defined order (last wins): Settings are loade from multiple locations in defined order (last wins):
- default settings defined by `self._get_defaults()` - default settings defined by `self._get_defaults()`
@ -36,77 +36,77 @@ class Config:
"config_file": { "config_file": {
"default": "", "default": "",
"env": "CONFIG_FILE", "env": "CONFIG_FILE",
"type": environs.Env().str, "type": environs.Env().str
}, },
"dry_run": { "dry_run": {
"default": False, "default": False,
"env": "DRY_RUN", "env": "DRY_RUN",
"file": True, "file": True,
"type": environs.Env().bool, "type": environs.Env().bool
}, },
"http_timeout": { "http_timeout": {
"default": 60, "default": 60,
"env": "HTTP_TIMEOUT", "env": "HTTP_TIMEOUT",
"file": True, "file": True,
"type": environs.Env().int, "type": environs.Env().int
}, },
"logging.level": { "logging.level": {
"default": "WARNING", "default": "WARNING",
"env": "LOG_LEVEL", "env": "LOG_LEVEL",
"file": True, "file": True,
"type": environs.Env().str, "type": environs.Env().str
}, },
"logging.json": { "logging.json": {
"default": False, "default": False,
"env": "LOG_JSON", "env": "LOG_JSON",
"file": True, "file": True,
"type": environs.Env().bool, "type": environs.Env().bool
}, },
"gc.max_container_age": { "gc.max_container_age": {
"default": "", "default": "",
"env": "GC_MAX_CONTAINER_AGE", "env": "GC_MAX_CONTAINER_AGE",
"file": True, "file": True,
"type": env.timedelta_validator, "type": env.timedelta_validator
}, },
"gc.max_image_age": { "gc.max_image_age": {
"default": "", "default": "",
"env": "GC_MAX_IMAGE_AGE", "env": "GC_MAX_IMAGE_AGE",
"file": True, "file": True,
"type": env.timedelta_validator, "type": env.timedelta_validator
}, },
"gc.dangling_volumes": { "gc.dangling_volumes": {
"default": False, "default": False,
"env": "GC_DANGLING_VOLUMES", "env": "GC_DANGLING_VOLUMES",
"file": True, "file": True,
"type": environs.Env().bool, "type": environs.Env().bool
}, },
"gc.exclude_images": { "gc.exclude_images": {
"default": [], "default": [],
"env": "GC_EXCLUDE_IMAGES", "env": "GC_EXCLUDE_IMAGES",
"file": True, "file": True,
"type": environs.Env().list, "type": environs.Env().list
}, },
"gc.exclude_container_labels": { "gc.exclude_container_labels": {
"default": [], "default": [],
"env": "GC_EXCLUDE_CONTAINER_LABELS", "env": "GC_EXCLUDE_CONTAINER_LABELS",
"file": True, "file": True,
"type": environs.Env().list, "type": environs.Env().list
}, },
"stop.max_run_time": { "stop.max_run_time": {
"default": "", "default": "",
"env": "STOP_MAX_RUN_TIME", "env": "STOP_MAX_RUN_TIME",
"file": True, "file": True,
"type": env.timedelta_validator, "type": env.timedelta_validator
}, },
"stop.prefix": { "stop.prefix": {
"default": [], "default": [],
"env": "STOP_PREFIX", "env": "STOP_PREFIX",
"file": True, "file": True,
"type": environs.Env().list, "type": environs.Env().list
}, },
} }
def __init__(self, args=None): def __init__(self, args={}):
""" """
Initialize a new settings class. Initialize a new settings class.
@ -115,10 +115,7 @@ class Config:
:returns: None :returns: None
""" """
if args is None: self._args = args
self._args = {}
else:
self._args = args
self._schema = None self._schema = None
self.config_file = default_config_file self.config_file = default_config_file
self.config = None self.config = None
@ -162,12 +159,12 @@ class Config:
value = item["type"](envname) value = item["type"](envname)
normalized = self._add_dict_branch(normalized, key.split("."), value) normalized = self._add_dict_branch(normalized, key.split("."), value)
except environs.EnvError as e: except environs.EnvError as e:
if f'"{envname}" not set' in str(e): if '"{}" not set'.format(envname) in str(e):
pass pass
else: else:
raise dockertidy.exception.ConfigError( raise dockertidy.exception.ConfigError(
"Unable to read environment variable", str(e) "Unable to read environment variable", str(e)
) from e )
return normalized return normalized
@ -191,15 +188,15 @@ class Config:
source_files.append(os.path.join(os.getcwd(), ".dockertidy.yaml")) source_files.append(os.path.join(os.getcwd(), ".dockertidy.yaml"))
for config in [i for i in source_files if os.path.exists(i)]: for config in [i for i in source_files if os.path.exists(i)]:
with open(config, encoding="utf8") as stream: with open(config, "r", encoding="utf8") as stream:
s = stream.read() s = stream.read()
try: try:
normalized = ruamel.yaml.YAML(typ="safe", pure=True).load(s) normalized = ruamel.yaml.safe_load(s)
except (ruamel.yaml.composer.ComposerError, ruamel.yaml.scanner.ScannerError) as e: except (ruamel.yaml.composer.ComposerError, ruamel.yaml.scanner.ScannerError) as e:
message = f"{e.context} {e.problem}" message = "{} {}".format(e.context, e.problem)
raise dockertidy.exception.ConfigError( raise dockertidy.exception.ConfigError(
f"Unable to read config file {config}", message "Unable to read config file {}".format(config), message
) from e )
if self._validate(normalized): if self._validate(normalized):
anyconfig.merge(files_raw, normalized, ac_merge=anyconfig.MS_DICTS) anyconfig.merge(files_raw, normalized, ac_merge=anyconfig.MS_DICTS)
@ -227,26 +224,27 @@ class Config:
if not os.path.isabs(path): if not os.path.isabs(path):
base = os.path.join(os.getcwd(), path) base = os.path.join(os.getcwd(), path)
return os.path.abspath(os.path.expanduser(os.path.expandvars(base))) return os.path.abspath(os.path.expanduser(os.path.expandvars(base)))
else:
return path return path
def _validate(self, config): def _validate(self, config):
try: try:
anyconfig.validate(config, self.schema, ac_schema_safe=False) anyconfig.validate(config, self.schema, ac_schema_safe=False)
except jsonschema.exceptions.ValidationError as e: except jsonschema.exceptions.ValidationError as e:
schema = format_as_index(list(e.relative_schema_path)[:-1]) schema_error = "Failed validating '{validator}' in schema{schema}\n{message}".format(
schema_error = f"Failed validating '{e.validator}' in schema {schema}\n{e.message}" validator=e.validator,
raise dockertidy.exception.ConfigError("Configuration error", schema_error) from e schema=format_as_index(list(e.relative_schema_path)[:-1]),
message=e.message
)
raise dockertidy.exception.ConfigError("Configuration error", schema_error)
return True return True
def _add_dict_branch(self, tree, vector, value): def _add_dict_branch(self, tree, vector, value):
key = vector[0] key = vector[0]
tree[key] = ( tree[key] = value \
value if len(vector) == 1 \
if len(vector) == 1 else self._add_dict_branch(tree[key] if key in tree else {}, vector[1:], value)
else self._add_dict_branch(tree.get(key, {}), vector[1:], value)
)
return tree return tree

View File

@ -6,7 +6,7 @@ class TidyError(Exception):
"""Generic exception class for docker-tidy.""" """Generic exception class for docker-tidy."""
def __init__(self, msg, original_exception=""): def __init__(self, msg, original_exception=""):
super().__init__(f"{msg}\n{original_exception}") super(TidyError, self).__init__("{msg}\n{org}".format(msg=msg, org=original_exception))
self.original_exception = original_exception self.original_exception = original_exception

View File

@ -5,10 +5,10 @@ import fnmatch
from collections import namedtuple from collections import namedtuple
import dateutil.parser import dateutil.parser
import docker
import docker.errors import docker.errors
import requests.exceptions import requests.exceptions
import docker
from dockertidy.config import SingleConfig from dockertidy.config import SingleConfig
from dockertidy.logger import SingleLog from dockertidy.logger import SingleLog
from dockertidy.parser import timedelta from dockertidy.parser import timedelta
@ -55,8 +55,7 @@ class GarbageCollector:
self.logger.info( self.logger.info(
"Removing container {} {} {}".format( "Removing container {} {} {}".format(
container["Id"][:16], container["Id"][:16],
container.get("Name", "").lstrip("/"), container.get("Name", "").lstrip("/"), container["State"]["FinishedAt"]
container["State"]["FinishedAt"],
) )
) )
@ -74,7 +73,9 @@ class GarbageCollector:
return containers return containers
def include_container(container): def include_container(container):
return not self._should_exclude_container_with_labels(container) if self._should_exclude_container_with_labels(container):
return False
return True
return filter(include_container, containers) return filter(include_container, containers)
@ -162,6 +163,7 @@ class GarbageCollector:
self._remove_image(image_summary, timedelta(config["gc"]["max_image_age"])) self._remove_image(image_summary, timedelta(config["gc"]["max_image_age"]))
def _filter_excluded_images(self, images, exclude_set): def _filter_excluded_images(self, images, exclude_set):
def include_image(image_summary): def include_image(image_summary):
image_tags = image_summary.get("RepoTags") image_tags = image_summary.get("RepoTags")
if self._no_image_tags(image_tags): if self._no_image_tags(image_tags):
@ -174,11 +176,12 @@ class GarbageCollector:
return filter(include_image, images) return filter(include_image, images)
def _filter_images_in_use(self, images, image_tags_in_use): def _filter_images_in_use(self, images, image_tags_in_use):
def get_tag_set(image_summary): def get_tag_set(image_summary):
image_tags = image_summary.get("RepoTags") image_tags = image_summary.get("RepoTags")
if self._no_image_tags(image_tags): if self._no_image_tags(image_tags):
# The repr of the image Id used by client.containers() # The repr of the image Id used by client.containers()
return {"{id}:latest".format(id=image_summary["Id"][:12])} return set(["{id}:latest".format(id=image_summary["Id"][:12])])
return set(image_tags) return set(image_tags)
def image_not_in_use(image_summary): def image_not_in_use(image_summary):
@ -187,6 +190,7 @@ class GarbageCollector:
return filter(image_not_in_use, images) return filter(image_not_in_use, images)
def _filter_images_in_use_by_id(self, images, image_ids_in_use): def _filter_images_in_use_by_id(self, images, image_ids_in_use):
def image_not_in_use(image_summary): def image_not_in_use(image_summary):
return image_summary["Id"] not in image_ids_in_use return image_summary["Id"] not in image_ids_in_use
@ -206,7 +210,9 @@ class GarbageCollector:
if not image or not self._is_image_old(image, min_date): if not image or not self._is_image_old(image, min_date):
return return
self.logger.info(f"Removing image {self._format_image(image, image_summary)}") self.logger.info(
"Removing image {name}".format(name=self._format_image(image, image_summary))
)
if config["dry_run"]: if config["dry_run"]:
return return
@ -245,13 +251,22 @@ class GarbageCollector:
try: try:
return func(**kwargs) return func(**kwargs)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
params = ",".join("%s=%s" % item for item in kwargs.items()) # noqa:UP031 params = ",".join("%s=%s" % item for item in kwargs.items()) # noqa
self.logger.warning(f"Failed to call {func.__name__} {params} {e!s}") self.logger.warn(
"Failed to call {name} {params} {msg}".format(
name=func.__name__, params=params, msg=str(e)
)
)
except docker.errors.APIError as e: except docker.errors.APIError as e:
params = ",".join("%s=%s" % item for item in kwargs.items()) # noqa:UP031 params = ",".join("%s=%s" % item for item in kwargs.items()) # noqa
self.logger.warning(f"Error calling {func.__name__} {params} {e!s}") self.logger.warn(
"Error calling {name} {params} {msg}".format(
name=func.__name__, params=params, msg=str(e)
)
)
def _format_image(self, image, image_summary): def _format_image(self, image, image_summary):
def get_tags(): def get_tags():
tags = image_summary.get("RepoTags") tags = image_summary.get("RepoTags")
if not tags or tags == ["<none>:<none>"]: if not tags or tags == ["<none>:<none>"]:
@ -262,11 +277,12 @@ class GarbageCollector:
def _build_exclude_set(self): def _build_exclude_set(self):
config = self.config.config config = self.config.config
exclude_set = set(config["gc"]["exclude_images"])
def is_image_tag(line): def is_image_tag(line):
return line and not line.startswith("#") return line and not line.startswith("#")
return set(config["gc"]["exclude_images"]) return exclude_set
def _format_exclude_labels(self): def _format_exclude_labels(self):
config = self.config.config config = self.config.config
@ -275,7 +291,10 @@ class GarbageCollector:
for exclude_label_arg in config["gc"]["exclude_container_labels"]: for exclude_label_arg in config["gc"]["exclude_container_labels"]:
split_exclude_label = exclude_label_arg.split("=", 1) split_exclude_label = exclude_label_arg.split("=", 1)
exclude_label_key = split_exclude_label[0] exclude_label_key = split_exclude_label[0]
exclude_label_value = split_exclude_label[1] if len(split_exclude_label) == 2 else None if len(split_exclude_label) == 2:
exclude_label_value = split_exclude_label[1]
else:
exclude_label_value = None
exclude_labels.append( exclude_labels.append(
self.ExcludeLabel( self.ExcludeLabel(
key=exclude_label_key, key=exclude_label_key,
@ -289,7 +308,7 @@ class GarbageCollector:
try: try:
return docker.APIClient(version="auto", timeout=config["http_timeout"]) return docker.APIClient(version="auto", timeout=config["http_timeout"])
except docker.errors.DockerException as e: except docker.errors.DockerException as e:
self.log.sysexit_with_message(f"Can't create docker client\n{e}") self.log.sysexit_with_message("Can't create docker client\n{}".format(e))
def run(self): def run(self):
"""Garbage collector main method.""" """Garbage collector main method."""
@ -308,8 +327,7 @@ class GarbageCollector:
self.cleanup_volumes() self.cleanup_volumes()
if ( if (
not config["gc"]["max_container_age"] not config["gc"]["max_container_age"] and not config["gc"]["max_image_age"]
and not config["gc"]["max_image_age"]
and not config["gc"]["dangling_volumes"] and not config["gc"]["dangling_volumes"]
): ):
self.logger.ing("Skipped, no arguments given") self.logger.warn("Skipped, no arguments given")

View File

@ -8,7 +8,8 @@ import sys
import colorama import colorama
from pythonjsonlogger import jsonlogger from pythonjsonlogger import jsonlogger
from dockertidy.utils import Singleton, to_bool from dockertidy.utils import Singleton
from dockertidy.utils import to_bool
CONSOLE_FORMAT = "{}[%(levelname)s]{} %(message)s" CONSOLE_FORMAT = "{}[%(levelname)s]{} %(message)s"
JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s" JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s"
@ -25,7 +26,7 @@ def _should_do_markup():
colorama.init(autoreset=True, strip=not _should_do_markup()) colorama.init(autoreset=True, strip=not _should_do_markup())
class LogFilter: class LogFilter(object):
"""A custom log filter which excludes log messages above the logged level.""" """A custom log filter which excludes log messages above the logged level."""
def __init__(self, level): def __init__(self, level):
@ -46,15 +47,15 @@ class LogFilter:
class MultilineFormatter(logging.Formatter): class MultilineFormatter(logging.Formatter):
"""Logging Formatter to reset color after newline characters.""" """Logging Formatter to reset color after newline characters."""
def format(self, record): def format(self, record): # noqa
record.msg = record.msg.replace("\n", f"\n{colorama.Style.RESET_ALL}... ") record.msg = record.msg.replace("\n", "\n{}... ".format(colorama.Style.RESET_ALL))
return logging.Formatter.format(self, record) return logging.Formatter.format(self, record)
class MultilineJsonFormatter(jsonlogger.JsonFormatter): class MultilineJsonFormatter(jsonlogger.JsonFormatter):
"""Logging Formatter to remove newline characters.""" """Logging Formatter to remove newline characters."""
def format(self, record): def format(self, record): # noqa
record.msg = record.msg.replace("\n", " ") record.msg = record.msg.replace("\n", " ")
return jsonlogger.JsonFormatter.format(self, record) return jsonlogger.JsonFormatter.format(self, record)
@ -62,11 +63,11 @@ class MultilineJsonFormatter(jsonlogger.JsonFormatter):
class Log: class Log:
"""Base logging object.""" """Base logging object."""
def __init__(self, level=logging.WARNING, name="dockertidy", json=False): def __init__(self, level=logging.WARN, name="dockertidy", json=False):
self.logger = logging.getLogger(name) self.logger = logging.getLogger(name)
self.logger.setLevel(level) self.logger.setLevel(level)
self.logger.addHandler(self._get_error_handler(json=json)) self.logger.addHandler(self._get_error_handler(json=json))
self.logger.addHandler(self._get_warning_handler(json=json)) self.logger.addHandler(self._get_warn_handler(json=json))
self.logger.addHandler(self._get_info_handler(json=json)) self.logger.addHandler(self._get_info_handler(json=json))
self.logger.addHandler(self._get_critical_handler(json=json)) self.logger.addHandler(self._get_critical_handler(json=json))
self.logger.addHandler(self._get_debug_handler(json=json)) self.logger.addHandler(self._get_debug_handler(json=json))
@ -87,13 +88,13 @@ class Log:
return handler return handler
def _get_warning_handler(self, json=False): def _get_warn_handler(self, json=False):
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.WARNING) handler.setLevel(logging.WARN)
handler.addFilter(LogFilter(logging.WARNING)) handler.addFilter(LogFilter(logging.WARN))
handler.setFormatter( handler.setFormatter(
MultilineFormatter( MultilineFormatter(
self.warning(CONSOLE_FORMAT.format(colorama.Fore.YELLOW, colorama.Style.RESET_ALL)) self.warn(CONSOLE_FORMAT.format(colorama.Fore.YELLOW, colorama.Style.RESET_ALL))
) )
) )
@ -163,8 +164,8 @@ class Log:
"""Format error messages and return string.""" """Format error messages and return string."""
return msg return msg
def warning(self, msg): def warn(self, msg):
"""Format warning messages and return string.""" """Format warn messages and return string."""
return msg return msg
def info(self, msg): def info(self, msg):
@ -180,7 +181,7 @@ class Log:
:returns: string :returns: string
""" """
return f"{color}{msg}{colorama.Style.RESET_ALL}" return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL)
def sysexit(self, code=1): def sysexit(self, code=1):
"""Exit running program with given exit code.""" """Exit running program with given exit code."""

View File

@ -10,8 +10,7 @@ env = environs.Env()
def timedelta_validator(value): def timedelta_validator(value):
""" """Return the dateparser string for a time in the past.
Return the dateparser string for a time in the past.
:param value: a string containing a time format supported by :param value: a string containing a time format supported by
mod:`dateparser` mod:`dateparser`
@ -20,14 +19,13 @@ def timedelta_validator(value):
return None return None
if not dateparser.parse(value): if not dateparser.parse(value):
raise ArgumentTypeError(f"'{value}' is not a valid timedelta string") raise ArgumentTypeError("'{}' is not a valid timedelta string".format(value))
return value return value
def timedelta(value, dt_format=None): def timedelta(value, dt_format=None):
""" """Return the :class:`datetime.datetime.DateTime` for a time in the past.
Return the :class:`datetime.datetime.DateTime` for a time in the past.
:param value: a string containing a time format supported by :param value: a string containing a time format supported by
mod:`dateparser` mod:`dateparser`
@ -36,7 +34,10 @@ def timedelta(value, dt_format=None):
return None return None
timedelta = dateparser.parse( timedelta = dateparser.parse(
value, settings={"TO_TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True} value, settings={
"TO_TIMEZONE": "UTC",
"RETURN_AS_TIMEZONE_AWARE": True
}
) )
if dt_format: if dt_format:
@ -51,4 +52,4 @@ def timedelta_parser(value):
timedelta_validator(value) timedelta_validator(value)
return value return value
except ArgumentTypeError as e: except ArgumentTypeError as e:
raise environs.EnvError(e) from e raise environs.EnvError(e)

View File

@ -1,8 +1,8 @@
"""Test Autostop class.""" """Test Autostop class."""
import docker
import pytest import pytest
import docker
from dockertidy import autostop from dockertidy import autostop
pytest_plugins = [ pytest_plugins = [

View File

@ -1,9 +1,9 @@
"""Test GarbageCollector class.""" """Test GarbageCollector class."""
import docker
import pytest import pytest
import requests import requests
import docker
from dockertidy import garbage_collector from dockertidy import garbage_collector
pytest_plugins = [ pytest_plugins = [
@ -358,7 +358,7 @@ def test_api_call_with_timeout(mocker, gc):
gc._api_call(func, image=image) gc._api_call(func, image=image)
func.assert_called_once_with(image="abcd") func.assert_called_once_with(image="abcd")
mock_log.warning.assert_called_once_with("Failed to call remove_image " + "image=abcd msg") mock_log.warn.assert_called_once_with("Failed to call remove_image " + "image=abcd msg")
def test_api_call_with_api_error(mocker, gc): def test_api_call_with_api_error(mocker, gc):
@ -376,7 +376,7 @@ def test_api_call_with_api_error(mocker, gc):
gc._api_call(func, image=image) gc._api_call(func, image=image)
func.assert_called_once_with(image="abcd") func.assert_called_once_with(image="abcd")
mock_log.warning.assert_called_once_with( mock_log.warn.assert_called_once_with(
"Error calling remove_image image=abcd " "Error calling remove_image image=abcd "
'409 Client Error for dummy: Conflict ("failed")' '409 Client Error for dummy: Conflict ("failed")'
) )

View File

@ -1,29 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Global utility methods and classes.""" """Global utility methods and classes."""
from distutils.util import strtobool
def strtobool(value):
"""Convert a string representation of truth to true or false."""
_map = {
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
try:
return _map[str(value).lower()]
except KeyError as err:
raise ValueError(f'"{value}" is not a valid bool value') from err
def to_bool(string): def to_bool(string):
@ -42,7 +20,8 @@ class Singleton(type):
_instances = {} _instances = {}
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs): # noqa
if cls not in cls._instances: if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs) cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls] return cls._instances[cls]

View File

@ -18,16 +18,11 @@ markup:
startLevel: 1 startLevel: 1
params: params:
description: >
Keep docker hosts tidy. docker-tidy is a tool to remove outdated and unused docker resources based on filters.
images:
- "socialmedia2.png"
geekdocMenuBundle: true geekdocMenuBundle: true
geekdocToC: 3 geekdocToC: 3
geekdocRepo: https://github.com/thegeeklab/docker-tidy geekdocRepo: https://github.com/thegeeklab/docker-tidy
geekdocEditPath: edit/main/docs geekdocEditPath: edit/main/docs/content
geekdocDateFormat: "Jan 2, 2006" geekdocDateFormat: "Jan 2, 2006"
geekdocSearch: true geekdocSearch: true

View File

@ -2,11 +2,12 @@
title: Documentation title: Documentation
--- ---
[![Build Status](https://ci.thegeeklab.de/api/badges/thegeeklab/docker-tidy/status.svg)](https://ci.thegeeklab.de/repos/thegeeklab/docker-tidy) [![Build Status](https://img.shields.io/drone/build/thegeeklab/docker-tidy?logo=drone&server=https%3A%2F%2Fdrone.thegeeklab.de)](https://drone.thegeeklab.de/thegeeklab/docker-tidy)
[![Docker Hub](https://img.shields.io/badge/docker-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/docker-tidy) [![Docker Hub](https://img.shields.io/badge/docker-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/docker-tidy)
[![Python Version](https://img.shields.io/pypi/pyversions/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![Python Version](https://img.shields.io/pypi/pyversions/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![PyPI Status](https://img.shields.io/pypi/status/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![PyPI Status](https://img.shields.io/pypi/status/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![PyPI Release](https://img.shields.io/pypi/v/docker-tidy.svg)](https://pypi.org/project/docker-tidy/) [![PyPI Release](https://img.shields.io/pypi/v/docker-tidy.svg)](https://pypi.org/project/docker-tidy/)
[![Codecov](https://img.shields.io/codecov/c/github/thegeeklab/docker-tidy)](https://codecov.io/gh/thegeeklab/docker-tidy)
[![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/graphs/contributors)
[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/docker-tidy) [![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/docker-tidy)
[![License: Apache-2.0](https://img.shields.io/github/license/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/blob/main/LICENSE) [![License: Apache-2.0](https://img.shields.io/github/license/thegeeklab/docker-tidy)](https://github.com/thegeeklab/docker-tidy/blob/main/LICENSE)

View File

@ -2,11 +2,9 @@
title: Setup title: Setup
--- ---
<!-- prettier-ignore-start -->
<!-- spellchecker-disable --> <!-- spellchecker-disable -->
{{< toc >}} {{< toc >}}
<!-- spellchecker-enable --> <!-- spellchecker-enable -->
<!-- prettier-ignore-end -->
## Pip ## Pip
@ -48,7 +46,8 @@ docker run \
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
{{< hint type=note >}} {{< hint info >}}
**Info**\
Keep in mind, that you have to pass SELinux labels (:Z or :z) to your mount option if you are working on SELinux enabled systems. Keep in mind, that you have to pass SELinux labels (:Z or :z) to your mount option if you are working on SELinux enabled systems.
{{< /hint >}} {{< /hint >}}
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

1846
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,10 @@ classifiers = [
"Natural Language :: English", "Natural Language :: English",
"Operating System :: POSIX", "Operating System :: POSIX",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: System :: Systems Administration", "Topic :: System :: Systems Administration",
"Topic :: Utilities", "Topic :: Utilities",
"Topic :: Software Development", "Topic :: Software Development",
@ -21,57 +21,81 @@ classifiers = [
description = "Keep docker hosts tidy." description = "Keep docker hosts tidy."
documentation = "https://docker-tidy.geekdocs.de/" documentation = "https://docker-tidy.geekdocs.de/"
homepage = "https://docker-tidy.geekdocs.de/" homepage = "https://docker-tidy.geekdocs.de/"
include = ["LICENSE"] include = [
"LICENSE",
]
keywords = ["docker", "gc", "prune", "garbage"] keywords = ["docker", "gc", "prune", "garbage"]
license = "Apache-2.0" license = "Apache-2.0"
name = "docker-tidy" name = "docker-tidy"
packages = [{ include = "dockertidy" }] packages = [
{include = "dockertidy"},
]
readme = "README.md" readme = "README.md"
repository = "https://github.com/thegeeklab/docker-tidy/" repository = "https://github.com/thegeeklab/docker-tidy/"
version = "0.0.0" version = "0.0.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
anyconfig = "0.14.0" anyconfig = "0.12.0"
appdirs = "1.4.4" appdirs = "1.4.4"
certifi = "2024.8.30" certifi = "2021.10.8"
colorama = "0.4.6" colorama = "0.4.4"
dateparser = "1.2.0" dateparser = "1.1.0"
docker = "7.1.0" docker = "5.0.3"
docker-pycreds = "0.4.0" docker-pycreds = "0.4.0"
environs = "11.1.0" environs = "9.5.0"
idna = "3.10" idna = "3.3"
ipaddress = "1.0.23" ipaddress = "1.0.23"
jsonschema = "4.23.0" jsonschema = "4.4.0"
nested-lookup = "0.2.25" nested-lookup = "0.2.23"
pathspec = "0.12.1" pathspec = "0.9.0"
python = "^3.10.0" python = "^3.7.0"
python-dateutil = "2.9.0.post0" python-dateutil = "2.8.2"
python-json-logger = "2.0.7" python-json-logger = "2.0.2"
requests = "2.32.3" requests = "2.27.1"
"ruamel.yaml" = "0.18.6" "ruamel.yaml" = "0.17.20"
websocket_client = "1.8.0" websocket_client = "1.2.3"
zipp = "3.21.0" zipp = "3.7.0"
[tool.poetry.dev-dependencies]
bandit = "1.7.2"
flake8 = "4.0.1"
flake8-blind-except = "0.2.0"
flake8-builtins = "1.5.3"
flake8-docstrings = "1.6.0"
flake8-eradicate = "1.2.0"
flake8-isort = "4.1.1"
flake8-logging-format = "0.6.0"
flake8-pep3101 = "1.3.0"
flake8-polyfill = "1.0.2"
flake8-quotes = "3.3.1"
pep8-naming = "0.12.1"
pydocstyle = "6.1.1"
pytest = "6.2.5"
pytest-cov = "3.0.0"
pytest-mock = "3.7.0"
tomli = "2.0.0"
yapf = "0.32.0"
[tool.poetry.scripts] [tool.poetry.scripts]
docker-tidy = "dockertidy.cli:main" docker-tidy = "dockertidy.cli:main"
[tool.poetry.group.dev.dependencies]
ruff = "0.7.3"
pytest = "8.3.3"
pytest-mock = "3.14.0"
pytest-cov = "6.0.0"
toml = "0.10.2"
[tool.poetry-dynamic-versioning] [tool.poetry-dynamic-versioning]
enable = true enable = true
style = "semver" style = "semver"
vcs = "git" vcs = "git"
[tool.isort]
default_section = "THIRDPARTY"
force_single_line = true
line_length = 99
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "dockertidy --cov=dockertidy --cov-report=xml:coverage.xml --cov-report=term --no-cov-on-fail" addopts = "dockertidy --cov=dockertidy --cov-report=xml:coverage.xml --cov-report=term --cov-append --no-cov-on-fail"
filterwarnings = [ filterwarnings = [
"ignore::FutureWarning", "ignore::FutureWarning",
"ignore::DeprecationWarning", "ignore:.*collections.*:DeprecationWarning",
"ignore:.*pep8.*:FutureWarning", "ignore:.*pep8.*:FutureWarning",
] ]
@ -79,70 +103,5 @@ filterwarnings = [
omit = ["**/test/*"] omit = ["**/test/*"]
[build-system] [build-system]
build-backend = "poetry_dynamic_versioning.backend" build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
[tool.ruff]
exclude = [
".git",
"__pycache__",
"build",
"dist",
"test",
"*.pyc",
"*.egg-info",
".cache",
".eggs",
"env*",
]
line-length = 99
indent-width = 4
[tool.ruff.lint]
# Explanation of errors
#
# D102: Missing docstring in public method
# D103: Missing docstring in public function
# D105: Missing docstring in magic method
# D107: Missing docstring in __init__
# D202: No blank lines allowed after function docstring
# D203: One blank line required before class docstring
# D212: Multi-line docstring summary should start at the first line
ignore = [
"D102",
"D103",
"D105",
"D107",
"D202",
"D203",
"D212",
"UP038",
"RUF012",
]
select = [
"D",
"E",
"F",
"Q",
"W",
"I",
"S",
"BLE",
"N",
"UP",
"B",
"A",
"C4",
"T20",
"SIM",
"RET",
"ARG",
"ERA",
"RUF",
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "lf"

View File

@ -1,12 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>thegeeklab/renovate-presets"], "extends": ["github>thegeeklab/renovate-presets"]
"packageRules": [
{
"matchManagers": ["woodpecker"],
"matchFileNames": [".woodpecker/test.yml"],
"matchPackageNames": ["docker.io/library/python"],
"enabled": false
}
]
} }

20
setup.cfg Normal file
View File

@ -0,0 +1,20 @@
[flake8]
# Explanation of errors
#
# D102: Missing docstring in public method
# D103: Missing docstring in public function
# D105: Missing docstring in magic method
# D107: Missing docstring in __init__
# D202: No blank lines allowed after function docstring
# W503:Line break occurred before a binary operator
ignore = D102, D103, D105, D107, D202, W503
max-line-length = 99
inline-quotes = double
exclude = .git, __pycache__, build, dist, test, *.pyc, *.egg-info, .cache, .eggs, env*
[yapf]
based_on_style = google
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true