From 5ffb9cc41bd8a4f0de06bfbb257faa361c33a16c Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 11 Oct 2021 02:58:05 +0200
Subject: [PATCH 001/135] dist scripts, search upwards for kiwi instance
---
.dockerignore | 10 ++
bump-version.sh => dist/bump-version.sh | 0
install.sh => dist/install.sh | 0
kiwi => dist/kiwi | 20 ++-
kiwi_scp/data/etc/version_tag | 1 -
poetry.lock | 225 +++++++++++++++++++++++-
pyproject.toml | 3 +-
7 files changed, 249 insertions(+), 10 deletions(-)
create mode 100644 .dockerignore
rename bump-version.sh => dist/bump-version.sh (100%)
rename install.sh => dist/install.sh (100%)
rename kiwi => dist/kiwi (90%)
delete mode 100644 kiwi_scp/data/etc/version_tag
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3930b8b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+.git/
+.idea/
+
+dist/
+example/
+
+Dockerfile
+.dockerignore
+.gitignore
+.drone.yml
\ No newline at end of file
diff --git a/bump-version.sh b/dist/bump-version.sh
similarity index 100%
rename from bump-version.sh
rename to dist/bump-version.sh
diff --git a/install.sh b/dist/install.sh
similarity index 100%
rename from install.sh
rename to dist/install.sh
diff --git a/kiwi b/dist/kiwi
similarity index 90%
rename from kiwi
rename to dist/kiwi
index 4038eb4..012fda1 100755
--- a/kiwi
+++ b/dist/kiwi
@@ -156,12 +156,20 @@ if [ "${run_kiwi_check}" = "yes" ]; then
chmod 0777 "${CANARY_FILENAME}"
fi
-# check if pwd is a kiwi folder
-if [ -f "./${KIWI_CONF_NAME}" ]; then
- # determine needed kiwi-scp version
- re_version_line='version\s*:\s*'
- eval "$(grep -E "${re_version_line}" "./${KIWI_CONF_NAME}" | sed -r "s/${re_version_line}/KIWI_VERSION=/")"
-fi
+# check if pwd is a kiwi instance
+path="$(pwd)"
+while [ "${path}" != "" ]; do
+ if [ -e "${path}/${KIWI_CONF_NAME}" ]; then
+ # cd into kiwi instance
+ cd "${path}" || yes_no "Could not enter kiwi instance at '${path}'. Continue anyway?" || exit 1
+
+ # determine needed kiwi-scp version
+ re_version_line='version\s*:\s*'
+ eval "$(grep -E "${re_version_line}" "./${KIWI_CONF_NAME}" | sed -r "s/${re_version_line}/KIWI_VERSION=/")"
+ break;
+ fi
+ path="${path%/*}"
+done
# install if kiwi-scp not found
if [ ! -x "$(kiwi_executable)" ]; then
diff --git a/kiwi_scp/data/etc/version_tag b/kiwi_scp/data/etc/version_tag
deleted file mode 100644
index 1180819..0000000
--- a/kiwi_scp/data/etc/version_tag
+++ /dev/null
@@ -1 +0,0 @@
-0.1.7
diff --git a/poetry.lock b/poetry.lock
index dd53b7f..4a2f0af 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,106 @@
+[[package]]
+name = "backports.entry-points-selectable"
+version = "1.1.0"
+description = "Compatibility shim providing selectable entry points for older implementations"
+category = "dev"
+optional = false
+python-versions = ">=2.7"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+
+[[package]]
+name = "dataclasses"
+version = "0.8"
+description = "A backport of the dataclasses module for Python 3.6"
+category = "main"
+optional = false
+python-versions = ">=3.6, <3.7"
+
+[[package]]
+name = "distlib"
+version = "0.3.3"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "filelock"
+version = "3.3.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
+[[package]]
+name = "importlib-metadata"
+version = "4.8.1"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.2.2"
+description = "Read resources from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[[package]]
+name = "platformdirs"
+version = "2.4.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "pydantic"
+version = "1.8.2"
+description = "Data validation and settings management using python 3.6 type hinting"
+category = "main"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
[[package]]
name = "pyyaml"
version = "5.4.1"
@@ -6,12 +109,113 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.2"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "virtualenv"
+version = "20.8.1"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+"backports.entry-points-selectable" = ">=1.0.4"
+distlib = ">=0.3.1,<1"
+filelock = ">=3.0.0,<4"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""}
+platformdirs = ">=2,<3"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+
+[[package]]
+name = "zipp"
+version = "3.6.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
[metadata]
lock-version = "1.1"
-python-versions = "^3.6"
-content-hash = "36970da0e8c6151dcf68abd9008ecef35673f04db53952bfb3fd7544c0516b7f"
+python-versions = "^3.6.1"
+content-hash = "c449a1d6048636533037af38cb647b63b19f8027b0a12d82408dd05ed4164c06"
[metadata.files]
+"backports.entry-points-selectable" = [
+ {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
+ {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+]
+dataclasses = [
+ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
+ {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
+]
+distlib = [
+ {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
+ {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
+]
+filelock = [
+ {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"},
+ {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
+ {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
+]
+importlib-resources = [
+ {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"},
+ {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"},
+]
+platformdirs = [
+ {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
+ {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
+]
+pydantic = [
+ {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
+ {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
+ {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
+ {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
+ {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
+ {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
+ {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
+ {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
+ {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
+ {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
+ {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
+ {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
+ {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
+ {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
+ {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
+ {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
+ {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
+ {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
+ {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
+ {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
+ {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
+ {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
+]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -43,3 +247,20 @@ pyyaml = [
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+typing-extensions = [
+ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+ {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+]
+virtualenv = [
+ {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"},
+ {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"},
+]
+zipp = [
+ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
+ {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index 9e5d4ca..1cb8df1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,10 +5,11 @@ description = "kiwi is the simple tool for managing container servers."
authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
[tool.poetry.dependencies]
-python = "^3.6"
+python = "^3.6.1"
PyYAML = "^5.4.1"
[tool.poetry.dev-dependencies]
+virtualenv = "^20.8.1"
[tool.poetry.scripts]
kiwi = "kiwi_scp.scripts.kiwi:main"
From c0edd3bcc576cb0ec934a25407e54a012068ad27 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 11 Oct 2021 02:58:49 +0200
Subject: [PATCH 002/135] new kiwi.yml format
---
.idea/runConfigurations/kiwi_next.xml | 23 +++
kiwi_scp/__init__.py | 20 ---
kiwi_scp/config.py | 208 ++++++++------------------
kiwi_scp/data/etc/kiwi_default.yml | 19 +--
kiwi_scp/scripts/kiwi_next.py | 14 ++
pyproject.toml | 1 +
6 files changed, 113 insertions(+), 172 deletions(-)
create mode 100644 .idea/runConfigurations/kiwi_next.xml
create mode 100644 kiwi_scp/scripts/kiwi_next.py
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
new file mode 100644
index 0000000..9b3bd56
--- /dev/null
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kiwi_scp/__init__.py b/kiwi_scp/__init__.py
index ba8912f..e69de29 100644
--- a/kiwi_scp/__init__.py
+++ b/kiwi_scp/__init__.py
@@ -1,20 +0,0 @@
-# local
-from .parser import Parser
-from .runner import Runner
-
-
-def verbosity():
- # ensure singleton is instantiated: runs subcommand setup routines
- _ = Runner()
- return Parser().get_args().verbosity
-
-
-def run():
- # pass down
- return Runner().run()
-
-
-__all__ = [
- 'verbosity',
- 'run'
-]
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 00c0dfc..b276fa6 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,157 +1,79 @@
-# system
-import copy
-import logging
-import os
import re
-import yaml
+from typing import Optional, Dict, List
-# local
-from ._constants import KIWI_CONF_NAME, HEADER_KIWI_CONF_NAME, DEFAULT_KIWI_CONF_NAME, VERSION_TAG_NAME
+import pydantic
-class Config:
+class _Storage(pydantic.BaseModel):
+ """a storage subsection"""
+
+ directory: str
+
+
+class _Project(pydantic.BaseModel):
+ """a project subsection"""
+
+ name: str
+ enabled: bool = True
+ storage: Optional[_Storage]
+
+ @pydantic.root_validator(pre=True)
+ @classmethod
+ def check_grammar(cls, values):
+ if isinstance(values, dict):
+ if "name" in values:
+ return values
+
+ elif len(values) == 1:
+ name, enabled = list(values.items())[0]
+ return {"name": name, "enabled": True if enabled is None else enabled}
+
+ elif isinstance(values, str):
+ return {"name": values}
+
+
+class _Network(pydantic.BaseModel):
+ """a network subsection"""
+
+ name: str
+ cidr: str
+
+
+class Config(pydantic.BaseModel):
"""represents a kiwi.yml"""
- __yml_content = {}
- __keys = {
- 'version': "kiwi-scp version to use in this instance",
+ version: str
+ shells: Optional[List[str]]
+ environment: Optional[Dict[str, Optional[str]]]
- 'runtime:storage': "local directory for service data",
- 'runtime:shells': "shell preference for working in service containers",
- 'runtime:env': "common environment for compose yml",
-
- 'markers:project': "marker string for project directories",
- 'markers:disabled': "marker string for disabled projects",
-
- 'network:name': "name for local network hub",
- 'network:cidr': "CIDR block for local network hub",
- }
-
- def __key_resolve(self, key):
- """
- Resolve nested dictionaries
-
- If __yml_content is {'a': {'b': {'c': "val"}}} and key is 'a:b:c',
- this returns a single dict {'c': "val"} and the direct key 'c'
- """
-
- # "a:b:c" => path = ['a', 'b'], key = 'c'
- path = key.split(':')
- path, key = path[:-1], path[-1]
-
- # resolve path
- container = self.__yml_content
- for step in path:
- container = container[step]
-
- return container, key
-
- def __getitem__(self, key):
- """array-like read access to __yml_content"""
-
- container, key = self.__key_resolve(key)
- return container[key]
-
- def __setitem__(self, key, value):
- """array-like write access to __yml_content"""
-
- container, key = self.__key_resolve(key)
- container[key] = value
-
- def __str__(self):
- """dump into textual representation"""
-
- # dump yml content
- yml_string = yaml.dump(
- self.__yml_content,
- default_flow_style=False, sort_keys=False
- ).strip()
-
- # insert newline before every main key
- yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
-
- # load header comment from file
- with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
- yml_string = stream.read() + yml_string
-
- return yml_string
-
- def _update_from_file(self, filename):
- """return a copy updated using a kiwi.yml file"""
-
- with open(filename, 'r') as stream:
- try:
- # create copy
- result = Config()
- result.__yml_content = copy.deepcopy(self.__yml_content)
-
- # read file
- logging.debug(f"Reading '{filename}' into '{id(result.__yml_content)}'")
- result.__yml_content.update(yaml.safe_load(stream))
-
- return result
- except yaml.YAMLError as exc:
- logging.error(exc)
-
- def user_query(self, key):
- """query user for new config value"""
-
- # prompt user as per argument
- try:
- result = input(f"Enter {self.__keys[key]} [{self[key]}] ").strip()
- except EOFError:
- print()
- result = None
-
- # store result if present
- if result:
- self[key] = result
-
- def save(self):
- """save current yml representation in current directory's kiwi.yml"""
-
- with open(KIWI_CONF_NAME, 'w') as stream:
- stream.write(str(self))
- stream.write('\n')
-
-
-class DefaultConfig(Config):
- """Singleton: The default kiwi.yml file"""
-
- __instance = None
+ projects: Optional[List[_Project]]
+ storage: _Storage
+ network: _Network
+ @pydantic.validator("version")
@classmethod
- def get(cls):
- if cls.__instance is None:
- # create singleton
- cls.__instance = cls()._update_from_file(DEFAULT_KIWI_CONF_NAME)
+ def check_version(cls, value: str) -> str:
+ if not re.match(r"^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$", value):
+ raise ValueError
- # add version data from separate file (keeps default config cleaner)
- with open(VERSION_TAG_NAME, 'r') as stream:
- cls.__instance['version'] = stream.read().strip()
-
- # return singleton
- return cls.__instance
-
-
-class LoadedConfig(Config):
- """Singleton collection: kiwi.yml files by path"""
-
- __instances = {}
+ return value
+ @pydantic.validator("environment", pre=True)
@classmethod
- def get(cls, directory='.'):
- if directory not in LoadedConfig.__instances:
- # create singleton for new path
- result = DefaultConfig.get()
+ def unify_env(cls, value) -> Optional[Dict[str, Optional[str]]]:
+ if isinstance(value, dict):
+ return value
+ elif isinstance(value, list):
+ result: Dict[str, Optional[str]] = {}
+ for item in value:
+ idx = item.find("=")
+ if idx == -1:
+ key, value = item, None
+ else:
+ key, value = item[:idx], item[idx + 1:]
- # update with that dir's kiwi.yml
- try:
- result = result._update_from_file(os.path.join(directory, KIWI_CONF_NAME))
- except FileNotFoundError:
- logging.info(f"No '{KIWI_CONF_NAME}' found at '{directory}'. Using defaults.")
+ result[key] = value
- LoadedConfig.__instances[directory] = result
-
- # return singleton
- return LoadedConfig.__instances[directory]
+ return result
+ else:
+ return None
diff --git a/kiwi_scp/data/etc/kiwi_default.yml b/kiwi_scp/data/etc/kiwi_default.yml
index 1404379..44766a7 100644
--- a/kiwi_scp/data/etc/kiwi_default.yml
+++ b/kiwi_scp/data/etc/kiwi_default.yml
@@ -1,12 +1,13 @@
-version:
-runtime:
- storage: /var/kiwi
- shells:
- - /bin/bash
- env: null
-markers:
- project: .project
- disabled: .disabled
+version: 0.2.0
+shells:
+ - /bin/bash
+projects:
+ - name: admin
+ enabled: true
+ - test:
+ - test2: false
+storage:
+ directory: /var/local/kiwi
network:
name: kiwi_hub
cidr: 10.22.46.0/24
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
new file mode 100644
index 0000000..22bb8a2
--- /dev/null
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -0,0 +1,14 @@
+from kiwi_scp.config import Config
+import yaml
+
+
+def main():
+ with open("./kiwi_scp/data/etc/kiwi_default.yml") as kc:
+ yml = yaml.safe_load(kc)
+ kiwi = Config(**yml)
+
+ print(repr(kiwi))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
index 1cb8df1..c7fe7ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,6 +7,7 @@ authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
[tool.poetry.dependencies]
python = "^3.6.1"
PyYAML = "^5.4.1"
+pydantic = "^1.8.2"
[tool.poetry.dev-dependencies]
virtualenv = "^20.8.1"
From 370097e5c38043f876e8112c72aab95b213a9d7a Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 12 Oct 2021 19:06:49 +0200
Subject: [PATCH 003/135] config.py constraints and doc
---
kiwi_scp/_constants.py | 12 ++++
kiwi_scp/config.py | 106 ++++++++++++++++++++---------
kiwi_scp/data/etc/kiwi_default.yml | 2 +-
3 files changed, 86 insertions(+), 34 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index cd12c09..62a46f9 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -1,6 +1,18 @@
# system
import os
+#############
+# REGEX PARTS
+
+# regex part for a number with no leading zeroes
+_RE_NUMBER: str = r"[0-9]|[1-9][0-9]*"
+
+# regex for a semantic version string
+RE_SEMVER = rf"^{_RE_NUMBER}(?:\.{_RE_NUMBER}(?:\.{_RE_NUMBER})?)?$"
+
+# regex for a lowercase variable name
+RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9])$"
+
#############
# ENVIRONMENT
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index b276fa6..ea5ba12 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,48 +1,67 @@
-import re
+from ipaddress import IPv4Network
+from pathlib import Path
from typing import Optional, Dict, List
import pydantic
+from ._constants import RE_SEMVER, RE_VARNAME
+
class _Storage(pydantic.BaseModel):
"""a storage subsection"""
- directory: str
+ directory: Path
class _Project(pydantic.BaseModel):
"""a project subsection"""
- name: str
+ name: pydantic.constr(
+ regex=RE_VARNAME
+ )
enabled: bool = True
- storage: Optional[_Storage]
+ override_storage: Optional[_Storage]
@pydantic.root_validator(pre=True)
@classmethod
- def check_grammar(cls, values):
- if isinstance(values, dict):
- if "name" in values:
- return values
+ def unify_project(cls, values):
+ """parse different project notations"""
- elif len(values) == 1:
- name, enabled = list(values.items())[0]
- return {"name": name, "enabled": True if enabled is None else enabled}
+ if "name" in values:
+ # default format
+ return values
- elif isinstance(values, str):
- return {"name": values}
+ elif len(values) == 1:
+ # short format:
+ # - :
+
+ name, enabled = list(values.items())[0]
+ return {
+ "name": name,
+ "enabled": True if enabled is None else enabled
+ }
+
+ else:
+ # undefined format
+ raise ValueError
class _Network(pydantic.BaseModel):
"""a network subsection"""
- name: str
- cidr: str
+ name: pydantic.constr(
+ to_lower=True,
+ regex=RE_VARNAME
+ )
+ cidr: IPv4Network
class Config(pydantic.BaseModel):
"""represents a kiwi.yml"""
- version: str
+ version: pydantic.constr(
+ regex=RE_SEMVER
+ )
shells: Optional[List[str]]
environment: Optional[Dict[str, Optional[str]]]
@@ -50,30 +69,51 @@ class Config(pydantic.BaseModel):
storage: _Storage
network: _Network
- @pydantic.validator("version")
- @classmethod
- def check_version(cls, value: str) -> str:
- if not re.match(r"^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$", value):
- raise ValueError
-
- return value
-
@pydantic.validator("environment", pre=True)
@classmethod
- def unify_env(cls, value) -> Optional[Dict[str, Optional[str]]]:
- if isinstance(value, dict):
+ def unify_environment(cls, value) -> Optional[Dict[str, Optional[str]]]:
+ """parse different environment notations"""
+
+ def parse_str(var_val: str) -> (str, Optional[str]):
+ """parse a "=" string"""
+
+ idx = var_val.find("=")
+ if idx == -1:
+ # don't split, just define the variable
+ return var_val, None
+ else:
+ # split string, set variable to value
+ return var_val[:idx], var_val[idx + 1:]
+
+ if value is None:
+ # empty environment
+ return None
+
+ elif isinstance(value, dict):
+ # native dict format
return value
+
elif isinstance(value, list):
+ # list format (multiple strings)
+
result: Dict[str, Optional[str]] = {}
for item in value:
- idx = item.find("=")
- if idx == -1:
- key, value = item, None
- else:
- key, value = item[:idx], item[idx + 1:]
-
+ key, value = parse_str(item)
result[key] = value
return result
+
+ elif isinstance(value, str):
+ # string format (single variable):
+ # "="
+
+ key, value = parse_str(value)
+ return {key: value}
+
+ elif isinstance(value, int):
+ # integer format (just define single oddly named variable)
+ return {str(value): None}
+
else:
- return None
+ # undefined format
+ raise ValueError
diff --git a/kiwi_scp/data/etc/kiwi_default.yml b/kiwi_scp/data/etc/kiwi_default.yml
index 44766a7..bb8335f 100644
--- a/kiwi_scp/data/etc/kiwi_default.yml
+++ b/kiwi_scp/data/etc/kiwi_default.yml
@@ -4,7 +4,7 @@ shells:
projects:
- name: admin
enabled: true
- - test:
+ - Test:
- test2: false
storage:
directory: /var/local/kiwi
From af02c410e5d985f12b60d95f8f06e7539171a857 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 12 Oct 2021 19:08:35 +0200
Subject: [PATCH 004/135] config.py imports
---
kiwi_scp/config.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index ea5ba12..9efe0e2 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -2,27 +2,27 @@ from ipaddress import IPv4Network
from pathlib import Path
from typing import Optional, Dict, List
-import pydantic
+from pydantic import BaseModel, constr, root_validator, validator
from ._constants import RE_SEMVER, RE_VARNAME
-class _Storage(pydantic.BaseModel):
+class _Storage(BaseModel):
"""a storage subsection"""
directory: Path
-class _Project(pydantic.BaseModel):
+class _Project(BaseModel):
"""a project subsection"""
- name: pydantic.constr(
+ name: constr(
regex=RE_VARNAME
)
enabled: bool = True
override_storage: Optional[_Storage]
- @pydantic.root_validator(pre=True)
+ @root_validator(pre=True)
@classmethod
def unify_project(cls, values):
"""parse different project notations"""
@@ -46,20 +46,20 @@ class _Project(pydantic.BaseModel):
raise ValueError
-class _Network(pydantic.BaseModel):
+class _Network(BaseModel):
"""a network subsection"""
- name: pydantic.constr(
+ name: constr(
to_lower=True,
regex=RE_VARNAME
)
cidr: IPv4Network
-class Config(pydantic.BaseModel):
+class Config(BaseModel):
"""represents a kiwi.yml"""
- version: pydantic.constr(
+ version: constr(
regex=RE_SEMVER
)
shells: Optional[List[str]]
@@ -69,7 +69,7 @@ class Config(pydantic.BaseModel):
storage: _Storage
network: _Network
- @pydantic.validator("environment", pre=True)
+ @validator("environment", pre=True)
@classmethod
def unify_environment(cls, value) -> Optional[Dict[str, Optional[str]]]:
"""parse different environment notations"""
From e42b426f2e3e99a21fb55b67463680c880e80647 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 12 Oct 2021 19:17:28 +0200
Subject: [PATCH 005/135] remove need for kiwi_default.yml
---
example/kiwi.yml | 16 +++++++--------
kiwi_scp/_constants.py | 2 +-
kiwi_scp/config.py | 32 ++++++++++++++++++------------
kiwi_scp/data/etc/kiwi_default.yml | 13 ------------
kiwi_scp/scripts/kiwi_next.py | 5 ++++-
5 files changed, 32 insertions(+), 36 deletions(-)
delete mode 100644 kiwi_scp/data/etc/kiwi_default.yml
diff --git a/example/kiwi.yml b/example/kiwi.yml
index 1eb6293..e40d97d 100644
--- a/example/kiwi.yml
+++ b/example/kiwi.yml
@@ -2,17 +2,17 @@
# kiwi-scp instance configuration #
###################################
-version: '0.1.7'
+version: '0.2.0'
-runtime:
- storage: /tmp/kiwi
- shells:
+shells:
- /bin/bash
- env: null
-markers:
- project: .project
- disabled: .disabled
+projects:
+ - name: hello-world.project
+ enabled: true
+
+storage:
+ directory: /var/local/kiwi
network:
name: kiwi_hub
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 62a46f9..54b9607 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -11,7 +11,7 @@ _RE_NUMBER: str = r"[0-9]|[1-9][0-9]*"
RE_SEMVER = rf"^{_RE_NUMBER}(?:\.{_RE_NUMBER}(?:\.{_RE_NUMBER})?)?$"
# regex for a lowercase variable name
-RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9])$"
+RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9\._-]*[A-Za-z0-9])$"
#############
# ENVIRONMENT
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 9efe0e2..792fd4c 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -49,29 +49,35 @@ class _Project(BaseModel):
class _Network(BaseModel):
"""a network subsection"""
- name: constr(
- to_lower=True,
- regex=RE_VARNAME
- )
+ name: constr(to_lower=True, regex=RE_VARNAME)
cidr: IPv4Network
class Config(BaseModel):
"""represents a kiwi.yml"""
- version: constr(
- regex=RE_SEMVER
- )
- shells: Optional[List[str]]
- environment: Optional[Dict[str, Optional[str]]]
+ version: constr(regex=RE_SEMVER) = "0.2.0"
+
+ shells: Optional[List[str]] = [
+ "/bin/bash",
+ ]
+
+ environment: Dict[str, Optional[str]] = {}
projects: Optional[List[_Project]]
- storage: _Storage
- network: _Network
+
+ storage: _Storage = _Storage.parse_obj({
+ "directory": "/var/local/kiwi",
+ })
+
+ network: _Network = _Network.parse_obj({
+ "name": "kiwi_hub",
+ "cidr": "10.22.46.0/24",
+ })
@validator("environment", pre=True)
@classmethod
- def unify_environment(cls, value) -> Optional[Dict[str, Optional[str]]]:
+ def unify_environment(cls, value) -> Dict[str, Optional[str]]:
"""parse different environment notations"""
def parse_str(var_val: str) -> (str, Optional[str]):
@@ -87,7 +93,7 @@ class Config(BaseModel):
if value is None:
# empty environment
- return None
+ return {}
elif isinstance(value, dict):
# native dict format
diff --git a/kiwi_scp/data/etc/kiwi_default.yml b/kiwi_scp/data/etc/kiwi_default.yml
deleted file mode 100644
index bb8335f..0000000
--- a/kiwi_scp/data/etc/kiwi_default.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-version: 0.2.0
-shells:
- - /bin/bash
-projects:
- - name: admin
- enabled: true
- - Test:
- - test2: false
-storage:
- directory: /var/local/kiwi
-network:
- name: kiwi_hub
- cidr: 10.22.46.0/24
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
index 22bb8a2..3650070 100644
--- a/kiwi_scp/scripts/kiwi_next.py
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -3,12 +3,15 @@ import yaml
def main():
- with open("./kiwi_scp/data/etc/kiwi_default.yml") as kc:
+ with open("./example/kiwi.yml") as kc:
yml = yaml.safe_load(kc)
kiwi = Config(**yml)
print(repr(kiwi))
+ kiwi = Config()
+ print(repr(kiwi))
+
if __name__ == "__main__":
main()
From c08d44edb32805db891730d13806d5b12ecf9dcc Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 13 Oct 2021 02:16:09 +0200
Subject: [PATCH 006/135] errata
---
dist/bump-version.sh | 7 ++++---
kiwi_scp/_constants.py | 2 +-
kiwi_scp/config.py | 18 +++++++++---------
pyproject.toml | 2 +-
4 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/dist/bump-version.sh b/dist/bump-version.sh
index 7c4b7b3..5d57040 100755
--- a/dist/bump-version.sh
+++ b/dist/bump-version.sh
@@ -6,7 +6,8 @@ this_dir="$(dirname "${this}")"
git_branch="$(git rev-parse --abbrev-ref HEAD)"
git_tag="$(git describe --abbrev=0)"
version_str="${git_branch##*/}"
+version_str="0.2.0"
-echo "${version_str}" > "${this_dir}/kiwi_scp/data/etc/version_tag"
-sed -ri "s/(version\s*:).*$/\1 '${version_str}'/" "${this_dir}/example/kiwi.yml"
-sed -ri "s/(version\s*=\s*).*$/\1\"${version_str}\"/" "${this_dir}/pyproject.toml"
+sed -ri "s/(version\s*:).*$/\1 '${version_str}'/" "${this_dir}/../example/kiwi.yml"
+sed -ri "s/(version\s*=\s*).*$/\1\"${version_str}\"/" "${this_dir}/../pyproject.toml"
+sed -ri "s/(version.*=\s*).*$/\1\"${version_str}\"/" "${this_dir}/../kiwi_scp/config.py"
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 54b9607..e0f23c2 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -10,7 +10,7 @@ _RE_NUMBER: str = r"[0-9]|[1-9][0-9]*"
# regex for a semantic version string
RE_SEMVER = rf"^{_RE_NUMBER}(?:\.{_RE_NUMBER}(?:\.{_RE_NUMBER})?)?$"
-# regex for a lowercase variable name
+# regex for a variable name
RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9\._-]*[A-Za-z0-9])$"
#############
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 792fd4c..37a020b 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -58,22 +58,22 @@ class Config(BaseModel):
version: constr(regex=RE_SEMVER) = "0.2.0"
- shells: Optional[List[str]] = [
- "/bin/bash",
+ shells: Optional[List[Path]] = [
+ Path("/bin/bash"),
]
environment: Dict[str, Optional[str]] = {}
projects: Optional[List[_Project]]
- storage: _Storage = _Storage.parse_obj({
- "directory": "/var/local/kiwi",
- })
+ storage: _Storage = _Storage(
+ directory="/var/local/kiwi",
+ )
- network: _Network = _Network.parse_obj({
- "name": "kiwi_hub",
- "cidr": "10.22.46.0/24",
- })
+ network: _Network = _Network(
+ name="kiwi_hub",
+ cidr="10.22.46.0/24",
+ )
@validator("environment", pre=True)
@classmethod
diff --git a/pyproject.toml b/pyproject.toml
index c7fe7ea..e1ab88b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "kiwi-scp"
-version = "0.1.7"
+version = "0.2.0"
description = "kiwi is the simple tool for managing container servers."
authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
From 175382184635303f74c9bb7b541945f090248fd2 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 13 Oct 2021 03:05:46 +0200
Subject: [PATCH 007/135] dump kiwi.yml
---
kiwi_scp/config.py | 91 ++++++++++++++++++++++++++++++++---
kiwi_scp/scripts/kiwi_next.py | 4 +-
2 files changed, 85 insertions(+), 10 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 37a020b..0cbf6d1 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,10 +1,18 @@
+import re
from ipaddress import IPv4Network
from pathlib import Path
-from typing import Optional, Dict, List
+from typing import Optional, Dict, List, Any
+import yaml
from pydantic import BaseModel, constr, root_validator, validator
-from ._constants import RE_SEMVER, RE_VARNAME
+from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME
+
+
+# indent yaml lists
+class _KiwiDumper(yaml.Dumper):
+ def increase_indent(self, flow=False, indentless=False):
+ return super().increase_indent(flow, False)
class _Storage(BaseModel):
@@ -12,16 +20,32 @@ class _Storage(BaseModel):
directory: Path
+ @property
+ def kiwi_dict(self) -> Dict[str, Any]:
+ """write this object as a dictionary of strings"""
+
+ return {"directory": str(self.directory)}
+
class _Project(BaseModel):
"""a project subsection"""
- name: constr(
- regex=RE_VARNAME
- )
+ name: constr(regex=RE_VARNAME)
enabled: bool = True
override_storage: Optional[_Storage]
+ @property
+ def kiwi_dict(self) -> Dict[str, Any]:
+ """write this object as a dictionary of strings"""
+
+ if self.override_storage is None:
+ return {self.name: self.enabled}
+
+ else:
+ result = self.dict(exclude={"override_storage"})
+ result["override_storage"] = self.override_storage.kiwi_dict
+ return result
+
@root_validator(pre=True)
@classmethod
def unify_project(cls, values):
@@ -52,6 +76,15 @@ class _Network(BaseModel):
name: constr(to_lower=True, regex=RE_VARNAME)
cidr: IPv4Network
+ @property
+ def kiwi_dict(self) -> Dict[str, Any]:
+ """write this object as a dictionary of strings"""
+
+ return {
+ "name": self.name,
+ "cidr": str(self.cidr)
+ }
+
class Config(BaseModel):
"""represents a kiwi.yml"""
@@ -62,10 +95,10 @@ class Config(BaseModel):
Path("/bin/bash"),
]
- environment: Dict[str, Optional[str]] = {}
-
projects: Optional[List[_Project]]
+ environment: Dict[str, Optional[str]] = {}
+
storage: _Storage = _Storage(
directory="/var/local/kiwi",
)
@@ -75,6 +108,50 @@ class Config(BaseModel):
cidr="10.22.46.0/24",
)
+ @property
+ def kiwi_dict(self) -> Dict[str, Any]:
+ """write this object as a dictionary of strings"""
+
+ result = {
+ "version": self.version,
+ "shells": [str(shell) for shell in self.shells],
+ }
+
+ if self.projects:
+ result["projects"] = [
+ project.kiwi_dict
+ for project in self.projects
+ ]
+
+ if self.environment:
+ result["environment"] = self.environment
+
+ result["storage"] = self.storage.kiwi_dict
+
+ result["network"] = self.network.kiwi_dict
+
+ return result
+
+ @property
+ def kiwi_yml(self) -> str:
+ """dump a kiwi.yml file"""
+
+ yml_string = yaml.dump(
+ self.kiwi_dict,
+ Dumper=_KiwiDumper,
+ default_flow_style=False,
+ sort_keys=False,
+ )
+
+ # insert newline before every main key
+ yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
+
+ # load header comment from file
+ with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
+ yml_string = stream.read() + yml_string
+
+ return yml_string
+
@validator("environment", pre=True)
@classmethod
def unify_environment(cls, value) -> Dict[str, Optional[str]]:
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
index 3650070..0885d92 100644
--- a/kiwi_scp/scripts/kiwi_next.py
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -8,9 +8,7 @@ def main():
kiwi = Config(**yml)
print(repr(kiwi))
-
- kiwi = Config()
- print(repr(kiwi))
+ print(kiwi.kiwi_yml)
if __name__ == "__main__":
From 734517719513a9b24d5429f96332c6a8f7e01dcd Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 04:34:09 +0200
Subject: [PATCH 008/135] Some pytests for config.py
---
.idea/runConfigurations/Tests.xml | 18 ++++
.idea/runConfigurations/kiwi_next.xml | 6 +-
kiwi_scp/config.py | 16 ++--
pyproject.toml | 3 +
tests/__init__.py | 0
tests/test_config.py | 118 ++++++++++++++++++++++++++
6 files changed, 151 insertions(+), 10 deletions(-)
create mode 100644 .idea/runConfigurations/Tests.xml
create mode 100644 tests/__init__.py
create mode 100644 tests/test_config.py
diff --git a/.idea/runConfigurations/Tests.xml b/.idea/runConfigurations/Tests.xml
new file mode 100644
index 0000000..17c8681
--- /dev/null
+++ b/.idea/runConfigurations/Tests.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
index 9b3bd56..b4babb9 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -1,5 +1,5 @@
-
+
@@ -11,11 +11,11 @@
-
+
-
+
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 0cbf6d1..a88eb4b 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -181,7 +181,7 @@ class Config(BaseModel):
result: Dict[str, Optional[str]] = {}
for item in value:
- key, value = parse_str(item)
+ key, value = parse_str(str(item))
result[key] = value
return result
@@ -193,10 +193,12 @@ class Config(BaseModel):
key, value = parse_str(value)
return {key: value}
- elif isinstance(value, int):
- # integer format (just define single oddly named variable)
- return {str(value): None}
-
else:
- # undefined format
- raise ValueError
+ # any other format (try to coerce to str first)
+ try:
+ key, value = parse_str(str(value))
+ return {key: value}
+
+ except Exception as e:
+ # undefined format
+ raise ValueError("Invalid Environment Format")
diff --git a/pyproject.toml b/pyproject.toml
index e1ab88b..85844ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,9 +11,12 @@ pydantic = "^1.8.2"
[tool.poetry.dev-dependencies]
virtualenv = "^20.8.1"
+pytest = "^6.2.5"
+toml = "^0.10.2"
[tool.poetry.scripts]
kiwi = "kiwi_scp.scripts.kiwi:main"
+kiwi_next = "kiwi_scp.scripts.kiwi_next:main"
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..fa08258
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,118 @@
+from ipaddress import IPv4Network
+from pathlib import Path
+
+import pytest
+import toml
+from pydantic import ValidationError
+
+from kiwi_scp.config import Config
+
+
+def test_default():
+ c = Config()
+ version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
+
+ assert c.version == version
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+ assert c.projects is None
+ assert c.environment == {}
+ assert c.storage.directory == Path("/var/local/kiwi")
+ assert c.network.name == "kiwi_hub"
+ assert c.network.cidr == IPv4Network("10.22.46.0/24")
+
+
+#############
+# ENVIRONMENT
+#############
+
+def test_env_empty():
+ c = Config(environment=None)
+
+ assert c.environment == {}
+
+
+def test_env_dict():
+ c = Config(environment={})
+
+ assert c.environment == {}
+
+ c = Config(environment={
+ "variable": "value"
+ })
+
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
+
+
+def test_env_list():
+ c = Config(environment=[])
+
+ assert c.environment == {}
+
+ c = Config(environment=[
+ "variable=value",
+ ])
+
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
+
+ c = Config(environment=[
+ "variable",
+ ])
+
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] is None
+
+ c = Config(environment=[
+ 123,
+ ])
+
+ assert len(c.environment) == 1
+ assert "123" in c.environment
+ assert c.environment["123"] is None
+
+
+def test_env_str():
+ c = Config(environment="variable=value")
+
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
+
+ c = Config(environment="variable")
+
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] is None
+
+
+def test_env_coercible():
+ c = Config(environment=123)
+
+ assert len(c.environment) == 1
+ assert "123" in c.environment
+ assert c.environment["123"] is None
+
+ c = Config(environment=123.4)
+
+ assert len(c.environment) == 1
+ assert "123.4" in c.environment
+ assert c.environment["123.4"] is None
+
+
+def test_env_undef():
+ class UnCoercible:
+ def __str__(self):
+ raise ValueError
+
+ with pytest.raises(ValidationError) as exc_info:
+ Config(environment=UnCoercible())
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Environment Format"
+ assert error["type"] == "value_error"
From 92802aa5eaef8dec2c9f516e7ba9e2a21b69b1d7 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 05:12:33 +0200
Subject: [PATCH 009/135] typo
---
kiwi_scp/config.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index a88eb4b..d9e0368 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -62,7 +62,7 @@ class _Project(BaseModel):
name, enabled = list(values.items())[0]
return {
"name": name,
- "enabled": True if enabled is None else enabled
+ "enabled": True if enabled is None else enabled,
}
else:
@@ -82,7 +82,7 @@ class _Network(BaseModel):
return {
"name": self.name,
- "cidr": str(self.cidr)
+ "cidr": str(self.cidr),
}
From 8d688b28b113d3c0238cb7d6920f932bacc83978 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 19:18:24 +0200
Subject: [PATCH 010/135] pytests projects
---
kiwi_scp/config.py | 36 +++++++++++++++++++++-
tests/test_config.py | 71 +++++++++++++++++++++++++++++++++++++++++---
2 files changed, 102 insertions(+), 5 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index d9e0368..44fc137 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -95,7 +95,7 @@ class Config(BaseModel):
Path("/bin/bash"),
]
- projects: Optional[List[_Project]]
+ projects: List[_Project] = []
environment: Dict[str, Optional[str]] = {}
@@ -152,6 +152,40 @@ class Config(BaseModel):
return yml_string
+ @validator("projects", pre=True)
+ @classmethod
+ def unify_projects(cls, value):
+ """parse different projects notations"""
+
+ if value is None:
+ # empty projects list
+ return []
+
+ elif isinstance(value, list):
+ # handle projects list
+
+ result = []
+ for entry in value:
+ # ignore empties
+ if entry is not None:
+ if isinstance(entry, dict):
+ # handle single project dict
+ result.append(entry)
+
+ else:
+ # handle single project name
+ result.append({"name": str(entry)})
+
+ return result
+
+ elif isinstance(value, dict):
+ # handle single project dict
+ return [value]
+
+ else:
+ # handle single project name
+ return [{"name": str(value)}]
+
@validator("environment", pre=True)
@classmethod
def unify_environment(cls, value) -> Dict[str, Optional[str]]:
diff --git a/tests/test_config.py b/tests/test_config.py
index fa08258..4571946 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -15,13 +15,78 @@ def test_default():
assert c.version == version
assert len(c.shells) == 1
assert c.shells[0] == Path("/bin/bash")
- assert c.projects is None
+ assert c.projects == []
assert c.environment == {}
assert c.storage.directory == Path("/var/local/kiwi")
assert c.network.name == "kiwi_hub"
assert c.network.cidr == IPv4Network("10.22.46.0/24")
+##########
+# PROJECTS
+##########
+
+def test_proj_empty():
+ c = Config(projects=None)
+
+ assert c.projects == []
+
+ c = Config(projects=[])
+
+ assert c.projects == []
+
+
+def test_proj_long():
+ c = Config(projects=[{
+ "name": "project",
+ "enabled": False,
+ "override_storage": {"directory": "/test/directory"},
+ }])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
+ assert p.override_storage.directory == Path("/test/directory")
+
+
+def test_proj_short():
+ c = Config(projects=[{
+ "project": False,
+ }])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is None
+
+
+def test_proj_dict():
+ c = Config(projects={"name": "project"})
+
+ assert c == Config(projects=[{"name": "project"}])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert p.enabled
+ assert p.override_storage is None
+
+
+def test_proj_name():
+ c = Config(projects="project")
+
+ assert c == Config(projects=["project"])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert p.enabled
+ assert p.override_storage is None
+
+
#############
# ENVIRONMENT
#############
@@ -37,9 +102,7 @@ def test_env_dict():
assert c.environment == {}
- c = Config(environment={
- "variable": "value"
- })
+ c = Config(environment={"variable": "value"})
assert len(c.environment) == 1
assert "variable" in c.environment
From 83ee4cee325c22314f6977b6be100e674bc9491b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 19:37:41 +0200
Subject: [PATCH 011/135] CI pytests
---
.drone.yml | 7 +++++++
Dockerfile | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/.drone.yml b/.drone.yml
index ecd6889..a0177de 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -3,6 +3,13 @@ kind: pipeline
name: default
steps:
+- name: pytest
+ image: python:3.6-alpine3.13
+ commands:
+ - wget -O- https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
+ - source $HOME/.poetry/env && poetry install
+ - source $HOME/.poetry/env && poetry run pytest
+
- name: docker
image: plugins/docker
settings:
diff --git a/Dockerfile b/Dockerfile
index dd4db5d..5a33c84 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,6 @@ RUN set -ex; \
COPY . /usr/src/kiwi_scp
RUN set -ex; \
- pip3 --use-feature=in-tree-build install /usr/src/kiwi_scp
+ pip3 --no-cache-dir --use-feature=in-tree-build install /usr/src/kiwi_scp
ENTRYPOINT ["kiwi"]
From 0adc2c1923a362f52c9ed35a326848f73aae103b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 19:39:28 +0200
Subject: [PATCH 012/135] poetry.lock
---
poetry.lock | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 151 insertions(+), 1 deletion(-)
diff --git a/poetry.lock b/poetry.lock
index 4a2f0af..1e00a2e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,25 @@
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+
[[package]]
name = "backports.entry-points-selectable"
version = "1.1.0"
@@ -13,6 +35,14 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
[[package]]
name = "dataclasses"
version = "0.8"
@@ -73,6 +103,25 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "packaging"
+version = "21.0"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
[[package]]
name = "platformdirs"
version = "2.4.0"
@@ -85,6 +134,29 @@ python-versions = ">=3.6"
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
[[package]]
name = "pydantic"
version = "1.8.2"
@@ -101,6 +173,36 @@ typing-extensions = ">=3.7.4.3"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
[[package]]
name = "pyyaml"
version = "5.4.1"
@@ -117,6 +219,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
@@ -161,13 +271,25 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "c449a1d6048636533037af38cb647b63b19f8027b0a12d82408dd05ed4164c06"
+content-hash = "7343a635c1769b0dda5c30d310a00bf6c4dac0d64f26ae2076b9b3e6fde69700"
[metadata.files]
+atomicwrites = [
+ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
"backports.entry-points-selectable" = [
{file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
{file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
]
+colorama = [
+ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
@@ -188,10 +310,26 @@ importlib-resources = [
{file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"},
{file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"},
]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+packaging = [
+ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+]
platformdirs = [
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+py = [
+ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
pydantic = [
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
@@ -216,6 +354,14 @@ pydantic = [
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
+pyparsing = [
+ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -251,6 +397,10 @@ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
From 8a28730954147c479377ce1f22db0c534e81a062 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 14 Oct 2021 19:51:52 +0200
Subject: [PATCH 013/135] install-poetry script
---
.drone.yml | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/.drone.yml b/.drone.yml
index a0177de..d0bae99 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -6,9 +6,10 @@ steps:
- name: pytest
image: python:3.6-alpine3.13
commands:
- - wget -O- https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 -
- - source $HOME/.poetry/env && poetry install
- - source $HOME/.poetry/env && poetry run pytest
+ - apk add --no-cache g++ libffi-dev
+ - wget -O- https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 -
+ - /root/.local/bin/poetry install
+ - /root/.local/bin/poetry run pytest
- name: docker
image: plugins/docker
From 243fee3f23ce5b00624cb170b0437ca4e6138e28 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 15 Oct 2021 19:39:14 +0200
Subject: [PATCH 014/135] pytest shells
---
kiwi_scp/config.py | 66 ++++++++++++++++++-------
tests/test_config.py | 112 ++++++++++++++++++++++++++++++++++++++-----
2 files changed, 150 insertions(+), 28 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 44fc137..23c471e 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -48,7 +48,7 @@ class _Project(BaseModel):
@root_validator(pre=True)
@classmethod
- def unify_project(cls, values):
+ def unify_project(cls, values) -> Dict[str, Any]:
"""parse different project notations"""
if "name" in values:
@@ -91,7 +91,7 @@ class Config(BaseModel):
version: constr(regex=RE_SEMVER) = "0.2.0"
- shells: Optional[List[Path]] = [
+ shells: List[Path] = [
Path("/bin/bash"),
]
@@ -152,9 +152,32 @@ class Config(BaseModel):
return yml_string
+ @validator("shells", pre=True)
+ @classmethod
+ def unify_shells(cls, value) -> List[str]:
+ """parse different shells notations"""
+
+ if value is None:
+ return []
+
+ elif isinstance(value, list):
+ return value
+
+ elif isinstance(value, dict):
+ return list(value)
+
+ else:
+ # any other format (try to coerce to str first)
+ try:
+ return [str(value)]
+
+ except Exception as e:
+ # undefined format
+ raise ValueError("Invalid Shells Format")
+
@validator("projects", pre=True)
@classmethod
- def unify_projects(cls, value):
+ def unify_projects(cls, value) -> List[Dict[str, str]]:
"""parse different projects notations"""
if value is None:
@@ -173,8 +196,13 @@ class Config(BaseModel):
result.append(entry)
else:
- # handle single project name
- result.append({"name": str(entry)})
+ try:
+ # handle single project name
+ result.append({"name": str(entry)})
+
+ except Exception as e:
+ # undefined format
+ raise ValueError("Invalid Projects Format")
return result
@@ -183,8 +211,14 @@ class Config(BaseModel):
return [value]
else:
- # handle single project name
- return [{"name": str(value)}]
+ # any other format (try to coerce to str first)
+ try:
+ # handle as a single project name
+ return [{"name": str(value)}]
+
+ except Exception as e:
+ # undefined format
+ raise ValueError("Invalid Projects Format")
@validator("environment", pre=True)
@classmethod
@@ -215,20 +249,20 @@ class Config(BaseModel):
result: Dict[str, Optional[str]] = {}
for item in value:
- key, value = parse_str(str(item))
- result[key] = value
+ try:
+ key, value = parse_str(str(item))
+ result[key] = value
+
+ except Exception as e:
+ # undefined format
+ raise ValueError("Invalid Environment Format")
return result
- elif isinstance(value, str):
- # string format (single variable):
- # "="
-
- key, value = parse_str(value)
- return {key: value}
-
else:
# any other format (try to coerce to str first)
+ # string format (single variable):
+ # "="
try:
key, value = parse_str(str(value))
return {key: value}
diff --git a/tests/test_config.py b/tests/test_config.py
index 4571946..8acc3ec 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -8,6 +8,12 @@ from pydantic import ValidationError
from kiwi_scp.config import Config
+class UnCoercible:
+ """A class that doesn't have a string representation"""
+ def __str__(self):
+ raise ValueError
+
+
def test_default():
c = Config()
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
@@ -22,6 +28,70 @@ def test_default():
assert c.network.cidr == IPv4Network("10.22.46.0/24")
+########
+# SHELLS
+########
+
+def test_shells_empty():
+ c = Config(shells=None)
+
+ assert c == Config(shells=[])
+
+ assert c.shells == []
+
+
+def test_shells_list():
+ c = Config(shells=["/bin/sh", "sh"])
+
+ assert len(c.shells) == 2
+ assert c.shells[0] == Path("/bin/sh")
+ assert c.shells[1] == Path("sh")
+
+ c = Config(shells=["/bin/bash"])
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+
+
+def test_shells_dict():
+ c = Config(shells={"/bin/bash": None})
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+
+
+def test_shells_coercible():
+ c = Config(shells="/bin/bash")
+
+ assert c == Config(shells=Path("/bin/bash"))
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+
+ c = Config(shells=123)
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("123")
+
+
+def test_shells_uncoercible():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(shells=UnCoercible())
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Shells Format"
+ assert error["type"] == "value_error"
+
+ with pytest.raises(ValidationError) as exc_info:
+ Config(shells=["/bin/bash", UnCoercible()])
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "value is not a valid path"
+ assert error["type"] == "type_error.path"
+
+
##########
# PROJECTS
##########
@@ -29,9 +99,7 @@ def test_default():
def test_proj_empty():
c = Config(projects=None)
- assert c.projects == []
-
- c = Config(projects=[])
+ assert c == Config(projects=[])
assert c.projects == []
@@ -75,7 +143,7 @@ def test_proj_dict():
assert p.override_storage is None
-def test_proj_name():
+def test_proj_coercible():
c = Config(projects="project")
assert c == Config(projects=["project"])
@@ -87,6 +155,24 @@ def test_proj_name():
assert p.override_storage is None
+def test_proj_uncoercible():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(projects=["valid", UnCoercible()])
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Projects Format"
+ assert error["type"] == "value_error"
+
+ with pytest.raises(ValidationError) as exc_info:
+ Config(projects=UnCoercible())
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Projects Format"
+ assert error["type"] == "value_error"
+
+
#############
# ENVIRONMENT
#############
@@ -139,7 +225,7 @@ def test_env_list():
assert c.environment["123"] is None
-def test_env_str():
+def test_env_coercible():
c = Config(environment="variable=value")
assert len(c.environment) == 1
@@ -152,8 +238,6 @@ def test_env_str():
assert "variable" in c.environment
assert c.environment["variable"] is None
-
-def test_env_coercible():
c = Config(environment=123)
assert len(c.environment) == 1
@@ -167,11 +251,7 @@ def test_env_coercible():
assert c.environment["123.4"] is None
-def test_env_undef():
- class UnCoercible:
- def __str__(self):
- raise ValueError
-
+def test_env_uncoercible():
with pytest.raises(ValidationError) as exc_info:
Config(environment=UnCoercible())
@@ -179,3 +259,11 @@ def test_env_undef():
error = exc_info.value.errors()[0]
assert error["msg"] == "Invalid Environment Format"
assert error["type"] == "value_error"
+
+ with pytest.raises(ValidationError) as exc_info:
+ Config(environment=["valid", UnCoercible()])
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Environment Format"
+ assert error["type"] == "value_error"
From b568e449f52c275b4849589f3a359da9d37c14a8 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 20 Oct 2021 03:05:32 +0200
Subject: [PATCH 015/135] pydantic + pytest for config.version, config.storage
and config.network
---
kiwi_scp/_constants.py | 3 +-
kiwi_scp/config.py | 90 +++++++++++++++++----
tests/test_config.py | 173 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 248 insertions(+), 18 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index e0f23c2..8cdae2f 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -1,11 +1,10 @@
-# system
import os
#############
# REGEX PARTS
# regex part for a number with no leading zeroes
-_RE_NUMBER: str = r"[0-9]|[1-9][0-9]*"
+_RE_NUMBER: str = r"(?:0|[1-9][0-9]*)"
# regex for a semantic version string
RE_SEMVER = rf"^{_RE_NUMBER}(?:\.{_RE_NUMBER}(?:\.{_RE_NUMBER})?)?$"
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 23c471e..434b984 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -26,6 +26,19 @@ class _Storage(BaseModel):
return {"directory": str(self.directory)}
+ @root_validator(pre=True)
+ @classmethod
+ def unify_storage(cls, values) -> Dict[str, Any]:
+ """parse different storage notations"""
+
+ if "directory" in values:
+ # default format
+ return values
+
+ else:
+ # undefined format
+ raise ValueError("Invalid Storage Format")
+
class _Project(BaseModel):
"""a project subsection"""
@@ -46,6 +59,21 @@ class _Project(BaseModel):
result["override_storage"] = self.override_storage.kiwi_dict
return result
+ @validator("override_storage", pre=True)
+ @classmethod
+ def unify_storage(cls, value):
+ if value is None or isinstance(value, dict):
+ return value
+
+ elif isinstance(value, str):
+ return {"directory": value}
+
+ elif isinstance(value, list) and len(value) == 1:
+ return {"directory": value[0]}
+
+ else:
+ raise ValueError("Invalid Storage Format")
+
@root_validator(pre=True)
@classmethod
def unify_project(cls, values) -> Dict[str, Any]:
@@ -67,7 +95,7 @@ class _Project(BaseModel):
else:
# undefined format
- raise ValueError
+ raise ValueError("Invalid Project Format")
class _Network(BaseModel):
@@ -225,10 +253,15 @@ class Config(BaseModel):
def unify_environment(cls, value) -> Dict[str, Optional[str]]:
"""parse different environment notations"""
- def parse_str(var_val: str) -> (str, Optional[str]):
+ def parse_str(var_val: Any) -> (str, Optional[str]):
"""parse a "=" string"""
- idx = var_val.find("=")
+ try:
+ idx = str(var_val).find("=")
+ except Exception:
+ # undefined format
+ raise ValueError("Invalid Environment Format")
+
if idx == -1:
# don't split, just define the variable
return var_val, None
@@ -249,13 +282,8 @@ class Config(BaseModel):
result: Dict[str, Optional[str]] = {}
for item in value:
- try:
- key, value = parse_str(str(item))
- result[key] = value
-
- except Exception as e:
- # undefined format
- raise ValueError("Invalid Environment Format")
+ key, value = parse_str(item)
+ result[key] = value
return result
@@ -263,10 +291,40 @@ class Config(BaseModel):
# any other format (try to coerce to str first)
# string format (single variable):
# "="
- try:
- key, value = parse_str(str(value))
- return {key: value}
+ key, value = parse_str(value)
+ return {key: value}
+
+ @validator("storage", pre=True)
+ @classmethod
+ def unify_storage(cls, value):
+ """parse different storage notations"""
+
+ if value is None:
+ raise ValueError("No Storage Given")
+
+ elif isinstance(value, dict):
+ return value
+
+ elif isinstance(value, str):
+ return {"directory": value}
+
+ elif isinstance(value, list) and len(value) == 1:
+ return {"directory": value[0]}
+
+ else:
+ raise ValueError("Invalid Storage Format")
+
+ @validator("network", pre=True)
+ @classmethod
+ def unify_network(cls, value):
+ """parse different network notations"""
+
+ if value is None:
+ raise ValueError("No Network Given")
+
+ elif isinstance(value, dict):
+ return value
+
+ else:
+ raise ValueError("Invalid Network Format")
- except Exception as e:
- # undefined format
- raise ValueError("Invalid Environment Format")
diff --git a/tests/test_config.py b/tests/test_config.py
index 8acc3ec..e00f2fb 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -10,6 +10,7 @@ from kiwi_scp.config import Config
class UnCoercible:
"""A class that doesn't have a string representation"""
+
def __str__(self):
raise ValueError
@@ -28,6 +29,53 @@ def test_default():
assert c.network.cidr == IPv4Network("10.22.46.0/24")
+#########
+# VERSION
+#########
+
+def test_version_valid():
+ c = Config(version="0.0.0")
+
+ assert c.version == "0.0.0"
+
+ c = Config(version="0.0")
+
+ assert c.version == "0.0"
+
+ c = Config(version="0")
+
+ assert c.version == "0"
+
+ c = Config(version=1.0)
+
+ assert c.version == "1.0"
+
+ c = Config(version=1)
+
+ assert c.version == "1"
+
+
+def test_version_invalid():
+ # definitely not a version
+ with pytest.raises(ValidationError) as exc_info:
+ Config(version="dnaf")
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"].find("string does not match regex") != -1
+ assert error["type"] == "value_error.str.regex"
+
+ # barely a version
+ with pytest.raises(ValidationError) as exc_info:
+ c = Config(version="0.0.0alpha")
+ print(c.version)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"].find("string does not match regex") != -1
+ assert error["type"] == "value_error.str.regex"
+
+
########
# SHELLS
########
@@ -143,6 +191,19 @@ def test_proj_dict():
assert p.override_storage is None
+def test_proj_invalid_dict():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(projects={
+ "random key 1": "random value 1",
+ "random key 2": "random value 2",
+ })
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Project Format"
+ assert error["type"] == "value_error"
+
+
def test_proj_coercible():
c = Config(projects="project")
@@ -267,3 +328,115 @@ def test_env_uncoercible():
error = exc_info.value.errors()[0]
assert error["msg"] == "Invalid Environment Format"
assert error["type"] == "value_error"
+
+
+#########
+# STORAGE
+#########
+
+def test_storage_empty():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(storage=None)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "No Storage Given"
+ assert error["type"] == "value_error"
+
+
+def test_storage_dict():
+ c = Config(storage={"directory": "/test/directory"})
+
+ assert c.storage.directory == Path("/test/directory")
+
+
+def test_storage_invalid_dict():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(storage={"random key": "random value"})
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
+
+
+def test_storage_str():
+ c = Config(storage="/test/directory")
+
+ assert c.storage.directory == Path("/test/directory")
+
+
+def test_storage_list():
+ c = Config(storage=["/test/directory"])
+
+ assert c.storage.directory == Path("/test/directory")
+
+
+def test_storage_invalid():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(storage=True)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
+
+
+#########
+# NETWORK
+#########
+
+def test_network_empty():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(network=None)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "No Network Given"
+ assert error["type"] == "value_error"
+
+
+def test_network_dict():
+ c = Config(network={
+ "name": "test_hub",
+ "cidr": "1.2.3.4/32",
+ })
+
+ assert c == Config(network={
+ "name": "TEST_HUB",
+ "cidr": "1.2.3.4/32",
+ })
+
+ assert c.network.name == "test_hub"
+ assert c.network.cidr == IPv4Network("1.2.3.4/32")
+
+
+def test_network_invalid_dict():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(network={"name": "test_hub"})
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "field required"
+ assert error["type"] == "value_error.missing"
+
+ with pytest.raises(ValidationError) as exc_info:
+ Config(network={
+ "name": "test_hub",
+ "cidr": "1.2.3.4/123",
+ })
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "value is not a valid IPv4 network"
+ assert error["type"] == "value_error.ipv4network"
+
+
+def test_network_invalid():
+ with pytest.raises(ValidationError) as exc_info:
+ Config(network=True)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Network Format"
+ assert error["type"] == "value_error"
From fdedf20ba3953bd6649ea6a0f02e2442ad086774 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 20 Oct 2021 03:11:33 +0200
Subject: [PATCH 016/135] DOC
---
kiwi_scp/config.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 434b984..85efa01 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -62,6 +62,8 @@ class _Project(BaseModel):
@validator("override_storage", pre=True)
@classmethod
def unify_storage(cls, value):
+ """parse different storage notations"""
+
if value is None or isinstance(value, dict):
return value
@@ -300,18 +302,23 @@ class Config(BaseModel):
"""parse different storage notations"""
if value is None:
+ # empty storage
raise ValueError("No Storage Given")
elif isinstance(value, dict):
+ # native dict format
return value
elif isinstance(value, str):
+ # just the directory string
return {"directory": value}
- elif isinstance(value, list) and len(value) == 1:
+ elif isinstance(value, list) and len(value) == 1 and isinstance(value[0], str):
+ # directory string as a single-item list
return {"directory": value[0]}
else:
+ # undefined format
raise ValueError("Invalid Storage Format")
@validator("network", pre=True)
@@ -320,11 +327,14 @@ class Config(BaseModel):
"""parse different network notations"""
if value is None:
+ # empty network
raise ValueError("No Network Given")
elif isinstance(value, dict):
+ # native dict format
return value
else:
+ # undefined format
raise ValueError("Invalid Network Format")
From 217a5fa75b67f8ea381dae1357a4898bdddbb6ad Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 20 Oct 2021 08:31:36 +0200
Subject: [PATCH 017/135] simple "complex type" $click_ interface
---
.idea/runConfigurations/kiwi_next.xml | 4 ++--
kiwi_scp/commands/__init__.py | 0
kiwi_scp/commands/cli.py | 20 ++++++++++++++++++++
kiwi_scp/commands/cmd_init.py | 21 +++++++++++++++++++++
kiwi_scp/scripts/kiwi_next.py | 19 +++++++++++++------
poetry.lock | 24 ++++++++++++++++++++----
pyproject.toml | 1 +
7 files changed, 77 insertions(+), 12 deletions(-)
create mode 100644 kiwi_scp/commands/__init__.py
create mode 100644 kiwi_scp/commands/cli.py
create mode 100644 kiwi_scp/commands/cmd_init.py
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
index b4babb9..ad46889 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -7,12 +7,12 @@
-
+
-
+
diff --git a/kiwi_scp/commands/__init__.py b/kiwi_scp/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
new file mode 100644
index 0000000..03e8e62
--- /dev/null
+++ b/kiwi_scp/commands/cli.py
@@ -0,0 +1,20 @@
+import os
+
+import click
+
+
+class KiwiCLI(click.MultiCommand):
+ def list_commands(self, ctx):
+ result = []
+ for filename in os.listdir(os.path.abspath(os.path.dirname(__file__))):
+ if filename.startswith("cmd_") and filename.endswith(".py"):
+ result.append(filename[4:-3])
+ result.sort()
+ return result
+
+ def get_command(self, ctx, name):
+ try:
+ mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["cmd"])
+ except ImportError:
+ return
+ return mod.cmd
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
new file mode 100644
index 0000000..9f61706
--- /dev/null
+++ b/kiwi_scp/commands/cmd_init.py
@@ -0,0 +1,21 @@
+import click
+
+from ..config import Config
+
+
+@click.command(
+ "init",
+ short_help="Initializes a repo."
+)
+@click.argument(
+ "path",
+ required=False,
+ type=click.Path(resolve_path=True)
+)
+@click.pass_context
+def cmd(ctx, path):
+ """Initializes a repository."""
+ kiwi: Config = ctx.obj["cfg"]
+ click.echo("Hello init")
+ click.echo(kiwi.kiwi_yml)
+ pass
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
index 0885d92..de7c32c 100644
--- a/kiwi_scp/scripts/kiwi_next.py
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -1,14 +1,21 @@
-from kiwi_scp.config import Config
+import click
import yaml
+from kiwi_scp.commands.cli import KiwiCLI
+from kiwi_scp.config import Config
-def main():
- with open("./example/kiwi.yml") as kc:
+
+@click.command(cls=KiwiCLI)
+@click.pass_context
+def main(ctx):
+ """A complex command line interface."""
+
+ with open("./kiwi.yml") as kc:
yml = yaml.safe_load(kc)
- kiwi = Config(**yml)
+ ctx.ensure_object(dict)
+ ctx.obj["cfg"] = Config(**yml)
- print(repr(kiwi))
- print(kiwi.kiwi_yml)
+ click.echo("Hello main")
if __name__ == "__main__":
diff --git a/poetry.lock b/poetry.lock
index 1e00a2e..978f60e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -35,11 +35,23 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+[[package]]
+name = "click"
+version = "8.0.3"
+description = "Composable command line interface toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -75,7 +87,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co
name = "importlib-metadata"
version = "4.8.1"
description = "Read metadata from Python packages"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -260,7 +272,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)",
name = "zipp"
version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=3.6"
@@ -271,7 +283,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "7343a635c1769b0dda5c30d310a00bf6c4dac0d64f26ae2076b9b3e6fde69700"
+content-hash = "eb1a3ab9af78ac7898062245858dbf9e9a27e82a6484f45f14b8db6c7fe812d6"
[metadata.files]
atomicwrites = [
@@ -286,6 +298,10 @@ attrs = [
{file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
{file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
]
+click = [
+ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
+ {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
+]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
diff --git a/pyproject.toml b/pyproject.toml
index 85844ed..1b61ceb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,6 +8,7 @@ authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
python = "^3.6.1"
PyYAML = "^5.4.1"
pydantic = "^1.8.2"
+click = "^8.0.3"
[tool.poetry.dev-dependencies]
virtualenv = "^20.8.1"
From 195fbd24febca442739bd9be5bbbf9212f55f937 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 20 Oct 2021 10:54:41 +0200
Subject: [PATCH 018/135] more well-defined CLI with custom mutable context
---
kiwi_scp/commands/cli.py | 37 +++++++++++++++++++++++++++++------
kiwi_scp/commands/cmd_init.py | 15 +++++++-------
kiwi_scp/config.py | 23 ++++++++++++++++++----
kiwi_scp/scripts/kiwi_next.py | 13 +++---------
poetry.lock | 18 ++++++++---------
pyproject.toml | 7 ++++---
6 files changed, 73 insertions(+), 40 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 03e8e62..7b67478 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,20 +1,45 @@
import os
+from pathlib import Path
+import attr
import click
+from ..config import Config
+
class KiwiCLI(click.MultiCommand):
+ """Command Line Interface spread over multiple files in this directory"""
+
def list_commands(self, ctx):
- result = []
- for filename in os.listdir(os.path.abspath(os.path.dirname(__file__))):
- if filename.startswith("cmd_") and filename.endswith(".py"):
- result.append(filename[4:-3])
- result.sort()
- return result
+ """list all the commands defined by cmd_*.py files in this directory"""
+
+ return (
+ filename[4:-3]
+ for filename in os.listdir(os.path.abspath(os.path.dirname(__file__)))
+ if filename.startswith("cmd_") and filename.endswith(".py")
+ )
def get_command(self, ctx, name):
+ """import and return a specific command"""
+
try:
mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["cmd"])
except ImportError:
return
return mod.cmd
+
+
+@attr.s
+class KiwiCTX:
+ """this class is used as the commands' shared context"""
+
+ instance: Path = attr.ib(factory=lambda: Path('.'))
+
+ @property
+ def config(self) -> Config:
+ """shorthand: get the current configuration"""
+
+ return Config.from_instance(self.instance)
+
+
+pass_kiwi_ctx = click.make_pass_decorator(KiwiCTX, ensure=True)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 9f61706..2e38817 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -1,21 +1,20 @@
import click
-from ..config import Config
+from .cli import KiwiCTX, pass_kiwi_ctx
@click.command(
"init",
- short_help="Initializes a repo."
+ short_help="Initializes kiwi-scp"
)
@click.argument(
"path",
required=False,
type=click.Path(resolve_path=True)
)
-@click.pass_context
-def cmd(ctx, path):
- """Initializes a repository."""
- kiwi: Config = ctx.obj["cfg"]
- click.echo("Hello init")
- click.echo(kiwi.kiwi_yml)
+@pass_kiwi_ctx
+def cmd(ctx: KiwiCTX, path):
+ """Initialize or reconfigure a kiwi-scp instance"""
+
+ click.echo(f"Hello init, kiwi version {ctx.config.version}")
pass
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 85efa01..025185d 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,3 +1,4 @@
+import functools
import re
from ipaddress import IPv4Network
from pathlib import Path
@@ -6,7 +7,7 @@ from typing import Optional, Dict, List, Any
import yaml
from pydantic import BaseModel, constr, root_validator, validator
-from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME
+from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME, KIWI_CONF_NAME
# indent yaml lists
@@ -138,6 +139,20 @@ class Config(BaseModel):
cidr="10.22.46.0/24",
)
+ @classmethod
+ @functools.lru_cache(maxsize=5)
+ def from_instance(cls, instance: Path):
+ """parses an actual kiwi.yml from disk"""
+
+ try:
+ with open(instance.joinpath(KIWI_CONF_NAME)) as kc:
+ yml = yaml.safe_load(kc)
+ return cls.parse_obj(yml)
+
+ except FileNotFoundError:
+ # return the defaults if no kiwi.yml found
+ return cls()
+
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
@@ -201,7 +216,7 @@ class Config(BaseModel):
try:
return [str(value)]
- except Exception as e:
+ except Exception:
# undefined format
raise ValueError("Invalid Shells Format")
@@ -230,7 +245,7 @@ class Config(BaseModel):
# handle single project name
result.append({"name": str(entry)})
- except Exception as e:
+ except Exception:
# undefined format
raise ValueError("Invalid Projects Format")
@@ -246,7 +261,7 @@ class Config(BaseModel):
# handle as a single project name
return [{"name": str(value)}]
- except Exception as e:
+ except Exception:
# undefined format
raise ValueError("Invalid Projects Format")
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
index de7c32c..5007843 100644
--- a/kiwi_scp/scripts/kiwi_next.py
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -1,21 +1,14 @@
import click
-import yaml
from kiwi_scp.commands.cli import KiwiCLI
-from kiwi_scp.config import Config
@click.command(cls=KiwiCLI)
-@click.pass_context
-def main(ctx):
- """A complex command line interface."""
-
- with open("./kiwi.yml") as kc:
- yml = yaml.safe_load(kc)
- ctx.ensure_object(dict)
- ctx.obj["cfg"] = Config(**yml)
+def main():
+ """main entry point for command line interface"""
click.echo("Hello main")
+ pass
if __name__ == "__main__":
diff --git a/poetry.lock b/poetry.lock
index 978f60e..c35b800 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -10,7 +10,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "attrs"
version = "21.2.0"
description = "Classes Without Boilerplate"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -73,7 +73,7 @@ python-versions = "*"
[[package]]
name = "filelock"
-version = "3.3.0"
+version = "3.3.1"
description = "A platform independent file lock."
category = "dev"
optional = false
@@ -102,7 +102,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[[package]]
name = "importlib-resources"
-version = "5.2.2"
+version = "5.3.0"
description = "Read resources from Python packages"
category = "dev"
optional = false
@@ -113,7 +113,7 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
[[package]]
name = "iniconfig"
@@ -283,7 +283,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "eb1a3ab9af78ac7898062245858dbf9e9a27e82a6484f45f14b8db6c7fe812d6"
+content-hash = "872024f4d335f920d178ef6b631785f4f6908f8f9f87d970932bd54dee7fd297"
[metadata.files]
atomicwrites = [
@@ -315,16 +315,16 @@ distlib = [
{file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
]
filelock = [
- {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"},
- {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"},
+ {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"},
+ {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"},
]
importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
importlib-resources = [
- {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"},
- {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"},
+ {file = "importlib_resources-5.3.0-py3-none-any.whl", hash = "sha256:7a65eb0d8ee98eedab76e6deb51195c67f8e575959f6356a6e15fd7e1148f2a3"},
+ {file = "importlib_resources-5.3.0.tar.gz", hash = "sha256:f2e58e721b505a79abe67f5868d99f8886aec8594c962c7490d0c22925f518da"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
diff --git a/pyproject.toml b/pyproject.toml
index 1b61ceb..50cd2a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,14 +6,15 @@ authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
[tool.poetry.dependencies]
python = "^3.6.1"
-PyYAML = "^5.4.1"
-pydantic = "^1.8.2"
+attrs = "^21.2.0"
click = "^8.0.3"
+pydantic = "^1.8.2"
+PyYAML = "^5.4.1"
[tool.poetry.dev-dependencies]
-virtualenv = "^20.8.1"
pytest = "^6.2.5"
toml = "^0.10.2"
+virtualenv = "^20.8.1"
[tool.poetry.scripts]
kiwi = "kiwi_scp.scripts.kiwi:main"
From 71943a19119f7ed683b2e1a34f85d4d48d6812a8 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 20 Oct 2021 14:32:45 +0200
Subject: [PATCH 019/135] "kiwi init" as in previous version
---
kiwi_scp/commands/cmd_init.py | 52 +++++++++++++++++++++++++----
kiwi_scp/config.py | 12 +++++--
kiwi_scp/misc.py | 23 +++++++++++++
kiwi_scp/subcommands/init.py | 63 -----------------------------------
tests/test_config.py | 2 ++
5 files changed, 79 insertions(+), 73 deletions(-)
delete mode 100644 kiwi_scp/subcommands/init.py
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 2e38817..5e6126a 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -1,20 +1,58 @@
+import logging
+import os
+from ipaddress import IPv4Network
+from pathlib import Path
+
import click
from .cli import KiwiCTX, pass_kiwi_ctx
+from .._constants import KIWI_CONF_NAME
+from ..config import Config
+from ..misc import user_query
@click.command(
"init",
short_help="Initializes kiwi-scp"
)
-@click.argument(
- "path",
- required=False,
- type=click.Path(resolve_path=True)
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"use default values even if {KIWI_CONF_NAME} is present",
+)
+@click.option(
+ "-s/-S",
+ "--show/--no-show",
+ help=f"show effective {KIWI_CONF_NAME} contents instead",
)
@pass_kiwi_ctx
-def cmd(ctx: KiwiCTX, path):
+def cmd(ctx: KiwiCTX, force: bool, show: bool):
"""Initialize or reconfigure a kiwi-scp instance"""
- click.echo(f"Hello init, kiwi version {ctx.config.version}")
- pass
+ current_config = Config() if force else ctx.config
+
+ if show:
+ # just show the currently effective kiwi.yml
+ click.echo_via_pager(current_config.kiwi_yml)
+ return
+
+ # check force switch
+ if force and os.path.isfile(KIWI_CONF_NAME):
+ logging.warning(f"Overwriting an existing '{KIWI_CONF_NAME}'!")
+
+ # build new kiwi dict
+ kiwi_dict = current_config.kiwi_dict
+ kiwi_dict.update({
+ "version": user_query("kiwi-scp version to use in this instance", current_config.version),
+ "storage": {
+ "directory": user_query("local directory for service data", current_config.storage.directory, Path),
+ },
+ "network": {
+ "name": user_query("name for local network hub", current_config.network.name),
+ "cidr": user_query("CIDRv4 block for local network hub", current_config.network.cidr, IPv4Network),
+ },
+ })
+
+ # write out as new kiwi.yml
+ with open(ctx.instance.joinpath(KIWI_CONF_NAME), "w") as file:
+ file.write(Config.parse_obj(kiwi_dict).kiwi_yml)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 025185d..cb9a0de 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -142,7 +142,7 @@ class Config(BaseModel):
@classmethod
@functools.lru_cache(maxsize=5)
def from_instance(cls, instance: Path):
- """parses an actual kiwi.yml from disk"""
+ """parses an actual kiwi.yml from disk (cached)"""
try:
with open(instance.joinpath(KIWI_CONF_NAME)) as kc:
@@ -151,7 +151,14 @@ class Config(BaseModel):
except FileNotFoundError:
# return the defaults if no kiwi.yml found
- return cls()
+ return cls.from_default()
+
+ @classmethod
+ @functools.lru_cache(maxsize=1)
+ def from_default(cls):
+ """returns the default config (cached)"""
+
+ return cls()
@property
def kiwi_dict(self) -> Dict[str, Any]:
@@ -352,4 +359,3 @@ class Config(BaseModel):
else:
# undefined format
raise ValueError("Invalid Network Format")
-
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index ed91da9..7202fd3 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,3 +1,26 @@
+from typing import Any, Type
+
+import click
+
+
+def user_query(description: str, default: Any, cast_to: Type[Any] = str):
+ # prompt user as per argument
+ while True:
+ try:
+ str_value = input(f"Enter {description} [{default}] ").strip()
+ if str_value:
+ return cast_to(str_value)
+ else:
+ return default
+
+ except EOFError:
+ click.echo("Input aborted.")
+ return default
+
+ except Exception as e:
+ click.echo(f"Invalid input: {e}")
+
+
def _surround(string, bang):
midlane = f"{bang * 3} {string} {bang * 3}"
sidelane = bang * len(midlane)
diff --git a/kiwi_scp/subcommands/init.py b/kiwi_scp/subcommands/init.py
deleted file mode 100644
index 1ca9c6a..0000000
--- a/kiwi_scp/subcommands/init.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# system
-import logging
-import os
-
-# local
-from .._constants import KIWI_CONF_NAME
-from ..config import DefaultConfig, LoadedConfig
-from ..subcommand import SubCommand
-
-
-class InitCommand(SubCommand):
- """kiwi init"""
-
- def __init__(self):
- super().__init__(
- 'init',
- action=f"Initializing '{KIWI_CONF_NAME}' in",
- description="Initialize or reconfigure kiwi-scp instance"
- )
-
- # -f switch: Initialize with default config
- self._sub_parser.add_argument(
- '-f', '--force',
- action='store_true',
- help=f"use default values even if {KIWI_CONF_NAME} is present"
- )
-
- # -s switch: Show current config instead
- self._sub_parser.add_argument(
- '-s', '--show',
- action='store_true',
- help=f"show effective {KIWI_CONF_NAME} contents instead"
- )
-
- def _run_instance(self, runner, args):
- config = LoadedConfig.get()
-
- # check show switch
- if args.show:
- print(config)
- return True
-
- # check force switch
- if args.force and os.path.isfile(KIWI_CONF_NAME):
- logging.warning(f"Overwriting existing '{KIWI_CONF_NAME}'!")
- config = DefaultConfig.get()
-
- # version
- config.user_query('version')
-
- # runtime
- config.user_query('runtime:storage')
-
- # markers
- config.user_query('markers:project')
- config.user_query('markers:disabled')
-
- # network
- config.user_query('network:name')
- config.user_query('network:cidr')
-
- config.save()
- return True
diff --git a/tests/test_config.py b/tests/test_config.py
index e00f2fb..a2fdee3 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -19,6 +19,8 @@ def test_default():
c = Config()
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
+ assert c == Config.from_default()
+
assert c.version == version
assert len(c.shells) == 1
assert c.shells[0] == Path("/bin/bash")
From 1621e133609b816450164caa1e37c6ae473ffe2a Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 21 Oct 2021 04:02:28 +0200
Subject: [PATCH 020/135] decorators for instancem, project and service
commands
---
kiwi_scp/misc.py | 43 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index 7202fd3..cfa808b 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,6 +1,47 @@
-from typing import Any, Type
+from typing import Any, Type, List, Callable
+import attr
import click
+from click.decorators import FC
+
+
+@attr.s
+class _MultiDecorator:
+ options: List[Callable[[FC], FC]] = attr.ib(factory=list)
+
+ def __call__(self, target: FC):
+ for option in reversed(self.options):
+ target = option(target)
+
+ return target
+
+
+_instance_args = []
+
+instance_command = _MultiDecorator(_instance_args)
+
+_project_args = [
+ *_instance_args,
+ click.argument(
+ "project",
+ required=False,
+ type=click.Path(exists=True),
+ default=".",
+ ),
+]
+
+project_command = _MultiDecorator(_project_args)
+
+_service_args = [
+ *_project_args,
+ click.argument(
+ "service",
+ required=False,
+ type=str,
+ ),
+]
+
+service_command = _MultiDecorator(_service_args)
def user_query(description: str, default: Any, cast_to: Type[Any] = str):
From a787f515dd6796b43c4c9b9474bf2dafd0365559 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 21 Oct 2021 04:02:38 +0200
Subject: [PATCH 021/135] "kiwi show" stub
---
.idea/runConfigurations/kiwi_next.xml | 2 +-
kiwi_scp/commands/cmd_show.py | 12 ++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
create mode 100644 kiwi_scp/commands/cmd_show.py
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
index ad46889..dbd0110 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/kiwi_scp/commands/cmd_show.py b/kiwi_scp/commands/cmd_show.py
new file mode 100644
index 0000000..3a39cbb
--- /dev/null
+++ b/kiwi_scp/commands/cmd_show.py
@@ -0,0 +1,12 @@
+from pathlib import Path
+
+import click
+
+from kiwi_scp.misc import service_command
+
+
+@click.command()
+@service_command
+def cmd(project: str, service: str):
+ project = str(Path(project))
+ print(f"project: {project!r}, service: {service!r}")
From a41843c9b2d2c717c579a3d8f093addff5430dfb Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 21 Oct 2021 10:34:53 +0200
Subject: [PATCH 022/135] cleanup
---
kiwi_scp/misc.py | 37 ++++++++++++++-----------------------
1 file changed, 14 insertions(+), 23 deletions(-)
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index cfa808b..7b6038e 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -16,32 +16,23 @@ class _MultiDecorator:
return target
-_instance_args = []
+_project_arg = click.argument(
+ "project",
+ required=False,
+ type=click.Path(exists=True),
+ default=".",
+)
-instance_command = _MultiDecorator(_instance_args)
+_service_arg = click.argument(
+ "service",
+ required=False,
+ type=str,
+)
-_project_args = [
- *_instance_args,
- click.argument(
- "project",
- required=False,
- type=click.Path(exists=True),
- default=".",
- ),
-]
+instance_command = _MultiDecorator([])
+project_command = _MultiDecorator([_project_arg])
+service_command = _MultiDecorator([_project_arg, _service_arg])
-project_command = _MultiDecorator(_project_args)
-
-_service_args = [
- *_project_args,
- click.argument(
- "service",
- required=False,
- type=str,
- ),
-]
-
-service_command = _MultiDecorator(_service_args)
def user_query(description: str, default: Any, cast_to: Type[Any] = str):
From 1ef0303c41f85163ae5f6ea918932cf80a53c419 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 22 Oct 2021 17:43:29 +0200
Subject: [PATCH 023/135] typos
---
kiwi_scp/commands/cmd_init.py | 2 +-
kiwi_scp/misc.py | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 5e6126a..497e9c5 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -13,7 +13,7 @@ from ..misc import user_query
@click.command(
"init",
- short_help="Initializes kiwi-scp"
+ short_help="Initializes kiwi-scp",
)
@click.option(
"-f/-F",
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index 7b6038e..580d5ef 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -34,7 +34,6 @@ project_command = _MultiDecorator([_project_arg])
service_command = _MultiDecorator([_project_arg, _service_arg])
-
def user_query(description: str, default: Any, cast_to: Type[Any] = str):
# prompt user as per argument
while True:
From cb464052b2618c65403a886f3f123d1a312148ba Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 22 Oct 2021 17:50:26 +0200
Subject: [PATCH 024/135] Service, Project, Instance classes
---
kiwi_scp/instance.py | 72 +++++++++++++++++++++++++++++++++++++++++++
tests/test_project.py | 32 +++++++++++++++++++
2 files changed, 104 insertions(+)
create mode 100644 kiwi_scp/instance.py
create mode 100644 tests/test_project.py
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
new file mode 100644
index 0000000..41facbe
--- /dev/null
+++ b/kiwi_scp/instance.py
@@ -0,0 +1,72 @@
+import functools
+import re
+from pathlib import Path
+from typing import Iterable, List, Dict, Any
+
+import attr
+import yaml
+
+from ._constants import COMPOSE_FILE_NAME
+from .config import Config
+
+_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
+
+
+@attr.s
+class Service:
+ name: str = attr.ib()
+ configs: List[Path] = attr.ib()
+
+ @classmethod
+ def from_description(cls, name: str, description: Dict[str, Any]):
+ configs: List[Path] = []
+
+ if "volumes" in description:
+ volumes: List[str] = description["volumes"]
+
+ for volume in volumes:
+ host_part = volume.split(":")[0]
+ confdir = _RE_CONFDIR.match(host_part)
+
+ if confdir:
+ configs.append(Path(confdir.group(1)))
+
+ return cls(
+ name=name,
+ configs=configs,
+ )
+
+
+@attr.s
+class Project:
+ directory: Path = attr.ib()
+ services: List[Service] = attr.ib()
+
+ @classmethod
+ @functools.lru_cache(maxsize=10)
+ def from_directory(cls, directory: Path):
+ with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
+ yml = yaml.safe_load(cf)
+
+ return cls(
+ directory=directory,
+ services=[
+ Service.from_description(name, description)
+ for name, description in yml["services"].items()
+ ],
+ )
+
+
+@attr.s
+class Instance:
+ directory: Path = attr.ib(default=Path('.'))
+
+ @property
+ def config(self) -> Config:
+ """shorthand: get the current configuration"""
+
+ return Config.from_instance(self.directory)
+
+ @property
+ def projects(self) -> Iterable[Project]:
+ return []
diff --git a/tests/test_project.py b/tests/test_project.py
new file mode 100644
index 0000000..eccff01
--- /dev/null
+++ b/tests/test_project.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+
+from kiwi_scp.instance import Project
+
+
+def test_example():
+ p = Project.from_directory(Path("example/hello-world.project"))
+
+ assert p.directory == Path("example/hello-world.project")
+ assert len(p.services) == 5
+
+ s = p.services[0]
+ assert s.name == "greeter"
+ assert len(s.configs) == 0
+
+ s = p.services[1]
+ assert s.name == "web"
+ assert len(s.configs) == 0
+
+ s = p.services[2]
+ assert s.name == "db"
+ assert len(s.configs) == 0
+
+ s = p.services[3]
+ assert s.name == "adminer"
+ assert len(s.configs) == 0
+
+ s = p.services[4]
+ assert s.name == "another-web"
+ assert len(s.configs) == 1
+ assert s.configs[0] == Path("html/index.html")
+
From c8cf2f652fea4a13b3709cc4ff8607efe39d5585 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 22 Oct 2021 17:52:13 +0200
Subject: [PATCH 025/135] "kiwi show" -> "kiwi list"
---
.idea/runConfigurations/kiwi_next.xml | 2 +-
kiwi_scp/commands/{cmd_show.py => cmd_list.py} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename kiwi_scp/commands/{cmd_show.py => cmd_list.py} (100%)
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
index dbd0110..fe75345 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/kiwi_scp/commands/cmd_show.py b/kiwi_scp/commands/cmd_list.py
similarity index 100%
rename from kiwi_scp/commands/cmd_show.py
rename to kiwi_scp/commands/cmd_list.py
From c96e2c55d4a9616d41c3cdc5324ce0021eb74626 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 22 Oct 2021 17:52:46 +0200
Subject: [PATCH 026/135] "kiwi list" stub
---
kiwi_scp/commands/cmd_list.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 3a39cbb..819b6da 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,12 +1,13 @@
-from pathlib import Path
-
import click
from kiwi_scp.misc import service_command
-@click.command()
+@click.command(
+ "list",
+ short_help="Inspect a kiwi-scp instance",
+)
@service_command
def cmd(project: str, service: str):
- project = str(Path(project))
+ """List projects in this instance, services inside a project or service(s) inside a project"""
print(f"project: {project!r}, service: {service!r}")
From baf2119867c0c10de15cc7687bf8d460f24b624f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 22 Oct 2021 17:53:33 +0200
Subject: [PATCH 027/135] compose file name constant (for Project class)
---
kiwi_scp/_constants.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 8cdae2f..3bccffd 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -19,6 +19,8 @@ RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9\._-]*[A-Za-z0-9])$"
KIWI_ROOT = os.path.dirname(__file__)
# default name of kiwi-scp file
KIWI_CONF_NAME = os.getenv('KIWI_CONF_NAME', "kiwi.yml")
+# default name of compose files
+COMPOSE_FILE_NAME = "docker-compose.yml"
############
# FILE NAMES
From 3ac23e0baed56a6de9107bd3fdc8ff30fdd993a5 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 25 Oct 2021 12:07:49 +0200
Subject: [PATCH 028/135] import cleanup
---
tests/test_config.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/tests/test_config.py b/tests/test_config.py
index a2fdee3..6871cd2 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -2,7 +2,6 @@ from ipaddress import IPv4Network
from pathlib import Path
import pytest
-import toml
from pydantic import ValidationError
from kiwi_scp.config import Config
@@ -16,6 +15,8 @@ class UnCoercible:
def test_default():
+ import toml
+
c = Config()
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
From df55aa69d22e2c994c840880256f4ec5fce31472 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 25 Oct 2021 12:08:04 +0200
Subject: [PATCH 029/135] pytest Project, Service
---
tests/test_project.py | 30 +++++++++------------
tests/test_service.py | 62 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 17 deletions(-)
create mode 100644 tests/test_service.py
diff --git a/tests/test_project.py b/tests/test_project.py
index eccff01..cd0233d 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -1,5 +1,7 @@
from pathlib import Path
+import pytest
+
from kiwi_scp.instance import Project
@@ -7,26 +9,20 @@ def test_example():
p = Project.from_directory(Path("example/hello-world.project"))
assert p.directory == Path("example/hello-world.project")
- assert len(p.services) == 5
+ assert p.services != []
- s = p.services[0]
- assert s.name == "greeter"
- assert len(s.configs) == 0
- s = p.services[1]
- assert s.name == "web"
- assert len(s.configs) == 0
+def test_caching():
+ p = Project.from_directory(Path("example/hello-world.project"))
- s = p.services[2]
- assert s.name == "db"
- assert len(s.configs) == 0
+ assert p is Project.from_directory(Path("example/hello-world.project"))
- s = p.services[3]
- assert s.name == "adminer"
- assert len(s.configs) == 0
- s = p.services[4]
- assert s.name == "another-web"
- assert len(s.configs) == 1
- assert s.configs[0] == Path("html/index.html")
+def test_no_such_dir():
+ nonexistent_path = Path("nonexistent")
+ with pytest.raises(FileNotFoundError) as exc_info:
+ Project.from_directory(nonexistent_path)
+
+ from kiwi_scp._constants import COMPOSE_FILE_NAME
+ assert exc_info.value.filename == str(nonexistent_path.joinpath(COMPOSE_FILE_NAME))
diff --git a/tests/test_service.py b/tests/test_service.py
new file mode 100644
index 0000000..b44bba8
--- /dev/null
+++ b/tests/test_service.py
@@ -0,0 +1,62 @@
+from pathlib import Path
+
+from kiwi_scp.instance import Service
+
+
+def test_no_description():
+ s = Service.from_description(
+ name="s",
+ description={},
+ )
+
+ assert s.name == "s"
+ assert s.configs == []
+
+
+def test_no_configs():
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ },
+ )
+
+ assert s.name == "s"
+ assert s.configs == []
+
+
+def test_no_configs_in_volumes():
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ "volumes": [
+ "docker_volume/third/dir:/path/to/third/mountpoint",
+ "${TARGETDIR}/some/dir:/path/to/some/mountpoint",
+ "$TARGETDIR/other/dir:/path/to/other/mountpoint",
+ ]
+ },
+ )
+
+ assert s.name == "s"
+ assert s.configs == []
+
+
+def test_with_configs():
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ "volumes": [
+ "${CONFDIR}/some/config:/path/to/some/config",
+ "$CONFDIR/other/config:/path/to/other/config",
+ ]
+ },
+ )
+
+ assert s.name == "s"
+ assert len(s.configs) == 2
+ assert s.configs == [
+ Path("some/config"),
+ Path("other/config"),
+ ]
From 7388b3f8eac6608c6a204a28fcd636ac49a3d24c Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 25 Oct 2021 13:00:11 +0200
Subject: [PATCH 030/135] pytest Instance
---
kiwi_scp/instance.py | 9 ++++++---
tests/test_instance.py | 31 +++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+), 3 deletions(-)
create mode 100644 tests/test_instance.py
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 41facbe..12b1052 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,7 +1,7 @@
import functools
import re
from pathlib import Path
-from typing import Iterable, List, Dict, Any
+from typing import List, Dict, Any, Generator
import attr
import yaml
@@ -68,5 +68,8 @@ class Instance:
return Config.from_instance(self.directory)
@property
- def projects(self) -> Iterable[Project]:
- return []
+ def projects(self) -> Generator[Project, None, None]:
+ return (
+ Project.from_directory(self.directory.joinpath(project.name))
+ for project in self.config.projects
+ )
diff --git a/tests/test_instance.py b/tests/test_instance.py
new file mode 100644
index 0000000..60dd462
--- /dev/null
+++ b/tests/test_instance.py
@@ -0,0 +1,31 @@
+from pathlib import Path
+
+from kiwi_scp.instance import Instance
+
+
+def test_example():
+ i = Instance(Path("example"))
+
+ assert i.config is not None
+ assert len(list(i.projects)) == 1
+
+ p = next(i.projects)
+
+ assert p.directory == Path("example/hello-world.project")
+
+
+def test_empty():
+ i = Instance()
+
+ assert i.directory == Path(".")
+ assert i.config is not None
+ assert len(list(i.projects)) == 0
+
+
+def test_no_such_dir():
+ nonexistent_path = Path("nonexistent")
+ i = Instance(nonexistent_path)
+
+ assert i.directory == nonexistent_path
+ assert i.config is not None
+ assert len(list(i.projects)) == 0
From 5b9ed4259e9f932fc9b286be428489549c1298d1 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 25 Oct 2021 13:29:12 +0200
Subject: [PATCH 031/135] KiwiCTX -> Instance, "kiwi init -o"
---
kiwi_scp/commands/cli.py | 20 --------------------
kiwi_scp/commands/cmd_init.py | 27 ++++++++++++++++++++++-----
kiwi_scp/instance.py | 4 ++++
3 files changed, 26 insertions(+), 25 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 7b67478..2d1d52d 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,11 +1,7 @@
import os
-from pathlib import Path
-import attr
import click
-from ..config import Config
-
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
@@ -27,19 +23,3 @@ class KiwiCLI(click.MultiCommand):
except ImportError:
return
return mod.cmd
-
-
-@attr.s
-class KiwiCTX:
- """this class is used as the commands' shared context"""
-
- instance: Path = attr.ib(factory=lambda: Path('.'))
-
- @property
- def config(self) -> Config:
- """shorthand: get the current configuration"""
-
- return Config.from_instance(self.instance)
-
-
-pass_kiwi_ctx = click.make_pass_decorator(KiwiCTX, ensure=True)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 497e9c5..65a3ca1 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -5,9 +5,9 @@ from pathlib import Path
import click
-from .cli import KiwiCTX, pass_kiwi_ctx
from .._constants import KIWI_CONF_NAME
from ..config import Config
+from ..instance import Instance, pass_instance
from ..misc import user_query
@@ -15,6 +15,16 @@ from ..misc import user_query
"init",
short_help="Initializes kiwi-scp",
)
+@click.option(
+ "-o",
+ "--output",
+ help=f"initialize a kiwi-scp instance in another directory",
+ type=click.Path(
+ path_type=Path,
+ dir_okay=True,
+ writable=True,
+ ),
+)
@click.option(
"-f/-F",
"--force/--no-force",
@@ -25,10 +35,13 @@ from ..misc import user_query
"--show/--no-show",
help=f"show effective {KIWI_CONF_NAME} contents instead",
)
-@pass_kiwi_ctx
-def cmd(ctx: KiwiCTX, force: bool, show: bool):
+@pass_instance
+def cmd(ctx: Instance, output: Path, force: bool, show: bool):
"""Initialize or reconfigure a kiwi-scp instance"""
+ if output is not None:
+ ctx.directory = output
+
current_config = Config() if force else ctx.config
if show:
@@ -53,6 +66,10 @@ def cmd(ctx: KiwiCTX, force: bool, show: bool):
},
})
- # write out as new kiwi.yml
- with open(ctx.instance.joinpath(KIWI_CONF_NAME), "w") as file:
+ # ensure output directory exists
+ if not os.path.isdir(ctx.directory):
+ os.mkdir(ctx.directory)
+
+ # write out the new kiwi.yml
+ with open(ctx.directory.joinpath(KIWI_CONF_NAME), "w") as file:
file.write(Config.parse_obj(kiwi_dict).kiwi_yml)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 12b1052..edbe985 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -4,6 +4,7 @@ from pathlib import Path
from typing import List, Dict, Any, Generator
import attr
+import click
import yaml
from ._constants import COMPOSE_FILE_NAME
@@ -73,3 +74,6 @@ class Instance:
Project.from_directory(self.directory.joinpath(project.name))
for project in self.config.projects
)
+
+
+pass_instance = click.make_pass_decorator(Instance, ensure=True)
From a2186a3c25bb3a80f100b67f900d3fa281102e4b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 25 Oct 2021 15:40:39 +0200
Subject: [PATCH 032/135] pytest coverage
---
.gitignore | 2 +
.idea/runConfigurations/Tests.xml | 4 +-
kiwi_scp/config.py | 2 +-
poetry.lock | 101 +++++++++++++++++++++++++++---
pyproject.toml | 1 +
5 files changed, 98 insertions(+), 12 deletions(-)
diff --git a/.gitignore b/.gitignore
index c18dd8d..03a44c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
__pycache__/
+htmlcov/
+.coverage
\ No newline at end of file
diff --git a/.idea/runConfigurations/Tests.xml b/.idea/runConfigurations/Tests.xml
index 17c8681..532689b 100644
--- a/.idea/runConfigurations/Tests.xml
+++ b/.idea/runConfigurations/Tests.xml
@@ -10,9 +10,9 @@
-
+
-
+
\ No newline at end of file
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index cb9a0de..cc7f895 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -13,7 +13,7 @@ from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME, KIWI_CONF_
# indent yaml lists
class _KiwiDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
- return super().increase_indent(flow, False)
+ return super().increase_indent(flow, False) # pragma: no cover
class _Storage(BaseModel):
diff --git a/poetry.lock b/poetry.lock
index c35b800..5c3cb7e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -55,6 +55,20 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+[[package]]
+name = "coverage"
+version = "6.0.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
[[package]]
name = "dataclasses"
version = "0.8"
@@ -187,11 +201,14 @@ email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyparsing"
-version = "2.4.7"
+version = "3.0.1"
description = "Python parsing module"
category = "dev"
optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = ">=3.6"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
@@ -215,6 +232,21 @@ toml = "*"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+[[package]]
+name = "pytest-cov"
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+
[[package]]
name = "pyyaml"
version = "5.4.1"
@@ -239,6 +271,14 @@ category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+[[package]]
+name = "tomli"
+version = "1.2.2"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
@@ -249,7 +289,7 @@ python-versions = "*"
[[package]]
name = "virtualenv"
-version = "20.8.1"
+version = "20.9.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -258,7 +298,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
-filelock = ">=3.0.0,<4"
+filelock = ">=3.2,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""}
platformdirs = ">=2,<3"
@@ -283,7 +323,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "872024f4d335f920d178ef6b631785f4f6908f8f9f87d970932bd54dee7fd297"
+content-hash = "a5e47a9cdd079f42b70c323cf971290e0da9be66a15317b609a1f937a892fa7a"
[metadata.files]
atomicwrites = [
@@ -306,6 +346,41 @@ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
+coverage = [
+ {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
+ {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
+ {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
+ {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
+ {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
+ {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
+ {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
+ {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
+ {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
+ {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
+ {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
+ {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
+ {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
+ {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
+ {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
+ {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
+ {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
+ {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
+]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
@@ -371,13 +446,17 @@ pydantic = [
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
pyparsing = [
- {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
- {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+ {file = "pyparsing-3.0.1-py3-none-any.whl", hash = "sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"},
+ {file = "pyparsing-3.0.1.tar.gz", hash = "sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
+pytest-cov = [
+ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
+ {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
+]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -417,14 +496,18 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
+tomli = [
+ {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
+ {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
+]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
virtualenv = [
- {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"},
- {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"},
+ {file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"},
+ {file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"},
]
zipp = [
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
diff --git a/pyproject.toml b/pyproject.toml
index 50cd2a5..f65fb53 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@ PyYAML = "^5.4.1"
[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
+pytest-cov = "^3.0.0"
toml = "^0.10.2"
virtualenv = "^20.8.1"
From 3c81021f1483251407e2be7d2f82e92af19e2f68 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 12:19:02 +0200
Subject: [PATCH 033/135] pyyaml -> ruamel.yaml, 100% cov on config.py
---
example/kiwi.yml | 2 +-
kiwi_scp/commands/cmd_init.py | 2 +-
kiwi_scp/config.py | 50 ++++++++----------
kiwi_scp/misc.py | 17 +++++-
poetry.lock | 81 ++++++++++++++++-------------
pyproject.toml | 2 +-
tests/test_config.py | 98 +++++++++++++++++++++++++++++++----
7 files changed, 175 insertions(+), 77 deletions(-)
diff --git a/example/kiwi.yml b/example/kiwi.yml
index e40d97d..581505a 100644
--- a/example/kiwi.yml
+++ b/example/kiwi.yml
@@ -2,7 +2,7 @@
# kiwi-scp instance configuration #
###################################
-version: '0.2.0'
+version: 0.2.0
shells:
- /bin/bash
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 65a3ca1..c8b68e7 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -72,4 +72,4 @@ def cmd(ctx: Instance, output: Path, force: bool, show: bool):
# write out the new kiwi.yml
with open(ctx.directory.joinpath(KIWI_CONF_NAME), "w") as file:
- file.write(Config.parse_obj(kiwi_dict).kiwi_yml)
+ Config.parse_obj(kiwi_dict).dump_kiwi_yml(file)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index cc7f895..e117ff6 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,19 +1,14 @@
import functools
-import re
+import io
from ipaddress import IPv4Network
from pathlib import Path
-from typing import Optional, Dict, List, Any
+from typing import Optional, Dict, List, Any, TextIO
-import yaml
+import ruamel.yaml
from pydantic import BaseModel, constr, root_validator, validator
-from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME, KIWI_CONF_NAME
-
-
-# indent yaml lists
-class _KiwiDumper(yaml.Dumper):
- def increase_indent(self, flow=False, indentless=False):
- return super().increase_indent(flow, False) # pragma: no cover
+from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
+from .misc import _format_kiwi_yml
class _Storage(BaseModel):
@@ -75,7 +70,8 @@ class _Project(BaseModel):
return {"directory": value[0]}
else:
- raise ValueError("Invalid Storage Format")
+ # undefined format
+ return {}
@root_validator(pre=True)
@classmethod
@@ -141,12 +137,12 @@ class Config(BaseModel):
@classmethod
@functools.lru_cache(maxsize=5)
- def from_instance(cls, instance: Path):
+ def from_directory(cls, instance: Path):
"""parses an actual kiwi.yml from disk (cached)"""
try:
with open(instance.joinpath(KIWI_CONF_NAME)) as kc:
- yml = yaml.safe_load(kc)
+ yml = ruamel.yaml.round_trip_load(kc)
return cls.parse_obj(yml)
except FileNotFoundError:
@@ -184,25 +180,23 @@ class Config(BaseModel):
return result
- @property
- def kiwi_yml(self) -> str:
+ def dump_kiwi_yml(self, stream: TextIO) -> None:
"""dump a kiwi.yml file"""
- yml_string = yaml.dump(
- self.kiwi_dict,
- Dumper=_KiwiDumper,
- default_flow_style=False,
- sort_keys=False,
- )
+ yml = ruamel.yaml.YAML()
+ yml.indent(offset=2)
+ yml.dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
- # insert newline before every main key
- yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
+ @property
+ def kiwi_yml(self) -> str:
+ """get a kiwi.yml dump as a string"""
- # load header comment from file
- with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
- yml_string = stream.read() + yml_string
+ sio = io.StringIO()
+ self.dump_kiwi_yml(sio)
+ result: str = sio.getvalue()
+ sio.close()
- return yml_string
+ return result
@validator("shells", pre=True)
@classmethod
@@ -341,7 +335,7 @@ class Config(BaseModel):
else:
# undefined format
- raise ValueError("Invalid Storage Format")
+ return {}
@validator("network", pre=True)
@classmethod
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index 580d5ef..845acf1 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,9 +1,12 @@
+import re
from typing import Any, Type, List, Callable
import attr
import click
from click.decorators import FC
+from ._constants import HEADER_KIWI_CONF_NAME
+
@attr.s
class _MultiDecorator:
@@ -19,8 +22,7 @@ class _MultiDecorator:
_project_arg = click.argument(
"project",
required=False,
- type=click.Path(exists=True),
- default=".",
+ type=str,
)
_service_arg = click.argument(
@@ -52,6 +54,17 @@ def user_query(description: str, default: Any, cast_to: Type[Any] = str):
click.echo(f"Invalid input: {e}")
+def _format_kiwi_yml(yml_string: str):
+ # insert newline before every main key
+ yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
+
+ # load header comment from file
+ with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
+ yml_string = stream.read() + yml_string
+
+ return yml_string
+
+
def _surround(string, bang):
midlane = f"{bang * 3} {string} {bang * 3}"
sidelane = bang * len(midlane)
diff --git a/poetry.lock b/poetry.lock
index 5c3cb7e..eb51feb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -248,12 +248,27 @@ pytest = ">=4.6"
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
[[package]]
-name = "pyyaml"
-version = "5.4.1"
-description = "YAML parser and emitter for Python"
+name = "ruamel.yaml"
+version = "0.17.16"
+description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+python-versions = ">=3"
+
+[package.dependencies]
+"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""}
+
+[package.extras]
+docs = ["ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
+
+[[package]]
+name = "ruamel.yaml.clib"
+version = "0.2.6"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+category = "main"
+optional = false
+python-versions = ">=3.5"
[[package]]
name = "six"
@@ -323,7 +338,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "a5e47a9cdd079f42b70c323cf971290e0da9be66a15317b609a1f937a892fa7a"
+content-hash = "631c4b1d21036305a2e9c891a631e66106549e1873b91264f481ba29207789fd"
[metadata.files]
atomicwrites = [
@@ -457,36 +472,32 @@ pytest-cov = [
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
]
-pyyaml = [
- {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
- {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
- {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
- {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
- {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
- {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
- {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
- {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
- {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
- {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
- {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
- {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
- {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
- {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
- {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
- {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
- {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
- {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
- {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
- {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
- {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
- {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
- {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
- {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
- {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
- {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
- {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
- {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
- {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
+"ruamel.yaml" = [
+ {file = "ruamel.yaml-0.17.16-py3-none-any.whl", hash = "sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1"},
+ {file = "ruamel.yaml-0.17.16.tar.gz", hash = "sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33"},
+]
+"ruamel.yaml.clib" = [
+ {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
+ {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
+ {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
+ {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
+ {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
+ {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
+ {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
+ {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
+ {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
+ {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
+ {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
+ {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
+ {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
+ {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
+ {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
+ {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
+ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
+ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
+ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
+ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
+ {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
diff --git a/pyproject.toml b/pyproject.toml
index f65fb53..b9b0370 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ python = "^3.6.1"
attrs = "^21.2.0"
click = "^8.0.3"
pydantic = "^1.8.2"
-PyYAML = "^5.4.1"
+"ruamel.yaml" = "^0.17.16"
[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
diff --git a/tests/test_config.py b/tests/test_config.py
index 6871cd2..e6b6e42 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,7 +1,9 @@
+import io
from ipaddress import IPv4Network
from pathlib import Path
import pytest
+import ruamel.yaml
from pydantic import ValidationError
from kiwi_scp.config import Config
@@ -31,6 +33,28 @@ def test_default():
assert c.network.name == "kiwi_hub"
assert c.network.cidr == IPv4Network("10.22.46.0/24")
+ kiwi_dict = {
+ "version": version,
+ "shells": ["/bin/bash"],
+ "storage": {"directory": "/var/local/kiwi"},
+ "network": {
+ "name": "kiwi_hub",
+ "cidr": "10.22.46.0/24",
+ },
+ }
+ assert c.kiwi_dict == kiwi_dict
+
+ yml = ruamel.yaml.YAML()
+ yml.indent(offset=2)
+
+ sio = io.StringIO()
+ from kiwi_scp.misc import _format_kiwi_yml
+ yml.dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
+ yml_string = sio.getvalue()
+ sio.close()
+
+ assert c.kiwi_yml == yml_string
+
#########
# VERSION
@@ -156,30 +180,79 @@ def test_proj_empty():
def test_proj_long():
- c = Config(projects=[{
+ kiwi_dict = {
"name": "project",
"enabled": False,
"override_storage": {"directory": "/test/directory"},
- }])
+ }
+ c = Config(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
assert p.name == "project"
assert not p.enabled
assert p.override_storage is not None
- assert p.override_storage.directory == Path("/test/directory")
+
+ assert c.kiwi_dict["projects"][0] == kiwi_dict
+
+
+def test_proj_storage_str():
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": "/test/directory",
+ }
+ c = Config(projects=[kiwi_dict])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
+
+
+def test_proj_storage_list():
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": ["/test/directory"],
+ }
+ c = Config(projects=[kiwi_dict])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
+
+
+def test_proj_storage_invalid():
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": True,
+ }
+ with pytest.raises(ValidationError) as exc_info:
+ Config(projects=[kiwi_dict])
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
def test_proj_short():
- c = Config(projects=[{
+ kiwi_dict = {
"project": False,
- }])
+ }
+ c = Config(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
assert p.name == "project"
assert not p.enabled
assert p.override_storage is None
+ assert p.kiwi_dict == kiwi_dict
def test_proj_dict():
@@ -252,12 +325,15 @@ def test_env_dict():
assert c.environment == {}
- c = Config(environment={"variable": "value"})
+ kiwi_dict = {"variable": "value"}
+ c = Config(environment=kiwi_dict)
assert len(c.environment) == 1
assert "variable" in c.environment
assert c.environment["variable"] == "value"
+ assert c.kiwi_dict["environment"] == kiwi_dict
+
def test_env_list():
c = Config(environment=[])
@@ -348,9 +424,11 @@ def test_storage_empty():
def test_storage_dict():
- c = Config(storage={"directory": "/test/directory"})
+ kiwi_dict = {"directory": "/test/directory"}
+ c = Config(storage=kiwi_dict)
assert c.storage.directory == Path("/test/directory")
+ assert c.storage.kiwi_dict == kiwi_dict
def test_storage_invalid_dict():
@@ -400,10 +478,11 @@ def test_network_empty():
def test_network_dict():
- c = Config(network={
+ kiwi_dict = {
"name": "test_hub",
"cidr": "1.2.3.4/32",
- })
+ }
+ c = Config(network=kiwi_dict)
assert c == Config(network={
"name": "TEST_HUB",
@@ -412,6 +491,7 @@ def test_network_dict():
assert c.network.name == "test_hub"
assert c.network.cidr == IPv4Network("1.2.3.4/32")
+ assert c.network.kiwi_dict == kiwi_dict
def test_network_invalid_dict():
From 7dc6e6789fbf83ed6881c98c4b5e02fb1504c9f1 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 12:28:06 +0200
Subject: [PATCH 034/135] stray pyyaml import
---
kiwi_scp/instance.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index edbe985..26f6eba 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -5,7 +5,7 @@ from typing import List, Dict, Any, Generator
import attr
import click
-import yaml
+import ruamel.yaml
from ._constants import COMPOSE_FILE_NAME
from .config import Config
@@ -47,7 +47,7 @@ class Project:
@functools.lru_cache(maxsize=10)
def from_directory(cls, directory: Path):
with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
- yml = yaml.safe_load(cf)
+ yml = ruamel.yaml.round_trip_load(cf)
return cls(
directory=directory,
@@ -66,12 +66,15 @@ class Instance:
def config(self) -> Config:
"""shorthand: get the current configuration"""
- return Config.from_instance(self.directory)
+ return Config.from_directory(self.directory)
+
+ def get_project(self, name: str) -> Project:
+ return Project.from_directory(self.directory.joinpath(name))
@property
def projects(self) -> Generator[Project, None, None]:
return (
- Project.from_directory(self.directory.joinpath(project.name))
+ self.get_project(project.name)
for project in self.config.projects
)
From 8260ddef3cf7f44c70ce2e1b0b146cc0c3a7c52b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 15:56:58 +0200
Subject: [PATCH 035/135] Config -> KiwiConfig, _Storage -> StorageConfig,
_Network -> NetworkConfig
---
kiwi_scp/commands/cmd_init.py | 6 +-
kiwi_scp/config.py | 27 ++++----
tests/test_config.py | 118 +++++++++++++++++-----------------
3 files changed, 75 insertions(+), 76 deletions(-)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index c8b68e7..edae74e 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -6,7 +6,7 @@ from pathlib import Path
import click
from .._constants import KIWI_CONF_NAME
-from ..config import Config
+from ..config import KiwiConfig
from ..instance import Instance, pass_instance
from ..misc import user_query
@@ -42,7 +42,7 @@ def cmd(ctx: Instance, output: Path, force: bool, show: bool):
if output is not None:
ctx.directory = output
- current_config = Config() if force else ctx.config
+ current_config = KiwiConfig() if force else ctx.config
if show:
# just show the currently effective kiwi.yml
@@ -72,4 +72,4 @@ def cmd(ctx: Instance, output: Path, force: bool, show: bool):
# write out the new kiwi.yml
with open(ctx.directory.joinpath(KIWI_CONF_NAME), "w") as file:
- Config.parse_obj(kiwi_dict).dump_kiwi_yml(file)
+ KiwiConfig.parse_obj(kiwi_dict).dump_kiwi_yml(file)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index e117ff6..1b8d868 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -4,14 +4,14 @@ from ipaddress import IPv4Network
from pathlib import Path
from typing import Optional, Dict, List, Any, TextIO
-import ruamel.yaml
from pydantic import BaseModel, constr, root_validator, validator
+from ruamel.yaml import YAML
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
from .misc import _format_kiwi_yml
-class _Storage(BaseModel):
+class StorageConfig(BaseModel):
"""a storage subsection"""
directory: Path
@@ -36,12 +36,12 @@ class _Storage(BaseModel):
raise ValueError("Invalid Storage Format")
-class _Project(BaseModel):
+class ProjectConfig(BaseModel):
"""a project subsection"""
name: constr(regex=RE_VARNAME)
enabled: bool = True
- override_storage: Optional[_Storage]
+ override_storage: Optional[StorageConfig]
@property
def kiwi_dict(self) -> Dict[str, Any]:
@@ -97,7 +97,7 @@ class _Project(BaseModel):
raise ValueError("Invalid Project Format")
-class _Network(BaseModel):
+class NetworkConfig(BaseModel):
"""a network subsection"""
name: constr(to_lower=True, regex=RE_VARNAME)
@@ -113,7 +113,7 @@ class _Network(BaseModel):
}
-class Config(BaseModel):
+class KiwiConfig(BaseModel):
"""represents a kiwi.yml"""
version: constr(regex=RE_SEMVER) = "0.2.0"
@@ -122,28 +122,27 @@ class Config(BaseModel):
Path("/bin/bash"),
]
- projects: List[_Project] = []
+ projects: List[ProjectConfig] = []
environment: Dict[str, Optional[str]] = {}
- storage: _Storage = _Storage(
+ storage: StorageConfig = StorageConfig(
directory="/var/local/kiwi",
)
- network: _Network = _Network(
+ network: NetworkConfig = NetworkConfig(
name="kiwi_hub",
cidr="10.22.46.0/24",
)
@classmethod
@functools.lru_cache(maxsize=5)
- def from_directory(cls, instance: Path):
+ def from_directory(cls, directory: Path):
"""parses an actual kiwi.yml from disk (cached)"""
try:
- with open(instance.joinpath(KIWI_CONF_NAME)) as kc:
- yml = ruamel.yaml.round_trip_load(kc)
- return cls.parse_obj(yml)
+ with open(directory.joinpath(KIWI_CONF_NAME)) as kc:
+ return cls.parse_obj(YAML().load(kc))
except FileNotFoundError:
# return the defaults if no kiwi.yml found
@@ -183,7 +182,7 @@ class Config(BaseModel):
def dump_kiwi_yml(self, stream: TextIO) -> None:
"""dump a kiwi.yml file"""
- yml = ruamel.yaml.YAML()
+ yml = YAML()
yml.indent(offset=2)
yml.dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
diff --git a/tests/test_config.py b/tests/test_config.py
index e6b6e42..143b7b1 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -6,7 +6,7 @@ import pytest
import ruamel.yaml
from pydantic import ValidationError
-from kiwi_scp.config import Config
+from kiwi_scp.config import KiwiConfig
class UnCoercible:
@@ -19,10 +19,10 @@ class UnCoercible:
def test_default():
import toml
- c = Config()
+ c = KiwiConfig()
version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
- assert c == Config.from_default()
+ assert c == KiwiConfig.from_default()
assert c.version == version
assert len(c.shells) == 1
@@ -61,23 +61,23 @@ def test_default():
#########
def test_version_valid():
- c = Config(version="0.0.0")
+ c = KiwiConfig(version="0.0.0")
assert c.version == "0.0.0"
- c = Config(version="0.0")
+ c = KiwiConfig(version="0.0")
assert c.version == "0.0"
- c = Config(version="0")
+ c = KiwiConfig(version="0")
assert c.version == "0"
- c = Config(version=1.0)
+ c = KiwiConfig(version=1.0)
assert c.version == "1.0"
- c = Config(version=1)
+ c = KiwiConfig(version=1)
assert c.version == "1"
@@ -85,7 +85,7 @@ def test_version_valid():
def test_version_invalid():
# definitely not a version
with pytest.raises(ValidationError) as exc_info:
- Config(version="dnaf")
+ KiwiConfig(version="dnaf")
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -94,7 +94,7 @@ def test_version_invalid():
# barely a version
with pytest.raises(ValidationError) as exc_info:
- c = Config(version="0.0.0alpha")
+ c = KiwiConfig(version="0.0.0alpha")
print(c.version)
assert len(exc_info.value.errors()) == 1
@@ -108,42 +108,42 @@ def test_version_invalid():
########
def test_shells_empty():
- c = Config(shells=None)
+ c = KiwiConfig(shells=None)
- assert c == Config(shells=[])
+ assert c == KiwiConfig(shells=[])
assert c.shells == []
def test_shells_list():
- c = Config(shells=["/bin/sh", "sh"])
+ c = KiwiConfig(shells=["/bin/sh", "sh"])
assert len(c.shells) == 2
assert c.shells[0] == Path("/bin/sh")
assert c.shells[1] == Path("sh")
- c = Config(shells=["/bin/bash"])
+ c = KiwiConfig(shells=["/bin/bash"])
assert len(c.shells) == 1
assert c.shells[0] == Path("/bin/bash")
def test_shells_dict():
- c = Config(shells={"/bin/bash": None})
+ c = KiwiConfig(shells={"/bin/bash": None})
assert len(c.shells) == 1
assert c.shells[0] == Path("/bin/bash")
def test_shells_coercible():
- c = Config(shells="/bin/bash")
+ c = KiwiConfig(shells="/bin/bash")
- assert c == Config(shells=Path("/bin/bash"))
+ assert c == KiwiConfig(shells=Path("/bin/bash"))
assert len(c.shells) == 1
assert c.shells[0] == Path("/bin/bash")
- c = Config(shells=123)
+ c = KiwiConfig(shells=123)
assert len(c.shells) == 1
assert c.shells[0] == Path("123")
@@ -151,7 +151,7 @@ def test_shells_coercible():
def test_shells_uncoercible():
with pytest.raises(ValidationError) as exc_info:
- Config(shells=UnCoercible())
+ KiwiConfig(shells=UnCoercible())
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -159,7 +159,7 @@ def test_shells_uncoercible():
assert error["type"] == "value_error"
with pytest.raises(ValidationError) as exc_info:
- Config(shells=["/bin/bash", UnCoercible()])
+ KiwiConfig(shells=["/bin/bash", UnCoercible()])
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -172,9 +172,9 @@ def test_shells_uncoercible():
##########
def test_proj_empty():
- c = Config(projects=None)
+ c = KiwiConfig(projects=None)
- assert c == Config(projects=[])
+ assert c == KiwiConfig(projects=[])
assert c.projects == []
@@ -185,7 +185,7 @@ def test_proj_long():
"enabled": False,
"override_storage": {"directory": "/test/directory"},
}
- c = Config(projects=[kiwi_dict])
+ c = KiwiConfig(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
@@ -202,7 +202,7 @@ def test_proj_storage_str():
"enabled": False,
"override_storage": "/test/directory",
}
- c = Config(projects=[kiwi_dict])
+ c = KiwiConfig(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
@@ -217,7 +217,7 @@ def test_proj_storage_list():
"enabled": False,
"override_storage": ["/test/directory"],
}
- c = Config(projects=[kiwi_dict])
+ c = KiwiConfig(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
@@ -233,7 +233,7 @@ def test_proj_storage_invalid():
"override_storage": True,
}
with pytest.raises(ValidationError) as exc_info:
- Config(projects=[kiwi_dict])
+ KiwiConfig(projects=[kiwi_dict])
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -245,7 +245,7 @@ def test_proj_short():
kiwi_dict = {
"project": False,
}
- c = Config(projects=[kiwi_dict])
+ c = KiwiConfig(projects=[kiwi_dict])
assert len(c.projects) == 1
p = c.projects[0]
@@ -256,9 +256,9 @@ def test_proj_short():
def test_proj_dict():
- c = Config(projects={"name": "project"})
+ c = KiwiConfig(projects={"name": "project"})
- assert c == Config(projects=[{"name": "project"}])
+ assert c == KiwiConfig(projects=[{"name": "project"}])
assert len(c.projects) == 1
p = c.projects[0]
@@ -269,7 +269,7 @@ def test_proj_dict():
def test_proj_invalid_dict():
with pytest.raises(ValidationError) as exc_info:
- Config(projects={
+ KiwiConfig(projects={
"random key 1": "random value 1",
"random key 2": "random value 2",
})
@@ -281,9 +281,9 @@ def test_proj_invalid_dict():
def test_proj_coercible():
- c = Config(projects="project")
+ c = KiwiConfig(projects="project")
- assert c == Config(projects=["project"])
+ assert c == KiwiConfig(projects=["project"])
assert len(c.projects) == 1
p = c.projects[0]
@@ -294,7 +294,7 @@ def test_proj_coercible():
def test_proj_uncoercible():
with pytest.raises(ValidationError) as exc_info:
- Config(projects=["valid", UnCoercible()])
+ KiwiConfig(projects=["valid", UnCoercible()])
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -302,7 +302,7 @@ def test_proj_uncoercible():
assert error["type"] == "value_error"
with pytest.raises(ValidationError) as exc_info:
- Config(projects=UnCoercible())
+ KiwiConfig(projects=UnCoercible())
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -315,18 +315,18 @@ def test_proj_uncoercible():
#############
def test_env_empty():
- c = Config(environment=None)
+ c = KiwiConfig(environment=None)
assert c.environment == {}
def test_env_dict():
- c = Config(environment={})
+ c = KiwiConfig(environment={})
assert c.environment == {}
kiwi_dict = {"variable": "value"}
- c = Config(environment=kiwi_dict)
+ c = KiwiConfig(environment=kiwi_dict)
assert len(c.environment) == 1
assert "variable" in c.environment
@@ -336,11 +336,11 @@ def test_env_dict():
def test_env_list():
- c = Config(environment=[])
+ c = KiwiConfig(environment=[])
assert c.environment == {}
- c = Config(environment=[
+ c = KiwiConfig(environment=[
"variable=value",
])
@@ -348,7 +348,7 @@ def test_env_list():
assert "variable" in c.environment
assert c.environment["variable"] == "value"
- c = Config(environment=[
+ c = KiwiConfig(environment=[
"variable",
])
@@ -356,7 +356,7 @@ def test_env_list():
assert "variable" in c.environment
assert c.environment["variable"] is None
- c = Config(environment=[
+ c = KiwiConfig(environment=[
123,
])
@@ -366,25 +366,25 @@ def test_env_list():
def test_env_coercible():
- c = Config(environment="variable=value")
+ c = KiwiConfig(environment="variable=value")
assert len(c.environment) == 1
assert "variable" in c.environment
assert c.environment["variable"] == "value"
- c = Config(environment="variable")
+ c = KiwiConfig(environment="variable")
assert len(c.environment) == 1
assert "variable" in c.environment
assert c.environment["variable"] is None
- c = Config(environment=123)
+ c = KiwiConfig(environment=123)
assert len(c.environment) == 1
assert "123" in c.environment
assert c.environment["123"] is None
- c = Config(environment=123.4)
+ c = KiwiConfig(environment=123.4)
assert len(c.environment) == 1
assert "123.4" in c.environment
@@ -393,7 +393,7 @@ def test_env_coercible():
def test_env_uncoercible():
with pytest.raises(ValidationError) as exc_info:
- Config(environment=UnCoercible())
+ KiwiConfig(environment=UnCoercible())
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -401,7 +401,7 @@ def test_env_uncoercible():
assert error["type"] == "value_error"
with pytest.raises(ValidationError) as exc_info:
- Config(environment=["valid", UnCoercible()])
+ KiwiConfig(environment=["valid", UnCoercible()])
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -415,7 +415,7 @@ def test_env_uncoercible():
def test_storage_empty():
with pytest.raises(ValidationError) as exc_info:
- Config(storage=None)
+ KiwiConfig(storage=None)
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -425,7 +425,7 @@ def test_storage_empty():
def test_storage_dict():
kiwi_dict = {"directory": "/test/directory"}
- c = Config(storage=kiwi_dict)
+ c = KiwiConfig(storage=kiwi_dict)
assert c.storage.directory == Path("/test/directory")
assert c.storage.kiwi_dict == kiwi_dict
@@ -433,7 +433,7 @@ def test_storage_dict():
def test_storage_invalid_dict():
with pytest.raises(ValidationError) as exc_info:
- Config(storage={"random key": "random value"})
+ KiwiConfig(storage={"random key": "random value"})
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -442,20 +442,20 @@ def test_storage_invalid_dict():
def test_storage_str():
- c = Config(storage="/test/directory")
+ c = KiwiConfig(storage="/test/directory")
assert c.storage.directory == Path("/test/directory")
def test_storage_list():
- c = Config(storage=["/test/directory"])
+ c = KiwiConfig(storage=["/test/directory"])
assert c.storage.directory == Path("/test/directory")
def test_storage_invalid():
with pytest.raises(ValidationError) as exc_info:
- Config(storage=True)
+ KiwiConfig(storage=True)
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -469,7 +469,7 @@ def test_storage_invalid():
def test_network_empty():
with pytest.raises(ValidationError) as exc_info:
- Config(network=None)
+ KiwiConfig(network=None)
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -482,9 +482,9 @@ def test_network_dict():
"name": "test_hub",
"cidr": "1.2.3.4/32",
}
- c = Config(network=kiwi_dict)
+ c = KiwiConfig(network=kiwi_dict)
- assert c == Config(network={
+ assert c == KiwiConfig(network={
"name": "TEST_HUB",
"cidr": "1.2.3.4/32",
})
@@ -496,7 +496,7 @@ def test_network_dict():
def test_network_invalid_dict():
with pytest.raises(ValidationError) as exc_info:
- Config(network={"name": "test_hub"})
+ KiwiConfig(network={"name": "test_hub"})
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
@@ -504,7 +504,7 @@ def test_network_invalid_dict():
assert error["type"] == "value_error.missing"
with pytest.raises(ValidationError) as exc_info:
- Config(network={
+ KiwiConfig(network={
"name": "test_hub",
"cidr": "1.2.3.4/123",
})
@@ -517,7 +517,7 @@ def test_network_invalid_dict():
def test_network_invalid():
with pytest.raises(ValidationError) as exc_info:
- Config(network=True)
+ KiwiConfig(network=True)
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
From 0ecee93241a38befc39068291e361d7b6c44789b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 15:59:38 +0200
Subject: [PATCH 036/135] Obsolete "Project" class
---
kiwi_scp/config.py | 9 +++++++++
kiwi_scp/instance.py | 45 ++++++++++++++----------------------------
tests/test_instance.py | 11 +++++------
tests/test_project.py | 28 --------------------------
4 files changed, 29 insertions(+), 64 deletions(-)
delete mode 100644 tests/test_project.py
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 1b8d868..3d79f0b 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -155,6 +155,15 @@ class KiwiConfig(BaseModel):
return cls()
+ def get_project_config(self, name: str) -> ProjectConfig:
+ """returns the config of a project with a given name"""
+
+ for project in self.projects:
+ if project.name == name:
+ return project
+
+ raise ValueError("No Such Project")
+
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 26f6eba..c822133 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -5,10 +5,10 @@ from typing import List, Dict, Any, Generator
import attr
import click
-import ruamel.yaml
+from ruamel.yaml import YAML
from ._constants import COMPOSE_FILE_NAME
-from .config import Config
+from .config import KiwiConfig, ProjectConfig
_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
@@ -38,44 +38,29 @@ class Service:
)
-@attr.s
-class Project:
- directory: Path = attr.ib()
- services: List[Service] = attr.ib()
-
- @classmethod
- @functools.lru_cache(maxsize=10)
- def from_directory(cls, directory: Path):
- with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
- yml = ruamel.yaml.round_trip_load(cf)
-
- return cls(
- directory=directory,
- services=[
- Service.from_description(name, description)
- for name, description in yml["services"].items()
- ],
- )
-
-
@attr.s
class Instance:
directory: Path = attr.ib(default=Path('.'))
@property
- def config(self) -> Config:
+ def config(self) -> KiwiConfig:
"""shorthand: get the current configuration"""
- return Config.from_directory(self.directory)
+ return KiwiConfig.from_directory(self.directory)
- def get_project(self, name: str) -> Project:
- return Project.from_directory(self.directory.joinpath(name))
+ @classmethod
+ @functools.lru_cache(maxsize=10)
+ def _parse_compose_file(cls, directory: Path):
+ with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
+ yml = YAML()
+ return yml.load(cf)
+
+ def get_services(self, project_name: str) -> Generator[Service, None, None]:
+ yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
- @property
- def projects(self) -> Generator[Project, None, None]:
return (
- self.get_project(project.name)
- for project in self.config.projects
+ Service.from_description(name, description)
+ for name, description in yml["services"].items()
)
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 60dd462..d008748 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -7,19 +7,18 @@ def test_example():
i = Instance(Path("example"))
assert i.config is not None
- assert len(list(i.projects)) == 1
+ assert len(i.config.projects) == 1
- p = next(i.projects)
+ p = i.config.projects[0]
- assert p.directory == Path("example/hello-world.project")
+ assert p.name == "hello-world.project"
def test_empty():
i = Instance()
- assert i.directory == Path(".")
assert i.config is not None
- assert len(list(i.projects)) == 0
+ assert len(i.config.projects) == 0
def test_no_such_dir():
@@ -28,4 +27,4 @@ def test_no_such_dir():
assert i.directory == nonexistent_path
assert i.config is not None
- assert len(list(i.projects)) == 0
+ assert len(i.config.projects) == 0
diff --git a/tests/test_project.py b/tests/test_project.py
deleted file mode 100644
index cd0233d..0000000
--- a/tests/test_project.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from pathlib import Path
-
-import pytest
-
-from kiwi_scp.instance import Project
-
-
-def test_example():
- p = Project.from_directory(Path("example/hello-world.project"))
-
- assert p.directory == Path("example/hello-world.project")
- assert p.services != []
-
-
-def test_caching():
- p = Project.from_directory(Path("example/hello-world.project"))
-
- assert p is Project.from_directory(Path("example/hello-world.project"))
-
-
-def test_no_such_dir():
- nonexistent_path = Path("nonexistent")
-
- with pytest.raises(FileNotFoundError) as exc_info:
- Project.from_directory(nonexistent_path)
-
- from kiwi_scp._constants import COMPOSE_FILE_NAME
- assert exc_info.value.filename == str(nonexistent_path.joinpath(COMPOSE_FILE_NAME))
From 8d2c1e1facc2efa4002d16ee4dc2ea0ee769ecaa Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 16:11:28 +0200
Subject: [PATCH 037/135] Test classes
---
tests/test_config.py | 780 +++++++++++++++++++----------------------
tests/test_instance.py | 35 +-
tests/test_service.py | 98 +++---
3 files changed, 431 insertions(+), 482 deletions(-)
diff --git a/tests/test_config.py b/tests/test_config.py
index 143b7b1..459079c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -16,510 +16,462 @@ class UnCoercible:
raise ValueError
-def test_default():
- import toml
+class TestDefault:
+ def test(self):
+ import toml
- c = KiwiConfig()
- version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
+ c = KiwiConfig()
+ version = toml.load("./pyproject.toml")["tool"]["poetry"]["version"]
- assert c == KiwiConfig.from_default()
+ assert c == KiwiConfig.from_default()
- assert c.version == version
- assert len(c.shells) == 1
- assert c.shells[0] == Path("/bin/bash")
- assert c.projects == []
- assert c.environment == {}
- assert c.storage.directory == Path("/var/local/kiwi")
- assert c.network.name == "kiwi_hub"
- assert c.network.cidr == IPv4Network("10.22.46.0/24")
+ assert c.version == version
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+ assert c.projects == []
+ assert c.environment == {}
+ assert c.storage.directory == Path("/var/local/kiwi")
+ assert c.network.name == "kiwi_hub"
+ assert c.network.cidr == IPv4Network("10.22.46.0/24")
- kiwi_dict = {
- "version": version,
- "shells": ["/bin/bash"],
- "storage": {"directory": "/var/local/kiwi"},
- "network": {
- "name": "kiwi_hub",
- "cidr": "10.22.46.0/24",
- },
- }
- assert c.kiwi_dict == kiwi_dict
+ kiwi_dict = {
+ "version": version,
+ "shells": ["/bin/bash"],
+ "storage": {"directory": "/var/local/kiwi"},
+ "network": {
+ "name": "kiwi_hub",
+ "cidr": "10.22.46.0/24",
+ },
+ }
+ assert c.kiwi_dict == kiwi_dict
- yml = ruamel.yaml.YAML()
- yml.indent(offset=2)
+ yml = ruamel.yaml.YAML()
+ yml.indent(offset=2)
- sio = io.StringIO()
- from kiwi_scp.misc import _format_kiwi_yml
- yml.dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
- yml_string = sio.getvalue()
- sio.close()
+ sio = io.StringIO()
+ from kiwi_scp.misc import _format_kiwi_yml
+ yml.dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
+ yml_string = sio.getvalue()
+ sio.close()
- assert c.kiwi_yml == yml_string
+ assert c.kiwi_yml == yml_string
-#########
-# VERSION
-#########
+class TestVersion:
+ def test_valid(self):
+ c = KiwiConfig(version="0.0.0")
+ assert c.version == "0.0.0"
-def test_version_valid():
- c = KiwiConfig(version="0.0.0")
+ c = KiwiConfig(version="0.0")
+ assert c.version == "0.0"
- assert c.version == "0.0.0"
+ c = KiwiConfig(version="0")
+ assert c.version == "0"
- c = KiwiConfig(version="0.0")
+ c = KiwiConfig(version=1.0)
+ assert c.version == "1.0"
- assert c.version == "0.0"
+ c = KiwiConfig(version=1)
+ assert c.version == "1"
- c = KiwiConfig(version="0")
+ def test_invalid(self):
+ # definitely not a version
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(version="dnaf")
- assert c.version == "0"
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"].find("string does not match regex") != -1
+ assert error["type"] == "value_error.str.regex"
- c = KiwiConfig(version=1.0)
+ # almost a version
+ with pytest.raises(ValidationError) as exc_info:
+ c = KiwiConfig(version="0.0.0alpha")
+ print(c.version)
- assert c.version == "1.0"
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"].find("string does not match regex") != -1
+ assert error["type"] == "value_error.str.regex"
- c = KiwiConfig(version=1)
- assert c.version == "1"
+class TestShells:
+ def test_empty(self):
+ c = KiwiConfig(shells=None)
+ assert c == KiwiConfig(shells=[])
-def test_version_invalid():
- # definitely not a version
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(version="dnaf")
+ assert c.shells == []
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"].find("string does not match regex") != -1
- assert error["type"] == "value_error.str.regex"
+ def test_list(self):
+ c = KiwiConfig(shells=["/bin/sh", "sh"])
- # barely a version
- with pytest.raises(ValidationError) as exc_info:
- c = KiwiConfig(version="0.0.0alpha")
- print(c.version)
+ assert len(c.shells) == 2
+ assert c.shells[0] == Path("/bin/sh")
+ assert c.shells[1] == Path("sh")
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"].find("string does not match regex") != -1
- assert error["type"] == "value_error.str.regex"
+ c = KiwiConfig(shells=["/bin/bash"])
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+ def test_dict(self):
+ c = KiwiConfig(shells={"/bin/bash": None})
-########
-# SHELLS
-########
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+
+ def test_coercible(self):
+ c = KiwiConfig(shells="/bin/bash")
+
+ assert c == KiwiConfig(shells=Path("/bin/bash"))
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("/bin/bash")
+
+ c = KiwiConfig(shells=123)
+
+ assert len(c.shells) == 1
+ assert c.shells[0] == Path("123")
-def test_shells_empty():
- c = KiwiConfig(shells=None)
+ def test_uncoercible(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(shells=UnCoercible())
- assert c == KiwiConfig(shells=[])
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Shells Format"
+ assert error["type"] == "value_error"
- assert c.shells == []
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(shells=["/bin/bash", UnCoercible()])
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "value is not a valid path"
+ assert error["type"] == "type_error.path"
-def test_shells_list():
- c = KiwiConfig(shells=["/bin/sh", "sh"])
- assert len(c.shells) == 2
- assert c.shells[0] == Path("/bin/sh")
- assert c.shells[1] == Path("sh")
+class TestProject:
+ def test_empty(self):
+ c = KiwiConfig(projects=None)
- c = KiwiConfig(shells=["/bin/bash"])
+ assert c == KiwiConfig(projects=[])
+
+ assert c.projects == []
- assert len(c.shells) == 1
- assert c.shells[0] == Path("/bin/bash")
+ def test_long(self):
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": {"directory": "/test/directory"},
+ }
+ c = KiwiConfig(projects=[kiwi_dict])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
+
+ assert c.kiwi_dict["projects"][0] == kiwi_dict
+
+ def test_storage_str(self):
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": "/test/directory",
+ }
+ c = KiwiConfig(projects=[kiwi_dict])
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
-def test_shells_dict():
- c = KiwiConfig(shells={"/bin/bash": None})
+ def test_storage_list(self):
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": ["/test/directory"],
+ }
+ c = KiwiConfig(projects=[kiwi_dict])
- assert len(c.shells) == 1
- assert c.shells[0] == Path("/bin/bash")
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is not None
+ def test_storage_invalid(self):
+ kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ "override_storage": True,
+ }
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(projects=[kiwi_dict])
-def test_shells_coercible():
- c = KiwiConfig(shells="/bin/bash")
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
- assert c == KiwiConfig(shells=Path("/bin/bash"))
+ def test_short(self):
+ kiwi_dict = {
+ "project": False,
+ }
+ c = KiwiConfig(projects=[kiwi_dict])
- assert len(c.shells) == 1
- assert c.shells[0] == Path("/bin/bash")
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert not p.enabled
+ assert p.override_storage is None
+ assert p.kiwi_dict == kiwi_dict
+
+ def test_dict(self):
+ c = KiwiConfig(projects={"name": "project"})
+
+ assert c == KiwiConfig(projects=[{"name": "project"}])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert p.enabled
+ assert p.override_storage is None
+
+ def test_invalid_dict(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(projects={
+ "random key 1": "random value 1",
+ "random key 2": "random value 2",
+ })
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Project Format"
+ assert error["type"] == "value_error"
+
+ def test_coercible(self):
+ c = KiwiConfig(projects="project")
+
+ assert c == KiwiConfig(projects=["project"])
+
+ assert len(c.projects) == 1
+ p = c.projects[0]
+ assert p.name == "project"
+ assert p.enabled
+ assert p.override_storage is None
+
+ def test_uncoercible(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(projects=["valid", UnCoercible()])
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Projects Format"
+ assert error["type"] == "value_error"
- c = KiwiConfig(shells=123)
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(projects=UnCoercible())
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Projects Format"
+ assert error["type"] == "value_error"
- assert len(c.shells) == 1
- assert c.shells[0] == Path("123")
+class TestEnvironment:
+ def test_empty(self):
+ c = KiwiConfig(environment=None)
-def test_shells_uncoercible():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(shells=UnCoercible())
+ assert c.environment == {}
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Shells Format"
- assert error["type"] == "value_error"
+ def test_dict(self):
+ c = KiwiConfig(environment={})
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(shells=["/bin/bash", UnCoercible()])
+ assert c.environment == {}
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "value is not a valid path"
- assert error["type"] == "type_error.path"
+ kiwi_dict = {"variable": "value"}
+ c = KiwiConfig(environment=kiwi_dict)
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
-##########
-# PROJECTS
-##########
+ assert c.kiwi_dict["environment"] == kiwi_dict
-def test_proj_empty():
- c = KiwiConfig(projects=None)
+ def test_list(self):
+ c = KiwiConfig(environment=[])
- assert c == KiwiConfig(projects=[])
+ assert c.environment == {}
- assert c.projects == []
+ c = KiwiConfig(environment=[
+ "variable=value",
+ ])
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
-def test_proj_long():
- kiwi_dict = {
- "name": "project",
- "enabled": False,
- "override_storage": {"directory": "/test/directory"},
- }
- c = KiwiConfig(projects=[kiwi_dict])
+ c = KiwiConfig(environment=[
+ "variable",
+ ])
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert not p.enabled
- assert p.override_storage is not None
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] is None
- assert c.kiwi_dict["projects"][0] == kiwi_dict
+ c = KiwiConfig(environment=[
+ 123,
+ ])
+ assert len(c.environment) == 1
+ assert "123" in c.environment
+ assert c.environment["123"] is None
-def test_proj_storage_str():
- kiwi_dict = {
- "name": "project",
- "enabled": False,
- "override_storage": "/test/directory",
- }
- c = KiwiConfig(projects=[kiwi_dict])
+ def test_coercible(self):
+ c = KiwiConfig(environment="variable=value")
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert not p.enabled
- assert p.override_storage is not None
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] == "value"
+ c = KiwiConfig(environment="variable")
-def test_proj_storage_list():
- kiwi_dict = {
- "name": "project",
- "enabled": False,
- "override_storage": ["/test/directory"],
- }
- c = KiwiConfig(projects=[kiwi_dict])
+ assert len(c.environment) == 1
+ assert "variable" in c.environment
+ assert c.environment["variable"] is None
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert not p.enabled
- assert p.override_storage is not None
+ c = KiwiConfig(environment=123)
+ assert len(c.environment) == 1
+ assert "123" in c.environment
+ assert c.environment["123"] is None
-def test_proj_storage_invalid():
- kiwi_dict = {
- "name": "project",
- "enabled": False,
- "override_storage": True,
- }
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(projects=[kiwi_dict])
+ c = KiwiConfig(environment=123.4)
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
+ assert len(c.environment) == 1
+ assert "123.4" in c.environment
+ assert c.environment["123.4"] is None
+ def test_uncoercible(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(environment=UnCoercible())
-def test_proj_short():
- kiwi_dict = {
- "project": False,
- }
- c = KiwiConfig(projects=[kiwi_dict])
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Environment Format"
+ assert error["type"] == "value_error"
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert not p.enabled
- assert p.override_storage is None
- assert p.kiwi_dict == kiwi_dict
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(environment=["valid", UnCoercible()])
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Environment Format"
+ assert error["type"] == "value_error"
-def test_proj_dict():
- c = KiwiConfig(projects={"name": "project"})
- assert c == KiwiConfig(projects=[{"name": "project"}])
+class TestStorage:
+ def test_empty(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(storage=None)
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert p.enabled
- assert p.override_storage is None
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "No Storage Given"
+ assert error["type"] == "value_error"
+ def test_dict(self):
+ kiwi_dict = {"directory": "/test/directory"}
+ c = KiwiConfig(storage=kiwi_dict)
-def test_proj_invalid_dict():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(projects={
- "random key 1": "random value 1",
- "random key 2": "random value 2",
- })
+ assert c.storage.directory == Path("/test/directory")
+ assert c.storage.kiwi_dict == kiwi_dict
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Project Format"
- assert error["type"] == "value_error"
+ def test_invalid_dict(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(storage={"random key": "random value"})
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
-def test_proj_coercible():
- c = KiwiConfig(projects="project")
+ def test_str(self):
+ c = KiwiConfig(storage="/test/directory")
- assert c == KiwiConfig(projects=["project"])
+ assert c.storage.directory == Path("/test/directory")
- assert len(c.projects) == 1
- p = c.projects[0]
- assert p.name == "project"
- assert p.enabled
- assert p.override_storage is None
+ def test_list(self):
+ c = KiwiConfig(storage=["/test/directory"])
+ assert c.storage.directory == Path("/test/directory")
-def test_proj_uncoercible():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(projects=["valid", UnCoercible()])
+ def test_invalid(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(storage=True)
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Projects Format"
- assert error["type"] == "value_error"
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Storage Format"
+ assert error["type"] == "value_error"
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(projects=UnCoercible())
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Projects Format"
- assert error["type"] == "value_error"
+class TestNetwork:
+ def test_empty(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(network=None)
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "No Network Given"
+ assert error["type"] == "value_error"
-#############
-# ENVIRONMENT
-#############
-
-def test_env_empty():
- c = KiwiConfig(environment=None)
-
- assert c.environment == {}
-
-
-def test_env_dict():
- c = KiwiConfig(environment={})
-
- assert c.environment == {}
-
- kiwi_dict = {"variable": "value"}
- c = KiwiConfig(environment=kiwi_dict)
-
- assert len(c.environment) == 1
- assert "variable" in c.environment
- assert c.environment["variable"] == "value"
-
- assert c.kiwi_dict["environment"] == kiwi_dict
-
-
-def test_env_list():
- c = KiwiConfig(environment=[])
-
- assert c.environment == {}
-
- c = KiwiConfig(environment=[
- "variable=value",
- ])
-
- assert len(c.environment) == 1
- assert "variable" in c.environment
- assert c.environment["variable"] == "value"
-
- c = KiwiConfig(environment=[
- "variable",
- ])
-
- assert len(c.environment) == 1
- assert "variable" in c.environment
- assert c.environment["variable"] is None
-
- c = KiwiConfig(environment=[
- 123,
- ])
-
- assert len(c.environment) == 1
- assert "123" in c.environment
- assert c.environment["123"] is None
-
-
-def test_env_coercible():
- c = KiwiConfig(environment="variable=value")
-
- assert len(c.environment) == 1
- assert "variable" in c.environment
- assert c.environment["variable"] == "value"
-
- c = KiwiConfig(environment="variable")
-
- assert len(c.environment) == 1
- assert "variable" in c.environment
- assert c.environment["variable"] is None
-
- c = KiwiConfig(environment=123)
-
- assert len(c.environment) == 1
- assert "123" in c.environment
- assert c.environment["123"] is None
-
- c = KiwiConfig(environment=123.4)
-
- assert len(c.environment) == 1
- assert "123.4" in c.environment
- assert c.environment["123.4"] is None
-
-
-def test_env_uncoercible():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(environment=UnCoercible())
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Environment Format"
- assert error["type"] == "value_error"
-
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(environment=["valid", UnCoercible()])
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Environment Format"
- assert error["type"] == "value_error"
-
-
-#########
-# STORAGE
-#########
-
-def test_storage_empty():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(storage=None)
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "No Storage Given"
- assert error["type"] == "value_error"
-
-
-def test_storage_dict():
- kiwi_dict = {"directory": "/test/directory"}
- c = KiwiConfig(storage=kiwi_dict)
-
- assert c.storage.directory == Path("/test/directory")
- assert c.storage.kiwi_dict == kiwi_dict
-
-
-def test_storage_invalid_dict():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(storage={"random key": "random value"})
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
-
-
-def test_storage_str():
- c = KiwiConfig(storage="/test/directory")
-
- assert c.storage.directory == Path("/test/directory")
-
-
-def test_storage_list():
- c = KiwiConfig(storage=["/test/directory"])
-
- assert c.storage.directory == Path("/test/directory")
-
-
-def test_storage_invalid():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(storage=True)
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
-
-
-#########
-# NETWORK
-#########
-
-def test_network_empty():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(network=None)
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "No Network Given"
- assert error["type"] == "value_error"
-
-
-def test_network_dict():
- kiwi_dict = {
- "name": "test_hub",
- "cidr": "1.2.3.4/32",
- }
- c = KiwiConfig(network=kiwi_dict)
-
- assert c == KiwiConfig(network={
- "name": "TEST_HUB",
- "cidr": "1.2.3.4/32",
- })
-
- assert c.network.name == "test_hub"
- assert c.network.cidr == IPv4Network("1.2.3.4/32")
- assert c.network.kiwi_dict == kiwi_dict
-
-
-def test_network_invalid_dict():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(network={"name": "test_hub"})
-
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "field required"
- assert error["type"] == "value_error.missing"
-
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(network={
+ def test_dict(self):
+ kiwi_dict = {
"name": "test_hub",
- "cidr": "1.2.3.4/123",
+ "cidr": "1.2.3.4/32",
+ }
+ c = KiwiConfig(network=kiwi_dict)
+
+ assert c == KiwiConfig(network={
+ "name": "TEST_HUB",
+ "cidr": "1.2.3.4/32",
})
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "value is not a valid IPv4 network"
- assert error["type"] == "value_error.ipv4network"
+ assert c.network.name == "test_hub"
+ assert c.network.cidr == IPv4Network("1.2.3.4/32")
+ assert c.network.kiwi_dict == kiwi_dict
+ def test_invalid_dict(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(network={"name": "test_hub"})
-def test_network_invalid():
- with pytest.raises(ValidationError) as exc_info:
- KiwiConfig(network=True)
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "field required"
+ assert error["type"] == "value_error.missing"
- assert len(exc_info.value.errors()) == 1
- error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Network Format"
- assert error["type"] == "value_error"
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(network={
+ "name": "test_hub",
+ "cidr": "1.2.3.4/123",
+ })
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "value is not a valid IPv4 network"
+ assert error["type"] == "value_error.ipv4network"
+
+ def test_invalid(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(network=True)
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Invalid Network Format"
+ assert error["type"] == "value_error"
diff --git a/tests/test_instance.py b/tests/test_instance.py
index d008748..68f4280 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -3,28 +3,27 @@ from pathlib import Path
from kiwi_scp.instance import Instance
-def test_example():
- i = Instance(Path("example"))
+class TestDefault:
+ def test_example(self):
+ i = Instance(Path("example"))
- assert i.config is not None
- assert len(i.config.projects) == 1
+ assert i.config is not None
+ assert len(i.config.projects) == 1
- p = i.config.projects[0]
+ p = i.config.projects[0]
- assert p.name == "hello-world.project"
+ assert p.name == "hello-world.project"
+ def test_empty(self):
+ i = Instance()
-def test_empty():
- i = Instance()
+ assert i.config is not None
+ assert len(i.config.projects) == 0
- assert i.config is not None
- assert len(i.config.projects) == 0
+ def test_no_such_dir(self):
+ nonexistent_path = Path("nonexistent")
+ i = Instance(nonexistent_path)
-
-def test_no_such_dir():
- nonexistent_path = Path("nonexistent")
- i = Instance(nonexistent_path)
-
- assert i.directory == nonexistent_path
- assert i.config is not None
- assert len(i.config.projects) == 0
+ assert i.directory == nonexistent_path
+ assert i.config is not None
+ assert len(i.config.projects) == 0
diff --git a/tests/test_service.py b/tests/test_service.py
index b44bba8..d0198a7 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -3,60 +3,58 @@ from pathlib import Path
from kiwi_scp.instance import Service
-def test_no_description():
- s = Service.from_description(
- name="s",
- description={},
- )
+class TestDefault:
+ def test_no_description(self):
+ s = Service.from_description(
+ name="s",
+ description={},
+ )
- assert s.name == "s"
- assert s.configs == []
+ assert s.name == "s"
+ assert s.configs == []
+ def test_no_configs(self):
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ },
+ )
-def test_no_configs():
- s = Service.from_description(
- name="s",
- description={
- "image": "repo/image:tag",
- },
- )
+ assert s.name == "s"
+ assert s.configs == []
- assert s.name == "s"
- assert s.configs == []
+ def test_no_configs_in_volumes(self):
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ "volumes": [
+ "docker_volume/third/dir:/path/to/third/mountpoint",
+ "${TARGETDIR}/some/dir:/path/to/some/mountpoint",
+ "$TARGETDIR/other/dir:/path/to/other/mountpoint",
+ ]
+ },
+ )
+ assert s.name == "s"
+ assert s.configs == []
-def test_no_configs_in_volumes():
- s = Service.from_description(
- name="s",
- description={
- "image": "repo/image:tag",
- "volumes": [
- "docker_volume/third/dir:/path/to/third/mountpoint",
- "${TARGETDIR}/some/dir:/path/to/some/mountpoint",
- "$TARGETDIR/other/dir:/path/to/other/mountpoint",
- ]
- },
- )
+ def test_with_configs(self):
+ s = Service.from_description(
+ name="s",
+ description={
+ "image": "repo/image:tag",
+ "volumes": [
+ "${CONFDIR}/some/config:/path/to/some/config",
+ "$CONFDIR/other/config:/path/to/other/config",
+ ]
+ },
+ )
- assert s.name == "s"
- assert s.configs == []
-
-
-def test_with_configs():
- s = Service.from_description(
- name="s",
- description={
- "image": "repo/image:tag",
- "volumes": [
- "${CONFDIR}/some/config:/path/to/some/config",
- "$CONFDIR/other/config:/path/to/other/config",
- ]
- },
- )
-
- assert s.name == "s"
- assert len(s.configs) == 2
- assert s.configs == [
- Path("some/config"),
- Path("other/config"),
- ]
+ assert s.name == "s"
+ assert len(s.configs) == 2
+ assert s.configs == [
+ Path("some/config"),
+ Path("other/config"),
+ ]
From b8027777d95350d6b90a3996bfb5967a63dae1f0 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 26 Oct 2021 16:19:33 +0200
Subject: [PATCH 038/135] Test coverage
---
tests/test_config.py | 7 ++++++-
tests/test_instance.py | 4 ++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/tests/test_config.py b/tests/test_config.py
index 459079c..26fdd1d 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -157,9 +157,13 @@ class TestProject:
c = KiwiConfig(projects=None)
assert c == KiwiConfig(projects=[])
-
assert c.projects == []
+ with pytest.raises(ValueError) as exc_info:
+ c.get_project_config("invalid")
+
+ assert str(exc_info.value) == "No Such Project"
+
def test_long(self):
kiwi_dict = {
"name": "project",
@@ -171,6 +175,7 @@ class TestProject:
assert len(c.projects) == 1
p = c.projects[0]
assert p.name == "project"
+ assert p == c.get_project_config("project")
assert not p.enabled
assert p.override_storage is not None
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 68f4280..68799a5 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -14,6 +14,10 @@ class TestDefault:
assert p.name == "hello-world.project"
+ s = list(i.get_services(p.name))
+
+ assert len(s) == 5
+
def test_empty(self):
i = Instance()
From c331e77060457b6cec0f175a3d15f41f35121b17 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 27 Oct 2021 13:33:26 +0200
Subject: [PATCH 039/135] Added kiwi_scp.misc.YAML
---
kiwi_scp/config.py | 7 ++-----
kiwi_scp/instance.py | 7 +++----
kiwi_scp/misc.py | 7 +++++++
tests/test_config.py | 7 ++-----
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 3d79f0b..e4bf1b9 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -5,10 +5,9 @@ from pathlib import Path
from typing import Optional, Dict, List, Any, TextIO
from pydantic import BaseModel, constr, root_validator, validator
-from ruamel.yaml import YAML
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
-from .misc import _format_kiwi_yml
+from .misc import YAML, _format_kiwi_yml
class StorageConfig(BaseModel):
@@ -191,9 +190,7 @@ class KiwiConfig(BaseModel):
def dump_kiwi_yml(self, stream: TextIO) -> None:
"""dump a kiwi.yml file"""
- yml = YAML()
- yml.indent(offset=2)
- yml.dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
+ YAML().dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
@property
def kiwi_yml(self) -> str:
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index c822133..bcc2c6f 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -5,10 +5,10 @@ from typing import List, Dict, Any, Generator
import attr
import click
-from ruamel.yaml import YAML
from ._constants import COMPOSE_FILE_NAME
-from .config import KiwiConfig, ProjectConfig
+from .config import KiwiConfig
+from .misc import YAML
_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
@@ -52,8 +52,7 @@ class Instance:
@functools.lru_cache(maxsize=10)
def _parse_compose_file(cls, directory: Path):
with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
- yml = YAML()
- return yml.load(cf)
+ return YAML().load(cf)
def get_services(self, project_name: str) -> Generator[Service, None, None]:
yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index 845acf1..72d5d8c 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -3,6 +3,7 @@ from typing import Any, Type, List, Callable
import attr
import click
+import ruamel.yaml
from click.decorators import FC
from ._constants import HEADER_KIWI_CONF_NAME
@@ -54,6 +55,12 @@ def user_query(description: str, default: Any, cast_to: Type[Any] = str):
click.echo(f"Invalid input: {e}")
+class YAML(ruamel.yaml.YAML):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.indent(offset=2)
+
+
def _format_kiwi_yml(yml_string: str):
# insert newline before every main key
yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
diff --git a/tests/test_config.py b/tests/test_config.py
index 26fdd1d..1d09e05 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -3,10 +3,10 @@ from ipaddress import IPv4Network
from pathlib import Path
import pytest
-import ruamel.yaml
from pydantic import ValidationError
from kiwi_scp.config import KiwiConfig
+from kiwi_scp.misc import YAML
class UnCoercible:
@@ -45,12 +45,9 @@ class TestDefault:
}
assert c.kiwi_dict == kiwi_dict
- yml = ruamel.yaml.YAML()
- yml.indent(offset=2)
-
sio = io.StringIO()
from kiwi_scp.misc import _format_kiwi_yml
- yml.dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
+ YAML(typ="safe").dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
yml_string = sio.getvalue()
sio.close()
From 34988e58943f67d71e3d5f63daf66fa74d9dfb26 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 13:51:53 +0200
Subject: [PATCH 040/135] Test run configs
---
.../{Tests.xml => Test_Coverage.xml} | 2 +-
.idea/runConfigurations/Tests__Debuggable_.xml | 18 ++++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
rename .idea/runConfigurations/{Tests.xml => Test_Coverage.xml} (91%)
create mode 100644 .idea/runConfigurations/Tests__Debuggable_.xml
diff --git a/.idea/runConfigurations/Tests.xml b/.idea/runConfigurations/Test_Coverage.xml
similarity index 91%
rename from .idea/runConfigurations/Tests.xml
rename to .idea/runConfigurations/Test_Coverage.xml
index 532689b..6278cf1 100644
--- a/.idea/runConfigurations/Tests.xml
+++ b/.idea/runConfigurations/Test_Coverage.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/.idea/runConfigurations/Tests__Debuggable_.xml b/.idea/runConfigurations/Tests__Debuggable_.xml
new file mode 100644
index 0000000..0e8c1a9
--- /dev/null
+++ b/.idea/runConfigurations/Tests__Debuggable_.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 2a8764577f693d11ca405c7aa2b5a4296a5323c0 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 13:54:02 +0200
Subject: [PATCH 041/135] misc.YAML.dump* methods
---
kiwi_scp/config.py | 13 ++++---------
kiwi_scp/misc.py | 30 ++++++++++++++++++++++--------
tests/test_config.py | 9 +--------
3 files changed, 27 insertions(+), 25 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index e4bf1b9..48a8217 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -7,7 +7,7 @@ from typing import Optional, Dict, List, Any, TextIO
from pydantic import BaseModel, constr, root_validator, validator
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
-from .misc import YAML, _format_kiwi_yml
+from .misc import YAML
class StorageConfig(BaseModel):
@@ -187,21 +187,16 @@ class KiwiConfig(BaseModel):
return result
- def dump_kiwi_yml(self, stream: TextIO) -> None:
+ def dump_kiwi_yml(self, stream: TextIO = None) -> Optional[str]:
"""dump a kiwi.yml file"""
- YAML().dump(self.kiwi_dict, stream=stream, transform=_format_kiwi_yml)
+ return YAML().dump_kiwi_yml(self.kiwi_dict, stream=stream)
@property
def kiwi_yml(self) -> str:
"""get a kiwi.yml dump as a string"""
- sio = io.StringIO()
- self.dump_kiwi_yml(sio)
- result: str = sio.getvalue()
- sio.close()
-
- return result
+ return self.dump_kiwi_yml()
@validator("shells", pre=True)
@classmethod
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index 72d5d8c..c4895f0 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,9 +1,10 @@
import re
-from typing import Any, Type, List, Callable
+from typing import Any, Type, List, Callable, Optional
import attr
import click
import ruamel.yaml
+import ruamel.yaml.compat
from click.decorators import FC
from ._constants import HEADER_KIWI_CONF_NAME
@@ -60,16 +61,29 @@ class YAML(ruamel.yaml.YAML):
super().__init__(*args, **kwargs)
self.indent(offset=2)
+ def dump(self, data, stream=None, **kwargs) -> Optional[str]:
+ into_str: bool = False
+ if stream is None:
+ into_str = True
+ stream = ruamel.yaml.compat.StringIO()
-def _format_kiwi_yml(yml_string: str):
- # insert newline before every main key
- yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
+ super().dump(data, stream=stream, **kwargs)
+ if into_str:
+ return stream.getvalue()
- # load header comment from file
- with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
- yml_string = stream.read() + yml_string
+ @staticmethod
+ def _format_kiwi_yml(yml_string: str):
+ # insert newline before every main key
+ yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
- return yml_string
+ # load header comment from file
+ with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
+ yml_string = stream.read() + yml_string
+
+ return yml_string
+
+ def dump_kiwi_yml(self, data, **kwargs) -> Optional[str]:
+ return self.dump(data, transform=YAML._format_kiwi_yml, **kwargs)
def _surround(string, bang):
diff --git a/tests/test_config.py b/tests/test_config.py
index 1d09e05..61b78a1 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,4 +1,3 @@
-import io
from ipaddress import IPv4Network
from pathlib import Path
@@ -45,13 +44,7 @@ class TestDefault:
}
assert c.kiwi_dict == kiwi_dict
- sio = io.StringIO()
- from kiwi_scp.misc import _format_kiwi_yml
- YAML(typ="safe").dump(kiwi_dict, stream=sio, transform=_format_kiwi_yml)
- yml_string = sio.getvalue()
- sio.close()
-
- assert c.kiwi_yml == yml_string
+ assert c.kiwi_yml == YAML().dump_kiwi_yml(kiwi_dict)
class TestVersion:
From 1dcf542c6d30a6df8c50c70403b51444be3b867a Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 15:39:11 +0200
Subject: [PATCH 042/135] one less exception
---
kiwi_scp/config.py | 4 +---
tests/test_config.py | 5 +----
2 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 48a8217..97956d3 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -154,15 +154,13 @@ class KiwiConfig(BaseModel):
return cls()
- def get_project_config(self, name: str) -> ProjectConfig:
+ def get_project_config(self, name: str) -> Optional[ProjectConfig]:
"""returns the config of a project with a given name"""
for project in self.projects:
if project.name == name:
return project
- raise ValueError("No Such Project")
-
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
diff --git a/tests/test_config.py b/tests/test_config.py
index 61b78a1..d986fff 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -149,10 +149,7 @@ class TestProject:
assert c == KiwiConfig(projects=[])
assert c.projects == []
- with pytest.raises(ValueError) as exc_info:
- c.get_project_config("invalid")
-
- assert str(exc_info.value) == "No Such Project"
+ assert c.get_project_config("invalid") is None
def test_long(self):
kiwi_dict = {
From a5a0ca9a6375d11ecf22e142c84c569c851c82ed Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 15:53:32 +0200
Subject: [PATCH 043/135] basic "kiwi list" command
---
.idea/runConfigurations/kiwi_next.xml | 2 +-
.../hello-world.project/docker-compose.yml | 10 +--
kiwi_scp/commands/cmd_list.py | 14 ++++-
kiwi_scp/instance.py | 63 ++++++++++++-------
tests/test_instance.py | 9 ++-
5 files changed, 65 insertions(+), 33 deletions(-)
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi_next.xml
index fe75345..49cad48 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi_next.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/example/hello-world.project/docker-compose.yml b/example/hello-world.project/docker-compose.yml
index 9a7ea1e..cdb4328 100644
--- a/example/hello-world.project/docker-compose.yml
+++ b/example/hello-world.project/docker-compose.yml
@@ -10,20 +10,20 @@ networks:
name: ${KIWI_HUB_NAME}
services:
- # simple loop producing (rather boring) logs
greeter:
+ # simple loop producing (rather boring) logs
image: alpine:latest
command: sh -c 'LOOP=1; while :; do echo Hello World "$$LOOP"; LOOP=$$(($$LOOP + 1)); sleep 10; done'
- # basic webserver listening on localhost:8080
web:
+ # basic webserver listening on localhost:8080
build: web
restart: unless-stopped
ports:
- "8080:80"
- # internal mariadb (mysql) instance with persistent storage
db:
+ # internal mariadb (mysql) instance with persistent storage
image: mariadb:10
restart: unless-stopped
networks:
@@ -33,8 +33,8 @@ services:
volumes:
- "${TARGETDIR}/db:/var/lib/mysql"
- # admin interface for databases
adminer:
+ # admin interface for databases
image: adminer:standalone
restart: unless-stopped
networks:
@@ -45,8 +45,8 @@ services:
ports:
- "8081:8080"
- # Another webserver just to show off the ${CONFDIR} variable
another-web:
+ # Another webserver just to show off the ${CONFDIR} variable
image: nginx:stable-alpine
restart: unless-stopped
ports:
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 819b6da..0918b0c 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,13 +1,21 @@
import click
-from kiwi_scp.misc import service_command
+from ..instance import Instance, pass_instance
+from ..misc import service_command
@click.command(
"list",
short_help="Inspect a kiwi-scp instance",
)
+@pass_instance
@service_command
-def cmd(project: str, service: str):
+def cmd(ctx: Instance, project: str, service: str):
"""List projects in this instance, services inside a project or service(s) inside a project"""
- print(f"project: {project!r}, service: {service!r}")
+ if project is not None:
+ if service is not None:
+ print(f"{ctx.get_service(project, service)}")
+ else:
+ print(f"services: {ctx.get_services(project)}")
+ else:
+ print(f"projects: {ctx.config.projects}")
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index bcc2c6f..4eda01f 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,10 +1,11 @@
import functools
import re
from pathlib import Path
-from typing import List, Dict, Any, Generator
+from typing import Generator, List
import attr
import click
+from ruamel.yaml.comments import CommentedMap
from ._constants import COMPOSE_FILE_NAME
from .config import KiwiConfig
@@ -16,27 +17,40 @@ _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE
@attr.s
class Service:
name: str = attr.ib()
- configs: List[Path] = attr.ib()
+ description: CommentedMap = attr.ib()
- @classmethod
- def from_description(cls, name: str, description: Dict[str, Any]):
- configs: List[Path] = []
+ def __str__(self) -> str:
+ return YAML().dump({
+ "service": {
+ self.name: self.description
+ }
+ })
- if "volumes" in description:
- volumes: List[str] = description["volumes"]
+ @property
+ def configs(self) -> Generator[Path, None, None]:
+ if "volumes" not in self.description:
+ return
- for volume in volumes:
- host_part = volume.split(":")[0]
- confdir = _RE_CONFDIR.match(host_part)
+ for volume in self.description["volumes"]:
+ host_part = volume.split(":")[0]
+ cd_match = _RE_CONFDIR.match(host_part)
- if confdir:
- configs.append(Path(confdir.group(1)))
+ if cd_match:
+ yield cd_match.group(1)
- return cls(
- name=name,
- configs=configs,
- )
+@attr.s
+class Services:
+ project_name: str = attr.ib()
+ content: List[Service] = attr.ib()
+
+ def __str__(self) -> str:
+ return YAML().dump({
+ "services": {
+ service.name: service.description
+ for service in self.content
+ }
+ })
@attr.s
class Instance:
@@ -48,19 +62,24 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
- @classmethod
+ @staticmethod
@functools.lru_cache(maxsize=10)
- def _parse_compose_file(cls, directory: Path):
+ def _parse_compose_file(directory: Path):
with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
return YAML().load(cf)
- def get_services(self, project_name: str) -> Generator[Service, None, None]:
+ def get_services(self, project_name: str) -> Services:
yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
- return (
- Service.from_description(name, description)
+ return Services(project_name, [
+ Service(name, description)
for name, description in yml["services"].items()
- )
+ ])
+
+ def get_service(self, project_name: str, service_name: str) -> Service:
+ yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
+
+ return Service(service_name, yml["services"][service_name])
pass_instance = click.make_pass_decorator(Instance, ensure=True)
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 68799a5..8e4f47e 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -14,9 +14,14 @@ class TestDefault:
assert p.name == "hello-world.project"
- s = list(i.get_services(p.name))
+ ss = list(i.get_services(p.name))
- assert len(s) == 5
+ assert len(ss) == 5
+
+ s = ss[0]
+
+ assert s.name == "greeter"
+ assert s == i.get_service(p.name, s.name)
def test_empty(self):
i = Instance()
From ed29243b71b0a26dadbd10ff2755dd674f1c3a41 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 16:48:54 +0200
Subject: [PATCH 044/135] "Services" class
---
kiwi_scp/instance.py | 28 ++++++++++++++++------------
tests/test_instance.py | 7 +++----
tests/test_service.py | 36 +++++++++++++++++++-----------------
3 files changed, 38 insertions(+), 33 deletions(-)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 4eda01f..b22ef1c 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,7 +1,7 @@
import functools
import re
from pathlib import Path
-from typing import Generator, List
+from typing import Generator, List, Tuple, Optional
import attr
import click
@@ -24,7 +24,7 @@ class Service:
"service": {
self.name: self.description
}
- })
+ }).strip()
@property
def configs(self) -> Generator[Path, None, None]:
@@ -36,7 +36,7 @@ class Service:
cd_match = _RE_CONFDIR.match(host_part)
if cd_match:
- yield cd_match.group(1)
+ yield Path(cd_match.group(1))
@attr.s
@@ -50,7 +50,8 @@ class Services:
service.name: service.description
for service in self.content
}
- })
+ }).strip()
+
@attr.s
class Instance:
@@ -68,18 +69,21 @@ class Instance:
with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
return YAML().load(cf)
- def get_services(self, project_name: str) -> Services:
+ def get_services(self, project_name: str, service_names: Optional[Tuple[str]] = None) -> Services:
yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
-
- return Services(project_name, [
+ services = [
Service(name, description)
for name, description in yml["services"].items()
- ])
+ ]
- def get_service(self, project_name: str, service_name: str) -> Service:
- yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
-
- return Service(service_name, yml["services"][service_name])
+ if service_names is None:
+ return Services(project_name, services)
+ else:
+ return Services(project_name, [
+ service
+ for service in services
+ if service.name in service_names
+ ])
pass_instance = click.make_pass_decorator(Instance, ensure=True)
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 8e4f47e..81d8d1b 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -14,14 +14,13 @@ class TestDefault:
assert p.name == "hello-world.project"
- ss = list(i.get_services(p.name))
+ ss = i.get_services(p.name)
- assert len(ss) == 5
+ assert len(ss.content) == 5
- s = ss[0]
+ s = ss.content[0]
assert s.name == "greeter"
- assert s == i.get_service(p.name, s.name)
def test_empty(self):
i = Instance()
diff --git a/tests/test_service.py b/tests/test_service.py
index d0198a7..42244c8 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -1,60 +1,62 @@
from pathlib import Path
+from ruamel.yaml import CommentedMap
+
from kiwi_scp.instance import Service
class TestDefault:
- def test_no_description(self):
- s = Service.from_description(
+ def test_empty(self):
+ s = Service(
name="s",
- description={},
+ description=CommentedMap(),
)
assert s.name == "s"
- assert s.configs == []
+ assert list(s.configs) == []
def test_no_configs(self):
- s = Service.from_description(
+ s = Service(
name="s",
- description={
+ description=CommentedMap({
"image": "repo/image:tag",
- },
+ }),
)
assert s.name == "s"
- assert s.configs == []
+ assert list(s.configs) == []
def test_no_configs_in_volumes(self):
- s = Service.from_description(
+ s = Service(
name="s",
- description={
+ description=CommentedMap({
"image": "repo/image:tag",
"volumes": [
"docker_volume/third/dir:/path/to/third/mountpoint",
"${TARGETDIR}/some/dir:/path/to/some/mountpoint",
"$TARGETDIR/other/dir:/path/to/other/mountpoint",
]
- },
+ }),
)
assert s.name == "s"
- assert s.configs == []
+ assert list(s.configs) == []
def test_with_configs(self):
- s = Service.from_description(
+ s = Service(
name="s",
- description={
+ description=CommentedMap({
"image": "repo/image:tag",
"volumes": [
"${CONFDIR}/some/config:/path/to/some/config",
"$CONFDIR/other/config:/path/to/other/config",
]
- },
+ }),
)
assert s.name == "s"
- assert len(s.configs) == 2
- assert s.configs == [
+ assert len(list(s.configs)) == 2
+ assert list(s.configs) == [
Path("some/config"),
Path("other/config"),
]
From fc55e9d677a0cb08d41c6044185259cdfac8b353 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 28 Oct 2021 16:53:40 +0200
Subject: [PATCH 045/135] more streamlined "list" command, but "list "
doesn't work
---
kiwi_scp/commands/cmd_list.py | 9 ++++-----
kiwi_scp/misc.py | 9 +++++----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 0918b0c..9b127bf 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,3 +1,5 @@
+from typing import Tuple
+
import click
from ..instance import Instance, pass_instance
@@ -10,12 +12,9 @@ from ..misc import service_command
)
@pass_instance
@service_command
-def cmd(ctx: Instance, project: str, service: str):
+def cmd(ctx: Instance, project: str, services: Tuple[str]):
"""List projects in this instance, services inside a project or service(s) inside a project"""
if project is not None:
- if service is not None:
- print(f"{ctx.get_service(project, service)}")
- else:
- print(f"services: {ctx.get_services(project)}")
+ print(ctx.get_services(project, services))
else:
print(f"projects: {ctx.config.projects}")
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index c4895f0..dccc434 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -27,15 +27,16 @@ _project_arg = click.argument(
type=str,
)
-_service_arg = click.argument(
- "service",
- required=False,
+_services_arg = click.argument(
+ "services",
+ metavar="[SERVICE]...",
+ nargs=-1,
type=str,
)
instance_command = _MultiDecorator([])
project_command = _MultiDecorator([_project_arg])
-service_command = _MultiDecorator([_project_arg, _service_arg])
+service_command = _MultiDecorator([_project_arg, _services_arg])
def user_query(description: str, default: Any, cast_to: Type[Any] = str):
From 69a0938daff6d30e1d5db2484e19748a97eaf393 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 29 Oct 2021 13:37:29 +0200
Subject: [PATCH 046/135] bug: get all services if service_names empty
---
kiwi_scp/instance.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index b22ef1c..da8c2eb 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -76,7 +76,7 @@ class Instance:
for name, description in yml["services"].items()
]
- if service_names is None:
+ if not service_names:
return Services(project_name, services)
else:
return Services(project_name, [
From 829938e44fdbd397642a94119e7c6715cec86763 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 29 Oct 2021 13:37:59 +0200
Subject: [PATCH 047/135] move pass_instance decorator
---
kiwi_scp/commands/cli.py | 5 +++++
kiwi_scp/commands/cmd_init.py | 3 ++-
kiwi_scp/instance.py | 3 ---
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 2d1d52d..165f1b4 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -2,6 +2,8 @@ import os
import click
+from ..instance import Instance
+
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
@@ -23,3 +25,6 @@ class KiwiCLI(click.MultiCommand):
except ImportError:
return
return mod.cmd
+
+
+pass_instance = click.make_pass_decorator(Instance, ensure=True)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index edae74e..69a322e 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -5,9 +5,10 @@ from pathlib import Path
import click
+from .cli import pass_instance
from .._constants import KIWI_CONF_NAME
from ..config import KiwiConfig
-from ..instance import Instance, pass_instance
+from ..instance import Instance
from ..misc import user_query
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index da8c2eb..1a27ff0 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -84,6 +84,3 @@ class Instance:
for service in services
if service.name in service_names
])
-
-
-pass_instance = click.make_pass_decorator(Instance, ensure=True)
From ff256d05d3e10429a3aabbf439dfc4b4ec3a90cf Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 29 Oct 2021 13:38:21 +0200
Subject: [PATCH 048/135] experimental KiwiCommand class
---
kiwi_scp/commands/cmd_list.py | 89 +++++++++++++++++++++++++++++++----
1 file changed, 79 insertions(+), 10 deletions(-)
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 9b127bf..df9d41a 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,20 +1,89 @@
+import typing as t
from typing import Tuple
import click
-from ..instance import Instance, pass_instance
+from .cli import pass_instance
+from ..config import ProjectConfig
+from ..instance import Instance, Services
from ..misc import service_command
-@click.command(
+class KiwiCommand:
+ @classmethod
+ def run_for_instance(cls, instance: Instance, **kwargs):
+ for project in instance.config.projects:
+ cls.run_for_project(instance, project, **kwargs)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: ProjectConfig, **kwargs):
+ cls.run_for_services(instance, project, instance.get_services(project.name, None), **kwargs)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, **kwargs):
+ pass
+
+
+def kiwi_command(
+ name: str,
+ **kwargs,
+) -> t.Callable:
+ def decorator(command_cls: t.Type[KiwiCommand]) -> t.Callable:
+
+ @click.command(name, **kwargs)
+ @pass_instance
+ @service_command
+ def cmd(ctx: Instance, project: t.Optional[str], services: Tuple[str], **cmd_kwargs) -> None:
+ print(f"{ctx.directory!r}: {project!r}, {services!r}")
+ if project is None:
+ # run for whole instance
+ print("instance")
+ command_cls.run_for_instance(ctx, **cmd_kwargs)
+
+ elif not services:
+ # run for one entire project
+ print("project")
+ for project_cfg in ctx.config.projects:
+ if project_cfg.name == project:
+ command_cls.run_for_project(ctx, project_cfg, **kwargs)
+
+ else:
+ # run for some services
+ print("services")
+ for project_cfg in ctx.config.projects:
+ if project_cfg.name == project:
+ services = ctx.get_services(project_cfg.name, services)
+ command_cls.run_for_services(ctx, project_cfg, services)
+
+ return cmd
+
+ return decorator
+
+
+@kiwi_command(
"list",
short_help="Inspect a kiwi-scp instance",
)
-@pass_instance
-@service_command
-def cmd(ctx: Instance, project: str, services: Tuple[str]):
- """List projects in this instance, services inside a project or service(s) inside a project"""
- if project is not None:
- print(ctx.get_services(project, services))
- else:
- print(f"projects: {ctx.config.projects}")
+class cmd(KiwiCommand):
+ @classmethod
+ def run_for_instance(cls, instance: Instance, **kwargs):
+ print(instance.config.projects)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, **kwargs):
+ print(services)
+
+
+# @click.command(
+# "list",
+# short_help="Inspect a kiwi-scp instance",
+# )
+# @pass_instance
+# @service_command
+# def cmd(ctx: Instance, project: str, services: Tuple[str]):
+# """List projects in this instance, services inside a project or service(s) inside a project"""
+# print(f"project: {project!r}, services: {services!r}")
+# if project is not None:
+# print(ctx.get_services(project, services))
+# else:
+# print(f"projects: {ctx.config.projects}")
From 57a9a50c90727c796c7f610778d89a966ff10838 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 29 Oct 2021 13:59:57 +0200
Subject: [PATCH 049/135] with an example option
---
kiwi_scp/commands/cmd_list.py | 23 +++++++++++++++--------
1 file changed, 15 insertions(+), 8 deletions(-)
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index df9d41a..29a3d8f 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -37,43 +37,50 @@ def kiwi_command(
print(f"{ctx.directory!r}: {project!r}, {services!r}")
if project is None:
# run for whole instance
- print("instance")
+ print(f"for instance: {cmd_kwargs}")
command_cls.run_for_instance(ctx, **cmd_kwargs)
elif not services:
# run for one entire project
- print("project")
+ print(f"for project {project}: {cmd_kwargs}")
for project_cfg in ctx.config.projects:
if project_cfg.name == project:
- command_cls.run_for_project(ctx, project_cfg, **kwargs)
+ command_cls.run_for_project(ctx, project_cfg, **cmd_kwargs)
else:
# run for some services
- print("services")
+ print(f"for services {project}.{services}: {cmd_kwargs}")
for project_cfg in ctx.config.projects:
if project_cfg.name == project:
services = ctx.get_services(project_cfg.name, services)
- command_cls.run_for_services(ctx, project_cfg, services)
+ command_cls.run_for_services(ctx, project_cfg, services, **cmd_kwargs)
return cmd
return decorator
+@click.option(
+ "-s/-S",
+ "--show/--no-show",
+ help=f"EXAMPLE",
+)
@kiwi_command(
"list",
short_help="Inspect a kiwi-scp instance",
)
class cmd(KiwiCommand):
@classmethod
- def run_for_instance(cls, instance: Instance, **kwargs):
+ def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs):
+ print(show)
print(instance.config.projects)
@classmethod
- def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, **kwargs):
+ def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, show: bool = None,
+ **kwargs):
+ print(show)
print(services)
-
# @click.command(
# "list",
# short_help="Inspect a kiwi-scp instance",
From 5d69f34de107199995629ca96f412304addac31d Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 2 Nov 2021 17:21:01 +0100
Subject: [PATCH 050/135] moved decorators to decorators.py
---
kiwi_scp/commands/cli.py | 27 +++++++++++-
kiwi_scp/commands/cmd_init.py | 4 +-
kiwi_scp/commands/cmd_list.py | 75 ++-------------------------------
kiwi_scp/commands/decorators.py | 61 +++++++++++++++++++++++++++
kiwi_scp/misc.py | 33 +--------------
5 files changed, 93 insertions(+), 107 deletions(-)
create mode 100644 kiwi_scp/commands/decorators.py
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 165f1b4..60f4323 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,4 +1,6 @@
import os
+from enum import Enum, auto
+from typing import List
import click
@@ -24,7 +26,28 @@ class KiwiCLI(click.MultiCommand):
mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["cmd"])
except ImportError:
return
- return mod.cmd
+ return mod.CMD
+
+
+class KiwiCommand:
+ @classmethod
+ def run_for_instance(cls, instance: Instance, **kwargs):
+ for project in instance.config.projects:
+ cls.run_for_project(instance, project.name, **kwargs)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project_name: str, **kwargs):
+ service_names = [service.name for service in instance.get_services(project_name, None).content]
+ cls.run_for_services(instance, project_name, service_names, **kwargs)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project_name: str, services: List[str], **kwargs):
+ pass
+
+
+class KiwiCommandType(Enum):
+ INSTANCE = auto()
+ PROJECT = auto()
+ SERVICE = auto()
-pass_instance = click.make_pass_decorator(Instance, ensure=True)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 69a322e..8843427 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -5,7 +5,7 @@ from pathlib import Path
import click
-from .cli import pass_instance
+from .decorators import _pass_instance as pass_instance
from .._constants import KIWI_CONF_NAME
from ..config import KiwiConfig
from ..instance import Instance
@@ -37,7 +37,7 @@ from ..misc import user_query
help=f"show effective {KIWI_CONF_NAME} contents instead",
)
@pass_instance
-def cmd(ctx: Instance, output: Path, force: bool, show: bool):
+def CMD(ctx: Instance, output: Path, force: bool, show: bool):
"""Initialize or reconfigure a kiwi-scp instance"""
if output is not None:
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 29a3d8f..dcf9140 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,63 +1,9 @@
-import typing as t
-from typing import Tuple
-
import click
-from .cli import pass_instance
+from .cli import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
from ..config import ProjectConfig
from ..instance import Instance, Services
-from ..misc import service_command
-
-
-class KiwiCommand:
- @classmethod
- def run_for_instance(cls, instance: Instance, **kwargs):
- for project in instance.config.projects:
- cls.run_for_project(instance, project, **kwargs)
-
- @classmethod
- def run_for_project(cls, instance: Instance, project: ProjectConfig, **kwargs):
- cls.run_for_services(instance, project, instance.get_services(project.name, None), **kwargs)
-
- @classmethod
- def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, **kwargs):
- pass
-
-
-def kiwi_command(
- name: str,
- **kwargs,
-) -> t.Callable:
- def decorator(command_cls: t.Type[KiwiCommand]) -> t.Callable:
-
- @click.command(name, **kwargs)
- @pass_instance
- @service_command
- def cmd(ctx: Instance, project: t.Optional[str], services: Tuple[str], **cmd_kwargs) -> None:
- print(f"{ctx.directory!r}: {project!r}, {services!r}")
- if project is None:
- # run for whole instance
- print(f"for instance: {cmd_kwargs}")
- command_cls.run_for_instance(ctx, **cmd_kwargs)
-
- elif not services:
- # run for one entire project
- print(f"for project {project}: {cmd_kwargs}")
- for project_cfg in ctx.config.projects:
- if project_cfg.name == project:
- command_cls.run_for_project(ctx, project_cfg, **cmd_kwargs)
-
- else:
- # run for some services
- print(f"for services {project}.{services}: {cmd_kwargs}")
- for project_cfg in ctx.config.projects:
- if project_cfg.name == project:
- services = ctx.get_services(project_cfg.name, services)
- command_cls.run_for_services(ctx, project_cfg, services, **cmd_kwargs)
-
- return cmd
-
- return decorator
@click.option(
@@ -67,9 +13,10 @@ def kiwi_command(
)
@kiwi_command(
"list",
+ KiwiCommandType.PROJECT,
short_help="Inspect a kiwi-scp instance",
)
-class cmd(KiwiCommand):
+class CMD(KiwiCommand):
@classmethod
def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs):
print(show)
@@ -80,17 +27,3 @@ class cmd(KiwiCommand):
**kwargs):
print(show)
print(services)
-
-# @click.command(
-# "list",
-# short_help="Inspect a kiwi-scp instance",
-# )
-# @pass_instance
-# @service_command
-# def cmd(ctx: Instance, project: str, services: Tuple[str]):
-# """List projects in this instance, services inside a project or service(s) inside a project"""
-# print(f"project: {project!r}, services: {services!r}")
-# if project is not None:
-# print(ctx.get_services(project, services))
-# else:
-# print(f"projects: {ctx.config.projects}")
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
new file mode 100644
index 0000000..7968992
--- /dev/null
+++ b/kiwi_scp/commands/decorators.py
@@ -0,0 +1,61 @@
+from typing import Callable, Type, Optional, Tuple
+
+import click
+
+from .cli import KiwiCommandType, KiwiCommand
+from ..instance import Instance
+
+_pass_instance = click.make_pass_decorator(
+ Instance,
+ ensure=True,
+)
+_project_arg = click.argument(
+ "project",
+ required=False,
+ type=str,
+)
+_services_arg = click.argument(
+ "services",
+ metavar="[SERVICE]...",
+ nargs=-1,
+ type=str,
+)
+
+
+def kiwi_command(
+ name: str,
+ command_type: KiwiCommandType,
+ **kwargs,
+) -> Callable:
+ def decorator(command_cls: Type[KiwiCommand]) -> Callable:
+
+ @click.command(name, **kwargs)
+ @_pass_instance
+ def cmd(ctx: Instance, project: Optional[str] = None, services: Optional[Tuple[str]] = None,
+ **cmd_kwargs) -> None:
+ print(f"{ctx.directory!r}: {project!r}, {services!r}")
+ if project is None:
+ # run for whole instance
+ print(f"for instance: {cmd_kwargs}")
+ command_cls.run_for_instance(ctx, **cmd_kwargs)
+
+ elif not services:
+ # run for one entire project
+ print(f"for project {project}: {cmd_kwargs}")
+ command_cls.run_for_project(ctx, project, **cmd_kwargs)
+
+ else:
+ # run for some services
+ print(f"for services {project}.{services}: {cmd_kwargs}")
+ command_cls.run_for_services(ctx, project, list(services), **cmd_kwargs)
+
+ if command_type is KiwiCommandType.PROJECT:
+ cmd = _project_arg(cmd)
+
+ elif command_type is KiwiCommandType.SERVICE:
+ cmd = _project_arg(cmd)
+ cmd = _services_arg(cmd)
+
+ return cmd
+
+ return decorator
\ No newline at end of file
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index dccc434..de07027 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,44 +1,13 @@
import re
-from typing import Any, Type, List, Callable, Optional
+from typing import Any, Type, Optional
-import attr
import click
import ruamel.yaml
import ruamel.yaml.compat
-from click.decorators import FC
from ._constants import HEADER_KIWI_CONF_NAME
-@attr.s
-class _MultiDecorator:
- options: List[Callable[[FC], FC]] = attr.ib(factory=list)
-
- def __call__(self, target: FC):
- for option in reversed(self.options):
- target = option(target)
-
- return target
-
-
-_project_arg = click.argument(
- "project",
- required=False,
- type=str,
-)
-
-_services_arg = click.argument(
- "services",
- metavar="[SERVICE]...",
- nargs=-1,
- type=str,
-)
-
-instance_command = _MultiDecorator([])
-project_command = _MultiDecorator([_project_arg])
-service_command = _MultiDecorator([_project_arg, _services_arg])
-
-
def user_query(description: str, default: Any, cast_to: Type[Any] = str):
# prompt user as per argument
while True:
From b16a423a9d4e098a04fc459516027faeb7907570 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 2 Nov 2021 17:21:31 +0100
Subject: [PATCH 051/135] Reformat
---
kiwi_scp/commands/cli.py | 2 --
kiwi_scp/commands/decorators.py | 2 +-
kiwi_scp/config.py | 1 -
kiwi_scp/instance.py | 1 -
kiwi_scp/subcommands/__init__.py | 1 -
kiwi_scp/subcommands/show.py | 1 +
6 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 60f4323..b863f4e 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -49,5 +49,3 @@ class KiwiCommandType(Enum):
INSTANCE = auto()
PROJECT = auto()
SERVICE = auto()
-
-
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 7968992..d86fc1e 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -58,4 +58,4 @@ def kiwi_command(
return cmd
- return decorator
\ No newline at end of file
+ return decorator
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 97956d3..e78b6cd 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,5 +1,4 @@
import functools
-import io
from ipaddress import IPv4Network
from pathlib import Path
from typing import Optional, Dict, List, Any, TextIO
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 1a27ff0..e6c9ee5 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -4,7 +4,6 @@ from pathlib import Path
from typing import Generator, List, Tuple, Optional
import attr
-import click
from ruamel.yaml.comments import CommentedMap
from ._constants import COMPOSE_FILE_NAME
diff --git a/kiwi_scp/subcommands/__init__.py b/kiwi_scp/subcommands/__init__.py
index f958982..cd108a6 100644
--- a/kiwi_scp/subcommands/__init__.py
+++ b/kiwi_scp/subcommands/__init__.py
@@ -6,7 +6,6 @@ from .cmd import CmdCommand
from .disable import DisableCommand
from .down import DownCommand
from .enable import EnableCommand
-from .init import InitCommand
from .logs import LogsCommand
from .new import NewCommand
from .pull import PullCommand
diff --git a/kiwi_scp/subcommands/show.py b/kiwi_scp/subcommands/show.py
index d8e7a34..cfb867a 100644
--- a/kiwi_scp/subcommands/show.py
+++ b/kiwi_scp/subcommands/show.py
@@ -1,6 +1,7 @@
# system
import logging
import os
+
import yaml
from ..project import Project
From 1f588d536445c762c31ffa4373fa1201c54a753f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 3 Nov 2021 01:12:32 +0100
Subject: [PATCH 052/135] CLI and list command for instance
---
kiwi_scp/commands/cli.py | 8 ++++----
kiwi_scp/commands/cmd_list.py | 32 +++++++++++++++++++++++---------
kiwi_scp/commands/decorators.py | 30 ++++++++++++++++++++----------
kiwi_scp/scripts/kiwi_next.py | 5 +----
4 files changed, 48 insertions(+), 27 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index b863f4e..567087b 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -23,7 +23,7 @@ class KiwiCLI(click.MultiCommand):
"""import and return a specific command"""
try:
- mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["cmd"])
+ mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["CMD"])
except ImportError:
return
return mod.CMD
@@ -31,17 +31,17 @@ class KiwiCLI(click.MultiCommand):
class KiwiCommand:
@classmethod
- def run_for_instance(cls, instance: Instance, **kwargs):
+ def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project in instance.config.projects:
cls.run_for_project(instance, project.name, **kwargs)
@classmethod
- def run_for_project(cls, instance: Instance, project_name: str, **kwargs):
+ def run_for_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
service_names = [service.name for service in instance.get_services(project_name, None).content]
cls.run_for_services(instance, project_name, service_names, **kwargs)
@classmethod
- def run_for_services(cls, instance: Instance, project_name: str, services: List[str], **kwargs):
+ def run_for_services(cls, instance: Instance, project_name: str, services: List[str], **kwargs) -> None:
pass
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index dcf9140..7e84fd5 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -1,29 +1,43 @@
+from typing import List
+
import click
from .cli import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
-from ..config import ProjectConfig
-from ..instance import Instance, Services
+from ..instance import Instance
@click.option(
"-s/-S",
"--show/--no-show",
- help=f"EXAMPLE",
+ help=f"show actual config contents instead",
)
@kiwi_command(
"list",
- KiwiCommandType.PROJECT,
+ KiwiCommandType.SERVICE,
short_help="Inspect a kiwi-scp instance",
)
class CMD(KiwiCommand):
- @classmethod
- def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs):
- print(show)
- print(instance.config.projects)
+ """List projects in this instance, services inside a project or service(s) inside a project"""
@classmethod
- def run_for_services(cls, instance: Instance, project: ProjectConfig, services: Services, show: bool = None,
+ def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs):
+ if show:
+ click.secho(f"Showing config for kiwi-scp instance at '{instance.directory}'.", fg="green", bold=True)
+ click.echo_via_pager(instance.config.kiwi_yml)
+
+ else:
+ click.secho(f"Projects in kiwi-scp instance at '{instance.directory}':", fg="green", bold=True)
+
+ for project in instance.config.projects:
+ click.echo(
+ click.style(" - ", fg="green") +
+ click.style(project.name, fg="blue") +
+ click.style(' (disabled)' if not project.enabled else '', fg="red")
+ )
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project_name: str, services: List[str], show: bool = None,
**kwargs):
print(show)
print(services)
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index d86fc1e..3ccaed6 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -1,3 +1,4 @@
+import logging
from typing import Callable, Type, Optional, Tuple
import click
@@ -9,11 +10,13 @@ _pass_instance = click.make_pass_decorator(
Instance,
ensure=True,
)
+
_project_arg = click.argument(
"project",
required=False,
type=str,
)
+
_services_arg = click.argument(
"services",
metavar="[SERVICE]...",
@@ -21,33 +24,40 @@ _services_arg = click.argument(
type=str,
)
+_logger = logging.getLogger(__name__)
+
def kiwi_command(
name: str,
command_type: KiwiCommandType,
- **kwargs,
+ **decorator_kwargs,
) -> Callable:
def decorator(command_cls: Type[KiwiCommand]) -> Callable:
- @click.command(name, **kwargs)
+ @click.command(
+ name,
+ help=command_cls.__doc__,
+ **decorator_kwargs,
+ )
@_pass_instance
def cmd(ctx: Instance, project: Optional[str] = None, services: Optional[Tuple[str]] = None,
- **cmd_kwargs) -> None:
- print(f"{ctx.directory!r}: {project!r}, {services!r}")
+ **kwargs) -> None:
+
+ _logger.debug(f"{ctx.directory!r}: {project!r}, {services!r}")
if project is None:
# run for whole instance
- print(f"for instance: {cmd_kwargs}")
- command_cls.run_for_instance(ctx, **cmd_kwargs)
+ _logger.debug(f"running for instance, kwargs={kwargs}")
+ command_cls.run_for_instance(ctx, **kwargs)
elif not services:
# run for one entire project
- print(f"for project {project}: {cmd_kwargs}")
- command_cls.run_for_project(ctx, project, **cmd_kwargs)
+ _logger.debug(f"running for project {project}, kwargs={kwargs}")
+ command_cls.run_for_project(ctx, project, **kwargs)
else:
# run for some services
- print(f"for services {project}.{services}: {cmd_kwargs}")
- command_cls.run_for_services(ctx, project, list(services), **cmd_kwargs)
+ _logger.debug(f"running for services {services} in project {project}, kwargs={kwargs}")
+ command_cls.run_for_services(ctx, project, list(services), **kwargs)
if command_type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
index 5007843..4b7f9aa 100644
--- a/kiwi_scp/scripts/kiwi_next.py
+++ b/kiwi_scp/scripts/kiwi_next.py
@@ -5,10 +5,7 @@ from kiwi_scp.commands.cli import KiwiCLI
@click.command(cls=KiwiCLI)
def main():
- """main entry point for command line interface"""
-
- click.echo("Hello main")
- pass
+ """kiwi is the simple tool for managing container servers."""
if __name__ == "__main__":
From 3686731f290eb313ecb65d947d5f34950b86984f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 3 Nov 2021 16:32:01 +0100
Subject: [PATCH 053/135] "kiwi list" done
---
kiwi_scp/commands/cli.py | 36 +++++++++++++++++--
kiwi_scp/commands/cmd_list.py | 55 ++++++++++++++++++++--------
kiwi_scp/instance.py | 68 ++++++++++++++++++-----------------
3 files changed, 109 insertions(+), 50 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 567087b..b5446f1 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,6 +1,7 @@
import os
+import sys
from enum import Enum, auto
-from typing import List
+from typing import List, Tuple, Iterable
import click
@@ -30,6 +31,28 @@ class KiwiCLI(click.MultiCommand):
class KiwiCommand:
+ @staticmethod
+ def print_multi_color(*content: Tuple[str, str]):
+ for message, color in content:
+ click.secho(message, fg=color, nl=False)
+ click.echo()
+
+ @staticmethod
+ def print_header(header: str):
+ click.secho(header, fg="green", bold=True)
+
+ @staticmethod
+ def print_error(header: str):
+ click.secho(header, file=sys.stderr, fg="red", bold=True)
+
+ @staticmethod
+ def print_list(content: Iterable[str]):
+ for item in content:
+ KiwiCommand.print_multi_color(
+ (" - ", "green"),
+ (item, "blue"),
+ )
+
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project in instance.config.projects:
@@ -37,11 +60,18 @@ class KiwiCommand:
@classmethod
def run_for_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
- service_names = [service.name for service in instance.get_services(project_name, None).content]
+ project = instance.get_project(project_name)
+
+ if project is None:
+ click.secho(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.", fg="red", bold=True)
+ return
+
+ service_names = [service.name for service in project.get_services().content]
+
cls.run_for_services(instance, project_name, service_names, **kwargs)
@classmethod
- def run_for_services(cls, instance: Instance, project_name: str, services: List[str], **kwargs) -> None:
+ def run_for_services(cls, instance: Instance, project_name: str, service_names: List[str], **kwargs) -> None:
pass
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 7e84fd5..af9211b 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -21,23 +21,50 @@ class CMD(KiwiCommand):
"""List projects in this instance, services inside a project or service(s) inside a project"""
@classmethod
- def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs):
+ def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs) -> None:
if show:
- click.secho(f"Showing config for kiwi-scp instance at '{instance.directory}'.", fg="green", bold=True)
+ KiwiCommand.print_header(f"Showing config for kiwi-scp instance at '{instance.directory}'.")
click.echo_via_pager(instance.config.kiwi_yml)
else:
- click.secho(f"Projects in kiwi-scp instance at '{instance.directory}':", fg="green", bold=True)
-
- for project in instance.config.projects:
- click.echo(
- click.style(" - ", fg="green") +
- click.style(project.name, fg="blue") +
- click.style(' (disabled)' if not project.enabled else '', fg="red")
- )
+ KiwiCommand.print_header(f"Projects in kiwi-scp instance at '{instance.directory}':")
+ KiwiCommand.print_list(
+ project.name + click.style(" (disabled)" if not project.enabled else "", fg="red")
+ for project in instance.config.projects
+ )
@classmethod
- def run_for_services(cls, instance: Instance, project_name: str, services: List[str], show: bool = None,
- **kwargs):
- print(show)
- print(services)
+ def run_for_project(cls, instance: Instance, project_name: str, show: bool = None, **kwargs) -> None:
+ project = instance.get_project(project_name)
+
+ if project is None:
+ KiwiCommand.print_error(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.")
+ return
+
+ services = project.get_services()
+ if show:
+ KiwiCommand.print_header(f"Showing config for all services in project '{project_name}'.")
+ click.echo_via_pager(str(services))
+
+ else:
+ KiwiCommand.print_header(f"Services in project '{project_name}':")
+ KiwiCommand.print_list(service.name for service in services.content)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project_name: str, service_names: List[str], show: bool = None,
+ **kwargs) -> None:
+ project = instance.get_project(project_name)
+
+ if project is None:
+ KiwiCommand.print_error(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.")
+ return
+
+ services = project.get_services(service_names)
+ if show:
+ KiwiCommand.print_header(
+ f"Showing config for services '{', '.join(service_names)}' in project '{project_name}'.")
+ click.echo_via_pager(str(services))
+
+ else:
+ KiwiCommand.print_header(f"Matching services in project '{project_name}':")
+ KiwiCommand.print_list(service.name for service in services.content)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index e6c9ee5..5927f80 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -16,21 +16,14 @@ _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE
@attr.s
class Service:
name: str = attr.ib()
- description: CommentedMap = attr.ib()
-
- def __str__(self) -> str:
- return YAML().dump({
- "service": {
- self.name: self.description
- }
- }).strip()
+ content: CommentedMap = attr.ib()
@property
def configs(self) -> Generator[Path, None, None]:
- if "volumes" not in self.description:
+ if "volumes" not in self.content:
return
- for volume in self.description["volumes"]:
+ for volume in self.content["volumes"]:
host_part = volume.split(":")[0]
cd_match = _RE_CONFDIR.match(host_part)
@@ -40,18 +33,44 @@ class Service:
@attr.s
class Services:
- project_name: str = attr.ib()
content: List[Service] = attr.ib()
def __str__(self) -> str:
return YAML().dump({
"services": {
- service.name: service.description
+ service.name: service.content
for service in self.content
}
}).strip()
+@attr.s
+class Project:
+ directory: Path = attr.ib()
+
+ @staticmethod
+ @functools.lru_cache(maxsize=10)
+ def _parse_compose_file(directory: Path) -> CommentedMap:
+ with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
+ return YAML().load(cf)
+
+ def get_services(self, service_names: Optional[List[str]] = None) -> Services:
+ yml = Project._parse_compose_file(self.directory)
+ services = [
+ Service(name, description)
+ for name, description in yml["services"].items()
+ ]
+
+ if not service_names:
+ return Services(services)
+ else:
+ return Services([
+ service
+ for service in services
+ if service.name in service_names
+ ])
+
+
@attr.s
class Instance:
directory: Path = attr.ib(default=Path('.'))
@@ -62,24 +81,7 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
- @staticmethod
- @functools.lru_cache(maxsize=10)
- def _parse_compose_file(directory: Path):
- with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
- return YAML().load(cf)
-
- def get_services(self, project_name: str, service_names: Optional[Tuple[str]] = None) -> Services:
- yml = Instance._parse_compose_file(self.directory.joinpath(project_name))
- services = [
- Service(name, description)
- for name, description in yml["services"].items()
- ]
-
- if not service_names:
- return Services(project_name, services)
- else:
- return Services(project_name, [
- service
- for service in services
- if service.name in service_names
- ])
+ def get_project(self, project_name: str) -> Optional[Project]:
+ for project in self.config.projects:
+ if project.name == project_name:
+ return Project(self.directory.joinpath(project.name))
From b4684998fda46af14dd333b19833ed7989f37706 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 3 Nov 2021 17:35:36 +0100
Subject: [PATCH 054/135] pytest
---
tests/test_instance.py | 12 +++++-------
tests/test_project.py | 31 +++++++++++++++++++++++++++++++
tests/test_service.py | 8 ++++----
tests/test_services.py | 14 ++++++++++++++
4 files changed, 54 insertions(+), 11 deletions(-)
create mode 100644 tests/test_project.py
create mode 100644 tests/test_services.py
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 81d8d1b..2f3f627 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -10,17 +10,15 @@ class TestDefault:
assert i.config is not None
assert len(i.config.projects) == 1
- p = i.config.projects[0]
+ pc = i.config.projects[0]
- assert p.name == "hello-world.project"
+ assert pc.name == "hello-world.project"
- ss = i.get_services(p.name)
+ p = i.get_project("hello-world.project")
- assert len(ss.content) == 5
+ assert p.directory == Path("example/hello-world.project")
- s = ss.content[0]
-
- assert s.name == "greeter"
+ assert i.get_project("nonexistent") is None
def test_empty(self):
i = Instance()
diff --git a/tests/test_project.py b/tests/test_project.py
new file mode 100644
index 0000000..917c2db
--- /dev/null
+++ b/tests/test_project.py
@@ -0,0 +1,31 @@
+from pathlib import Path
+
+import pytest
+
+from kiwi_scp._constants import COMPOSE_FILE_NAME
+from kiwi_scp.instance import Project
+
+
+class TestDefault:
+ def test_example(self):
+ p = Project(Path("example/hello-world.project"))
+
+ ss = p.get_services()
+
+ assert len(ss.content) == 5
+
+ s = ss.content[0]
+
+ assert s.name == "greeter"
+
+ ss2 = p.get_services(["nonexistent"])
+
+ assert len(ss2.content) == 0
+
+ def test_empty(self):
+ p = Project(Path("nonexistent"))
+
+ with pytest.raises(FileNotFoundError) as exc_info:
+ p.get_services()
+
+ assert exc_info.value.filename == f"nonexistent/{COMPOSE_FILE_NAME}"
diff --git a/tests/test_service.py b/tests/test_service.py
index 42244c8..1eb9e0a 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -9,7 +9,7 @@ class TestDefault:
def test_empty(self):
s = Service(
name="s",
- description=CommentedMap(),
+ content=CommentedMap(),
)
assert s.name == "s"
@@ -18,7 +18,7 @@ class TestDefault:
def test_no_configs(self):
s = Service(
name="s",
- description=CommentedMap({
+ content=CommentedMap({
"image": "repo/image:tag",
}),
)
@@ -29,7 +29,7 @@ class TestDefault:
def test_no_configs_in_volumes(self):
s = Service(
name="s",
- description=CommentedMap({
+ content=CommentedMap({
"image": "repo/image:tag",
"volumes": [
"docker_volume/third/dir:/path/to/third/mountpoint",
@@ -45,7 +45,7 @@ class TestDefault:
def test_with_configs(self):
s = Service(
name="s",
- description=CommentedMap({
+ content=CommentedMap({
"image": "repo/image:tag",
"volumes": [
"${CONFDIR}/some/config:/path/to/some/config",
diff --git a/tests/test_services.py b/tests/test_services.py
new file mode 100644
index 0000000..ff349ef
--- /dev/null
+++ b/tests/test_services.py
@@ -0,0 +1,14 @@
+from ruamel.yaml import CommentedMap
+
+from kiwi_scp.instance import Service, Services
+
+
+class TestServices:
+ def test_empty(self):
+ s = Service(
+ name="s",
+ content=CommentedMap(),
+ )
+ ss = Services([s])
+
+ assert str(ss) == "services:\n s: {}"
From a784475479ecd790cff0634e6ed2eacf1cd65acf Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 3 Nov 2021 17:58:18 +0100
Subject: [PATCH 055/135] "kiwi init" as a KiwiCommand
---
kiwi_scp/commands/cli.py | 24 +++++++++-
kiwi_scp/commands/cmd_init.py | 83 +++++++++++++++++------------------
kiwi_scp/misc.py | 21 +--------
3 files changed, 64 insertions(+), 64 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index b5446f1..6f519ae 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,7 +1,7 @@
import os
import sys
from enum import Enum, auto
-from typing import List, Tuple, Iterable
+from typing import List, Tuple, Iterable, Any, Type
import click
@@ -53,6 +53,28 @@ class KiwiCommand:
(item, "blue"),
)
+ @staticmethod
+ def user_query(description: str, default: Any, cast_to: Type[Any] = str):
+ # prompt user as per argument
+ while True:
+ try:
+ prompt = \
+ click.style(f"Enter {description} [", fg="green") + \
+ click.style(default, fg="blue") + \
+ click.style("] ", fg="green")
+ str_value = input(prompt).strip()
+ if str_value:
+ return cast_to(str_value)
+ else:
+ return default
+
+ except EOFError:
+ click.echo("Input aborted.")
+ return default
+
+ except Exception as e:
+ click.echo(f"Invalid input: {e}")
+
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project in instance.config.projects:
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 8843427..848c6aa 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -5,20 +5,18 @@ from pathlib import Path
import click
-from .decorators import _pass_instance as pass_instance
+from .cli import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
from ..config import KiwiConfig
from ..instance import Instance
-from ..misc import user_query
+
+_logger = logging.getLogger(__name__)
-@click.command(
- "init",
- short_help="Initializes kiwi-scp",
-)
@click.option(
- "-o",
- "--output",
+ "-d",
+ "--directory",
help=f"initialize a kiwi-scp instance in another directory",
type=click.Path(
path_type=Path,
@@ -31,46 +29,45 @@ from ..misc import user_query
"--force/--no-force",
help=f"use default values even if {KIWI_CONF_NAME} is present",
)
-@click.option(
- "-s/-S",
- "--show/--no-show",
- help=f"show effective {KIWI_CONF_NAME} contents instead",
+@kiwi_command(
+ "init",
+ KiwiCommandType.INSTANCE,
+ short_help="Initializes kiwi-scp",
)
-@pass_instance
-def CMD(ctx: Instance, output: Path, force: bool, show: bool):
+class CMD(KiwiCommand):
"""Initialize or reconfigure a kiwi-scp instance"""
- if output is not None:
- ctx.directory = output
+ @classmethod
+ def run_for_instance(cls, instance: Instance, output: Path = None, force: bool = None, **kwargs) -> None:
+ if output is not None:
+ instance.directory = output
- current_config = KiwiConfig() if force else ctx.config
+ current_config = KiwiConfig() if force else instance.config
- if show:
- # just show the currently effective kiwi.yml
- click.echo_via_pager(current_config.kiwi_yml)
- return
+ # check force switch
+ if force and os.path.isfile(KIWI_CONF_NAME):
+ _logger.warning(f"About to overwrite an existing '{KIWI_CONF_NAME}'!")
- # check force switch
- if force and os.path.isfile(KIWI_CONF_NAME):
- logging.warning(f"Overwriting an existing '{KIWI_CONF_NAME}'!")
+ # build new kiwi dict
+ kiwi_dict = current_config.kiwi_dict
+ kiwi_dict.update({
+ "version": KiwiCommand.user_query("kiwi-scp version to use in this instance", current_config.version),
+ "storage": {
+ "directory": KiwiCommand.user_query("local directory for service data",
+ current_config.storage.directory, Path),
+ },
+ "network": {
+ "name": KiwiCommand.user_query("name for local network hub", current_config.network.name),
+ "cidr": KiwiCommand.user_query("CIDRv4 block for local network hub", current_config.network.cidr,
+ IPv4Network),
+ },
+ })
- # build new kiwi dict
- kiwi_dict = current_config.kiwi_dict
- kiwi_dict.update({
- "version": user_query("kiwi-scp version to use in this instance", current_config.version),
- "storage": {
- "directory": user_query("local directory for service data", current_config.storage.directory, Path),
- },
- "network": {
- "name": user_query("name for local network hub", current_config.network.name),
- "cidr": user_query("CIDRv4 block for local network hub", current_config.network.cidr, IPv4Network),
- },
- })
+ # ensure output directory exists
+ if not os.path.isdir(instance.directory):
+ os.mkdir(instance.directory)
- # ensure output directory exists
- if not os.path.isdir(ctx.directory):
- os.mkdir(ctx.directory)
-
- # write out the new kiwi.yml
- with open(ctx.directory.joinpath(KIWI_CONF_NAME), "w") as file:
- KiwiConfig.parse_obj(kiwi_dict).dump_kiwi_yml(file)
+ # write out the new kiwi.yml
+ cfg = KiwiConfig.parse_obj(kiwi_dict)
+ with open(instance.directory.joinpath(KIWI_CONF_NAME), "w") as file:
+ cfg.dump_kiwi_yml(file)
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index de07027..eb2ad3b 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -1,31 +1,12 @@
import re
-from typing import Any, Type, Optional
+from typing import Optional
-import click
import ruamel.yaml
import ruamel.yaml.compat
from ._constants import HEADER_KIWI_CONF_NAME
-def user_query(description: str, default: Any, cast_to: Type[Any] = str):
- # prompt user as per argument
- while True:
- try:
- str_value = input(f"Enter {description} [{default}] ").strip()
- if str_value:
- return cast_to(str_value)
- else:
- return default
-
- except EOFError:
- click.echo("Input aborted.")
- return default
-
- except Exception as e:
- click.echo(f"Invalid input: {e}")
-
-
class YAML(ruamel.yaml.YAML):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
From bf70db567afe954a3db77d3de5212900065af1ff Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 3 Nov 2021 21:06:02 +0100
Subject: [PATCH 056/135] Fix "Executable" class
---
kiwi_scp/executable.py | 94 ++++++++++++++++++------------------------
1 file changed, 40 insertions(+), 54 deletions(-)
diff --git a/kiwi_scp/executable.py b/kiwi_scp/executable.py
index 1c054f5..f2646cc 100644
--- a/kiwi_scp/executable.py
+++ b/kiwi_scp/executable.py
@@ -1,74 +1,60 @@
-# system
+import functools
import logging
import os
import subprocess
+from pathlib import Path
+from typing import Optional, List, Any
+
+import attr
+
+_logger = logging.getLogger(__name__)
-def _is_executable(filename):
- if filename is None:
- return False
-
- return os.path.isfile(filename) and os.access(filename, os.X_OK)
-
-
-def _find_exe_file(exe_name):
- for path in os.environ['PATH'].split(os.pathsep):
- exe_file = os.path.join(path, exe_name)
- if _is_executable(exe_file):
- return exe_file
-
- raise FileNotFoundError(f"Executable '{exe_name}' not found in $PATH!")
-
-
+@attr.s
class Executable:
- class __Executable:
- __exe_path = None
+ exe_name: str = attr.ib()
- def __init__(self, exe_name):
- self.__exe_path = _find_exe_file(exe_name)
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def __find_exe_file(exe_name: str) -> Optional[Path]:
+ for path in os.environ['PATH'].split(os.pathsep):
+ exe_file = Path(path).joinpath(exe_name)
+ if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
+ return exe_file
- def __build_cmd(self, args, kwargs):
- cmd = [self.__exe_path, *args]
+ raise FileNotFoundError(f"Executable '{exe_name}' not found in $PATH!")
- logging.debug(f"Executable cmd{cmd}, kwargs{kwargs}")
- return cmd
+ @property
+ def exe_file(self) -> Optional[Path]:
+ return self.__find_exe_file(self.exe_name)
- def run(self, process_args, **kwargs):
- return subprocess.run(
- self.__build_cmd(process_args, kwargs),
- **kwargs
- )
+ def __build_cmd(self, args, kwargs) -> List[Path, Any, ...]:
+ cmd = [self.exe_file, *args]
- def Popen(self, process_args, **kwargs):
- return subprocess.Popen(
- self.__build_cmd(process_args, kwargs),
- **kwargs
- )
+ _logger.debug(f"Executable cmd{cmd}, kwargs{kwargs}")
+ return cmd
- def run_less(self, process_args, **kwargs):
- kwargs['stdout'] = subprocess.PIPE
- kwargs['stderr'] = subprocess.DEVNULL
+ def run(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
+ return subprocess.run(
+ self.__build_cmd(process_args, kwargs),
+ **kwargs
+ )
- process = self.Popen(
- process_args,
- **kwargs
- )
+ def Popen(self, process_args, **kwargs) -> subprocess.Popen[str]:
+ return subprocess.Popen(
+ self.__build_cmd(process_args, kwargs),
+ **kwargs
+ )
+ def run_less(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
+ kwargs['stdout'] = subprocess.PIPE
+ kwargs['stderr'] = subprocess.DEVNULL
+
+ with self.Popen(process_args, **kwargs) as process:
less_process = Executable('less').run([
'-R', '+G'
], stdin=process.stdout)
process.communicate()
- return less_process
- __exe_name = None
- __instances = {}
-
- def __init__(self, exe_name):
- self.__exe_name = exe_name
-
- if exe_name not in Executable.__instances:
- Executable.__instances[exe_name] = Executable.__Executable(exe_name)
-
- def __getattr__(self, item):
- return getattr(self.__instances[self.__exe_name], item)
+ return less_process
From 52f0e9b8a3c6656d2be07c0675e163263ccc5be7 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 4 Nov 2021 21:59:17 +0100
Subject: [PATCH 057/135] Fix "Rootkit" class
---
kiwi_scp/executable.py | 4 +-
kiwi_scp/rootkit.py | 126 +++++++++++++++++++----------------------
2 files changed, 61 insertions(+), 69 deletions(-)
diff --git a/kiwi_scp/executable.py b/kiwi_scp/executable.py
index f2646cc..48168d1 100644
--- a/kiwi_scp/executable.py
+++ b/kiwi_scp/executable.py
@@ -28,7 +28,7 @@ class Executable:
def exe_file(self) -> Optional[Path]:
return self.__find_exe_file(self.exe_name)
- def __build_cmd(self, args, kwargs) -> List[Path, Any, ...]:
+ def __build_cmd(self, args, kwargs) -> List:
cmd = [self.exe_file, *args]
_logger.debug(f"Executable cmd{cmd}, kwargs{kwargs}")
@@ -40,7 +40,7 @@ class Executable:
**kwargs
)
- def Popen(self, process_args, **kwargs) -> subprocess.Popen[str]:
+ def Popen(self, process_args, **kwargs) -> subprocess.Popen:
return subprocess.Popen(
self.__build_cmd(process_args, kwargs),
**kwargs
diff --git a/kiwi_scp/rootkit.py b/kiwi_scp/rootkit.py
index a3c7937..7332c43 100644
--- a/kiwi_scp/rootkit.py
+++ b/kiwi_scp/rootkit.py
@@ -1,86 +1,78 @@
-# system
+import functools
import logging
-import os
import subprocess
+from pathlib import Path
+from typing import Optional, TypeVar, Union, List
+
+import attr
-# local
from ._constants import IMAGES_DIRECTORY_NAME, LOCAL_IMAGES_NAME, DEFAULT_IMAGE_NAME
from .executable import Executable
+_logger = logging.getLogger(__name__)
+
+PSL = TypeVar("PSL", Union[Path, str], List[Union[Path, str]])
+
+
+def prefix_path(path: PSL, prefix: Path = Path("/mnt")) -> PSL:
+ if isinstance(path, Path):
+ return prefix.joinpath(path.absolute())
-def _prefix_path(prefix, path):
if isinstance(path, str):
- abs_path = os.path.abspath(path)
- return os.path.realpath(f"{prefix}/{abs_path}")
+ return prefix_path(Path(path), prefix)
elif isinstance(path, list):
- return [_prefix_path(prefix, p) for p in path]
-
-
-def prefix_path_mnt(path):
- return _prefix_path('/mnt/', path)
-
-
-def _image_name(image_tag):
- if image_tag is not None:
- return f"{LOCAL_IMAGES_NAME}:{image_tag}"
- else:
- return DEFAULT_IMAGE_NAME
+ return [prefix_path(prefix, p) for p in path]
+@attr.s
class Rootkit:
- class __Rootkit:
- __image_tag = None
+ image_tag: str = attr.ib()
- def __init__(self, image_tag=None):
- self.__image_tag = image_tag
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def __image_name(image_tag: Optional[str]) -> str:
+ if image_tag is not None:
+ return f"{LOCAL_IMAGES_NAME}:{image_tag}"
+ else:
+ return DEFAULT_IMAGE_NAME
- def __exists(self):
- ps = Executable('docker').run([
- 'images',
- '--filter', f"reference={_image_name(self.__image_tag)}",
- '--format', '{{.Repository}}:{{.Tag}}'
- ], stdout=subprocess.PIPE)
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def __exists(image_tag: str) -> bool:
+ ps = Executable('docker').run([
+ 'images',
+ '--filter', f"reference={Rootkit.__image_name(image_tag)}",
+ '--format', '{{.Repository}}:{{.Tag}}'
+ ], stdout=subprocess.PIPE)
- return str(ps.stdout, 'utf-8').strip() == _image_name(self.__image_tag)
+ return str(ps.stdout, 'utf-8').strip() == Rootkit.__image_name(image_tag)
+
+ def __build_image(self) -> None:
+ if Rootkit.__exists(self.image_tag):
+ _logger.info(f"Using image {Rootkit.__image_name(self.image_tag)}")
+ else:
+ if self.image_tag is None:
+ _logger.info(f"Pulling image {Rootkit.__image_name(self.image_tag)}")
+ Executable('docker').run([
+ 'pull', Rootkit.__image_name(self.image_tag)
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- def __build_image(self):
- if self.__exists():
- logging.info(f"Using image {_image_name(self.__image_tag)}")
else:
- if self.__image_tag is None:
- logging.info(f"Pulling image {_image_name(self.__image_tag)}")
- Executable('docker').run([
- 'pull', _image_name(self.__image_tag)
- ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ _logger.info(f"Building image {Rootkit.__image_name(self.image_tag)}")
+ Executable('docker').run([
+ 'build',
+ '-t', Rootkit.__image_name(self.image_tag),
+ '-f', f"{IMAGES_DIRECTORY_NAME}/{self.image_tag}.Dockerfile",
+ f"{IMAGES_DIRECTORY_NAME}"
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- else:
- logging.info(f"Building image {_image_name(self.__image_tag)}")
- Executable('docker').run([
- 'build',
- '-t', _image_name(self.__image_tag),
- '-f', f"{IMAGES_DIRECTORY_NAME}/{self.__image_tag}.Dockerfile",
- f"{IMAGES_DIRECTORY_NAME}"
- ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
- def run(self, process_args, **kwargs):
- self.__build_image()
- Executable('docker').run([
- 'run', '--rm',
- '-v', '/:/mnt',
- '-u', 'root',
- _image_name(self.__image_tag),
- *process_args
- ], **kwargs)
-
- __image_tag = None
- __instances = {}
-
- def __init__(self, image_tag=None):
- self.__image_tag = image_tag
-
- if _image_name(self.__image_tag) not in Rootkit.__instances:
- Rootkit.__instances[_image_name(self.__image_tag)] = Rootkit.__Rootkit(image_tag)
-
- def __getattr__(self, item):
- return getattr(self.__instances[_image_name(self.__image_tag)], item)
+ def run(self, process_args, **kwargs):
+ self.__build_image()
+ Executable('docker').run([
+ 'run', '--rm',
+ '-v', '/:/mnt',
+ '-u', 'root',
+ Rootkit.__image_name(self.image_tag),
+ *process_args
+ ], **kwargs)
From 2e37291a68eb57bdd63afd23b3aa3fa7cf05c272 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 6 Nov 2021 03:45:27 +0100
Subject: [PATCH 058/135] KiwiCommand API
---
kiwi_scp/commands/cli.py | 32 +++++++++++++--------------
kiwi_scp/commands/cmd_list.py | 38 +++++++++++++--------------------
kiwi_scp/commands/decorators.py | 32 ++++++++++++++++++---------
kiwi_scp/instance.py | 31 ++++++++++++++++-----------
4 files changed, 71 insertions(+), 62 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 6f519ae..84ebbd1 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -5,7 +5,7 @@ from typing import List, Tuple, Iterable, Any, Type
import click
-from ..instance import Instance
+from ..instance import Instance, Project, Services
class KiwiCLI(click.MultiCommand):
@@ -42,8 +42,8 @@ class KiwiCommand:
click.secho(header, fg="green", bold=True)
@staticmethod
- def print_error(header: str):
- click.secho(header, file=sys.stderr, fg="red", bold=True)
+ def print_error(error: str):
+ click.secho(error, file=sys.stderr, fg="red", bold=True)
@staticmethod
def print_list(content: Iterable[str]):
@@ -77,24 +77,22 @@ class KiwiCommand:
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
- for project in instance.config.projects:
- cls.run_for_project(instance, project.name, **kwargs)
+ for project_config in instance.config.projects:
+ project = instance.get_project(project_config.name)
+ cls.run_for_existing_project(instance, project, **kwargs)
@classmethod
- def run_for_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
- project = instance.get_project(project_name)
-
- if project is None:
- click.secho(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.", fg="red", bold=True)
- return
-
- service_names = [service.name for service in project.get_services().content]
-
- cls.run_for_services(instance, project_name, service_names, **kwargs)
+ def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
+ raise Exception
@classmethod
- def run_for_services(cls, instance: Instance, project_name: str, service_names: List[str], **kwargs) -> None:
- pass
+ def run_for_existing_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ service_names = [service.name for service in project.services.content]
+ cls.run_for_services(instance, project, service_names, **kwargs)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ raise Exception
class KiwiCommandType(Enum):
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index af9211b..ac6b420 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -4,7 +4,7 @@ import click
from .cli import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
-from ..instance import Instance
+from ..instance import Instance, Project
@click.option(
@@ -34,37 +34,29 @@ class CMD(KiwiCommand):
)
@classmethod
- def run_for_project(cls, instance: Instance, project_name: str, show: bool = None, **kwargs) -> None:
- project = instance.get_project(project_name)
-
- if project is None:
- KiwiCommand.print_error(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.")
- return
-
- services = project.get_services()
+ def run_for_existing_project(cls, instance: Instance, project: Project, show: bool = None, **kwargs) -> None:
if show:
- KiwiCommand.print_header(f"Showing config for all services in project '{project_name}'.")
- click.echo_via_pager(str(services))
+ KiwiCommand.print_header(f"Showing config for all services in project '{project.name}'.")
+ click.echo_via_pager(str(project.services))
else:
- KiwiCommand.print_header(f"Services in project '{project_name}':")
- KiwiCommand.print_list(service.name for service in services.content)
+ KiwiCommand.print_header(f"Services in project '{project.name}':")
+ KiwiCommand.print_list(service.name for service in project.services.content)
@classmethod
- def run_for_services(cls, instance: Instance, project_name: str, service_names: List[str], show: bool = None,
+ def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
+ KiwiCommand.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], show: bool = None,
**kwargs) -> None:
- project = instance.get_project(project_name)
-
- if project is None:
- KiwiCommand.print_error(f"No project '{project_name}' in kiwi-scp instance at '{instance.directory}'.")
- return
-
- services = project.get_services(service_names)
+ services = project.services.filter_existing(service_names)
if show:
+ service_names = [service.name for service in services.content]
KiwiCommand.print_header(
- f"Showing config for services '{', '.join(service_names)}' in project '{project_name}'.")
+ f"Showing config for matching services '{', '.join(service_names)}' in project '{project.name}'.")
click.echo_via_pager(str(services))
else:
- KiwiCommand.print_header(f"Matching services in project '{project_name}':")
+ KiwiCommand.print_header(f"Matching services in project '{project.name}':")
KiwiCommand.print_list(service.name for service in services.content)
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 3ccaed6..c432779 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -12,13 +12,14 @@ _pass_instance = click.make_pass_decorator(
)
_project_arg = click.argument(
- "project",
+ "project_name",
+ metavar="[PROJECT]",
required=False,
type=str,
)
_services_arg = click.argument(
- "services",
+ "service_names",
metavar="[SERVICE]...",
nargs=-1,
type=str,
@@ -40,24 +41,35 @@ def kiwi_command(
**decorator_kwargs,
)
@_pass_instance
- def cmd(ctx: Instance, project: Optional[str] = None, services: Optional[Tuple[str]] = None,
+ def cmd(ctx: Instance, project_name: Optional[str] = None, service_names: Optional[Tuple[str]] = None,
**kwargs) -> None:
- _logger.debug(f"{ctx.directory!r}: {project!r}, {services!r}")
- if project is None:
+ _logger.debug(f"{ctx.directory!r}: {project_name!r}, {service_names!r}")
+ if project_name is None:
# run for whole instance
_logger.debug(f"running for instance, kwargs={kwargs}")
command_cls.run_for_instance(ctx, **kwargs)
- elif not services:
+ elif not service_names:
# run for one entire project
- _logger.debug(f"running for project {project}, kwargs={kwargs}")
- command_cls.run_for_project(ctx, project, **kwargs)
+ project = ctx.get_project(project_name)
+ if project is not None:
+ _logger.debug(f"running for existing project {project}, kwargs={kwargs}")
+ command_cls.run_for_existing_project(ctx, project, **kwargs)
+
+ else:
+ _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
+ command_cls.run_for_new_project(ctx, project_name, **kwargs)
else:
# run for some services
- _logger.debug(f"running for services {services} in project {project}, kwargs={kwargs}")
- command_cls.run_for_services(ctx, project, list(services), **kwargs)
+ project = ctx.get_project(project_name)
+ if project is not None:
+ _logger.debug(f"running for services {service_names} in project {project}, kwargs={kwargs}")
+ command_cls.run_for_services(ctx, project, list(service_names), **kwargs)
+
+ else:
+ KiwiCommand.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{ctx.directory}'!")
if command_type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 5927f80..a3b2189 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -43,6 +43,16 @@ class Services:
}
}).strip()
+ def __bool__(self) -> bool:
+ return bool(self.content)
+
+ def filter_existing(self, service_names: List[str]):
+ return Services([
+ service
+ for service in self.content
+ if service.name in service_names
+ ])
+
@attr.s
class Project:
@@ -54,21 +64,18 @@ class Project:
with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
return YAML().load(cf)
- def get_services(self, service_names: Optional[List[str]] = None) -> Services:
+ @property
+ def name(self) -> str:
+ return self.directory.name
+
+ @property
+ def services(self) -> Services:
yml = Project._parse_compose_file(self.directory)
- services = [
+
+ return Services([
Service(name, description)
for name, description in yml["services"].items()
- ]
-
- if not service_names:
- return Services(services)
- else:
- return Services([
- service
- for service in services
- if service.name in service_names
- ])
+ ])
@attr.s
From e7e6e5867a3e647e8592ef3b40ce0fc15b789c90 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 13 Nov 2021 01:12:27 +0100
Subject: [PATCH 059/135] Removed Clutter
---
kiwi_scp/commands/cli.py | 4 ++--
kiwi_scp/commands/cmd_init.py | 2 +-
kiwi_scp/commands/cmd_list.py | 11 +++--------
3 files changed, 6 insertions(+), 11 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 84ebbd1..3c3feef 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -5,7 +5,7 @@ from typing import List, Tuple, Iterable, Any, Type
import click
-from ..instance import Instance, Project, Services
+from ..instance import Instance, Project
class KiwiCLI(click.MultiCommand):
@@ -83,7 +83,7 @@ class KiwiCommand:
@classmethod
def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
- raise Exception
+ cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
@classmethod
def run_for_existing_project(cls, instance: Instance, project: Project, **kwargs) -> None:
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 848c6aa..428a011 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -38,7 +38,7 @@ class CMD(KiwiCommand):
"""Initialize or reconfigure a kiwi-scp instance"""
@classmethod
- def run_for_instance(cls, instance: Instance, output: Path = None, force: bool = None, **kwargs) -> None:
+ def run_for_instance(cls, instance: Instance, output: Path = None, force: bool = None) -> None:
if output is not None:
instance.directory = output
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index ac6b420..ffd3e20 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -21,7 +21,7 @@ class CMD(KiwiCommand):
"""List projects in this instance, services inside a project or service(s) inside a project"""
@classmethod
- def run_for_instance(cls, instance: Instance, show: bool = None, **kwargs) -> None:
+ def run_for_instance(cls, instance: Instance, show: bool = None) -> None:
if show:
KiwiCommand.print_header(f"Showing config for kiwi-scp instance at '{instance.directory}'.")
click.echo_via_pager(instance.config.kiwi_yml)
@@ -34,7 +34,7 @@ class CMD(KiwiCommand):
)
@classmethod
- def run_for_existing_project(cls, instance: Instance, project: Project, show: bool = None, **kwargs) -> None:
+ def run_for_existing_project(cls, instance: Instance, project: Project, show: bool = None) -> None:
if show:
KiwiCommand.print_header(f"Showing config for all services in project '{project.name}'.")
click.echo_via_pager(str(project.services))
@@ -44,12 +44,7 @@ class CMD(KiwiCommand):
KiwiCommand.print_list(service.name for service in project.services.content)
@classmethod
- def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
- KiwiCommand.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
-
- @classmethod
- def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], show: bool = None,
- **kwargs) -> None:
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], show: bool = None) -> None:
services = project.services.filter_existing(service_names)
if show:
service_names = [service.name for service in services.content]
From 2241aa500f4b813c1a8932061d161a54b7a758de Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 13 Nov 2021 03:26:32 +0100
Subject: [PATCH 060/135] Common Executables
---
kiwi_scp/executable.py | 18 +++++++++++-------
kiwi_scp/rootkit.py | 10 +++++-----
2 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/kiwi_scp/executable.py b/kiwi_scp/executable.py
index 48168d1..184e41e 100644
--- a/kiwi_scp/executable.py
+++ b/kiwi_scp/executable.py
@@ -3,7 +3,7 @@ import logging
import os
import subprocess
from pathlib import Path
-from typing import Optional, List, Any
+from typing import Optional, List
import attr
@@ -17,7 +17,7 @@ class Executable:
@staticmethod
@functools.lru_cache(maxsize=None)
def __find_exe_file(exe_name: str) -> Optional[Path]:
- for path in os.environ['PATH'].split(os.pathsep):
+ for path in os.environ["PATH"].split(os.pathsep):
exe_file = Path(path).joinpath(exe_name)
if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
return exe_file
@@ -46,15 +46,19 @@ class Executable:
**kwargs
)
- def run_less(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
- kwargs['stdout'] = subprocess.PIPE
- kwargs['stderr'] = subprocess.DEVNULL
+ def run_with_pager(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
+ kwargs["stdout"] = subprocess.PIPE
+ kwargs["stderr"] = subprocess.DEVNULL
with self.Popen(process_args, **kwargs) as process:
- less_process = Executable('less').run([
- '-R', '+G'
+ less_process = Executable("less").run([
+ "-R", "+G"
], stdin=process.stdout)
process.communicate()
return less_process
+
+
+DOCKER_EXE = Executable("docker")
+COMPOSE_EXE = Executable("docker-compose")
diff --git a/kiwi_scp/rootkit.py b/kiwi_scp/rootkit.py
index 7332c43..adef3f1 100644
--- a/kiwi_scp/rootkit.py
+++ b/kiwi_scp/rootkit.py
@@ -7,7 +7,7 @@ from typing import Optional, TypeVar, Union, List
import attr
from ._constants import IMAGES_DIRECTORY_NAME, LOCAL_IMAGES_NAME, DEFAULT_IMAGE_NAME
-from .executable import Executable
+from .executable import DOCKER_EXE
_logger = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ class Rootkit:
@staticmethod
@functools.lru_cache(maxsize=None)
def __exists(image_tag: str) -> bool:
- ps = Executable('docker').run([
+ ps = DOCKER_EXE.run([
'images',
'--filter', f"reference={Rootkit.__image_name(image_tag)}",
'--format', '{{.Repository}}:{{.Tag}}'
@@ -54,13 +54,13 @@ class Rootkit:
else:
if self.image_tag is None:
_logger.info(f"Pulling image {Rootkit.__image_name(self.image_tag)}")
- Executable('docker').run([
+ DOCKER_EXE.run([
'pull', Rootkit.__image_name(self.image_tag)
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
_logger.info(f"Building image {Rootkit.__image_name(self.image_tag)}")
- Executable('docker').run([
+ DOCKER_EXE.run([
'build',
'-t', Rootkit.__image_name(self.image_tag),
'-f', f"{IMAGES_DIRECTORY_NAME}/{self.image_tag}.Dockerfile",
@@ -69,7 +69,7 @@ class Rootkit:
def run(self, process_args, **kwargs):
self.__build_image()
- Executable('docker').run([
+ DOCKER_EXE.run([
'run', '--rm',
'-v', '/:/mnt',
'-u', 'root',
From 7ee7d5f3007dd7c9ecc658115ce601535bbe1d6e Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 13 Nov 2021 03:27:27 +0100
Subject: [PATCH 061/135] "kiwi cmd"
---
kiwi_scp/commands/cmd_cmd.py | 32 +++++++++++++++++++++
kiwi_scp/instance.py | 54 ++++++++++++++++++++++++++++++++----
2 files changed, 80 insertions(+), 6 deletions(-)
create mode 100644 kiwi_scp/commands/cmd_cmd.py
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
new file mode 100644
index 0000000..4d13a67
--- /dev/null
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -0,0 +1,32 @@
+from typing import Tuple
+
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance, Project
+
+
+@click.argument(
+ "compose_args",
+ metavar="[ARG]...",
+ nargs=-1,
+)
+@click.argument(
+ "compose_cmd",
+ metavar="CMD",
+)
+@kiwi_command(
+ "cmd",
+ KiwiCommandType.PROJECT,
+ short_help="Run docker-compose command",
+)
+class CMD(KiwiCommand):
+ """Run raw docker-compose command in a project"""
+
+ @classmethod
+ def run_for_existing_project(cls, instance: Instance, project: Project, compose_cmd: str = None,
+ compose_args: Tuple[str] = None) -> None:
+ if project.project_config.enabled:
+ COMPOSE_EXE.run([compose_cmd, *compose_args], **project.process_kwargs)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index a3b2189..b94a4fb 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,13 +1,13 @@
import functools
import re
from pathlib import Path
-from typing import Generator, List, Tuple, Optional
+from typing import Generator, List, Optional, Dict, Any
import attr
from ruamel.yaml.comments import CommentedMap
-from ._constants import COMPOSE_FILE_NAME
-from .config import KiwiConfig
+from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
+from .config import KiwiConfig, ProjectConfig
from .misc import YAML
_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
@@ -57,6 +57,7 @@ class Services:
@attr.s
class Project:
directory: Path = attr.ib()
+ config: KiwiConfig = attr.ib()
@staticmethod
@functools.lru_cache(maxsize=10)
@@ -68,6 +69,34 @@ class Project:
def name(self) -> str:
return self.directory.name
+ @property
+ def project_config(self) -> ProjectConfig:
+ return self.config.get_project_config(self.name)
+
+ @property
+ def process_kwargs(self) -> Dict[str, Any]:
+ directory: Path = self.directory
+ project_name: str = self.name
+ kiwi_hub_name: str = self.config.network.name
+ target_root_dir: Path = self.config.storage.directory
+ conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
+ target_dir: Path = target_root_dir.joinpath(project_name)
+
+ result: Dict[str, Any] = {
+ "cwd": str(directory),
+ "env": {
+ "COMPOSE_PROJECT_NAME": project_name,
+ "KIWI_HUB_NAME": kiwi_hub_name,
+ "TARGETROOT": str(target_root_dir),
+ "CONFDIR": str(conf_dir),
+ "TARGETDIR": str(target_dir),
+ },
+ }
+
+ result["env"].update(self.config.environment)
+
+ return result
+
@property
def services(self) -> Services:
yml = Project._parse_compose_file(self.directory)
@@ -88,7 +117,20 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
- def get_project(self, project_name: str) -> Optional[Project]:
- for project in self.config.projects:
+ @staticmethod
+ @functools.lru_cache(maxsize=None)
+ def __get_project(instance_directory: Path, project_name: str) -> Optional[Project]:
+ instance = Instance(instance_directory)
+ config = instance.config
+
+ for project in config.projects:
if project.name == project_name:
- return Project(self.directory.joinpath(project.name))
+ return Project(
+ directory=instance_directory.joinpath(project.name),
+ config=config,
+ )
+
+ def get_project(self, project_name: str) -> Optional[Project]:
+ project = Instance.__get_project(self.directory, project_name)
+ project.instance = self
+ return project
From 0f7c7f695529ca05a5db98150060449f4582645d Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 13 Nov 2021 03:35:52 +0100
Subject: [PATCH 062/135] Fix for docker-compose options
---
kiwi_scp/commands/cmd_cmd.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index 4d13a67..211cf74 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -21,6 +21,9 @@ from ..instance import Instance, Project
"cmd",
KiwiCommandType.PROJECT,
short_help="Run docker-compose command",
+ # ignore arguments looking like options
+ # just pass everything down to docker-compose
+ context_settings={"ignore_unknown_options": True},
)
class CMD(KiwiCommand):
"""Run raw docker-compose command in a project"""
From 7f892bcff2335dbc2ef2fdb6f1d644ec5d8c8ef0 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 17 Nov 2021 15:06:44 +0100
Subject: [PATCH 063/135] fixed regression: Changed "directory" option name
---
kiwi_scp/commands/cmd_init.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 428a011..1823206 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -38,9 +38,9 @@ class CMD(KiwiCommand):
"""Initialize or reconfigure a kiwi-scp instance"""
@classmethod
- def run_for_instance(cls, instance: Instance, output: Path = None, force: bool = None) -> None:
- if output is not None:
- instance.directory = output
+ def run_for_instance(cls, instance: Instance, directory: Path = None, force: bool = None) -> None:
+ if directory is not None:
+ instance.directory = directory
current_config = KiwiConfig() if force else instance.config
From 512ee9ba871d4019fd3127515d1f84a44a11974f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 17 Nov 2021 15:58:31 +0100
Subject: [PATCH 064/135] fix: get_project on nonexistent project
---
kiwi_scp/instance.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index b94a4fb..84f7c22 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -132,5 +132,8 @@ class Instance:
def get_project(self, project_name: str) -> Optional[Project]:
project = Instance.__get_project(self.directory, project_name)
+ if project is None:
+ return
+
project.instance = self
return project
From 9095b57b23fd167ca73fc09cb5880be916b0c566 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 17 Nov 2021 16:23:55 +0100
Subject: [PATCH 065/135] Type hinting in CLI, KiwiCommand class names, removed
kiwi_command name parameter
---
kiwi_scp/commands/cli.py | 33 +++++++++++++++++++++------------
kiwi_scp/commands/cmd_cmd.py | 7 +++----
kiwi_scp/commands/cmd_init.py | 5 ++---
kiwi_scp/commands/cmd_list.py | 5 ++---
kiwi_scp/commands/decorators.py | 8 +++-----
5 files changed, 31 insertions(+), 27 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 3c3feef..a699763 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,7 +1,8 @@
+import importlib
import os
import sys
from enum import Enum, auto
-from typing import List, Tuple, Iterable, Any, Type
+from typing import List, Tuple, Iterable, Type, Optional, TypeVar
import click
@@ -11,42 +12,50 @@ from ..instance import Instance, Project
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
- def list_commands(self, ctx):
+ def list_commands(self, ctx: click.Context) -> List[str]:
"""list all the commands defined by cmd_*.py files in this directory"""
- return (
+ return [
filename[4:-3]
for filename in os.listdir(os.path.abspath(os.path.dirname(__file__)))
if filename.startswith("cmd_") and filename.endswith(".py")
- )
+ ]
- def get_command(self, ctx, name):
+ def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
"""import and return a specific command"""
try:
- mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["CMD"])
+ cmd_module = importlib.import_module(f"kiwi_scp.commands.cmd_{cmd_name}")
+
except ImportError:
return
- return mod.CMD
+
+ for cmd_name in dir(cmd_module):
+ member = getattr(cmd_module, cmd_name)
+ if isinstance(member, click.Command):
+ return member
+
+
+T = TypeVar("T")
class KiwiCommand:
@staticmethod
- def print_multi_color(*content: Tuple[str, str]):
+ def print_multi_color(*content: Tuple[str, str]) -> None:
for message, color in content:
click.secho(message, fg=color, nl=False)
click.echo()
@staticmethod
- def print_header(header: str):
+ def print_header(header: str) -> None:
click.secho(header, fg="green", bold=True)
@staticmethod
- def print_error(error: str):
+ def print_error(error: str) -> None:
click.secho(error, file=sys.stderr, fg="red", bold=True)
@staticmethod
- def print_list(content: Iterable[str]):
+ def print_list(content: Iterable[str]) -> None:
for item in content:
KiwiCommand.print_multi_color(
(" - ", "green"),
@@ -54,7 +63,7 @@ class KiwiCommand:
)
@staticmethod
- def user_query(description: str, default: Any, cast_to: Type[Any] = str):
+ def user_query(description: str, default: T, cast_to: Type[T] = str) -> T:
# prompt user as per argument
while True:
try:
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index 211cf74..d0bc116 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -15,17 +15,16 @@ from ..instance import Instance, Project
)
@click.argument(
"compose_cmd",
- metavar="CMD",
+ metavar="COMMAND",
)
@kiwi_command(
- "cmd",
- KiwiCommandType.PROJECT,
+ cmd_type=KiwiCommandType.PROJECT,
short_help="Run docker-compose command",
# ignore arguments looking like options
# just pass everything down to docker-compose
context_settings={"ignore_unknown_options": True},
)
-class CMD(KiwiCommand):
+class CmdCommand(KiwiCommand):
"""Run raw docker-compose command in a project"""
@classmethod
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index 1823206..dc4b8f5 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -30,11 +30,10 @@ _logger = logging.getLogger(__name__)
help=f"use default values even if {KIWI_CONF_NAME} is present",
)
@kiwi_command(
- "init",
- KiwiCommandType.INSTANCE,
+ cmd_type=KiwiCommandType.INSTANCE,
short_help="Initializes kiwi-scp",
)
-class CMD(KiwiCommand):
+class InitCommand(KiwiCommand):
"""Initialize or reconfigure a kiwi-scp instance"""
@classmethod
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index ffd3e20..cb8f2e6 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -13,11 +13,10 @@ from ..instance import Instance, Project
help=f"show actual config contents instead",
)
@kiwi_command(
- "list",
- KiwiCommandType.SERVICE,
+ cmd_type=KiwiCommandType.SERVICE,
short_help="Inspect a kiwi-scp instance",
)
-class CMD(KiwiCommand):
+class ListCommand(KiwiCommand):
"""List projects in this instance, services inside a project or service(s) inside a project"""
@classmethod
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index c432779..76b5238 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -29,14 +29,12 @@ _logger = logging.getLogger(__name__)
def kiwi_command(
- name: str,
- command_type: KiwiCommandType,
+ cmd_type: KiwiCommandType = KiwiCommandType.SERVICE,
**decorator_kwargs,
) -> Callable:
def decorator(command_cls: Type[KiwiCommand]) -> Callable:
@click.command(
- name,
help=command_cls.__doc__,
**decorator_kwargs,
)
@@ -71,10 +69,10 @@ def kiwi_command(
else:
KiwiCommand.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{ctx.directory}'!")
- if command_type is KiwiCommandType.PROJECT:
+ if cmd_type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
- elif command_type is KiwiCommandType.SERVICE:
+ elif cmd_type is KiwiCommandType.SERVICE:
cmd = _project_arg(cmd)
cmd = _services_arg(cmd)
From 1c8f016208bb9e8e99e8451bf1c298557915f58b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 17 Nov 2021 16:36:34 +0100
Subject: [PATCH 066/135] pytests passed
---
tests/test_project.py | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/tests/test_project.py b/tests/test_project.py
index 917c2db..72fcf79 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -3,14 +3,20 @@ from pathlib import Path
import pytest
from kiwi_scp._constants import COMPOSE_FILE_NAME
+from kiwi_scp.config import KiwiConfig
from kiwi_scp.instance import Project
class TestDefault:
- def test_example(self):
- p = Project(Path("example/hello-world.project"))
+ cfg = KiwiConfig()
- ss = p.get_services()
+ def test_example(self):
+ p = Project(
+ directory=Path("example/hello-world.project"),
+ config=TestDefault.cfg,
+ )
+
+ ss = p.services
assert len(ss.content) == 5
@@ -18,14 +24,17 @@ class TestDefault:
assert s.name == "greeter"
- ss2 = p.get_services(["nonexistent"])
+ ss2 = p.services.filter_existing(["nonexistent"])
assert len(ss2.content) == 0
def test_empty(self):
- p = Project(Path("nonexistent"))
+ p = Project(
+ directory=Path("nonexistent"),
+ config=TestDefault.cfg,
+ )
with pytest.raises(FileNotFoundError) as exc_info:
- p.get_services()
+ _ = p.services
assert exc_info.value.filename == f"nonexistent/{COMPOSE_FILE_NAME}"
From e151562ab9bd0c3596118bb737bd2ee51cdee534 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 16:21:54 +0100
Subject: [PATCH 067/135] missing/wrong type hints
---
kiwi_scp/config.py | 14 +++++++-------
kiwi_scp/instance.py | 2 +-
kiwi_scp/misc.py | 2 +-
kiwi_scp/rootkit.py | 4 ++--
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index e78b6cd..08c4a3a 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -1,7 +1,7 @@
import functools
from ipaddress import IPv4Network
from pathlib import Path
-from typing import Optional, Dict, List, Any, TextIO
+from typing import Optional, Dict, List, Any, TextIO, Tuple
from pydantic import BaseModel, constr, root_validator, validator
@@ -55,7 +55,7 @@ class ProjectConfig(BaseModel):
@validator("override_storage", pre=True)
@classmethod
- def unify_storage(cls, value):
+ def unify_storage(cls, value) -> Dict[str, Any]:
"""parse different storage notations"""
if value is None or isinstance(value, dict):
@@ -135,7 +135,7 @@ class KiwiConfig(BaseModel):
@classmethod
@functools.lru_cache(maxsize=5)
- def from_directory(cls, directory: Path):
+ def from_directory(cls, directory: Path) -> "KiwiConfig":
"""parses an actual kiwi.yml from disk (cached)"""
try:
@@ -148,7 +148,7 @@ class KiwiConfig(BaseModel):
@classmethod
@functools.lru_cache(maxsize=1)
- def from_default(cls):
+ def from_default(cls) -> "KiwiConfig":
"""returns the default config (cached)"""
return cls()
@@ -268,7 +268,7 @@ class KiwiConfig(BaseModel):
def unify_environment(cls, value) -> Dict[str, Optional[str]]:
"""parse different environment notations"""
- def parse_str(var_val: Any) -> (str, Optional[str]):
+ def parse_str(var_val: Any) -> Tuple[str, Optional[str]]:
"""parse a "=" string"""
try:
@@ -311,7 +311,7 @@ class KiwiConfig(BaseModel):
@validator("storage", pre=True)
@classmethod
- def unify_storage(cls, value):
+ def unify_storage(cls, value) -> Dict[str, Any]:
"""parse different storage notations"""
if value is None:
@@ -336,7 +336,7 @@ class KiwiConfig(BaseModel):
@validator("network", pre=True)
@classmethod
- def unify_network(cls, value):
+ def unify_network(cls, value) -> Dict[str, Any]:
"""parse different network notations"""
if value is None:
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 84f7c22..7b358b0 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -46,7 +46,7 @@ class Services:
def __bool__(self) -> bool:
return bool(self.content)
- def filter_existing(self, service_names: List[str]):
+ def filter_existing(self, service_names: List[str]) -> "Services":
return Services([
service
for service in self.content
diff --git a/kiwi_scp/misc.py b/kiwi_scp/misc.py
index eb2ad3b..b38d4a0 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/misc.py
@@ -23,7 +23,7 @@ class YAML(ruamel.yaml.YAML):
return stream.getvalue()
@staticmethod
- def _format_kiwi_yml(yml_string: str):
+ def _format_kiwi_yml(yml_string: str) -> str:
# insert newline before every main key
yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
diff --git a/kiwi_scp/rootkit.py b/kiwi_scp/rootkit.py
index adef3f1..c1a42da 100644
--- a/kiwi_scp/rootkit.py
+++ b/kiwi_scp/rootkit.py
@@ -67,9 +67,9 @@ class Rootkit:
f"{IMAGES_DIRECTORY_NAME}"
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- def run(self, process_args, **kwargs):
+ def run(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
self.__build_image()
- DOCKER_EXE.run([
+ return DOCKER_EXE.run([
'run', '--rm',
'-v', '/:/mnt',
'-u', 'root',
From 8f390bdfa262a0f8271d1f69c5fa9be489fa49b3 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 17:39:43 +0100
Subject: [PATCH 068/135] Really aggressive confirmation prompt
---
kiwi_scp/commands/cli.py | 34 +++++++++++++
kiwi_scp/wstring.py | 104 +++++++++++++++++++++++++++++++++++++++
poetry.lock | 96 +++++++++++++++++++++++-------------
pyproject.toml | 1 +
4 files changed, 200 insertions(+), 35 deletions(-)
create mode 100644 kiwi_scp/wstring.py
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index a699763..4929d27 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -7,6 +7,7 @@ from typing import List, Tuple, Iterable, Type, Optional, TypeVar
import click
from ..instance import Instance, Project
+from ..wstring import WParagraph, WAlignment
class KiwiCLI(click.MultiCommand):
@@ -84,6 +85,39 @@ class KiwiCommand:
except Exception as e:
click.echo(f"Invalid input: {e}")
+ @staticmethod
+ def danger_confirm(*prompt_lines: str, default: Optional[bool] = None) -> bool:
+ if default is True:
+ suffix = "[YES|no]"
+ elif default is False:
+ suffix = "[yes|NO]"
+ else:
+ suffix = "[yes|no]"
+
+ dumb = WParagraph.from_strings(
+ click.style("WARNING", bold=True, underline=True, blink=True, fg="red"),
+ click.style("ここにゴミ", fg="cyan"),
+ click.style("を捨てないで下さい", fg="cyan"),
+ click.style("DO NOT DUMB HERE", fg="yellow"),
+ click.style("NO DUMB AREA", fg="yellow"),
+ ).align().surround("!")
+
+ prompt = WParagraph.from_strings(*prompt_lines).align(WAlignment.LEFT).emphasize(3)
+
+ answer = input(
+ f"{dumb}\n\n"
+ f"{prompt}\n\n"
+ f"Are you sure you want to proceed? {suffix} "
+ ).strip().lower()
+
+ if answer == '':
+ answer = default
+
+ while answer not in ['yes', 'no']:
+ answer = input("Please type 'yes' or 'no' explicitly: ").strip().lower()
+
+ return answer == 'yes'
+
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project_config in instance.config.projects:
diff --git a/kiwi_scp/wstring.py b/kiwi_scp/wstring.py
new file mode 100644
index 0000000..a6f07af
--- /dev/null
+++ b/kiwi_scp/wstring.py
@@ -0,0 +1,104 @@
+import re
+from enum import Enum, auto
+from typing import List
+
+import attr
+import wcwidth
+
+
+class WAlignment(Enum):
+ LEFT = auto()
+ RIGHT = auto()
+ CENTER = auto()
+
+
+@attr.s
+class WString:
+ s: str = attr.ib()
+
+ # from https://stackoverflow.com/a/38662876
+ ANSI_ESCAPES = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]")
+
+ def __str__(self) -> str:
+ return self.s
+
+ def __len__(self) -> int:
+ return wcwidth.wcswidth(WString.ANSI_ESCAPES.sub("", self.s))
+
+ def pad(self, alignment: WAlignment = WAlignment.CENTER, wlen: int = 0, char: str = " ") -> "WString":
+ char = char[0]
+
+ if alignment is WAlignment.LEFT:
+ return WString(f"{self}{char * wlen}")
+ elif alignment is WAlignment.RIGHT:
+ return WString(f"{char * wlen}{self}")
+ else:
+ pad_l, pad_r = wlen // 2, wlen - (wlen // 2)
+ return WString(f"{char * pad_l}{self}{char * pad_r}")
+
+
+@attr.s
+class WParagraph:
+ lines: List[WString] = attr.ib()
+
+ def __str__(self) -> str:
+ return "\n".join(
+ str(line)
+ for line
+ in self.lines
+ )
+
+ @classmethod
+ def from_strings(cls, *source: str) -> "WParagraph":
+ return cls([
+ WString(line)
+ for line
+ in source
+ ])
+
+ def align(self, alignment: WAlignment = WAlignment.CENTER, padding: int = 0, char: str = " ") -> "WParagraph":
+ total_length = max(
+ len(line)
+ for line
+ in self.lines
+ ) + padding
+ pad_lengths = (
+ total_length - len(line)
+ for line
+ in self.lines
+ )
+
+ return WParagraph([
+ line.pad(alignment, wlen, char)
+ for line, wlen
+ in zip(self.lines, pad_lengths)
+ ])
+
+ def surround(self, char: str, padding: int = 1) -> "WParagraph":
+ char = char[0]
+ padding = " " * padding
+
+ l_border, r_border = char + padding, padding + char
+
+ lines = [
+ WString(f"{l_border}{line}{r_border}")
+ for line
+ in self.lines
+ ]
+ extra_line = char * len(lines[0])
+
+ return WParagraph([
+ extra_line,
+ *lines,
+ extra_line,
+ ])
+
+ def emphasize(self, count: int = 3, padding: int = 1) -> "WParagraph":
+ padding = " " * padding
+ l_border, r_border = (">" * count) + padding, padding + ("<" * count)
+
+ return WParagraph([
+ WString(f"{l_border}{line}{r_border}")
+ for line
+ in self.lines
+ ])
diff --git a/poetry.lock b/poetry.lock
index eb51feb..f6ccca5 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -57,7 +57,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "coverage"
-version = "6.0.2"
+version = "6.1.2"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
@@ -323,6 +323,14 @@ six = ">=1.9.0,<2"
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+[[package]]
+name = "wcwidth"
+version = "0.2.5"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "main"
+optional = false
+python-versions = "*"
+
[[package]]
name = "zipp"
version = "3.6.0"
@@ -338,7 +346,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "631c4b1d21036305a2e9c891a631e66106549e1873b91264f481ba29207789fd"
+content-hash = "9083370030b96548400ebb4f9f2a82a3f5dd3a55bd83a9377df492180dc59c3e"
[metadata.files]
atomicwrites = [
@@ -362,39 +370,53 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
coverage = [
- {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
- {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
- {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
- {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
- {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
- {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
- {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
- {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
- {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
- {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
- {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
- {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
- {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
- {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
- {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
- {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
- {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
- {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
- {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
- {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
- {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
- {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
- {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
- {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
- {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
- {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
- {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
- {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
- {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
- {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
- {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
- {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
- {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
+ {file = "coverage-6.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9"},
+ {file = "coverage-6.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758"},
+ {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c"},
+ {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649"},
+ {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d"},
+ {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4"},
+ {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab"},
+ {file = "coverage-6.1.2-cp310-cp310-win32.whl", hash = "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc"},
+ {file = "coverage-6.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b"},
+ {file = "coverage-6.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052"},
+ {file = "coverage-6.1.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e"},
+ {file = "coverage-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266"},
+ {file = "coverage-6.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a"},
+ {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388"},
+ {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d"},
+ {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204"},
+ {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf"},
+ {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c"},
+ {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0"},
+ {file = "coverage-6.1.2-cp36-cp36m-win32.whl", hash = "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f"},
+ {file = "coverage-6.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b"},
+ {file = "coverage-6.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f"},
+ {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954"},
+ {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c"},
+ {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c"},
+ {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c"},
+ {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2"},
+ {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186"},
+ {file = "coverage-6.1.2-cp37-cp37m-win32.whl", hash = "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193"},
+ {file = "coverage-6.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93"},
+ {file = "coverage-6.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd"},
+ {file = "coverage-6.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13"},
+ {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696"},
+ {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373"},
+ {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263"},
+ {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d"},
+ {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091"},
+ {file = "coverage-6.1.2-cp38-cp38-win32.whl", hash = "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e"},
+ {file = "coverage-6.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b"},
+ {file = "coverage-6.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59"},
+ {file = "coverage-6.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225"},
+ {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71"},
+ {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4"},
+ {file = "coverage-6.1.2-cp39-cp39-win32.whl", hash = "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de"},
+ {file = "coverage-6.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc"},
+ {file = "coverage-6.1.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929"},
+ {file = "coverage-6.1.2.tar.gz", hash = "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972"},
]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
@@ -520,6 +542,10 @@ virtualenv = [
{file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"},
{file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"},
]
+wcwidth = [
+ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
+]
zipp = [
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
diff --git a/pyproject.toml b/pyproject.toml
index b9b0370..eab3e9d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ attrs = "^21.2.0"
click = "^8.0.3"
pydantic = "^1.8.2"
"ruamel.yaml" = "^0.17.16"
+wcwidth = "^0.2.5"
[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
From 973468be6a340f018ff10a7f27ce7b4b59ccb60d Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 18:13:31 +0100
Subject: [PATCH 069/135] "kiwi up", "kiwi down"
---
kiwi_scp/commands/cmd_down.py | 60 +++++++++++++++++++++++++++++++++++
kiwi_scp/commands/cmd_up.py | 38 ++++++++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_down.py
create mode 100644 kiwi_scp/commands/cmd_up.py
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
new file mode 100644
index 0000000..59bf376
--- /dev/null
+++ b/kiwi_scp/commands/cmd_down.py
@@ -0,0 +1,60 @@
+from typing import List
+
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance, Project
+
+
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"skip confirmation",
+)
+@kiwi_command(
+ cmd_type=KiwiCommandType.SERVICE,
+ short_help="Bring down kiwi services",
+)
+class DownCommand(KiwiCommand):
+ """Bring down the whole instance, a project or service(s) inside a project"""
+
+ @classmethod
+ def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
+ if not force:
+ if not KiwiCommand.danger_confirm(
+ "This will bring down the entire instance.",
+ "",
+ "This may not be what you intended, because:",
+ " - Bringing down the instance stops ALL services in here",
+ ):
+ return
+
+ super().run_for_instance(instance)
+
+ # TODO net-down
+
+ @classmethod
+ def run_for_existing_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ COMPOSE_EXE.run(["down"], **project.process_kwargs)
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ services = project.services.filter_existing(service_names)
+ existing_service_names = [
+ service.name
+ for service in services.content
+ ]
+ all_service_names_exist = len(existing_service_names) == len(service_names)
+
+ if not existing_service_names and not all_service_names_exist:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Bring down the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ COMPOSE_EXE.run(["stop", *existing_service_names], **project.process_kwargs)
+ COMPOSE_EXE.run(["rm", "-f", *existing_service_names], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
new file mode 100644
index 0000000..a548543
--- /dev/null
+++ b/kiwi_scp/commands/cmd_up.py
@@ -0,0 +1,38 @@
+from typing import List
+
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance, Project
+
+
+@kiwi_command(
+ cmd_type=KiwiCommandType.SERVICE,
+ short_help="Bring up kiwi services",
+)
+class UpCommand(KiwiCommand):
+ """Bring up the whole instance, a project or service(s) inside a project"""
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ # TODO conf-copy
+ # TODO net-up
+
+ services = project.services.filter_existing(service_names)
+ existing_service_names = [
+ service.name
+ for service in services.content
+ ]
+ all_service_names_exist = len(existing_service_names) == len(service_names)
+
+ if not existing_service_names and not all_service_names_exist:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Bring up the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ COMPOSE_EXE.run(["up", "-d", *existing_service_names], **project.process_kwargs)
From 3e52ba6e12685f9c4ac4be0ecc2c2eec0cf6fdff Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 18:25:38 +0100
Subject: [PATCH 070/135] style
---
kiwi_scp/wstring.py | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/kiwi_scp/wstring.py b/kiwi_scp/wstring.py
index a6f07af..c23c31a 100644
--- a/kiwi_scp/wstring.py
+++ b/kiwi_scp/wstring.py
@@ -44,34 +44,29 @@ class WParagraph:
def __str__(self) -> str:
return "\n".join(
str(line)
- for line
- in self.lines
+ for line in self.lines
)
@classmethod
def from_strings(cls, *source: str) -> "WParagraph":
return cls([
WString(line)
- for line
- in source
+ for line in source
])
def align(self, alignment: WAlignment = WAlignment.CENTER, padding: int = 0, char: str = " ") -> "WParagraph":
total_length = max(
len(line)
- for line
- in self.lines
+ for line in self.lines
) + padding
pad_lengths = (
total_length - len(line)
- for line
- in self.lines
+ for line in self.lines
)
return WParagraph([
line.pad(alignment, wlen, char)
- for line, wlen
- in zip(self.lines, pad_lengths)
+ for line, wlen in zip(self.lines, pad_lengths)
])
def surround(self, char: str, padding: int = 1) -> "WParagraph":
@@ -82,8 +77,7 @@ class WParagraph:
lines = [
WString(f"{l_border}{line}{r_border}")
- for line
- in self.lines
+ for line in self.lines
]
extra_line = char * len(lines[0])
@@ -99,6 +93,5 @@ class WParagraph:
return WParagraph([
WString(f"{l_border}{line}{r_border}")
- for line
- in self.lines
+ for line in self.lines
])
From 7afd540062daf78e59a5ed2c506686fd4f4a1cdb Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 18:32:51 +0100
Subject: [PATCH 071/135] "run_for_existing_project" -> "run_for_project"
---
kiwi_scp/commands/cli.py | 12 ++++++------
kiwi_scp/commands/cmd_cmd.py | 4 ++--
kiwi_scp/commands/cmd_down.py | 2 +-
kiwi_scp/commands/cmd_list.py | 2 +-
kiwi_scp/commands/decorators.py | 2 +-
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 4929d27..cf30a1e 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -122,17 +122,17 @@ class KiwiCommand:
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project_config in instance.config.projects:
project = instance.get_project(project_config.name)
- cls.run_for_existing_project(instance, project, **kwargs)
+ cls.run_for_project(instance, project, **kwargs)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ service_names = [service.name for service in project.services.content]
+ cls.run_for_services(instance, project, service_names, **kwargs)
@classmethod
def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
- @classmethod
- def run_for_existing_project(cls, instance: Instance, project: Project, **kwargs) -> None:
- service_names = [service.name for service in project.services.content]
- cls.run_for_services(instance, project, service_names, **kwargs)
-
@classmethod
def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
raise Exception
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index d0bc116..ebb181c 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -28,7 +28,7 @@ class CmdCommand(KiwiCommand):
"""Run raw docker-compose command in a project"""
@classmethod
- def run_for_existing_project(cls, instance: Instance, project: Project, compose_cmd: str = None,
- compose_args: Tuple[str] = None) -> None:
+ def run_for_project(cls, instance: Instance, project: Project, compose_cmd: str = None,
+ compose_args: Tuple[str] = None) -> None:
if project.project_config.enabled:
COMPOSE_EXE.run([compose_cmd, *compose_args], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index 59bf376..28ab477 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -36,7 +36,7 @@ class DownCommand(KiwiCommand):
# TODO net-down
@classmethod
- def run_for_existing_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
COMPOSE_EXE.run(["down"], **project.process_kwargs)
@classmethod
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index cb8f2e6..092c8f9 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -33,7 +33,7 @@ class ListCommand(KiwiCommand):
)
@classmethod
- def run_for_existing_project(cls, instance: Instance, project: Project, show: bool = None) -> None:
+ def run_for_project(cls, instance: Instance, project: Project, show: bool = None) -> None:
if show:
KiwiCommand.print_header(f"Showing config for all services in project '{project.name}'.")
click.echo_via_pager(str(project.services))
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 76b5238..22e5aec 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -53,7 +53,7 @@ def kiwi_command(
project = ctx.get_project(project_name)
if project is not None:
_logger.debug(f"running for existing project {project}, kwargs={kwargs}")
- command_cls.run_for_existing_project(ctx, project, **kwargs)
+ command_cls.run_for_project(ctx, project, **kwargs)
else:
_logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
From f194ea935621e8da378af30f6a298f00697c1256 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Sat, 27 Nov 2021 18:33:46 +0100
Subject: [PATCH 072/135] new "run_for_filtered_services"
---
kiwi_scp/commands/cli.py | 16 +++++++++++++++-
kiwi_scp/commands/cmd_down.py | 18 ++++++------------
kiwi_scp/commands/cmd_up.py | 16 +++++-----------
kiwi_scp/instance.py | 7 +++++++
4 files changed, 33 insertions(+), 24 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index cf30a1e..c06f682 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -6,7 +6,7 @@ from typing import List, Tuple, Iterable, Type, Optional, TypeVar
import click
-from ..instance import Instance, Project
+from ..instance import Instance, Project, Services
from ..wstring import WParagraph, WAlignment
@@ -135,6 +135,20 @@ class KiwiCommand:
@classmethod
def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ services = project.services.filter_existing(service_names)
+
+ new_service_names = [
+ service_name
+ for service_name
+ in service_names
+ if service_name not in list(services.names)
+ ]
+
+ cls.run_for_filtered_services(instance, project, services, new_service_names, **kwargs)
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
raise Exception
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index 28ab477..80e4bec 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -5,7 +5,7 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project
+from ..instance import Instance, Project, Services
@click.option(
@@ -40,15 +40,9 @@ class DownCommand(KiwiCommand):
COMPOSE_EXE.run(["down"], **project.process_kwargs)
@classmethod
- def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
- services = project.services.filter_existing(service_names)
- existing_service_names = [
- service.name
- for service in services.content
- ]
- all_service_names_exist = len(existing_service_names) == len(service_names)
-
- if not existing_service_names and not all_service_names_exist:
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ if not services:
if not click.confirm(
"Did not find any of those services. \n"
f"Bring down the entire project {project.name} instead?",
@@ -56,5 +50,5 @@ class DownCommand(KiwiCommand):
):
return
- COMPOSE_EXE.run(["stop", *existing_service_names], **project.process_kwargs)
- COMPOSE_EXE.run(["rm", "-f", *existing_service_names], **project.process_kwargs)
+ COMPOSE_EXE.run(["stop", *services.names], **project.process_kwargs)
+ COMPOSE_EXE.run(["rm", "-f", *services.names], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
index a548543..193cc77 100644
--- a/kiwi_scp/commands/cmd_up.py
+++ b/kiwi_scp/commands/cmd_up.py
@@ -5,7 +5,7 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project
+from ..instance import Instance, Project, Services
@kiwi_command(
@@ -16,18 +16,12 @@ class UpCommand(KiwiCommand):
"""Bring up the whole instance, a project or service(s) inside a project"""
@classmethod
- def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
# TODO conf-copy
# TODO net-up
- services = project.services.filter_existing(service_names)
- existing_service_names = [
- service.name
- for service in services.content
- ]
- all_service_names_exist = len(existing_service_names) == len(service_names)
-
- if not existing_service_names and not all_service_names_exist:
+ if not services:
if not click.confirm(
"Did not find any of those services. \n"
f"Bring up the entire project {project.name} instead?",
@@ -35,4 +29,4 @@ class UpCommand(KiwiCommand):
):
return
- COMPOSE_EXE.run(["up", "-d", *existing_service_names], **project.process_kwargs)
+ COMPOSE_EXE.run(["up", "-d", *services.names], **project.process_kwargs)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 7b358b0..47453c9 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -46,6 +46,13 @@ class Services:
def __bool__(self) -> bool:
return bool(self.content)
+ @property
+ def names(self) -> Generator[str, None, None]:
+ return (
+ service.name
+ for service in self.content
+ )
+
def filter_existing(self, service_names: List[str]) -> "Services":
return Services([
service
From 99a50f14600747506f9f84d644f41ec56a95637a Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 30 Nov 2021 19:25:53 +0100
Subject: [PATCH 073/135] remove run logic from kiwi_command
---
kiwi_scp/commands/cli.py | 34 +++++++++++++++++++++++++++++++++
kiwi_scp/commands/decorators.py | 32 +++----------------------------
2 files changed, 37 insertions(+), 29 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index c06f682..8616068 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,4 +1,5 @@
import importlib
+import logging
import os
import sys
from enum import Enum, auto
@@ -9,6 +10,8 @@ import click
from ..instance import Instance, Project, Services
from ..wstring import WParagraph, WAlignment
+_logger = logging.getLogger(__name__)
+
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
@@ -118,6 +121,37 @@ class KiwiCommand:
return answer == 'yes'
+ @classmethod
+ def run(cls, instance: Instance, project_name: Optional[str] = None, service_names: Optional[List[str]] = None,
+ **kwargs):
+
+ _logger.debug(f"{instance.directory!r}: {project_name!r}, {service_names!r}")
+ if project_name is None:
+ # run for whole instance
+ _logger.debug(f"running for instance, kwargs={kwargs}")
+ cls.run_for_instance(instance, **kwargs)
+
+ elif not service_names:
+ # run for one entire project
+ project = instance.get_project(project_name)
+ if project is None:
+ _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
+ cls.run_for_new_project(instance, project_name, **kwargs)
+
+ else:
+ _logger.debug(f"running for project {project.name}, kwargs={kwargs}")
+ cls.run_for_project(instance, project, **kwargs)
+
+ else:
+ # run for some services
+ project = instance.get_project(project_name)
+ if project is not None:
+ _logger.debug(f"running for services {service_names} in project {project}, kwargs={kwargs}")
+ cls.run_for_services(instance, project, service_names, **kwargs)
+
+ else:
+ cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
+
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project_config in instance.config.projects:
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 22e5aec..04e470f 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -1,4 +1,3 @@
-import logging
from typing import Callable, Type, Optional, Tuple
import click
@@ -25,8 +24,6 @@ _services_arg = click.argument(
type=str,
)
-_logger = logging.getLogger(__name__)
-
def kiwi_command(
cmd_type: KiwiCommandType = KiwiCommandType.SERVICE,
@@ -41,33 +38,10 @@ def kiwi_command(
@_pass_instance
def cmd(ctx: Instance, project_name: Optional[str] = None, service_names: Optional[Tuple[str]] = None,
**kwargs) -> None:
+ if service_names is not None:
+ service_names = list(service_names)
- _logger.debug(f"{ctx.directory!r}: {project_name!r}, {service_names!r}")
- if project_name is None:
- # run for whole instance
- _logger.debug(f"running for instance, kwargs={kwargs}")
- command_cls.run_for_instance(ctx, **kwargs)
-
- elif not service_names:
- # run for one entire project
- project = ctx.get_project(project_name)
- if project is not None:
- _logger.debug(f"running for existing project {project}, kwargs={kwargs}")
- command_cls.run_for_project(ctx, project, **kwargs)
-
- else:
- _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
- command_cls.run_for_new_project(ctx, project_name, **kwargs)
-
- else:
- # run for some services
- project = ctx.get_project(project_name)
- if project is not None:
- _logger.debug(f"running for services {service_names} in project {project}, kwargs={kwargs}")
- command_cls.run_for_services(ctx, project, list(service_names), **kwargs)
-
- else:
- KiwiCommand.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{ctx.directory}'!")
+ command_cls.run(ctx, project_name, service_names, **kwargs)
if cmd_type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
From 0c18ba2dd5c9ec4c534b7c45000ae325c3ff964c Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 30 Nov 2021 19:26:31 +0100
Subject: [PATCH 074/135] "kiwi build"
---
kiwi_scp/commands/cmd_build.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_build.py
diff --git a/kiwi_scp/commands/cmd_build.py b/kiwi_scp/commands/cmd_build.py
new file mode 100644
index 0000000..2f5de78
--- /dev/null
+++ b/kiwi_scp/commands/cmd_build.py
@@ -0,0 +1,18 @@
+from typing import List
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance, Project
+
+
+@kiwi_command(
+ cmd_type=KiwiCommandType.SERVICE,
+ short_help="Build docker images",
+)
+class BuildCommand(KiwiCommand):
+ """Build images for the whole instance, a project or service(s) inside a project"""
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ COMPOSE_EXE.run(["build", "--pull", *service_names], **project.process_kwargs)
From 88004f0d2dbe0bfc953200ae16bead91ac5c3f81 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 12:29:40 +0100
Subject: [PATCH 075/135] "kiwi enable", "kiwi disable"
---
kiwi_scp/commands/cmd_disable.py | 39 ++++++++++++++++++++++++++++++++
kiwi_scp/commands/cmd_enable.py | 39 ++++++++++++++++++++++++++++++++
2 files changed, 78 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_disable.py
create mode 100644 kiwi_scp/commands/cmd_enable.py
diff --git a/kiwi_scp/commands/cmd_disable.py b/kiwi_scp/commands/cmd_disable.py
new file mode 100644
index 0000000..69d0c77
--- /dev/null
+++ b/kiwi_scp/commands/cmd_disable.py
@@ -0,0 +1,39 @@
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from .._constants import KIWI_CONF_NAME
+from ..instance import Instance, Project
+
+
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"skip confirmation",
+)
+@kiwi_command(
+ cmd_type=KiwiCommandType.PROJECT,
+)
+class DisableCommand(KiwiCommand):
+ """Disable a project"""
+
+ @classmethod
+ def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
+ if not force:
+ if not KiwiCommand.danger_confirm("This will disable all projects in this instance."):
+ return
+
+ super().run_for_instance(instance)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ if not project.project_config.enabled:
+ KiwiCommand.print_error(f"Project {project.name} is already disabled!")
+ return
+
+ project.project_config.enabled = False
+ KiwiCommand.print_header(f"Project {project.name} disabled")
+
+ # write out the new kiwi.yml
+ with open(instance.directory.joinpath(KIWI_CONF_NAME), "w") as file:
+ instance.config.dump_kiwi_yml(file)
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
new file mode 100644
index 0000000..d0e952e
--- /dev/null
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -0,0 +1,39 @@
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from .._constants import KIWI_CONF_NAME
+from ..instance import Instance, Project
+
+
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"skip confirmation",
+)
+@kiwi_command(
+ cmd_type=KiwiCommandType.PROJECT,
+)
+class DisableCommand(KiwiCommand):
+ """Enable a project"""
+
+ @classmethod
+ def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
+ if not force:
+ if not KiwiCommand.danger_confirm("This will enable all projects in this instance."):
+ return
+
+ super().run_for_instance(instance)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ if project.project_config.enabled:
+ KiwiCommand.print_error(f"Project {project.name} is already enabled!")
+ return
+
+ project.project_config.enabled = True
+ KiwiCommand.print_header(f"Project {project.name} enabled")
+
+ # write out the new kiwi.yml
+ with open(instance.directory.joinpath(KIWI_CONF_NAME), "w") as file:
+ instance.config.dump_kiwi_yml(file)
From 10e600a4ace065e8fd4cf7acb05f25091951fda2 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 13:02:49 +0100
Subject: [PATCH 076/135] cleanup
---
kiwi_scp/commands/cli.py | 17 +++++++++++------
kiwi_scp/commands/cmd_list.py | 2 +-
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 8616068..755162d 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -92,10 +92,15 @@ class KiwiCommand:
def danger_confirm(*prompt_lines: str, default: Optional[bool] = None) -> bool:
if default is True:
suffix = "[YES|no]"
+ default_answer = "yes"
+
elif default is False:
suffix = "[yes|NO]"
+ default_answer = "no"
+
else:
suffix = "[yes|no]"
+ default_answer = None
dumb = WParagraph.from_strings(
click.style("WARNING", bold=True, underline=True, blink=True, fg="red"),
@@ -103,23 +108,23 @@ class KiwiCommand:
click.style("を捨てないで下さい", fg="cyan"),
click.style("DO NOT DUMB HERE", fg="yellow"),
click.style("NO DUMB AREA", fg="yellow"),
- ).align().surround("!")
+ ).align(WAlignment.CENTER).surround("!")
prompt = WParagraph.from_strings(*prompt_lines).align(WAlignment.LEFT).emphasize(3)
answer = input(
f"{dumb}\n\n"
f"{prompt}\n\n"
- f"Are you sure you want to proceed? {suffix} "
+ f"Are you sure you want to proceed? {suffix}: "
).strip().lower()
- if answer == '':
- answer = default
+ if not answer:
+ answer = default_answer
- while answer not in ['yes', 'no']:
+ while answer not in ["yes", "no"]:
answer = input("Please type 'yes' or 'no' explicitly: ").strip().lower()
- return answer == 'yes'
+ return answer == "yes"
@classmethod
def run(cls, instance: Instance, project_name: Optional[str] = None, service_names: Optional[List[str]] = None,
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 092c8f9..0dca29d 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -28,7 +28,7 @@ class ListCommand(KiwiCommand):
else:
KiwiCommand.print_header(f"Projects in kiwi-scp instance at '{instance.directory}':")
KiwiCommand.print_list(
- project.name + click.style(" (disabled)" if not project.enabled else "", fg="red")
+ project.name + (click.style(" (disabled)", fg="red") if not project.enabled else "")
for project in instance.config.projects
)
From ef593d6cd9c17d8b87368398b4810eb3350bddbc Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 13:07:35 +0100
Subject: [PATCH 077/135] preparation for KiwiCommandType.PROJECTS
---
kiwi_scp/commands/cli.py | 1 +
kiwi_scp/commands/decorators.py | 23 ++++++++++++++++++++---
2 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 755162d..2d01adf 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -194,4 +194,5 @@ class KiwiCommand:
class KiwiCommandType(Enum):
INSTANCE = auto()
PROJECT = auto()
+ PROJECTS = auto()
SERVICE = auto()
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 04e470f..61777b1 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -11,13 +11,27 @@ _pass_instance = click.make_pass_decorator(
)
_project_arg = click.argument(
+ "project_name",
+ metavar="PROJECT",
+ required=False, # TODO remove this line when PROJECTS logic is implemented
+ type=str,
+)
+
+_projects_arg = click.argument(
+ "project_names",
+ metavar="[PROJECT]...",
+ nargs=-1,
+ type=str,
+)
+
+_services_arg_p = click.argument(
"project_name",
metavar="[PROJECT]",
required=False,
type=str,
)
-_services_arg = click.argument(
+_services_arg_s = click.argument(
"service_names",
metavar="[SERVICE]...",
nargs=-1,
@@ -46,9 +60,12 @@ def kiwi_command(
if cmd_type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
+ elif cmd_type is KiwiCommandType.PROJECTS:
+ cmd = _projects_arg(cmd)
+
elif cmd_type is KiwiCommandType.SERVICE:
- cmd = _project_arg(cmd)
- cmd = _services_arg(cmd)
+ cmd = _services_arg_p(cmd)
+ cmd = _services_arg_s(cmd)
return cmd
From ed659ba50fa669b1b765810d73ea46a994936623 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 13:08:49 +0100
Subject: [PATCH 078/135] PyCharm exclude htmlcov
---
.idea/kiwi-scp.iml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.idea/kiwi-scp.iml b/.idea/kiwi-scp.iml
index 127e464..fddd591 100644
--- a/.idea/kiwi-scp.iml
+++ b/.idea/kiwi-scp.iml
@@ -4,6 +4,7 @@
+
From 3cdc68885ce39c78b3d08d59d0db81873c400d14 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 17:47:03 +0100
Subject: [PATCH 079/135] Logic for PROJECTS command type; kiwi_command args ->
KiwiCommand vars
---
kiwi_scp/commands/cli.py | 81 +++++++++++++++++++--------------
kiwi_scp/commands/decorators.py | 31 +++++++++----
2 files changed, 68 insertions(+), 44 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 2d01adf..2d987fb 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -3,7 +3,7 @@ import logging
import os
import sys
from enum import Enum, auto
-from typing import List, Tuple, Iterable, Type, Optional, TypeVar
+from typing import List, Iterable, Type, Optional, TypeVar
import click
@@ -40,15 +40,19 @@ class KiwiCLI(click.MultiCommand):
return member
+class KiwiCommandType(Enum):
+ INSTANCE = auto()
+ PROJECT = auto()
+ PROJECTS = auto()
+ SERVICES = auto()
+
+
T = TypeVar("T")
class KiwiCommand:
- @staticmethod
- def print_multi_color(*content: Tuple[str, str]) -> None:
- for message, color in content:
- click.secho(message, fg=color, nl=False)
- click.echo()
+ type: KiwiCommandType = KiwiCommandType.SERVICES
+ enabled_only: bool = False
@staticmethod
def print_header(header: str) -> None:
@@ -61,10 +65,7 @@ class KiwiCommand:
@staticmethod
def print_list(content: Iterable[str]) -> None:
for item in content:
- KiwiCommand.print_multi_color(
- (" - ", "green"),
- (item, "blue"),
- )
+ click.echo(click.style(" - ", fg="green") + click.style(item, fg="blue"))
@staticmethod
def user_query(description: str, default: T, cast_to: Type[T] = str) -> T:
@@ -127,39 +128,58 @@ class KiwiCommand:
return answer == "yes"
@classmethod
- def run(cls, instance: Instance, project_name: Optional[str] = None, service_names: Optional[List[str]] = None,
- **kwargs):
+ def run(cls, instance: Instance, project_names: List[str], service_names: List[str], **kwargs):
- _logger.debug(f"{instance.directory!r}: {project_name!r}, {service_names!r}")
- if project_name is None:
+ _logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
+
+ projects = [
+ instance.get_project(project_name)
+ for project_name in project_names
+ ]
+
+ if not projects:
# run for whole instance
_logger.debug(f"running for instance, kwargs={kwargs}")
cls.run_for_instance(instance, **kwargs)
elif not service_names:
- # run for one entire project
- project = instance.get_project(project_name)
- if project is None:
- _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
- cls.run_for_new_project(instance, project_name, **kwargs)
+ # run for entire project(s)
+ for project_name, project in zip(project_names, projects):
+ if project is None:
+ _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
+ cls.run_for_new_project(instance, project_name, **kwargs)
- else:
- _logger.debug(f"running for project {project.name}, kwargs={kwargs}")
- cls.run_for_project(instance, project, **kwargs)
+ else:
+ if cls.enabled_only and not project.project_config.enabled:
+ cls.print_error(f"Can't interact with disabled project {project_name}!")
+ return
+
+ _logger.debug(f"running for project {project.name}, kwargs={kwargs}")
+ cls.run_for_project(instance, project, **kwargs)
else:
# run for some services
- project = instance.get_project(project_name)
- if project is not None:
- _logger.debug(f"running for services {service_names} in project {project}, kwargs={kwargs}")
- cls.run_for_services(instance, project, service_names, **kwargs)
+ project_name = project_names[0]
+ project = projects[0]
+
+ if project is None:
+ cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
else:
- cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
+ if cls.enabled_only and not project.project_config.enabled:
+ cls.print_error(f"Can't interact with disabled project {project_name}!")
+ return
+
+ _logger.debug(f"running for services {service_names} in project {project_name}, kwargs={kwargs}")
+ cls.run_for_services(instance, project, service_names, **kwargs)
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
for project_config in instance.config.projects:
+ if cls.enabled_only and not project_config.enabled:
+ cls.print_header(f"Skipping disabled project {project_config.name}")
+ continue
+
project = instance.get_project(project_config.name)
cls.run_for_project(instance, project, **kwargs)
@@ -189,10 +209,3 @@ class KiwiCommand:
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
raise Exception
-
-
-class KiwiCommandType(Enum):
- INSTANCE = auto()
- PROJECT = auto()
- PROJECTS = auto()
- SERVICE = auto()
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index 61777b1..f8b9ca7 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -13,7 +13,6 @@ _pass_instance = click.make_pass_decorator(
_project_arg = click.argument(
"project_name",
metavar="PROJECT",
- required=False, # TODO remove this line when PROJECTS logic is implemented
type=str,
)
@@ -40,7 +39,6 @@ _services_arg_s = click.argument(
def kiwi_command(
- cmd_type: KiwiCommandType = KiwiCommandType.SERVICE,
**decorator_kwargs,
) -> Callable:
def decorator(command_cls: Type[KiwiCommand]) -> Callable:
@@ -50,20 +48,33 @@ def kiwi_command(
**decorator_kwargs,
)
@_pass_instance
- def cmd(ctx: Instance, project_name: Optional[str] = None, service_names: Optional[Tuple[str]] = None,
- **kwargs) -> None:
- if service_names is not None:
- service_names = list(service_names)
+ def cmd(ctx: Instance, project_name: Optional[str] = None, project_names: Optional[Tuple[str]] = None,
+ service_names: Optional[Tuple[str]] = None, **kwargs) -> None:
+ if command_cls.type is KiwiCommandType.INSTANCE:
+ project_names = []
- command_cls.run(ctx, project_name, service_names, **kwargs)
+ elif command_cls.type is KiwiCommandType.PROJECTS:
+ project_names = list(project_names)
- if cmd_type is KiwiCommandType.PROJECT:
+ else:
+ if project_name is None:
+ project_names = []
+
+ else:
+ project_names = [project_name]
+
+ if command_cls.type is KiwiCommandType.SERVICES:
+ service_names = list(service_names)
+
+ command_cls.run(ctx, project_names, service_names, **kwargs)
+
+ if command_cls.type is KiwiCommandType.PROJECT:
cmd = _project_arg(cmd)
- elif cmd_type is KiwiCommandType.PROJECTS:
+ elif command_cls.type is KiwiCommandType.PROJECTS:
cmd = _projects_arg(cmd)
- elif cmd_type is KiwiCommandType.SERVICE:
+ elif command_cls.type is KiwiCommandType.SERVICES:
cmd = _services_arg_p(cmd)
cmd = _services_arg_s(cmd)
From d197398c3b27b1d05fcf7f22a0854a1ecaf46166 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 17:49:19 +0100
Subject: [PATCH 080/135] As per new command API
---
kiwi_scp/commands/cmd_build.py | 4 +++-
kiwi_scp/commands/cmd_cmd.py | 7 ++++---
kiwi_scp/commands/cmd_disable.py | 8 ++++----
kiwi_scp/commands/cmd_down.py | 4 +++-
kiwi_scp/commands/cmd_enable.py | 8 ++++----
kiwi_scp/commands/cmd_init.py | 3 ++-
kiwi_scp/commands/cmd_list.py | 6 ++++--
kiwi_scp/commands/cmd_up.py | 8 ++++----
8 files changed, 28 insertions(+), 20 deletions(-)
diff --git a/kiwi_scp/commands/cmd_build.py b/kiwi_scp/commands/cmd_build.py
index 2f5de78..57008a0 100644
--- a/kiwi_scp/commands/cmd_build.py
+++ b/kiwi_scp/commands/cmd_build.py
@@ -7,12 +7,14 @@ from ..instance import Instance, Project
@kiwi_command(
- cmd_type=KiwiCommandType.SERVICE,
short_help="Build docker images",
)
class BuildCommand(KiwiCommand):
"""Build images for the whole instance, a project or service(s) inside a project"""
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
@classmethod
def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
COMPOSE_EXE.run(["build", "--pull", *service_names], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index ebb181c..58996f7 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -18,7 +18,6 @@ from ..instance import Instance, Project
metavar="COMMAND",
)
@kiwi_command(
- cmd_type=KiwiCommandType.PROJECT,
short_help="Run docker-compose command",
# ignore arguments looking like options
# just pass everything down to docker-compose
@@ -27,8 +26,10 @@ from ..instance import Instance, Project
class CmdCommand(KiwiCommand):
"""Run raw docker-compose command in a project"""
+ type = KiwiCommandType.PROJECT
+ enabled_only = True
+
@classmethod
def run_for_project(cls, instance: Instance, project: Project, compose_cmd: str = None,
compose_args: Tuple[str] = None) -> None:
- if project.project_config.enabled:
- COMPOSE_EXE.run([compose_cmd, *compose_args], **project.process_kwargs)
+ COMPOSE_EXE.run([compose_cmd, *compose_args], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_disable.py b/kiwi_scp/commands/cmd_disable.py
index 69d0c77..adb1687 100644
--- a/kiwi_scp/commands/cmd_disable.py
+++ b/kiwi_scp/commands/cmd_disable.py
@@ -11,11 +11,11 @@ from ..instance import Instance, Project
"--force/--no-force",
help=f"skip confirmation",
)
-@kiwi_command(
- cmd_type=KiwiCommandType.PROJECT,
-)
+@kiwi_command()
class DisableCommand(KiwiCommand):
- """Disable a project"""
+ """Disable project(s)"""
+
+ type = KiwiCommandType.PROJECTS
@classmethod
def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index 80e4bec..b055328 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -14,12 +14,14 @@ from ..instance import Instance, Project, Services
help=f"skip confirmation",
)
@kiwi_command(
- cmd_type=KiwiCommandType.SERVICE,
short_help="Bring down kiwi services",
)
class DownCommand(KiwiCommand):
"""Bring down the whole instance, a project or service(s) inside a project"""
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
@classmethod
def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
if not force:
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
index d0e952e..580644a 100644
--- a/kiwi_scp/commands/cmd_enable.py
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -11,11 +11,11 @@ from ..instance import Instance, Project
"--force/--no-force",
help=f"skip confirmation",
)
-@kiwi_command(
- cmd_type=KiwiCommandType.PROJECT,
-)
+@kiwi_command()
class DisableCommand(KiwiCommand):
- """Enable a project"""
+ """Enable project(s)"""
+
+ type = KiwiCommandType.PROJECTS
@classmethod
def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index dc4b8f5..ab8ee98 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -30,12 +30,13 @@ _logger = logging.getLogger(__name__)
help=f"use default values even if {KIWI_CONF_NAME} is present",
)
@kiwi_command(
- cmd_type=KiwiCommandType.INSTANCE,
short_help="Initializes kiwi-scp",
)
class InitCommand(KiwiCommand):
"""Initialize or reconfigure a kiwi-scp instance"""
+ type = KiwiCommandType.INSTANCE
+
@classmethod
def run_for_instance(cls, instance: Instance, directory: Path = None, force: bool = None) -> None:
if directory is not None:
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 0dca29d..5c98568 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -13,12 +13,13 @@ from ..instance import Instance, Project
help=f"show actual config contents instead",
)
@kiwi_command(
- cmd_type=KiwiCommandType.SERVICE,
short_help="Inspect a kiwi-scp instance",
)
class ListCommand(KiwiCommand):
"""List projects in this instance, services inside a project or service(s) inside a project"""
+ type = KiwiCommandType.SERVICES
+
@classmethod
def run_for_instance(cls, instance: Instance, show: bool = None) -> None:
if show:
@@ -43,7 +44,8 @@ class ListCommand(KiwiCommand):
KiwiCommand.print_list(service.name for service in project.services.content)
@classmethod
- def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], show: bool = None) -> None:
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str],
+ show: bool = None) -> None:
services = project.services.filter_existing(service_names)
if show:
service_names = [service.name for service in services.content]
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
index 193cc77..8485bdb 100644
--- a/kiwi_scp/commands/cmd_up.py
+++ b/kiwi_scp/commands/cmd_up.py
@@ -8,13 +8,13 @@ from ..executable import COMPOSE_EXE
from ..instance import Instance, Project, Services
-@kiwi_command(
- cmd_type=KiwiCommandType.SERVICE,
- short_help="Bring up kiwi services",
-)
+@kiwi_command(short_help="Bring up kiwi services")
class UpCommand(KiwiCommand):
"""Bring up the whole instance, a project or service(s) inside a project"""
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
@classmethod
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
From 15f0ac30b5067a2694ddb8dca9c730d33714d12b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 1 Dec 2021 19:12:54 +0100
Subject: [PATCH 081/135] "kiwi logs"
---
kiwi_scp/commands/cmd_logs.py | 42 +++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_logs.py
diff --git a/kiwi_scp/commands/cmd_logs.py b/kiwi_scp/commands/cmd_logs.py
new file mode 100644
index 0000000..f31a066
--- /dev/null
+++ b/kiwi_scp/commands/cmd_logs.py
@@ -0,0 +1,42 @@
+from typing import List
+
+import click
+
+from .cli import KiwiCommand, KiwiCommandType
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance, Project, Services
+
+
+@click.option(
+ "-f/-F",
+ "--follow/--no-follow",
+ help="output appended data as log grows",
+)
+@kiwi_command(
+ short_help="Show logs",
+)
+class LogsCommand(KiwiCommand):
+ """Show logs of a project or service(s) inside a project"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], follow: bool = None) -> None:
+ # include timestamps
+ compose_cmd = ["logs", "-t"]
+
+ # handle following the log output
+ if follow:
+ compose_cmd.extend(("-f", "--tail=10"))
+
+ compose_cmd.extend(services.names)
+
+ if follow:
+ COMPOSE_EXE.run(compose_cmd, **project.process_kwargs)
+
+ else:
+ # output is static, use pager
+ COMPOSE_EXE.run_with_pager(compose_cmd, **project.process_kwargs)
From 0fc55154f2258f84b7f4b0fbe21ce765383946ce Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 02:38:29 +0100
Subject: [PATCH 082/135] instance parent relations
---
kiwi_scp/commands/cli.py | 4 +--
kiwi_scp/commands/cmd_disable.py | 4 +--
kiwi_scp/commands/cmd_enable.py | 4 +--
kiwi_scp/instance.py | 47 +++++++++++++-------------------
tests/test_project.py | 2 --
5 files changed, 25 insertions(+), 36 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 2d987fb..01e2087 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -150,7 +150,7 @@ class KiwiCommand:
cls.run_for_new_project(instance, project_name, **kwargs)
else:
- if cls.enabled_only and not project.project_config.enabled:
+ if cls.enabled_only and not project.config.enabled:
cls.print_error(f"Can't interact with disabled project {project_name}!")
return
@@ -166,7 +166,7 @@ class KiwiCommand:
cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
else:
- if cls.enabled_only and not project.project_config.enabled:
+ if cls.enabled_only and not project.config.enabled:
cls.print_error(f"Can't interact with disabled project {project_name}!")
return
diff --git a/kiwi_scp/commands/cmd_disable.py b/kiwi_scp/commands/cmd_disable.py
index adb1687..6a146d0 100644
--- a/kiwi_scp/commands/cmd_disable.py
+++ b/kiwi_scp/commands/cmd_disable.py
@@ -27,11 +27,11 @@ class DisableCommand(KiwiCommand):
@classmethod
def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
- if not project.project_config.enabled:
+ if not project.config.enabled:
KiwiCommand.print_error(f"Project {project.name} is already disabled!")
return
- project.project_config.enabled = False
+ project.config.enabled = False
KiwiCommand.print_header(f"Project {project.name} disabled")
# write out the new kiwi.yml
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
index 580644a..9f7eadc 100644
--- a/kiwi_scp/commands/cmd_enable.py
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -27,11 +27,11 @@ class DisableCommand(KiwiCommand):
@classmethod
def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
- if project.project_config.enabled:
+ if project.config.enabled:
KiwiCommand.print_error(f"Project {project.name} is already enabled!")
return
- project.project_config.enabled = True
+ project.config.enabled = True
KiwiCommand.print_header(f"Project {project.name} enabled")
# write out the new kiwi.yml
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 47453c9..3d47c87 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,5 +1,6 @@
import functools
import re
+import subprocess
from pathlib import Path
from typing import Generator, List, Optional, Dict, Any
@@ -8,15 +9,17 @@ from ruamel.yaml.comments import CommentedMap
from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
from .config import KiwiConfig, ProjectConfig
+from .executable import COMPOSE_EXE
from .misc import YAML
-_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
-
@attr.s
class Service:
name: str = attr.ib()
content: CommentedMap = attr.ib()
+ parent: Optional["Project"] = attr.ib(default=None)
+
+ _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
@property
def configs(self) -> Generator[Path, None, None]:
@@ -25,7 +28,7 @@ class Service:
for volume in self.content["volumes"]:
host_part = volume.split(":")[0]
- cd_match = _RE_CONFDIR.match(host_part)
+ cd_match = Service._RE_CONFDIR.match(host_part)
if cd_match:
yield Path(cd_match.group(1))
@@ -64,7 +67,7 @@ class Services:
@attr.s
class Project:
directory: Path = attr.ib()
- config: KiwiConfig = attr.ib()
+ parent: Optional["Instance"] = attr.ib(default=None)
@staticmethod
@functools.lru_cache(maxsize=10)
@@ -77,15 +80,15 @@ class Project:
return self.directory.name
@property
- def project_config(self) -> ProjectConfig:
- return self.config.get_project_config(self.name)
+ def config(self) -> ProjectConfig:
+ return self.parent.config.get_project_config(self.name)
@property
def process_kwargs(self) -> Dict[str, Any]:
directory: Path = self.directory
project_name: str = self.name
- kiwi_hub_name: str = self.config.network.name
- target_root_dir: Path = self.config.storage.directory
+ kiwi_hub_name: str = self.parent.config.network.name
+ target_root_dir: Path = self.parent.config.storage.directory
conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
target_dir: Path = target_root_dir.joinpath(project_name)
@@ -100,7 +103,7 @@ class Project:
},
}
- result["env"].update(self.config.environment)
+ result["env"].update(self.parent.config.environment)
return result
@@ -109,12 +112,12 @@ class Project:
yml = Project._parse_compose_file(self.directory)
return Services([
- Service(name, description)
- for name, description in yml["services"].items()
+ Service(name, content, self)
+ for name, content in yml["services"].items()
])
-@attr.s
+@attr.s(frozen=True)
class Instance:
directory: Path = attr.ib(default=Path('.'))
@@ -124,23 +127,11 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
- @staticmethod
@functools.lru_cache(maxsize=None)
- def __get_project(instance_directory: Path, project_name: str) -> Optional[Project]:
- instance = Instance(instance_directory)
- config = instance.config
-
- for project in config.projects:
+ def get_project(self, project_name: str) -> Optional[Project]:
+ for project in self.config.projects:
if project.name == project_name:
return Project(
- directory=instance_directory.joinpath(project.name),
- config=config,
+ directory=self.directory.joinpath(project.name),
+ parent=self,
)
-
- def get_project(self, project_name: str) -> Optional[Project]:
- project = Instance.__get_project(self.directory, project_name)
- if project is None:
- return
-
- project.instance = self
- return project
diff --git a/tests/test_project.py b/tests/test_project.py
index 72fcf79..aef5915 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -13,7 +13,6 @@ class TestDefault:
def test_example(self):
p = Project(
directory=Path("example/hello-world.project"),
- config=TestDefault.cfg,
)
ss = p.services
@@ -31,7 +30,6 @@ class TestDefault:
def test_empty(self):
p = Project(
directory=Path("nonexistent"),
- config=TestDefault.cfg,
)
with pytest.raises(FileNotFoundError) as exc_info:
From d9f66f069c20afd8edc90011935f215bb4ec0aad Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 02:48:41 +0100
Subject: [PATCH 083/135] Service.has_executable
---
kiwi_scp/instance.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 3d47c87..8cf1f80 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -33,6 +33,19 @@ class Service:
if cd_match:
yield Path(cd_match.group(1))
+ def has_executable(self, exe_name: str) -> bool:
+ try:
+ # test if desired executable exists
+ COMPOSE_EXE.run(
+ ["exec", "-T", self.name, "/bin/sh", "-c", f"command -v {exe_name}"],
+ check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ **self.parent.process_kwargs,
+ )
+ return True
+
+ except subprocess.CalledProcessError:
+ return False
+
@attr.s
class Services:
From 03c3c68fa9f97dd2da72d9ec93dc6e2bc77f58a2 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 16:01:19 +0100
Subject: [PATCH 084/135] Instance.get_project() -> Instance.projects
---
kiwi_scp/commands/cli.py | 12 ++++++------
kiwi_scp/instance.py | 28 +++++++++++++++-------------
tests/test_instance.py | 6 ------
3 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 01e2087..cb538bc 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -133,8 +133,9 @@ class KiwiCommand:
_logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
projects = [
- instance.get_project(project_name)
- for project_name in project_names
+ project
+ for project in instance.projects
+ if project.name in project_names
]
if not projects:
@@ -175,12 +176,11 @@ class KiwiCommand:
@classmethod
def run_for_instance(cls, instance: Instance, **kwargs) -> None:
- for project_config in instance.config.projects:
- if cls.enabled_only and not project_config.enabled:
- cls.print_header(f"Skipping disabled project {project_config.name}")
+ for project in instance.projects:
+ if cls.enabled_only and not project.config.enabled:
+ cls.print_header(f"Skipping disabled project {project.name}")
continue
- project = instance.get_project(project_config.name)
cls.run_for_project(instance, project, **kwargs)
@classmethod
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 8cf1f80..c19ad97 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -17,7 +17,7 @@ from .misc import YAML
class Service:
name: str = attr.ib()
content: CommentedMap = attr.ib()
- parent: Optional["Project"] = attr.ib(default=None)
+ parent: "Project" = attr.ib()
_RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
@@ -80,7 +80,7 @@ class Services:
@attr.s
class Project:
directory: Path = attr.ib()
- parent: Optional["Instance"] = attr.ib(default=None)
+ parent: "Instance" = attr.ib()
@staticmethod
@functools.lru_cache(maxsize=10)
@@ -93,7 +93,7 @@ class Project:
return self.directory.name
@property
- def config(self) -> ProjectConfig:
+ def config(self) -> Optional[ProjectConfig]:
return self.parent.config.get_project_config(self.name)
@property
@@ -125,12 +125,15 @@ class Project:
yml = Project._parse_compose_file(self.directory)
return Services([
- Service(name, content, self)
- for name, content in yml["services"].items()
+ Service(
+ name=name,
+ content=content,
+ parent=self,
+ ) for name, content in yml["services"].items()
])
-@attr.s(frozen=True)
+@attr.s
class Instance:
directory: Path = attr.ib(default=Path('.'))
@@ -140,11 +143,10 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
- @functools.lru_cache(maxsize=None)
- def get_project(self, project_name: str) -> Optional[Project]:
+ @property
+ def projects(self) -> Generator[Project, None, None]:
for project in self.config.projects:
- if project.name == project_name:
- return Project(
- directory=self.directory.joinpath(project.name),
- parent=self,
- )
+ yield Project(
+ directory=self.directory.joinpath(project.name),
+ parent=self,
+ )
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 2f3f627..1a5beb3 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -14,12 +14,6 @@ class TestDefault:
assert pc.name == "hello-world.project"
- p = i.get_project("hello-world.project")
-
- assert p.directory == Path("example/hello-world.project")
-
- assert i.get_project("nonexistent") is None
-
def test_empty(self):
i = Instance()
From 00bb2adee4253ac50d5e7ea818a11b628e91408e Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:06:13 +0100
Subject: [PATCH 085/135] removed legacy modules
---
kiwi_scp/config.py | 2 +-
kiwi_scp/instance.py | 2 +-
kiwi_scp/parser.py | 66 -------------
kiwi_scp/project.py | 133 --------------------------
kiwi_scp/projects.py | 83 ----------------
kiwi_scp/runner.py | 73 --------------
kiwi_scp/scripts/kiwi.py | 26 ++---
kiwi_scp/scripts/kiwi_next.py | 12 ---
kiwi_scp/subcommand.py | 128 -------------------------
kiwi_scp/{misc.py => yaml.py} | 39 --------
poetry.lock | 173 +++++++++++++++++-----------------
pyproject.toml | 3 +-
tests/test_config.py | 2 +-
13 files changed, 100 insertions(+), 642 deletions(-)
delete mode 100644 kiwi_scp/parser.py
delete mode 100644 kiwi_scp/project.py
delete mode 100644 kiwi_scp/projects.py
delete mode 100644 kiwi_scp/runner.py
mode change 100755 => 100644 kiwi_scp/scripts/kiwi.py
delete mode 100644 kiwi_scp/scripts/kiwi_next.py
delete mode 100644 kiwi_scp/subcommand.py
rename kiwi_scp/{misc.py => yaml.py} (54%)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 08c4a3a..20b01f3 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Any, TextIO, Tuple
from pydantic import BaseModel, constr, root_validator, validator
from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
-from .misc import YAML
+from .yaml import YAML
class StorageConfig(BaseModel):
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index c19ad97..3c7cd03 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -10,7 +10,7 @@ from ruamel.yaml.comments import CommentedMap
from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
from .config import KiwiConfig, ProjectConfig
from .executable import COMPOSE_EXE
-from .misc import YAML
+from .yaml import YAML
@attr.s
diff --git a/kiwi_scp/parser.py b/kiwi_scp/parser.py
deleted file mode 100644
index 5276152..0000000
--- a/kiwi_scp/parser.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# system
-import argparse
-
-# local
-from ._constants import COMMAND_HELP_TEXT_NAME, KIWI_HELP_TEXT_NAME
-
-
-class Parser:
- """Singleton: Main CLI arguments parser"""
-
- class __Parser:
- """Singleton type"""
-
- # argparse objects
- __parser = None
- __subparsers = None
- __args = None
-
- def __init__(self):
- # add version data from separate file (keeps default config cleaner)
- with open(KIWI_HELP_TEXT_NAME, 'r') as stream:
- kiwi_help = stream.read()
-
- with open(COMMAND_HELP_TEXT_NAME, 'r') as stream:
- command_help_text = stream.read()
-
- # create main parser
- self.__parser = argparse.ArgumentParser(
- prog='kiwi',
- description=kiwi_help,
- epilog=command_help_text,
- )
- self.__parser.formatter_class = argparse.RawDescriptionHelpFormatter
-
- # main arguments
- self.__parser.add_argument(
- '-v', '--verbosity',
- action='count', default=0
- )
-
- # attach subparsers
- self.__subparsers = self.__parser.add_subparsers()
- self.__subparsers.required = True
- self.__subparsers.dest = 'command'
-
- def get_subparsers(self):
- return self.__subparsers
-
- def get_args(self):
- if self.__args is None:
- # parse args if needed
- self.__args, unknowns = self.__parser.parse_known_args()
- self.__args.unknowns = unknowns
-
- return self.__args
-
- __instance = None
-
- def __init__(self):
- if Parser.__instance is None:
- # create singleton
- Parser.__instance = Parser.__Parser()
-
- def __getattr__(self, item):
- """Inner singleton direct access"""
- return getattr(self.__instance, item)
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
deleted file mode 100644
index 60a3fbd..0000000
--- a/kiwi_scp/project.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import logging
-import os
-
-from ._constants import CONF_DIRECTORY_NAME
-from .config import LoadedConfig
-from .executable import Executable
-
-
-class Project:
- __name = None
-
- def __init__(self, name):
- self.__name = name
-
- @classmethod
- def from_file_name(cls, file_name):
- if os.path.isdir(file_name):
- config = LoadedConfig.get()
-
- if file_name.endswith(config['markers:disabled']):
- file_name = file_name[:-len(config['markers:disabled'])]
-
- if file_name.endswith(config['markers:project']):
- file_name = file_name[:-len(config['markers:project'])]
- return cls(file_name)
-
- return None
-
- def get_name(self):
- return self.__name
-
- def dir_name(self):
- if self.is_enabled():
- return self.enabled_dir_name()
- elif self.is_disabled():
- return self.disabled_dir_name()
- else:
- return None
-
- def enabled_dir_name(self):
- return f"{self.__name}{LoadedConfig.get()['markers:project']}"
-
- def disabled_dir_name(self):
- return f"{self.enabled_dir_name()}{LoadedConfig.get()['markers:disabled']}"
-
- def conf_dir_name(self):
- return os.path.join(self.dir_name(), CONF_DIRECTORY_NAME)
-
- def compose_file_name(self):
- return os.path.join(self.dir_name(), 'docker-compose.yml')
-
- def target_dir_name(self):
- return os.path.join(LoadedConfig.get()['runtime:storage'], self.enabled_dir_name())
-
- def exists(self):
- return os.path.isdir(self.enabled_dir_name()) or os.path.isdir(self.disabled_dir_name())
-
- def is_enabled(self):
- return os.path.isdir(self.enabled_dir_name())
-
- def is_disabled(self):
- return os.path.isdir(self.disabled_dir_name())
-
- def has_configs(self):
- return os.path.isdir(self.conf_dir_name())
-
- def __update_kwargs(self, kwargs):
- if not self.is_enabled():
- # cannot compose in a disabled project
- logging.warning(f"Project '{self.get_name()}' is not enabled!")
- return False
-
- config = LoadedConfig.get()
-
- # execute command in project directory
- kwargs['cwd'] = self.dir_name()
-
- # ensure there is an environment
- if 'env' not in kwargs:
- kwargs['env'] = {}
-
- # create environment variables for docker commands
- kwargs['env'].update({
- 'COMPOSE_PROJECT_NAME': self.get_name(),
- 'KIWI_HUB_NAME': config['network:name'],
- 'TARGETROOT': config['runtime:storage'],
- 'CONFDIR': os.path.join(config['runtime:storage'], CONF_DIRECTORY_NAME),
- 'TARGETDIR': self.target_dir_name()
- })
-
- # add common environment from config
- if config['runtime:env'] is not None:
- kwargs['env'].update(config['runtime:env'])
-
- logging.debug(f"kwargs updated: {kwargs}")
-
- return True
-
- def compose_run(self, compose_args, **kwargs):
- if self.__update_kwargs(kwargs):
- Executable('docker-compose').run(compose_args, **kwargs)
-
- def compose_run_less(self, compose_args, **kwargs):
- if self.__update_kwargs(kwargs):
- Executable('docker-compose').run_less(compose_args, **kwargs)
-
- def enable(self):
- if self.is_disabled():
- logging.info(f"Enabling project '{self.get_name()}'")
- os.rename(self.dir_name(), self.enabled_dir_name())
-
- elif self.is_enabled():
- logging.warning(f"Project '{self.get_name()}' is enabled!")
-
- else:
- logging.warning(f"Project '{self.get_name()}' not found in instance!")
- return False
-
- return True
-
- def disable(self):
- if self.is_enabled():
- logging.info(f"Disabling project '{self.get_name()}'")
- os.rename(self.dir_name(), self.disabled_dir_name())
-
- elif self.is_disabled():
- logging.warning(f"Project '{self.get_name()}' is disabled!")
-
- else:
- logging.warning(f"Project '{self.get_name()}' not found in instance!")
- return False
-
- return True
diff --git a/kiwi_scp/projects.py b/kiwi_scp/projects.py
deleted file mode 100644
index 1444646..0000000
--- a/kiwi_scp/projects.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import os
-
-from .project import Project
-
-
-class Projects:
- __projects = None
-
- def __getitem__(self, item):
- return self.__projects[item]
-
- def __str__(self):
- return str([
- project.get_name()
- for project
- in self.__projects
- ])
-
- def __bool__(self):
- return bool(self.__projects)
-
- @classmethod
- def from_names(cls, project_names):
- result = cls()
- result.__projects = [
- Project(name)
- for name in project_names if isinstance(name, str)
- ]
- return result
-
- @classmethod
- def from_projects(cls, projects):
- result = cls()
- result.__projects = [
- project
- for project in projects if isinstance(project, Project)
- ]
- return result
-
- @classmethod
- def from_dir(cls, directory='.'):
- return cls.from_projects([
- Project.from_file_name(file_name)
- for file_name in os.listdir(directory)
- ])
-
- @classmethod
- def from_args(cls, args):
- if args is not None and 'projects' in args:
- if isinstance(args.projects, list) and args.projects:
- return cls.from_names(args.projects)
-
- elif isinstance(args.projects, str):
- return cls.from_names([args.projects])
-
- return cls()
-
- def filter_exists(self):
- result = Projects()
- result.__projects = [
- project
- for project in self.__projects
- if project.exists()
- ]
- return result
-
- def filter_enabled(self):
- result = Projects()
- result.__projects = [
- project
- for project in self.__projects
- if project.is_enabled()
- ]
- return result
-
- def filter_disabled(self):
- result = Projects()
- result.__projects = [
- project
- for project in self.__projects
- if project.is_disabled()
- ]
- return result
diff --git a/kiwi_scp/runner.py b/kiwi_scp/runner.py
deleted file mode 100644
index a2b33dd..0000000
--- a/kiwi_scp/runner.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# system
-import logging
-import subprocess
-
-# local
-from . import subcommands
-from .executable import Executable
-from .parser import Parser
-
-
-class Runner:
- """Singleton: Subcommands setup and run"""
-
- class __Runner:
- """Singleton type"""
-
- __commands = []
-
- def __init__(self):
- # probe for Docker access
- try:
- Executable('docker').run([
- 'ps'
- ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
- except subprocess.CalledProcessError:
- logging.critical("Cannot access docker, please get into the docker group or run as root!")
- quit(1)
-
- # setup all subcommands
- for className in subcommands.__all__:
- cmd = getattr(subcommands, className)
- self.__commands.append(cmd())
-
- def run(self, command=None, args=None):
- """run the desired subcommand"""
-
- if args is None:
- args = Parser().get_args()
-
- if command is None:
- command = args.command
-
- for cmd in self.__commands:
- if str(cmd) == command:
- # command found
- logging.debug(f"Running '{cmd}' with args: {args}")
-
- try:
- result = cmd.run(self, args)
-
- except KeyboardInterrupt:
- print()
- logging.warning(f"'{cmd}' aborted, inputs may have been discarded.")
- result = False
-
- return result
-
- # command not found
- logging.error(f"kiwi command '{command}' unknown")
- return False
-
- __instance = None
-
- def __init__(self):
- if Runner.__instance is None:
- # create singleton
- Runner.__instance = Runner.__Runner()
-
- def __getattr__(self, item):
- """Inner singleton direct access"""
-
- return getattr(self.__instance, item)
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
old mode 100755
new mode 100644
index 9bcbc20..b64bfef
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -1,14 +1,15 @@
-#!/usr/bin/env python3
-
-# system
import logging
-# local
-import kiwi_scp
+import click
+
+from kiwi_scp.commands.cli import KiwiCLI
-def set_verbosity(logger, handler, verbosity):
- """set logging default verbosity level and format"""
+@click.command(cls=KiwiCLI)
+def main():
+ """kiwi is the simple tool for managing container servers."""
+
+ verbosity = 0
if verbosity >= 2:
log_level = logging.DEBUG
@@ -20,19 +21,12 @@ def set_verbosity(logger, handler, verbosity):
log_level = logging.WARNING
log_format = "%(levelname)s: %(message)s"
- logger.setLevel(log_level)
- handler.setFormatter(logging.Formatter(log_format))
-
-
-def main():
# add a new handler (needed to set the level)
log_handler = logging.StreamHandler()
logging.getLogger().addHandler(log_handler)
- set_verbosity(logging.getLogger(), log_handler, kiwi_scp.verbosity())
- # run the app
- if not kiwi_scp.run():
- quit(1)
+ logging.getLogger().setLevel(log_level)
+ log_handler.setFormatter(logging.Formatter(log_format))
if __name__ == "__main__":
diff --git a/kiwi_scp/scripts/kiwi_next.py b/kiwi_scp/scripts/kiwi_next.py
deleted file mode 100644
index 4b7f9aa..0000000
--- a/kiwi_scp/scripts/kiwi_next.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import click
-
-from kiwi_scp.commands.cli import KiwiCLI
-
-
-@click.command(cls=KiwiCLI)
-def main():
- """kiwi is the simple tool for managing container servers."""
-
-
-if __name__ == "__main__":
- main()
diff --git a/kiwi_scp/subcommand.py b/kiwi_scp/subcommand.py
deleted file mode 100644
index b5955d3..0000000
--- a/kiwi_scp/subcommand.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# system
-import logging
-import os
-
-# local
-from .parser import Parser
-from .projects import Projects
-
-
-class SubCommand:
- """represents kiwi [anything] command"""
-
- # actual command string
- __name = None
- # command parser
- _sub_parser = None
-
- _action = None
-
- def __init__(self, name, action, add_parser=True, **kwargs):
- self.__name = name
- self._action = action
-
- if add_parser:
- self._sub_parser = Parser().get_subparsers().add_parser(
- name,
- **kwargs
- )
-
- def __str__(self):
- return self.__name
-
- def _run_instance(self, runner, args):
- pass
-
- def run(self, runner, args):
- """actually run command with parsed CLI args"""
-
- # run for entire instance
- logging.info(f"{self._action} kiwi-scp instance at '{os.getcwd()}'")
- return self._run_instance(runner, args)
-
-
-class ProjectCommand(SubCommand):
- """this command concerns a project in current instance"""
-
- def __init__(self, name, num_projects, action, add_parser=True, **kwargs):
- super().__init__(
- name, action=action, add_parser=add_parser,
- **kwargs
- )
-
- if num_projects == 1:
- projects = "a project"
- else:
- projects = "project(s)"
-
- self._sub_parser.add_argument(
- 'projects', metavar='project', nargs=num_projects, type=str,
- help=f"select {projects} in this instance"
- )
-
- def _run_instance(self, runner, args):
- # default: run for all enabled projects
- return self._run_projects(runner, args, Projects.from_dir().filter_enabled())
-
- def _run_projects(self, runner, args, projects):
- # default: run for all given projects
- return all([
- self._run_project(runner, args, project)
- for project in projects
- ])
-
- def _run_project(self, runner, args, project):
- pass
-
- def run(self, runner, args):
- projects = Projects.from_args(args)
-
- if projects:
- # project(s) given
- logging.info(f"{self._action} projects {projects}")
- return self._run_projects(runner, args, projects)
-
- else:
- return super().run(runner, args)
-
-
-class ServiceCommand(ProjectCommand):
- """this command concerns service(s) in a project"""
-
- def __init__(self, name, num_projects, num_services, action, add_parser=True, **kwargs):
- super().__init__(
- name, num_projects=num_projects, action=action, add_parser=add_parser,
- **kwargs
- )
-
- if (isinstance(num_projects, str) and num_projects == '*') \
- or (isinstance(num_projects, int) and num_projects > 1):
- raise ValueError(f"Invalid choice for project count: {num_projects}")
-
- if num_services == 1:
- services = "a service"
- else:
- services = "service(s)"
-
- self._sub_parser.add_argument(
- 'services', metavar='service', nargs=num_services, type=str,
- help=f"select {services} in a project"
- )
-
- def _run_project(self, runner, args, project):
- # default: run with empty service list
- return self._run_services(runner, args, project, [])
-
- def _run_services(self, runner, args, project, services):
- pass
-
- def run(self, runner, args):
- if 'services' in args and args.services:
- project = Projects.from_args(args)[0]
-
- # run for service(s) inside project
- logging.info(f"{self._action} project '{project.get_name()}', services {args.services}")
- return self._run_services(runner, args, project, args.services)
-
- else:
- return super().run(runner, args)
diff --git a/kiwi_scp/misc.py b/kiwi_scp/yaml.py
similarity index 54%
rename from kiwi_scp/misc.py
rename to kiwi_scp/yaml.py
index b38d4a0..972b12d 100644
--- a/kiwi_scp/misc.py
+++ b/kiwi_scp/yaml.py
@@ -35,42 +35,3 @@ class YAML(ruamel.yaml.YAML):
def dump_kiwi_yml(self, data, **kwargs) -> Optional[str]:
return self.dump(data, transform=YAML._format_kiwi_yml, **kwargs)
-
-
-def _surround(string, bang):
- midlane = f"{bang * 3} {string} {bang * 3}"
- sidelane = bang * len(midlane)
-
- return f"{sidelane}\n{midlane}\n{sidelane}"
-
-
-def _emphasize(lines):
- if isinstance(lines, list):
- return '\n'.join([_emphasize(line) for line in lines])
- elif lines:
- return f">>> {lines} <<<"
- else:
- return lines
-
-
-def are_you_sure(prompt, default="no"):
- if default.lower() == 'yes':
- suffix = "[YES|no]"
- else:
- suffix = "[yes|NO]"
-
- answer = input(
- f"{_surround('MUST HAVE CAREFULING IN PROCESS', '!')}\n"
- f"\n"
- f"{_emphasize(prompt)}\n"
- f"\n"
- f"Are you sure you want to proceed? {suffix} "
- ).strip().lower()
-
- if answer == '':
- answer = default
-
- while answer not in ['yes', 'no']:
- answer = input("Please type 'yes' or 'no' explicitly: ").strip().lower()
-
- return answer == 'yes'
diff --git a/poetry.lock b/poetry.lock
index f6ccca5..764a946 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -22,7 +22,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]]
name = "backports.entry-points-selectable"
-version = "1.1.0"
+version = "1.1.1"
description = "Compatibility shim providing selectable entry points for older implementations"
category = "dev"
optional = false
@@ -33,7 +33,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
[[package]]
name = "click"
@@ -57,7 +57,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "coverage"
-version = "6.1.2"
+version = "6.2"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
@@ -87,7 +87,7 @@ python-versions = "*"
[[package]]
name = "filelock"
-version = "3.3.1"
+version = "3.4.0"
description = "A platform independent file lock."
category = "dev"
optional = false
@@ -99,7 +99,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co
[[package]]
name = "importlib-metadata"
-version = "4.8.1"
+version = "4.8.2"
description = "Read metadata from Python packages"
category = "main"
optional = false
@@ -112,11 +112,11 @@ zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
name = "importlib-resources"
-version = "5.3.0"
+version = "5.4.0"
description = "Read resources from Python packages"
category = "dev"
optional = false
@@ -139,14 +139,14 @@ python-versions = "*"
[[package]]
name = "packaging"
-version = "21.0"
+version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-pyparsing = ">=2.0.2"
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "platformdirs"
@@ -177,11 +177,11 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
-version = "1.10.0"
+version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pydantic"
@@ -201,7 +201,7 @@ email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyparsing"
-version = "3.0.1"
+version = "3.0.6"
description = "Python parsing module"
category = "dev"
optional = false
@@ -249,7 +249,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale
[[package]]
name = "ruamel.yaml"
-version = "0.17.16"
+version = "0.17.17"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
@@ -296,15 +296,15 @@ python-versions = ">=3.6"
[[package]]
name = "typing-extensions"
-version = "3.10.0.2"
-description = "Backported and Experimental Type Hints for Python 3.5+"
+version = "4.0.1"
+description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
[[package]]
name = "virtualenv"
-version = "20.9.0"
+version = "20.10.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -320,7 +320,7 @@ platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
[package.extras]
-docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
[[package]]
@@ -346,7 +346,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "9083370030b96548400ebb4f9f2a82a3f5dd3a55bd83a9377df492180dc59c3e"
+content-hash = "bdbab568cf9ce133ae55b74cc1a6939c6aff96d3c60fee5ebffd7f498a6d97a6"
[metadata.files]
atomicwrites = [
@@ -358,8 +358,8 @@ attrs = [
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
"backports.entry-points-selectable" = [
- {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
- {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+ {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"},
+ {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
@@ -370,53 +370,53 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
coverage = [
- {file = "coverage-6.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9"},
- {file = "coverage-6.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758"},
- {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c"},
- {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649"},
- {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d"},
- {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4"},
- {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab"},
- {file = "coverage-6.1.2-cp310-cp310-win32.whl", hash = "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc"},
- {file = "coverage-6.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b"},
- {file = "coverage-6.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052"},
- {file = "coverage-6.1.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e"},
- {file = "coverage-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266"},
- {file = "coverage-6.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a"},
- {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388"},
- {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d"},
- {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204"},
- {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf"},
- {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c"},
- {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0"},
- {file = "coverage-6.1.2-cp36-cp36m-win32.whl", hash = "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f"},
- {file = "coverage-6.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b"},
- {file = "coverage-6.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f"},
- {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954"},
- {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c"},
- {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c"},
- {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c"},
- {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2"},
- {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186"},
- {file = "coverage-6.1.2-cp37-cp37m-win32.whl", hash = "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193"},
- {file = "coverage-6.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93"},
- {file = "coverage-6.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd"},
- {file = "coverage-6.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13"},
- {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696"},
- {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373"},
- {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263"},
- {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d"},
- {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091"},
- {file = "coverage-6.1.2-cp38-cp38-win32.whl", hash = "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e"},
- {file = "coverage-6.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b"},
- {file = "coverage-6.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59"},
- {file = "coverage-6.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225"},
- {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71"},
- {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4"},
- {file = "coverage-6.1.2-cp39-cp39-win32.whl", hash = "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de"},
- {file = "coverage-6.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc"},
- {file = "coverage-6.1.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929"},
- {file = "coverage-6.1.2.tar.gz", hash = "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972"},
+ {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"},
+ {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"},
+ {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"},
+ {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"},
+ {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"},
+ {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"},
+ {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"},
+ {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"},
+ {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"},
+ {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"},
+ {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"},
+ {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"},
+ {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"},
+ {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"},
+ {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"},
+ {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"},
+ {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"},
+ {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"},
+ {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"},
+ {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"},
+ {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"},
+ {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"},
+ {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"},
+ {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"},
+ {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"},
+ {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"},
+ {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"},
+ {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"},
+ {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"},
+ {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"},
+ {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"},
+ {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"},
+ {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"},
+ {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"},
+ {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"},
+ {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"},
+ {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"},
+ {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"},
+ {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"},
+ {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"},
+ {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"},
+ {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"},
+ {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"},
+ {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"},
+ {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"},
+ {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"},
+ {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"},
]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
@@ -427,24 +427,24 @@ distlib = [
{file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
]
filelock = [
- {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"},
- {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"},
+ {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"},
+ {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
- {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
+ {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"},
+ {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"},
]
importlib-resources = [
- {file = "importlib_resources-5.3.0-py3-none-any.whl", hash = "sha256:7a65eb0d8ee98eedab76e6deb51195c67f8e575959f6356a6e15fd7e1148f2a3"},
- {file = "importlib_resources-5.3.0.tar.gz", hash = "sha256:f2e58e721b505a79abe67f5868d99f8886aec8594c962c7490d0c22925f518da"},
+ {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"},
+ {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
packaging = [
- {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
- {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
platformdirs = [
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
@@ -455,8 +455,8 @@ pluggy = [
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
- {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
- {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pydantic = [
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
@@ -483,8 +483,8 @@ pydantic = [
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
pyparsing = [
- {file = "pyparsing-3.0.1-py3-none-any.whl", hash = "sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"},
- {file = "pyparsing-3.0.1.tar.gz", hash = "sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531"},
+ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
+ {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
@@ -495,8 +495,8 @@ pytest-cov = [
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
]
"ruamel.yaml" = [
- {file = "ruamel.yaml-0.17.16-py3-none-any.whl", hash = "sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1"},
- {file = "ruamel.yaml-0.17.16.tar.gz", hash = "sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33"},
+ {file = "ruamel.yaml-0.17.17-py3-none-any.whl", hash = "sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f"},
+ {file = "ruamel.yaml-0.17.17.tar.gz", hash = "sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be"},
]
"ruamel.yaml.clib" = [
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
@@ -534,13 +534,12 @@ tomli = [
{file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
]
typing-extensions = [
- {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
- {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
- {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
+ {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
]
virtualenv = [
- {file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"},
- {file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"},
+ {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"},
+ {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index eab3e9d..866fa70 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,11 +16,10 @@ wcwidth = "^0.2.5"
pytest = "^6.2.5"
pytest-cov = "^3.0.0"
toml = "^0.10.2"
-virtualenv = "^20.8.1"
+virtualenv = "^20.10.0"
[tool.poetry.scripts]
kiwi = "kiwi_scp.scripts.kiwi:main"
-kiwi_next = "kiwi_scp.scripts.kiwi_next:main"
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/tests/test_config.py b/tests/test_config.py
index d986fff..3a344e0 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -5,7 +5,7 @@ import pytest
from pydantic import ValidationError
from kiwi_scp.config import KiwiConfig
-from kiwi_scp.misc import YAML
+from kiwi_scp.yaml import YAML
class UnCoercible:
From 1d5c4ccbee0bf7f23cf8436db79cb74fd151f515 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:08:14 +0100
Subject: [PATCH 086/135] Split instance.py
---
kiwi_scp/commands/cli.py | 4 +-
kiwi_scp/commands/cmd_build.py | 3 +-
kiwi_scp/commands/cmd_cmd.py | 3 +-
kiwi_scp/commands/cmd_disable.py | 3 +-
kiwi_scp/commands/cmd_down.py | 4 +-
kiwi_scp/commands/cmd_enable.py | 3 +-
kiwi_scp/commands/cmd_list.py | 3 +-
kiwi_scp/commands/cmd_logs.py | 4 +-
kiwi_scp/commands/cmd_up.py | 4 +-
kiwi_scp/instance.py | 132 +------------------------------
kiwi_scp/project.py | 69 ++++++++++++++++
kiwi_scp/service.py | 44 +++++++++++
kiwi_scp/services.py | 36 +++++++++
tests/test_project.py | 2 +-
tests/test_service.py | 2 +-
tests/test_services.py | 3 +-
16 files changed, 178 insertions(+), 141 deletions(-)
create mode 100644 kiwi_scp/project.py
create mode 100644 kiwi_scp/service.py
create mode 100644 kiwi_scp/services.py
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index cb538bc..acd09b6 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -7,7 +7,9 @@ from typing import List, Iterable, Type, Optional, TypeVar
import click
-from ..instance import Instance, Project, Services
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
from ..wstring import WParagraph, WAlignment
_logger = logging.getLogger(__name__)
diff --git a/kiwi_scp/commands/cmd_build.py b/kiwi_scp/commands/cmd_build.py
index 57008a0..338f5b7 100644
--- a/kiwi_scp/commands/cmd_build.py
+++ b/kiwi_scp/commands/cmd_build.py
@@ -3,7 +3,8 @@ from typing import List
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project
+from ..instance import Instance
+from ..project import Project
@kiwi_command(
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index 58996f7..15cfdfa 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -5,7 +5,8 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project
+from ..instance import Instance
+from ..project import Project
@click.argument(
diff --git a/kiwi_scp/commands/cmd_disable.py b/kiwi_scp/commands/cmd_disable.py
index 6a146d0..8b6b2ef 100644
--- a/kiwi_scp/commands/cmd_disable.py
+++ b/kiwi_scp/commands/cmd_disable.py
@@ -3,7 +3,8 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
-from ..instance import Instance, Project
+from ..instance import Instance
+from ..project import Project
@click.option(
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index b055328..960559a 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -5,7 +5,9 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project, Services
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
@click.option(
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
index 9f7eadc..6f3676b 100644
--- a/kiwi_scp/commands/cmd_enable.py
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -3,7 +3,8 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
-from ..instance import Instance, Project
+from ..instance import Instance
+from ..project import Project
@click.option(
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index 5c98568..e07719a 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -4,7 +4,8 @@ import click
from .cli import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
-from ..instance import Instance, Project
+from ..instance import Instance
+from ..project import Project
@click.option(
diff --git a/kiwi_scp/commands/cmd_logs.py b/kiwi_scp/commands/cmd_logs.py
index f31a066..581c966 100644
--- a/kiwi_scp/commands/cmd_logs.py
+++ b/kiwi_scp/commands/cmd_logs.py
@@ -5,7 +5,9 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project, Services
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
@click.option(
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
index 8485bdb..8764953 100644
--- a/kiwi_scp/commands/cmd_up.py
+++ b/kiwi_scp/commands/cmd_up.py
@@ -5,7 +5,9 @@ import click
from .cli import KiwiCommand, KiwiCommandType
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
-from ..instance import Instance, Project, Services
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
@kiwi_command(short_help="Bring up kiwi services")
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 3c7cd03..f5f63b6 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,136 +1,10 @@
-import functools
-import re
-import subprocess
from pathlib import Path
-from typing import Generator, List, Optional, Dict, Any
+from typing import Generator
import attr
-from ruamel.yaml.comments import CommentedMap
-from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
-from .config import KiwiConfig, ProjectConfig
-from .executable import COMPOSE_EXE
-from .yaml import YAML
-
-
-@attr.s
-class Service:
- name: str = attr.ib()
- content: CommentedMap = attr.ib()
- parent: "Project" = attr.ib()
-
- _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
-
- @property
- def configs(self) -> Generator[Path, None, None]:
- if "volumes" not in self.content:
- return
-
- for volume in self.content["volumes"]:
- host_part = volume.split(":")[0]
- cd_match = Service._RE_CONFDIR.match(host_part)
-
- if cd_match:
- yield Path(cd_match.group(1))
-
- def has_executable(self, exe_name: str) -> bool:
- try:
- # test if desired executable exists
- COMPOSE_EXE.run(
- ["exec", "-T", self.name, "/bin/sh", "-c", f"command -v {exe_name}"],
- check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
- **self.parent.process_kwargs,
- )
- return True
-
- except subprocess.CalledProcessError:
- return False
-
-
-@attr.s
-class Services:
- content: List[Service] = attr.ib()
-
- def __str__(self) -> str:
- return YAML().dump({
- "services": {
- service.name: service.content
- for service in self.content
- }
- }).strip()
-
- def __bool__(self) -> bool:
- return bool(self.content)
-
- @property
- def names(self) -> Generator[str, None, None]:
- return (
- service.name
- for service in self.content
- )
-
- def filter_existing(self, service_names: List[str]) -> "Services":
- return Services([
- service
- for service in self.content
- if service.name in service_names
- ])
-
-
-@attr.s
-class Project:
- directory: Path = attr.ib()
- parent: "Instance" = attr.ib()
-
- @staticmethod
- @functools.lru_cache(maxsize=10)
- def _parse_compose_file(directory: Path) -> CommentedMap:
- with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
- return YAML().load(cf)
-
- @property
- def name(self) -> str:
- return self.directory.name
-
- @property
- def config(self) -> Optional[ProjectConfig]:
- return self.parent.config.get_project_config(self.name)
-
- @property
- def process_kwargs(self) -> Dict[str, Any]:
- directory: Path = self.directory
- project_name: str = self.name
- kiwi_hub_name: str = self.parent.config.network.name
- target_root_dir: Path = self.parent.config.storage.directory
- conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
- target_dir: Path = target_root_dir.joinpath(project_name)
-
- result: Dict[str, Any] = {
- "cwd": str(directory),
- "env": {
- "COMPOSE_PROJECT_NAME": project_name,
- "KIWI_HUB_NAME": kiwi_hub_name,
- "TARGETROOT": str(target_root_dir),
- "CONFDIR": str(conf_dir),
- "TARGETDIR": str(target_dir),
- },
- }
-
- result["env"].update(self.parent.config.environment)
-
- return result
-
- @property
- def services(self) -> Services:
- yml = Project._parse_compose_file(self.directory)
-
- return Services([
- Service(
- name=name,
- content=content,
- parent=self,
- ) for name, content in yml["services"].items()
- ])
+from .config import KiwiConfig
+from .project import Project
@attr.s
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
new file mode 100644
index 0000000..b6154a4
--- /dev/null
+++ b/kiwi_scp/project.py
@@ -0,0 +1,69 @@
+import functools
+from pathlib import Path
+from typing import Optional, Dict, Any
+
+import attr
+from ruamel.yaml import CommentedMap
+
+from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
+from .config import ProjectConfig
+from .instance import Instance
+from .service import Service
+from .services import Services
+from .yaml import YAML
+
+
+@attr.s
+class Project:
+ directory: Path = attr.ib()
+ parent: "Instance" = attr.ib()
+
+ @staticmethod
+ @functools.lru_cache(maxsize=10)
+ def _parse_compose_file(directory: Path) -> CommentedMap:
+ with open(directory.joinpath(COMPOSE_FILE_NAME), "r") as cf:
+ return YAML().load(cf)
+
+ @property
+ def name(self) -> str:
+ return self.directory.name
+
+ @property
+ def config(self) -> Optional[ProjectConfig]:
+ return self.parent.config.get_project_config(self.name)
+
+ @property
+ def process_kwargs(self) -> Dict[str, Any]:
+ directory: Path = self.directory
+ project_name: str = self.name
+ kiwi_hub_name: str = self.parent.config.network.name
+ target_root_dir: Path = self.parent.config.storage.directory
+ conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
+ target_dir: Path = target_root_dir.joinpath(project_name)
+
+ result: Dict[str, Any] = {
+ "cwd": str(directory),
+ "env": {
+ "COMPOSE_PROJECT_NAME": project_name,
+ "KIWI_HUB_NAME": kiwi_hub_name,
+ "TARGETROOT": str(target_root_dir),
+ "CONFDIR": str(conf_dir),
+ "TARGETDIR": str(target_dir),
+ },
+ }
+
+ result["env"].update(self.parent.config.environment)
+
+ return result
+
+ @property
+ def services(self) -> Services:
+ yml = Project._parse_compose_file(self.directory)
+
+ return Services([
+ Service(
+ name=name,
+ content=content,
+ parent=self,
+ ) for name, content in yml["services"].items()
+ ])
\ No newline at end of file
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
new file mode 100644
index 0000000..ae282b1
--- /dev/null
+++ b/kiwi_scp/service.py
@@ -0,0 +1,44 @@
+import re
+import subprocess
+from pathlib import Path
+from typing import Generator
+
+import attr
+from ruamel.yaml import CommentedMap
+
+from .executable import COMPOSE_EXE
+from .project import Project
+
+
+@attr.s
+class Service:
+ name: str = attr.ib()
+ content: CommentedMap = attr.ib()
+ parent: "Project" = attr.ib()
+
+ _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
+
+ @property
+ def configs(self) -> Generator[Path, None, None]:
+ if "volumes" not in self.content:
+ return
+
+ for volume in self.content["volumes"]:
+ host_part = volume.split(":")[0]
+ cd_match = Service._RE_CONFDIR.match(host_part)
+
+ if cd_match:
+ yield Path(cd_match.group(1))
+
+ def has_executable(self, exe_name: str) -> bool:
+ try:
+ # test if desired executable exists
+ COMPOSE_EXE.run(
+ ["exec", "-T", self.name, "/bin/sh", "-c", f"command -v {exe_name}"],
+ check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ **self.parent.process_kwargs,
+ )
+ return True
+
+ except subprocess.CalledProcessError:
+ return False
\ No newline at end of file
diff --git a/kiwi_scp/services.py b/kiwi_scp/services.py
new file mode 100644
index 0000000..df8187b
--- /dev/null
+++ b/kiwi_scp/services.py
@@ -0,0 +1,36 @@
+from typing import List, Generator
+
+import attr
+
+from kiwi_scp.service import Service
+from kiwi_scp.yaml import YAML
+
+
+@attr.s
+class Services:
+ content: List[Service] = attr.ib()
+
+ def __str__(self) -> str:
+ return YAML().dump({
+ "services": {
+ service.name: service.content
+ for service in self.content
+ }
+ }).strip()
+
+ def __bool__(self) -> bool:
+ return bool(self.content)
+
+ @property
+ def names(self) -> Generator[str, None, None]:
+ return (
+ service.name
+ for service in self.content
+ )
+
+ def filter_existing(self, service_names: List[str]) -> "Services":
+ return Services([
+ service
+ for service in self.content
+ if service.name in service_names
+ ])
\ No newline at end of file
diff --git a/tests/test_project.py b/tests/test_project.py
index aef5915..f6fabff 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -4,7 +4,7 @@ import pytest
from kiwi_scp._constants import COMPOSE_FILE_NAME
from kiwi_scp.config import KiwiConfig
-from kiwi_scp.instance import Project
+from kiwi_scp.project import Project
class TestDefault:
diff --git a/tests/test_service.py b/tests/test_service.py
index 1eb9e0a..9206374 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -2,7 +2,7 @@ from pathlib import Path
from ruamel.yaml import CommentedMap
-from kiwi_scp.instance import Service
+from kiwi_scp.service import Service
class TestDefault:
diff --git a/tests/test_services.py b/tests/test_services.py
index ff349ef..b6dd451 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -1,6 +1,7 @@
from ruamel.yaml import CommentedMap
-from kiwi_scp.instance import Service, Services
+from kiwi_scp.services import Services
+from kiwi_scp.service import Service
class TestServices:
From 45fc16c132dfbaa8a47fda05aaeea7f10067c415 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:12:30 +0100
Subject: [PATCH 087/135] Resolve import cycle
---
kiwi_scp/project.py | 8 +++++---
kiwi_scp/service.py | 8 +++++---
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index b6154a4..47e0699 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -1,17 +1,19 @@
import functools
from pathlib import Path
-from typing import Optional, Dict, Any
+from typing import TYPE_CHECKING, Optional, Dict, Any
import attr
from ruamel.yaml import CommentedMap
from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
from .config import ProjectConfig
-from .instance import Instance
from .service import Service
from .services import Services
from .yaml import YAML
+if TYPE_CHECKING:
+ from .instance import Instance
+
@attr.s
class Project:
@@ -66,4 +68,4 @@ class Project:
content=content,
parent=self,
) for name, content in yml["services"].items()
- ])
\ No newline at end of file
+ ])
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index ae282b1..f7e81a5 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -1,13 +1,15 @@
import re
import subprocess
from pathlib import Path
-from typing import Generator
+from typing import TYPE_CHECKING, Generator
import attr
from ruamel.yaml import CommentedMap
from .executable import COMPOSE_EXE
-from .project import Project
+
+if TYPE_CHECKING:
+ from .project import Project
@attr.s
@@ -41,4 +43,4 @@ class Service:
return True
except subprocess.CalledProcessError:
- return False
\ No newline at end of file
+ return False
From a416b7f2fc37f0d9c931b7c63c7535bf2f37e721 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:17:42 +0100
Subject: [PATCH 088/135] Missing type hints
---
kiwi_scp/commands/cli.py | 2 +-
kiwi_scp/scripts/kiwi.py | 2 +-
kiwi_scp/yaml.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index acd09b6..cc4cb04 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -130,7 +130,7 @@ class KiwiCommand:
return answer == "yes"
@classmethod
- def run(cls, instance: Instance, project_names: List[str], service_names: List[str], **kwargs):
+ def run(cls, instance: Instance, project_names: List[str], service_names: List[str], **kwargs) -> None:
_logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
index b64bfef..764c335 100644
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -6,7 +6,7 @@ from kiwi_scp.commands.cli import KiwiCLI
@click.command(cls=KiwiCLI)
-def main():
+def main() -> None:
"""kiwi is the simple tool for managing container servers."""
verbosity = 0
diff --git a/kiwi_scp/yaml.py b/kiwi_scp/yaml.py
index 972b12d..1b8729c 100644
--- a/kiwi_scp/yaml.py
+++ b/kiwi_scp/yaml.py
@@ -8,7 +8,7 @@ from ._constants import HEADER_KIWI_CONF_NAME
class YAML(ruamel.yaml.YAML):
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.indent(offset=2)
From ea9f5e227968e2175dacc6aa5586d4eb857d2053 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:19:14 +0100
Subject: [PATCH 089/135] Split cli.py
---
kiwi_scp/commands/cli.py | 181 +-----------------------------
kiwi_scp/commands/cmd.py | 184 +++++++++++++++++++++++++++++++
kiwi_scp/commands/cmd_build.py | 2 +-
kiwi_scp/commands/cmd_cmd.py | 2 +-
kiwi_scp/commands/cmd_disable.py | 2 +-
kiwi_scp/commands/cmd_down.py | 2 +-
kiwi_scp/commands/cmd_enable.py | 2 +-
kiwi_scp/commands/cmd_init.py | 2 +-
kiwi_scp/commands/cmd_list.py | 2 +-
kiwi_scp/commands/cmd_logs.py | 2 +-
kiwi_scp/commands/cmd_up.py | 2 +-
kiwi_scp/commands/decorators.py | 2 +-
12 files changed, 195 insertions(+), 190 deletions(-)
create mode 100644 kiwi_scp/commands/cmd.py
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index cc4cb04..f38b72e 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,19 +1,9 @@
import importlib
-import logging
import os
-import sys
-from enum import Enum, auto
-from typing import List, Iterable, Type, Optional, TypeVar
+from typing import List, Optional
import click
-from ..instance import Instance
-from ..project import Project
-from ..services import Services
-from ..wstring import WParagraph, WAlignment
-
-_logger = logging.getLogger(__name__)
-
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
@@ -42,172 +32,3 @@ class KiwiCLI(click.MultiCommand):
return member
-class KiwiCommandType(Enum):
- INSTANCE = auto()
- PROJECT = auto()
- PROJECTS = auto()
- SERVICES = auto()
-
-
-T = TypeVar("T")
-
-
-class KiwiCommand:
- type: KiwiCommandType = KiwiCommandType.SERVICES
- enabled_only: bool = False
-
- @staticmethod
- def print_header(header: str) -> None:
- click.secho(header, fg="green", bold=True)
-
- @staticmethod
- def print_error(error: str) -> None:
- click.secho(error, file=sys.stderr, fg="red", bold=True)
-
- @staticmethod
- def print_list(content: Iterable[str]) -> None:
- for item in content:
- click.echo(click.style(" - ", fg="green") + click.style(item, fg="blue"))
-
- @staticmethod
- def user_query(description: str, default: T, cast_to: Type[T] = str) -> T:
- # prompt user as per argument
- while True:
- try:
- prompt = \
- click.style(f"Enter {description} [", fg="green") + \
- click.style(default, fg="blue") + \
- click.style("] ", fg="green")
- str_value = input(prompt).strip()
- if str_value:
- return cast_to(str_value)
- else:
- return default
-
- except EOFError:
- click.echo("Input aborted.")
- return default
-
- except Exception as e:
- click.echo(f"Invalid input: {e}")
-
- @staticmethod
- def danger_confirm(*prompt_lines: str, default: Optional[bool] = None) -> bool:
- if default is True:
- suffix = "[YES|no]"
- default_answer = "yes"
-
- elif default is False:
- suffix = "[yes|NO]"
- default_answer = "no"
-
- else:
- suffix = "[yes|no]"
- default_answer = None
-
- dumb = WParagraph.from_strings(
- click.style("WARNING", bold=True, underline=True, blink=True, fg="red"),
- click.style("ここにゴミ", fg="cyan"),
- click.style("を捨てないで下さい", fg="cyan"),
- click.style("DO NOT DUMB HERE", fg="yellow"),
- click.style("NO DUMB AREA", fg="yellow"),
- ).align(WAlignment.CENTER).surround("!")
-
- prompt = WParagraph.from_strings(*prompt_lines).align(WAlignment.LEFT).emphasize(3)
-
- answer = input(
- f"{dumb}\n\n"
- f"{prompt}\n\n"
- f"Are you sure you want to proceed? {suffix}: "
- ).strip().lower()
-
- if not answer:
- answer = default_answer
-
- while answer not in ["yes", "no"]:
- answer = input("Please type 'yes' or 'no' explicitly: ").strip().lower()
-
- return answer == "yes"
-
- @classmethod
- def run(cls, instance: Instance, project_names: List[str], service_names: List[str], **kwargs) -> None:
-
- _logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
-
- projects = [
- project
- for project in instance.projects
- if project.name in project_names
- ]
-
- if not projects:
- # run for whole instance
- _logger.debug(f"running for instance, kwargs={kwargs}")
- cls.run_for_instance(instance, **kwargs)
-
- elif not service_names:
- # run for entire project(s)
- for project_name, project in zip(project_names, projects):
- if project is None:
- _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
- cls.run_for_new_project(instance, project_name, **kwargs)
-
- else:
- if cls.enabled_only and not project.config.enabled:
- cls.print_error(f"Can't interact with disabled project {project_name}!")
- return
-
- _logger.debug(f"running for project {project.name}, kwargs={kwargs}")
- cls.run_for_project(instance, project, **kwargs)
-
- else:
- # run for some services
- project_name = project_names[0]
- project = projects[0]
-
- if project is None:
- cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
-
- else:
- if cls.enabled_only and not project.config.enabled:
- cls.print_error(f"Can't interact with disabled project {project_name}!")
- return
-
- _logger.debug(f"running for services {service_names} in project {project_name}, kwargs={kwargs}")
- cls.run_for_services(instance, project, service_names, **kwargs)
-
- @classmethod
- def run_for_instance(cls, instance: Instance, **kwargs) -> None:
- for project in instance.projects:
- if cls.enabled_only and not project.config.enabled:
- cls.print_header(f"Skipping disabled project {project.name}")
- continue
-
- cls.run_for_project(instance, project, **kwargs)
-
- @classmethod
- def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
- service_names = [service.name for service in project.services.content]
- cls.run_for_services(instance, project, service_names, **kwargs)
-
- @classmethod
- def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
- cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
-
- @classmethod
- def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
- services = project.services.filter_existing(service_names)
-
- new_service_names = [
- service_name
- for service_name
- in service_names
- if service_name not in list(services.names)
- ]
-
- cls.run_for_filtered_services(instance, project, services, new_service_names, **kwargs)
-
- @classmethod
- def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
- new_service_names: List[str], **kwargs) -> None:
- raise Exception
diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py
new file mode 100644
index 0000000..6163526
--- /dev/null
+++ b/kiwi_scp/commands/cmd.py
@@ -0,0 +1,184 @@
+import logging
+import sys
+from enum import Enum, auto
+from typing import TypeVar, Iterable, Type, Optional, List
+
+import click
+
+from kiwi_scp.instance import Instance
+from kiwi_scp.project import Project
+from kiwi_scp.services import Services
+from kiwi_scp.wstring import WParagraph, WAlignment
+
+_logger = logging.getLogger(__name__)
+
+
+class KiwiCommandType(Enum):
+ INSTANCE = auto()
+ PROJECT = auto()
+ PROJECTS = auto()
+ SERVICES = auto()
+
+
+T = TypeVar("T")
+
+
+class KiwiCommand:
+ type: KiwiCommandType = KiwiCommandType.SERVICES
+ enabled_only: bool = False
+
+ @staticmethod
+ def print_header(header: str) -> None:
+ click.secho(header, fg="green", bold=True)
+
+ @staticmethod
+ def print_error(error: str) -> None:
+ click.secho(error, file=sys.stderr, fg="red", bold=True)
+
+ @staticmethod
+ def print_list(content: Iterable[str]) -> None:
+ for item in content:
+ click.echo(click.style(" - ", fg="green") + click.style(item, fg="blue"))
+
+ @staticmethod
+ def user_query(description: str, default: T, cast_to: Type[T] = str) -> T:
+ # prompt user as per argument
+ while True:
+ try:
+ prompt = \
+ click.style(f"Enter {description} [", fg="green") + \
+ click.style(default, fg="blue") + \
+ click.style("] ", fg="green")
+ str_value = input(prompt).strip()
+ if str_value:
+ return cast_to(str_value)
+ else:
+ return default
+
+ except EOFError:
+ click.echo("Input aborted.")
+ return default
+
+ except Exception as e:
+ click.echo(f"Invalid input: {e}")
+
+ @staticmethod
+ def danger_confirm(*prompt_lines: str, default: Optional[bool] = None) -> bool:
+ if default is True:
+ suffix = "[YES|no]"
+ default_answer = "yes"
+
+ elif default is False:
+ suffix = "[yes|NO]"
+ default_answer = "no"
+
+ else:
+ suffix = "[yes|no]"
+ default_answer = None
+
+ dumb = WParagraph.from_strings(
+ click.style("WARNING", bold=True, underline=True, blink=True, fg="red"),
+ click.style("ここにゴミ", fg="cyan"),
+ click.style("を捨てないで下さい", fg="cyan"),
+ click.style("DO NOT DUMB HERE", fg="yellow"),
+ click.style("NO DUMB AREA", fg="yellow"),
+ ).align(WAlignment.CENTER).surround("!")
+
+ prompt = WParagraph.from_strings(*prompt_lines).align(WAlignment.LEFT).emphasize(3)
+
+ answer = input(
+ f"{dumb}\n\n"
+ f"{prompt}\n\n"
+ f"Are you sure you want to proceed? {suffix}: "
+ ).strip().lower()
+
+ if not answer:
+ answer = default_answer
+
+ while answer not in ["yes", "no"]:
+ answer = input("Please type 'yes' or 'no' explicitly: ").strip().lower()
+
+ return answer == "yes"
+
+ @classmethod
+ def run(cls, instance: Instance, project_names: List[str], service_names: List[str], **kwargs) -> None:
+
+ _logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
+
+ projects = [
+ project
+ for project in instance.projects
+ if project.name in project_names
+ ]
+
+ if not projects:
+ # run for whole instance
+ _logger.debug(f"running for instance, kwargs={kwargs}")
+ cls.run_for_instance(instance, **kwargs)
+
+ elif not service_names:
+ # run for entire project(s)
+ for project_name, project in zip(project_names, projects):
+ if project is None:
+ _logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
+ cls.run_for_new_project(instance, project_name, **kwargs)
+
+ else:
+ if cls.enabled_only and not project.config.enabled:
+ cls.print_error(f"Can't interact with disabled project {project_name}!")
+ return
+
+ _logger.debug(f"running for project {project.name}, kwargs={kwargs}")
+ cls.run_for_project(instance, project, **kwargs)
+
+ else:
+ # run for some services
+ project_name = project_names[0]
+ project = projects[0]
+
+ if project is None:
+ cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
+
+ else:
+ if cls.enabled_only and not project.config.enabled:
+ cls.print_error(f"Can't interact with disabled project {project_name}!")
+ return
+
+ _logger.debug(f"running for services {service_names} in project {project_name}, kwargs={kwargs}")
+ cls.run_for_services(instance, project, service_names, **kwargs)
+
+ @classmethod
+ def run_for_instance(cls, instance: Instance, **kwargs) -> None:
+ for project in instance.projects:
+ if cls.enabled_only and not project.config.enabled:
+ cls.print_header(f"Skipping disabled project {project.name}")
+ continue
+
+ cls.run_for_project(instance, project, **kwargs)
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ service_names = [service.name for service in project.services.content]
+ cls.run_for_services(instance, project, service_names, **kwargs)
+
+ @classmethod
+ def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
+ cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
+
+ @classmethod
+ def run_for_services(cls, instance: Instance, project: Project, service_names: List[str], **kwargs) -> None:
+ services = project.services.filter_existing(service_names)
+
+ new_service_names = [
+ service_name
+ for service_name
+ in service_names
+ if service_name not in list(services.names)
+ ]
+
+ cls.run_for_filtered_services(instance, project, services, new_service_names, **kwargs)
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ raise Exception
\ No newline at end of file
diff --git a/kiwi_scp/commands/cmd_build.py b/kiwi_scp/commands/cmd_build.py
index 338f5b7..720632c 100644
--- a/kiwi_scp/commands/cmd_build.py
+++ b/kiwi_scp/commands/cmd_build.py
@@ -1,6 +1,6 @@
from typing import List
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_cmd.py b/kiwi_scp/commands/cmd_cmd.py
index 15cfdfa..8a03692 100644
--- a/kiwi_scp/commands/cmd_cmd.py
+++ b/kiwi_scp/commands/cmd_cmd.py
@@ -2,7 +2,7 @@ from typing import Tuple
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_disable.py b/kiwi_scp/commands/cmd_disable.py
index 8b6b2ef..e7ee373 100644
--- a/kiwi_scp/commands/cmd_disable.py
+++ b/kiwi_scp/commands/cmd_disable.py
@@ -1,6 +1,6 @@
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index 960559a..7476013 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -2,7 +2,7 @@ from typing import List
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
index 6f3676b..54d89be 100644
--- a/kiwi_scp/commands/cmd_enable.py
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -1,6 +1,6 @@
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index ab8ee98..a93d565 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -5,7 +5,7 @@ from pathlib import Path
import click
-from .cli import KiwiCommandType, KiwiCommand
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from .._constants import KIWI_CONF_NAME
from ..config import KiwiConfig
diff --git a/kiwi_scp/commands/cmd_list.py b/kiwi_scp/commands/cmd_list.py
index e07719a..8fc82a4 100644
--- a/kiwi_scp/commands/cmd_list.py
+++ b/kiwi_scp/commands/cmd_list.py
@@ -2,7 +2,7 @@ from typing import List
import click
-from .cli import KiwiCommandType, KiwiCommand
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..instance import Instance
from ..project import Project
diff --git a/kiwi_scp/commands/cmd_logs.py b/kiwi_scp/commands/cmd_logs.py
index 581c966..d7669a1 100644
--- a/kiwi_scp/commands/cmd_logs.py
+++ b/kiwi_scp/commands/cmd_logs.py
@@ -2,7 +2,7 @@ from typing import List
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
from ..instance import Instance
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
index 8764953..7c6a777 100644
--- a/kiwi_scp/commands/cmd_up.py
+++ b/kiwi_scp/commands/cmd_up.py
@@ -2,7 +2,7 @@ from typing import List
import click
-from .cli import KiwiCommand, KiwiCommandType
+from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from ..executable import COMPOSE_EXE
from ..instance import Instance
diff --git a/kiwi_scp/commands/decorators.py b/kiwi_scp/commands/decorators.py
index f8b9ca7..7b28fc9 100644
--- a/kiwi_scp/commands/decorators.py
+++ b/kiwi_scp/commands/decorators.py
@@ -2,7 +2,7 @@ from typing import Callable, Type, Optional, Tuple
import click
-from .cli import KiwiCommandType, KiwiCommand
+from .cmd import KiwiCommandType, KiwiCommand
from ..instance import Instance
_pass_instance = click.make_pass_decorator(
From fe10bbb33b4fa1b8b5bdf4c7e9afee57f43aeaa9 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:21:44 +0100
Subject: [PATCH 090/135] output verbosity option
---
kiwi_scp/scripts/kiwi.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
index 764c335..3503270 100644
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -5,16 +5,19 @@ import click
from kiwi_scp.commands.cli import KiwiCLI
+@click.option(
+ "-v", "--verbose",
+ help="increase output verbosity",
+ count=True,
+)
@click.command(cls=KiwiCLI)
-def main() -> None:
+def main(verbose: int) -> None:
"""kiwi is the simple tool for managing container servers."""
- verbosity = 0
-
- if verbosity >= 2:
+ if verbose >= 2:
log_level = logging.DEBUG
log_format = "[%(asctime)s] %(levelname)s @ %(filename)s:%(funcName)s:%(lineno)d: %(message)s"
- elif verbosity >= 1:
+ elif verbose >= 1:
log_level = logging.INFO
log_format = "[%(asctime)s] %(levelname)s: %(message)s"
else:
From c8b69c73a9a84d045da87ac348f90f9edc415e5d Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:32:55 +0100
Subject: [PATCH 091/135] Fix pytests
---
tests/test_project.py | 2 ++
tests/test_service.py | 4 ++++
tests/test_services.py | 1 +
3 files changed, 7 insertions(+)
diff --git a/tests/test_project.py b/tests/test_project.py
index f6fabff..6247950 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -13,6 +13,7 @@ class TestDefault:
def test_example(self):
p = Project(
directory=Path("example/hello-world.project"),
+ parent=None,
)
ss = p.services
@@ -30,6 +31,7 @@ class TestDefault:
def test_empty(self):
p = Project(
directory=Path("nonexistent"),
+ parent=None,
)
with pytest.raises(FileNotFoundError) as exc_info:
diff --git a/tests/test_service.py b/tests/test_service.py
index 9206374..4024bdd 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -10,6 +10,7 @@ class TestDefault:
s = Service(
name="s",
content=CommentedMap(),
+ parent=None,
)
assert s.name == "s"
@@ -21,6 +22,7 @@ class TestDefault:
content=CommentedMap({
"image": "repo/image:tag",
}),
+ parent=None,
)
assert s.name == "s"
@@ -37,6 +39,7 @@ class TestDefault:
"$TARGETDIR/other/dir:/path/to/other/mountpoint",
]
}),
+ parent=None,
)
assert s.name == "s"
@@ -52,6 +55,7 @@ class TestDefault:
"$CONFDIR/other/config:/path/to/other/config",
]
}),
+ parent=None,
)
assert s.name == "s"
diff --git a/tests/test_services.py b/tests/test_services.py
index b6dd451..151d242 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -9,6 +9,7 @@ class TestServices:
s = Service(
name="s",
content=CommentedMap(),
+ parent=None,
)
ss = Services([s])
From 229beaafd7f16d9ac030274d47a4292b6f371c9f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 17:33:07 +0100
Subject: [PATCH 092/135] relative imports
---
kiwi_scp/commands/cmd.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py
index 6163526..99e3606 100644
--- a/kiwi_scp/commands/cmd.py
+++ b/kiwi_scp/commands/cmd.py
@@ -5,10 +5,10 @@ from typing import TypeVar, Iterable, Type, Optional, List
import click
-from kiwi_scp.instance import Instance
-from kiwi_scp.project import Project
-from kiwi_scp.services import Services
-from kiwi_scp.wstring import WParagraph, WAlignment
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+from ..wstring import WParagraph, WAlignment
_logger = logging.getLogger(__name__)
From 4d76cfa6694aa52730dbe1f2cb5b012c5466f9d5 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 18:56:00 +0100
Subject: [PATCH 093/135] updated launch_conf
---
.idea/runConfigurations/{kiwi_next.xml => kiwi.xml} | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
rename .idea/runConfigurations/{kiwi_next.xml => kiwi.xml} (77%)
diff --git a/.idea/runConfigurations/kiwi_next.xml b/.idea/runConfigurations/kiwi.xml
similarity index 77%
rename from .idea/runConfigurations/kiwi_next.xml
rename to .idea/runConfigurations/kiwi.xml
index 49cad48..127b64e 100644
--- a/.idea/runConfigurations/kiwi_next.xml
+++ b/.idea/runConfigurations/kiwi.xml
@@ -1,5 +1,5 @@
-
+
@@ -11,8 +11,8 @@
-
-
+
+
From 4ddad29de02087521c82d62b80b298cf99d914b7 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 18:58:08 +0100
Subject: [PATCH 094/135] "kiwi shell"
---
kiwi_scp/commands/cmd_shell.py | 76 ++++++++++++++++++++++++++++++++++
kiwi_scp/service.py | 18 +++++++-
2 files changed, 93 insertions(+), 1 deletion(-)
create mode 100644 kiwi_scp/commands/cmd_shell.py
diff --git a/kiwi_scp/commands/cmd_shell.py b/kiwi_scp/commands/cmd_shell.py
new file mode 100644
index 0000000..5bd4ffa
--- /dev/null
+++ b/kiwi_scp/commands/cmd_shell.py
@@ -0,0 +1,76 @@
+import logging
+from collections import deque
+from typing import List, Optional
+
+import click
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+
+_logger = logging.getLogger(__name__)
+
+
+@click.option(
+ "-s", "--shell",
+ help="shell to spawn",
+ type=str,
+)
+@click.option(
+ "-u", "--user",
+ help="container user to run shell",
+ type=str,
+)
+@kiwi_command(
+ short_help="Spawn shell",
+)
+class ShellCommand(KiwiCommand):
+ """Spawn shell inside a project's service"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], shell: Optional[str] = None,
+ user: Optional[str] = None) -> None:
+ # builtin shells: as a last resort, fallback to '/bin/sh' and 'sh'
+ shells = deque(["/bin/sh", "sh"])
+
+ # add shells from KiwiConfig
+ config_shells = map(str, instance.config.shells)
+ shells.extendleft(config_shells)
+
+ # add shell from argument
+ if shell is not None:
+ shells.appendleft(shell)
+
+ user_args = ["-u", user] if user is not None else []
+
+ for service in services.content:
+ existing_shells = service.existing_executables(list(shells))
+
+ try:
+ use_shell = next(existing_shells)
+ _logger.debug(f"Using shell {use_shell!r}")
+
+ except StopIteration:
+ if shell is not None:
+ use_shell = shell
+ _logger.warning(
+ "Could not find any working shell in this container. "
+ f"Launching provided {use_shell!r} nevertheless. This might fail!"
+ )
+
+ else:
+ _logger.warning(
+ f"Could not find any working shell among {shells!r} in this container. "
+ "Please suggest a shell using the '-s SHELL' command line option!"
+ )
+ return
+
+ # spawn shell
+ COMPOSE_EXE.run(['exec', *user_args, service.name, use_shell], **project.process_kwargs)
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index f7e81a5..c85652f 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -1,7 +1,9 @@
+import logging
import re
import subprocess
+from itertools import zip_longest
from pathlib import Path
-from typing import TYPE_CHECKING, Generator
+from typing import TYPE_CHECKING, Generator, Sequence
import attr
from ruamel.yaml import CommentedMap
@@ -11,6 +13,8 @@ from .executable import COMPOSE_EXE
if TYPE_CHECKING:
from .project import Project
+_logger = logging.getLogger(__name__)
+
@attr.s
class Service:
@@ -44,3 +48,15 @@ class Service:
except subprocess.CalledProcessError:
return False
+
+ def existing_executables(self, exe_names: Sequence[str]) -> Generator[str, None, None]:
+ for cur, nxt in zip_longest(exe_names, exe_names[1:]):
+ if self.has_executable(cur):
+ # found working shell
+ _logger.debug(f"Found executable '{cur}'")
+ yield cur
+
+ elif nxt is not None:
+ # try next in list
+ _logger.info(f"Executable '{cur}' not found in container, trying '{nxt}'")
+
From ff8df05708e930cd039c0eed73d2b27e58e18714 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 19:04:37 +0100
Subject: [PATCH 095/135] "kiwi shell" flaws
---
kiwi_scp/commands/cmd_shell.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/kiwi_scp/commands/cmd_shell.py b/kiwi_scp/commands/cmd_shell.py
index 5bd4ffa..c32bea6 100644
--- a/kiwi_scp/commands/cmd_shell.py
+++ b/kiwi_scp/commands/cmd_shell.py
@@ -48,10 +48,11 @@ class ShellCommand(KiwiCommand):
if shell is not None:
shells.appendleft(shell)
+ shells = list(shells)
user_args = ["-u", user] if user is not None else []
for service in services.content:
- existing_shells = service.existing_executables(list(shells))
+ existing_shells = service.existing_executables(shells)
try:
use_shell = next(existing_shells)
@@ -61,8 +62,8 @@ class ShellCommand(KiwiCommand):
if shell is not None:
use_shell = shell
_logger.warning(
- "Could not find any working shell in this container. "
- f"Launching provided {use_shell!r} nevertheless. This might fail!"
+ "Could not find a working shell in this container. "
+ f"Launching provided shell {use_shell!r} nevertheless. This might fail!"
)
else:
@@ -70,7 +71,7 @@ class ShellCommand(KiwiCommand):
f"Could not find any working shell among {shells!r} in this container. "
"Please suggest a shell using the '-s SHELL' command line option!"
)
- return
+ continue
# spawn shell
COMPOSE_EXE.run(['exec', *user_args, service.name, use_shell], **project.process_kwargs)
From 61cbfb40d992a631c662165f43d867bb250cc15e Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Thu, 2 Dec 2021 19:32:38 +0100
Subject: [PATCH 096/135] projects ordering in KiwiCommand.run
---
kiwi_scp/commands/cmd.py | 12 ++++--------
kiwi_scp/instance.py | 18 +++++++++++++++++-
2 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py
index 99e3606..ed3fa9f 100644
--- a/kiwi_scp/commands/cmd.py
+++ b/kiwi_scp/commands/cmd.py
@@ -105,11 +105,7 @@ class KiwiCommand:
_logger.debug(f"{instance.directory!r}: {project_names!r}, {service_names!r}")
- projects = [
- project
- for project in instance.projects
- if project.name in project_names
- ]
+ projects = instance.get_projects(project_names)
if not projects:
# run for whole instance
@@ -118,7 +114,7 @@ class KiwiCommand:
elif not service_names:
# run for entire project(s)
- for project_name, project in zip(project_names, projects):
+ for project_name, project in projects.items():
if project is None:
_logger.debug(f"running for new project {project_name}, kwargs={kwargs}")
cls.run_for_new_project(instance, project_name, **kwargs)
@@ -133,8 +129,8 @@ class KiwiCommand:
else:
# run for some services
- project_name = project_names[0]
- project = projects[0]
+ project_name = list(projects)[0]
+ project = projects[project_name]
if project is None:
cls.print_error(f"Project '{project_name}' not in kiwi-scp instance at '{instance.directory}'!")
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index f5f63b6..30389e7 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,5 +1,5 @@
from pathlib import Path
-from typing import Generator
+from typing import Generator, Dict, Sequence
import attr
@@ -24,3 +24,19 @@ class Instance:
directory=self.directory.joinpath(project.name),
parent=self,
)
+
+ def get_projects(self, project_names: Sequence[str]) -> Dict[str, Project]:
+ existing_projects = {
+ project.name: project
+ for project in self.projects
+ if project.name in project_names
+ }
+ nonexistent_projects = {
+ name: None
+ for name in project_names
+ if name not in existing_projects
+ }
+ return {
+ **existing_projects,
+ **nonexistent_projects
+ }
From 801f08137f06ab179fe1334f76de9e7ce7d23ea3 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 3 Dec 2021 14:43:03 +0100
Subject: [PATCH 097/135] remove need for deque
---
kiwi_scp/commands/cmd_shell.py | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
diff --git a/kiwi_scp/commands/cmd_shell.py b/kiwi_scp/commands/cmd_shell.py
index c32bea6..65c2f40 100644
--- a/kiwi_scp/commands/cmd_shell.py
+++ b/kiwi_scp/commands/cmd_shell.py
@@ -1,5 +1,4 @@
import logging
-from collections import deque
from typing import List, Optional
import click
@@ -37,25 +36,22 @@ class ShellCommand(KiwiCommand):
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], shell: Optional[str] = None,
user: Optional[str] = None) -> None:
- # builtin shells: as a last resort, fallback to '/bin/sh' and 'sh'
- shells = deque(["/bin/sh", "sh"])
-
- # add shells from KiwiConfig
- config_shells = map(str, instance.config.shells)
- shells.extendleft(config_shells)
+ # shells from KiwiConfig
+ shells = [
+ *(str(path) for path in instance.config.shells),
+ # as a last resort, fall back to '/bin/sh' and 'sh'
+ "/bin/sh", "sh",
+ ]
# add shell from argument
if shell is not None:
- shells.appendleft(shell)
+ shells.insert(0, shell)
- shells = list(shells)
user_args = ["-u", user] if user is not None else []
for service in services.content:
- existing_shells = service.existing_executables(shells)
-
try:
- use_shell = next(existing_shells)
+ use_shell = next(service.existing_executables(shells))
_logger.debug(f"Using shell {use_shell!r}")
except StopIteration:
From 1590041ab826eb3bea6c0544133c4accc1a4ffcb Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 3 Dec 2021 15:03:53 +0100
Subject: [PATCH 098/135] "kiwi push", "kiwi pull" & "kiwi restart"
---
kiwi_scp/commands/cmd_pull.py | 33 ++++++++++++++++++++++
kiwi_scp/commands/cmd_push.py | 33 ++++++++++++++++++++++
kiwi_scp/commands/cmd_restart.py | 48 ++++++++++++++++++++++++++++++++
3 files changed, 114 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_pull.py
create mode 100644 kiwi_scp/commands/cmd_push.py
create mode 100644 kiwi_scp/commands/cmd_restart.py
diff --git a/kiwi_scp/commands/cmd_pull.py b/kiwi_scp/commands/cmd_pull.py
new file mode 100644
index 0000000..b5f0517
--- /dev/null
+++ b/kiwi_scp/commands/cmd_pull.py
@@ -0,0 +1,33 @@
+from typing import List
+
+import click
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+
+
+@kiwi_command(
+ short_help="Pull docker images",
+)
+class PullCommand(KiwiCommand):
+ """Pull images for the whole instance, a project or service(s) inside a project"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ if not services:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Pull images for the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ COMPOSE_EXE.run(["pull", "--ignore-pull-failures", *services.names], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_push.py b/kiwi_scp/commands/cmd_push.py
new file mode 100644
index 0000000..999677b
--- /dev/null
+++ b/kiwi_scp/commands/cmd_push.py
@@ -0,0 +1,33 @@
+from typing import List
+
+import click
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+
+
+@kiwi_command(
+ short_help="Push docker images",
+)
+class PushCommand(KiwiCommand):
+ """Push images for the whole instance, a project or service(s) inside a project"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ if not services:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Push images for the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ COMPOSE_EXE.run(["push", *services.names], **project.process_kwargs)
diff --git a/kiwi_scp/commands/cmd_restart.py b/kiwi_scp/commands/cmd_restart.py
new file mode 100644
index 0000000..fbd1cf3
--- /dev/null
+++ b/kiwi_scp/commands/cmd_restart.py
@@ -0,0 +1,48 @@
+from typing import List
+
+import click
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from ..executable import COMPOSE_EXE
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+
+
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"skip confirmation",
+)
+@kiwi_command(
+ short_help="Restart kiwi services",
+)
+class RestartCommand(KiwiCommand):
+ """Restart the whole instance, a project or service(s) inside a project"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
+ if not force:
+ if not KiwiCommand.danger_confirm(
+ "This will restart the entire instance.",
+ ):
+ return
+
+ super().run_for_instance(instance)
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ if not services:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Restart the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ COMPOSE_EXE.run(["restart", *services.names], **project.process_kwargs)
From 7641f1846aeb24ef893320b4a4f88096ab2d0263 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 3 Dec 2021 15:04:03 +0100
Subject: [PATCH 099/135] remove legacy
---
kiwi_scp/subcommands/__init__.py | 38 -----------
kiwi_scp/subcommands/build.py | 18 ------
kiwi_scp/subcommands/cmd.py | 40 ------------
kiwi_scp/subcommands/disable.py | 16 -----
kiwi_scp/subcommands/down.py | 56 -----------------
kiwi_scp/subcommands/enable.py | 16 -----
kiwi_scp/subcommands/logs.py | 40 ------------
kiwi_scp/subcommands/pull.py | 18 ------
kiwi_scp/subcommands/push.py | 18 ------
kiwi_scp/subcommands/restart.py | 26 --------
kiwi_scp/subcommands/shell.py | 104 -------------------------------
kiwi_scp/subcommands/show.py | 99 -----------------------------
kiwi_scp/subcommands/up.py | 26 --------
13 files changed, 515 deletions(-)
delete mode 100644 kiwi_scp/subcommands/__init__.py
delete mode 100644 kiwi_scp/subcommands/build.py
delete mode 100644 kiwi_scp/subcommands/cmd.py
delete mode 100644 kiwi_scp/subcommands/disable.py
delete mode 100644 kiwi_scp/subcommands/down.py
delete mode 100644 kiwi_scp/subcommands/enable.py
delete mode 100644 kiwi_scp/subcommands/logs.py
delete mode 100644 kiwi_scp/subcommands/pull.py
delete mode 100644 kiwi_scp/subcommands/push.py
delete mode 100644 kiwi_scp/subcommands/restart.py
delete mode 100644 kiwi_scp/subcommands/shell.py
delete mode 100644 kiwi_scp/subcommands/show.py
delete mode 100644 kiwi_scp/subcommands/up.py
diff --git a/kiwi_scp/subcommands/__init__.py b/kiwi_scp/subcommands/__init__.py
deleted file mode 100644
index cd108a6..0000000
--- a/kiwi_scp/subcommands/__init__.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# local
-from ._hidden import ConfCopyCommand, NetUpCommand
-
-from .build import BuildCommand
-from .cmd import CmdCommand
-from .disable import DisableCommand
-from .down import DownCommand
-from .enable import EnableCommand
-from .logs import LogsCommand
-from .new import NewCommand
-from .pull import PullCommand
-from .push import PushCommand
-from .restart import RestartCommand
-from .shell import ShellCommand
-from .show import ShowCommand
-from .up import UpCommand
-from .update import UpdateCommand
-
-__all__ = [
- 'ConfCopyCommand',
- 'NetUpCommand',
-
- 'BuildCommand',
- 'CmdCommand',
- 'DisableCommand',
- 'DownCommand',
- 'EnableCommand',
- 'InitCommand',
- 'LogsCommand',
- 'NewCommand',
- 'PullCommand',
- 'PushCommand',
- 'RestartCommand',
- 'ShellCommand',
- 'ShowCommand',
- 'UpCommand',
- 'UpdateCommand',
-]
diff --git a/kiwi_scp/subcommands/build.py b/kiwi_scp/subcommands/build.py
deleted file mode 100644
index 35cb4e5..0000000
--- a/kiwi_scp/subcommands/build.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# local
-from ..subcommand import ServiceCommand
-
-
-class BuildCommand(ServiceCommand):
- """kiwi build"""
-
- def __init__(self):
- super().__init__(
- 'build', num_projects='?', num_services='*',
- action="Building images for",
- description="Build images for the whole instance, a project or service(s) inside a project"
- )
-
- def _run_services(self, runner, args, project, services):
- project.compose_run(['build', '--pull', *services])
-
- return True
diff --git a/kiwi_scp/subcommands/cmd.py b/kiwi_scp/subcommands/cmd.py
deleted file mode 100644
index c890edf..0000000
--- a/kiwi_scp/subcommands/cmd.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# system
-import logging
-
-# local
-from ..subcommand import ProjectCommand
-
-
-class CmdCommand(ProjectCommand):
- """kiwi cmd"""
-
- def __init__(self):
- super().__init__(
- 'cmd', num_projects=1,
- action="Running docker-compose in",
- description="Run raw docker-compose command in a project"
- )
-
- # command for docker-compose
- self._sub_parser.add_argument(
- 'compose_cmd', metavar='cmd', type=str,
- help="command for 'docker-compose'"
- )
-
- # arguments for docker-compose command
- self._sub_parser.add_argument(
- 'compose_args', metavar='arg', nargs='*', type=str,
- help="arguments for 'docker-compose' commands"
- )
-
- def _run_project(self, runner, args, project):
- if args.unknowns:
- args.compose_args = [*args.compose_args, *args.unknowns]
- args.unknowns = []
-
- logging.debug(f"Updated args: {args}")
-
- # run with split compose_cmd argument
- project.compose_run([args.compose_cmd, *args.compose_args])
-
- return True
diff --git a/kiwi_scp/subcommands/disable.py b/kiwi_scp/subcommands/disable.py
deleted file mode 100644
index d4d762d..0000000
--- a/kiwi_scp/subcommands/disable.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# local
-from ..subcommand import ProjectCommand
-
-
-class DisableCommand(ProjectCommand):
- """kiwi disable"""
-
- def __init__(self):
- super().__init__(
- 'disable', num_projects='+',
- action="Disabling",
- description="Disable project(s) in this instance"
- )
-
- def _run_project(self, runner, args, project):
- return project.disable()
diff --git a/kiwi_scp/subcommands/down.py b/kiwi_scp/subcommands/down.py
deleted file mode 100644
index b0a1147..0000000
--- a/kiwi_scp/subcommands/down.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# system
-import logging
-import subprocess
-
-# local
-from ._hidden import _find_net
-from ..config import LoadedConfig
-from ..executable import Executable
-from ..misc import are_you_sure
-from ..subcommand import ServiceCommand
-
-
-class DownCommand(ServiceCommand):
- """kiwi down"""
-
- def __init__(self):
- super().__init__(
- 'down', num_projects='?', num_services='*',
- action="Bringing down",
- description="Bring down the whole instance, a project or service(s) inside a project"
- )
-
- def _run_instance(self, runner, args):
- net_name = LoadedConfig.get()['network:name']
-
- if are_you_sure([
- "This will bring down the entire instance.",
- "",
- "This may not be what you intended, because:",
- " - Bringing down the instance stops ALL services in here",
- ]):
- if super()._run_instance(runner, args):
- # remove the hub network afterwards
- if _find_net(net_name):
- Executable('docker').run([
- 'network', 'rm', net_name
- ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
- logging.info(f"Network '{net_name}' removed")
-
- else:
- logging.info(f"Network '{net_name}' does not exist")
-
- return True
-
- return False
-
- def _run_project(self, runner, args, project):
- project.compose_run(['down'])
- return True
-
- def _run_services(self, runner, args, project, services):
- project.compose_run(['stop', *services])
- project.compose_run(['rm', '-f', *services])
-
- return True
diff --git a/kiwi_scp/subcommands/enable.py b/kiwi_scp/subcommands/enable.py
deleted file mode 100644
index 1262a7f..0000000
--- a/kiwi_scp/subcommands/enable.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# local
-from ..subcommand import ProjectCommand
-
-
-class EnableCommand(ProjectCommand):
- """kiwi enable"""
-
- def __init__(self):
- super().__init__(
- 'enable', num_projects='+',
- action="Enabling",
- description="Enable project(s) in this instance"
- )
-
- def _run_project(self, runner, args, project):
- return project.enable()
diff --git a/kiwi_scp/subcommands/logs.py b/kiwi_scp/subcommands/logs.py
deleted file mode 100644
index 2407220..0000000
--- a/kiwi_scp/subcommands/logs.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# local
-from ..subcommand import ServiceCommand
-
-
-class LogsCommand(ServiceCommand):
- """kiwi logs"""
-
- def __init__(self):
- super().__init__(
- 'logs', num_projects=1, num_services='*',
- action="Showing logs of",
- description="Show logs of a project or service(s) inside a project"
- )
-
- # -f switch: Follow logs
- self._sub_parser.add_argument(
- '-f', '--follow', action='store_true',
- help="output appended data as log grows"
- )
-
- def _run_services(self, runner, args, project, services):
- # include timestamps
- compose_cmd = ['logs', '-t']
-
- # handle following the log output
- if args.follow:
- compose_cmd = [*compose_cmd, '-f', '--tail=10']
-
- # append if one or more services are given
- if services:
- compose_cmd = [*compose_cmd, *args.services]
-
- if args.follow:
- project.compose_run(compose_cmd)
-
- else:
- # use 'less' viewer if output is static
- project.compose_run_less(compose_cmd)
-
- return True
diff --git a/kiwi_scp/subcommands/pull.py b/kiwi_scp/subcommands/pull.py
deleted file mode 100644
index a1ed790..0000000
--- a/kiwi_scp/subcommands/pull.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# local
-from ..subcommand import ServiceCommand
-
-
-class PullCommand(ServiceCommand):
- """kiwi pull"""
-
- def __init__(self):
- super().__init__(
- 'pull', num_projects='?', num_services='*',
- action="Pulling images for",
- description="Pull images for the whole instance, a project or service(s) inside a project"
- )
-
- def _run_services(self, runner, args, project, services):
- project.compose_run(['pull', '--ignore-pull-failures', *services])
-
- return True
diff --git a/kiwi_scp/subcommands/push.py b/kiwi_scp/subcommands/push.py
deleted file mode 100644
index 0f83c52..0000000
--- a/kiwi_scp/subcommands/push.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# local
-from ..subcommand import ServiceCommand
-
-
-class PushCommand(ServiceCommand):
- """kiwi push"""
-
- def __init__(self):
- super().__init__(
- 'push', num_projects='?', num_services='*',
- action="Pushing images for",
- description="Push images for the whole instance, a project or service(s) inside a project"
- )
-
- def _run_services(self, runner, args, project, services):
- project.compose_run(['push', *services])
-
- return True
diff --git a/kiwi_scp/subcommands/restart.py b/kiwi_scp/subcommands/restart.py
deleted file mode 100644
index 86fce22..0000000
--- a/kiwi_scp/subcommands/restart.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# local
-from ..misc import are_you_sure
-from ..subcommand import ServiceCommand
-
-
-class RestartCommand(ServiceCommand):
- """kiwi restart"""
-
- def __init__(self):
- super().__init__(
- 'restart', num_projects='?', num_services='*',
- action="Restarting",
- description="Restart the whole instance, a project or service(s) inside a project"
- )
-
- def _run_instance(self, runner, args):
- if are_you_sure([
- "This will restart the entire instance."
- ]):
- return super()._run_instance(runner, args)
-
- return False
-
- def _run_services(self, runner, args, project, services):
- project.compose_run(['restart', *services])
- return True
diff --git a/kiwi_scp/subcommands/shell.py b/kiwi_scp/subcommands/shell.py
deleted file mode 100644
index 7547ced..0000000
--- a/kiwi_scp/subcommands/shell.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# system
-import logging
-import subprocess
-
-from ..config import LoadedConfig
-# local
-from ..subcommand import ServiceCommand
-
-
-def _service_has_executable(project, service, exe_name):
- """
- Test if service in project has an executable exe_name in its PATH.
- Requires /bin/sh.
- """
-
- try:
- # test if desired shell exists
- project.compose_run(
- ['exec', service, '/bin/sh', '-c', f"command -v {exe_name}"],
- check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
- )
- return True
-
- except subprocess.CalledProcessError as e:
- # fallback
- return False
-
-
-def _find_shell(args, project, service):
- """find first working shell (provided by config and args) in service in project"""
-
- # builtin shells: as a last resort, fallback to '/bin/sh' and 'sh'
- shells = ['/bin/sh', 'sh']
-
- # load favorite shells from config
- config = LoadedConfig.get()
- if config['runtime:shells']:
- shells = [*config['runtime:shells'], *shells]
-
- # consider shell from args
- if args.shell:
- shells = [args.shell, *shells]
-
- logging.debug(f"Shells priority: {shells}")
-
- # actually try shells
- for i, shell in enumerate(shells):
- if _service_has_executable(project, service, shell):
- # found working shell
- logging.debug(f"Using shell '{shell}'")
- return shell
-
- elif i + 1 < len(shells):
- # try next in list
- logging.info(f"Shell '{shell}' not found in container, trying '{shells[i + 1]}'")
-
- elif args.shell:
- # not found, user suggestion provided
- logging.warning(f"Could not find any working shell in this container. "
- f"Launching provided '{args.shell}' nevertheless. "
- f"Don't get mad if this fails!")
- return args.shell
-
- else:
- # not found, search exhausted
- logging.error(f"Could not find any working shell among '{shells}' in this container. "
- f"Please suggest a shell using the '-s SHELL' command line option!")
- return None
-
-
-class ShellCommand(ServiceCommand):
- """kiwi shell"""
-
- def __init__(self):
- super().__init__(
- 'shell', num_projects=1, num_services=1,
- action="Spawning shell in",
- description="Spawn shell inside a project's service"
- )
-
- # -s argument: Select shell
- self._sub_parser.add_argument(
- '-s', '--shell', type=str,
- help="shell to spawn"
- )
-
- # -u argument: Run as user
- self._sub_parser.add_argument(
- '-u', '--user', type=str,
- help="container user to run shell"
- )
-
- def _run_services(self, runner, args, project, services):
- service = services[0]
- shell = _find_shell(args, project, service)
-
- user_args = ['-u', args.user] if args.user else []
-
- if shell is not None:
- # spawn shell
- project.compose_run(['exec', *user_args, service, shell])
- return True
-
- return False
diff --git a/kiwi_scp/subcommands/show.py b/kiwi_scp/subcommands/show.py
deleted file mode 100644
index cfb867a..0000000
--- a/kiwi_scp/subcommands/show.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# system
-import logging
-import os
-
-import yaml
-
-from ..project import Project
-from ..projects import Projects
-# local
-from ..subcommand import ServiceCommand
-
-
-def _print_list(strings):
- if isinstance(strings, str):
- print(f" - {strings}")
-
- elif isinstance(strings, Project):
- _print_list(strings.get_name())
-
- elif isinstance(strings, list):
- for string in strings:
- _print_list(string)
-
- else:
- _print_list(list(strings))
-
-
-class ShowCommand(ServiceCommand):
- """kiwi show"""
-
- def __init__(self):
- super().__init__(
- 'show', num_projects='?', num_services='*',
- action="Showing",
- description="Show projects in this instance, services inside a project or service(s) inside a project"
- )
-
- def _run_instance(self, runner, args):
- print(f"kiwi-scp instance at '{os.getcwd()}'")
- print("#########")
- projects = Projects.from_dir()
-
- enabled_projects = projects.filter_enabled()
- if enabled_projects:
- print(f"Enabled projects:")
- _print_list(enabled_projects)
-
- disabled_projects = projects.filter_disabled()
- if disabled_projects:
- print(f"Disabled projects:")
- _print_list(disabled_projects)
-
- return True
-
- def _run_project(self, runner, args, project):
- if not project.exists():
- logging.warning(f"Project '{project.get_name()}' not found")
- return False
-
- print(f"Services in project '{project.get_name()}':")
- print("#########")
-
- with open(project.compose_file_name(), 'r') as stream:
- try:
- docker_compose_yml = yaml.safe_load(stream)
- _print_list(docker_compose_yml['services'].keys())
-
- except yaml.YAMLError as exc:
- logging.error(exc)
-
- return True
-
- def _run_services(self, runner, args, project, services):
- if not project.exists():
- logging.error(f"Project '{project.get_name()}' not found")
- return False
-
- print(f"Configuration of services {services} in project '{project.get_name()}':")
- print("#########")
-
- with open(project.compose_file_name(), 'r') as stream:
- try:
- docker_compose_yml = yaml.safe_load(stream)
-
- for service_name in services:
- try:
- print(yaml.dump(
- {service_name: docker_compose_yml['services'][service_name]},
- default_flow_style=False, sort_keys=False
- ).strip())
- except KeyError:
- logging.error(f"Service '{service_name}' not found")
-
- return True
-
- except yaml.YAMLError as exc:
- logging.error(exc)
-
- return False
diff --git a/kiwi_scp/subcommands/up.py b/kiwi_scp/subcommands/up.py
deleted file mode 100644
index b369545..0000000
--- a/kiwi_scp/subcommands/up.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# local
-from ..subcommand import ServiceCommand
-
-
-class UpCommand(ServiceCommand):
- """kiwi up"""
-
- def __init__(self):
- super().__init__(
- 'up', num_projects='?', num_services='*',
- action="Bringing up",
- description="Bring up the whole instance, a project or service(s) inside a project"
- )
-
- def _run_instance(self, runner, args):
- if runner.run('conf-copy'):
- return super()._run_instance(runner, args)
-
- return False
-
- def _run_services(self, runner, args, project, services):
- if runner.run('net-up'):
- project.compose_run(['up', '-d', *services])
- return True
-
- return False
From 3ad9c5c9548da126868e32ac0ee68ab892b3030e Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Fri, 3 Dec 2021 15:16:53 +0100
Subject: [PATCH 100/135] commands module export
---
kiwi_scp/commands/__init__.py | 3 +++
kiwi_scp/scripts/kiwi.py | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/kiwi_scp/commands/__init__.py b/kiwi_scp/commands/__init__.py
index e69de29..ffeb421 100644
--- a/kiwi_scp/commands/__init__.py
+++ b/kiwi_scp/commands/__init__.py
@@ -0,0 +1,3 @@
+from .cli import KiwiCLI
+
+__all__ = ["KiwiCLI"]
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
index 3503270..f408986 100644
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -2,7 +2,7 @@ import logging
import click
-from kiwi_scp.commands.cli import KiwiCLI
+from kiwi_scp.commands import KiwiCLI
@click.option(
From 558726f2618c463e35d85ea62a3e34403be33159 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Wed, 19 Jan 2022 12:03:42 +0100
Subject: [PATCH 101/135] "kiwi update"
---
kiwi_scp/commands/cmd_update.py | 48 +++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 kiwi_scp/commands/cmd_update.py
diff --git a/kiwi_scp/commands/cmd_update.py b/kiwi_scp/commands/cmd_update.py
new file mode 100644
index 0000000..1330ca5
--- /dev/null
+++ b/kiwi_scp/commands/cmd_update.py
@@ -0,0 +1,48 @@
+from typing import List
+
+import click
+from click import get_current_context
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from ..instance import Instance
+from ..project import Project
+from ..services import Services
+
+
+@click.option(
+ "-f/-F",
+ "--force/--no-force",
+ help=f"skip confirmation",
+)
+@kiwi_command(
+ short_help="Update kiwi services",
+)
+class UpdateCommand(KiwiCommand):
+ """Update the whole instance, a project or service(s) inside a project"""
+
+ type = KiwiCommandType.SERVICES
+ enabled_only = True
+
+ @classmethod
+ def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
+ new_service_names: List[str], **kwargs) -> None:
+ if not services:
+ if not click.confirm(
+ "Did not find any of those services. \n"
+ f"Update the entire project {project.name} instead?",
+ default=True
+ ):
+ return
+
+ from .cmd_build import BuildCommand
+ from .cmd_down import DownCommand
+ from .cmd_pull import PullCommand
+ from .cmd_up import UpCommand
+
+ ctx = get_current_context()
+ ctx.forward(BuildCommand)
+ ctx.forward(PullCommand)
+ # TODO conf-copy
+ ctx.forward(DownCommand)
+ ctx.forward(UpCommand)
From 3862ea190197304cac4c6f5a71322a3888b15a93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Wed, 19 Jan 2022 15:35:33 +0100
Subject: [PATCH 102/135] get_command member find method
---
kiwi_scp/commands/cli.py | 11 +++++++++--
kiwi_scp/commands/cmd_enable.py | 2 +-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index f38b72e..88812d3 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -26,9 +26,16 @@ class KiwiCLI(click.MultiCommand):
except ImportError:
return
- for cmd_name in dir(cmd_module):
- member = getattr(cmd_module, cmd_name)
+ member_name = f"{cmd_name.capitalize()}Command"
+
+ if member_name in dir(cmd_module):
+ member = getattr(cmd_module, member_name)
+
if isinstance(member, click.Command):
return member
+ else:
+ raise Exception("Fail class")
+ else:
+ raise Exception("Fail member name")
diff --git a/kiwi_scp/commands/cmd_enable.py b/kiwi_scp/commands/cmd_enable.py
index 54d89be..def3017 100644
--- a/kiwi_scp/commands/cmd_enable.py
+++ b/kiwi_scp/commands/cmd_enable.py
@@ -13,7 +13,7 @@ from ..project import Project
help=f"skip confirmation",
)
@kiwi_command()
-class DisableCommand(KiwiCommand):
+class EnableCommand(KiwiCommand):
"""Enable project(s)"""
type = KiwiCommandType.PROJECTS
From cce894b9429e6c871b92c03dd585404d7ba5117c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Wed, 19 Jan 2022 15:36:41 +0100
Subject: [PATCH 103/135] update command import pattern
---
kiwi_scp/commands/cmd_update.py | 27 ++++++++++++++++++++++-----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/kiwi_scp/commands/cmd_update.py b/kiwi_scp/commands/cmd_update.py
index 1330ca5..6b18fe0 100644
--- a/kiwi_scp/commands/cmd_update.py
+++ b/kiwi_scp/commands/cmd_update.py
@@ -4,6 +4,10 @@ import click
from click import get_current_context
from .cmd import KiwiCommandType, KiwiCommand
+from .cmd_build import BuildCommand
+from .cmd_down import DownCommand
+from .cmd_pull import PullCommand
+from .cmd_up import UpCommand
from .decorators import kiwi_command
from ..instance import Instance
from ..project import Project
@@ -24,6 +28,20 @@ class UpdateCommand(KiwiCommand):
type = KiwiCommandType.SERVICES
enabled_only = True
+ @classmethod
+ def run_for_instance(cls, instance: Instance, force: bool = None) -> None:
+ if not force:
+ if not KiwiCommand.danger_confirm(
+ "This will update the entire instance at once.",
+ "",
+ "This may not be what you intended, because:",
+ " - Updates may take a long time",
+ " - Updates may break beloved functionality",
+ ):
+ return
+
+ super().run_for_instance(instance)
+
@classmethod
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
@@ -35,14 +53,13 @@ class UpdateCommand(KiwiCommand):
):
return
- from .cmd_build import BuildCommand
- from .cmd_down import DownCommand
- from .cmd_pull import PullCommand
- from .cmd_up import UpCommand
-
ctx = get_current_context()
+ assert isinstance(BuildCommand, click.Command)
ctx.forward(BuildCommand)
+ assert isinstance(PullCommand, click.Command)
ctx.forward(PullCommand)
# TODO conf-copy
+ assert isinstance(DownCommand, click.Command)
ctx.forward(DownCommand)
+ assert isinstance(UpCommand, click.Command)
ctx.forward(UpCommand)
From fc35e4525bbcdde757e1696d9d3790958e059406 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Wed, 19 Jan 2022 15:37:20 +0100
Subject: [PATCH 104/135] sdk location
---
.idea/runConfigurations/Test_Coverage.xml | 4 ++--
.idea/runConfigurations/Tests__Debuggable_.xml | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.idea/runConfigurations/Test_Coverage.xml b/.idea/runConfigurations/Test_Coverage.xml
index 6278cf1..89e6064 100644
--- a/.idea/runConfigurations/Test_Coverage.xml
+++ b/.idea/runConfigurations/Test_Coverage.xml
@@ -3,9 +3,9 @@
-
+
-
+
diff --git a/.idea/runConfigurations/Tests__Debuggable_.xml b/.idea/runConfigurations/Tests__Debuggable_.xml
index 0e8c1a9..e648d65 100644
--- a/.idea/runConfigurations/Tests__Debuggable_.xml
+++ b/.idea/runConfigurations/Tests__Debuggable_.xml
@@ -3,9 +3,9 @@
-
+
-
+
From 64138abb92e0c1bdebaef5894dc07fc7ba504640 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Wed, 19 Jan 2022 16:14:31 +0100
Subject: [PATCH 105/135] remove legacy
---
kiwi_scp/subcommands/purge.py | 45 ----------------------------------
kiwi_scp/subcommands/update.py | 37 ----------------------------
2 files changed, 82 deletions(-)
delete mode 100644 kiwi_scp/subcommands/purge.py
delete mode 100644 kiwi_scp/subcommands/update.py
diff --git a/kiwi_scp/subcommands/purge.py b/kiwi_scp/subcommands/purge.py
deleted file mode 100644
index 86cd372..0000000
--- a/kiwi_scp/subcommands/purge.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# system
-import logging
-import subprocess
-
-# local
-from ._hidden import _find_net
-from ..config import LoadedConfig
-from ..executable import Executable
-from ..misc import are_you_sure
-from ..subcommand import SubCommand
-
-
-class PurgeCommand(SubCommand):
- """kiwi purge"""
-
- def __init__(self):
- super().__init__(
- 'purge',
- action="Tearing down",
- description="Remove all running docker artifacts of this instance"
- )
-
- def _run_instance(self, runner, args):
- net_name = LoadedConfig.get()['network:name']
-
- if not _find_net(net_name):
- logging.info(f"Network '{net_name}' does not exist")
- return True
-
- try:
- if are_you_sure("This will bring down this instance's hub network!"):
- if runner.run('down'):
- Executable('docker').run([
- 'network', 'rm', net_name
- ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
- logging.info(f"Network '{net_name}' removed")
- else:
- return False
-
- except subprocess.CalledProcessError:
- logging.error(f"Error removing network '{net_name}'")
- return False
-
- return True
diff --git a/kiwi_scp/subcommands/update.py b/kiwi_scp/subcommands/update.py
deleted file mode 100644
index 35bc6dd..0000000
--- a/kiwi_scp/subcommands/update.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# local
-from ..misc import are_you_sure
-from ..subcommand import ServiceCommand
-
-
-class UpdateCommand(ServiceCommand):
- """kiwi update"""
-
- def __init__(self):
- super().__init__(
- 'update', num_projects='?', num_services='*',
- action="Updating",
- description="Update the whole instance, a project or service(s) inside a project"
- )
-
- def _run_instance(self, runner, args):
- if are_you_sure([
- "This will update the entire instance at once.",
- "",
- "This is probably not what you intended, because:",
- " - Updates may take a long time",
- " - Updates may break beloved functionality",
- ]):
- return super()._run_instance(runner, args)
-
- return False
-
- def _run_services(self, runner, args, project, services):
- result = True
-
- result &= runner.run('build')
- result &= runner.run('pull')
- result &= runner.run('conf-copy')
- result &= runner.run('down')
- result &= runner.run('up')
-
- return result
From e896e8a06c1d33dc891a8b792f32a5dd19895e00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Wed, 19 Jan 2022 16:14:36 +0100
Subject: [PATCH 106/135] typo
---
kiwi_scp/commands/cmd.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py
index ed3fa9f..1da8cc4 100644
--- a/kiwi_scp/commands/cmd.py
+++ b/kiwi_scp/commands/cmd.py
@@ -177,4 +177,4 @@ class KiwiCommand:
@classmethod
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
- raise Exception
\ No newline at end of file
+ raise Exception
From cd11167fda4d888b3fd275a5a62d80bf98247137 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 20 Jan 2022 10:20:57 +0100
Subject: [PATCH 107/135] Instance.save_config
---
kiwi_scp/commands/cmd_init.py | 4 +---
kiwi_scp/instance.py | 5 +++++
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/commands/cmd_init.py b/kiwi_scp/commands/cmd_init.py
index a93d565..a5ae22d 100644
--- a/kiwi_scp/commands/cmd_init.py
+++ b/kiwi_scp/commands/cmd_init.py
@@ -68,6 +68,4 @@ class InitCommand(KiwiCommand):
os.mkdir(instance.directory)
# write out the new kiwi.yml
- cfg = KiwiConfig.parse_obj(kiwi_dict)
- with open(instance.directory.joinpath(KIWI_CONF_NAME), "w") as file:
- cfg.dump_kiwi_yml(file)
+ instance.save_config(KiwiConfig.parse_obj(kiwi_dict))
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 30389e7..9f41977 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -3,6 +3,7 @@ from typing import Generator, Dict, Sequence
import attr
+from ._constants import KIWI_CONF_NAME
from .config import KiwiConfig
from .project import Project
@@ -17,6 +18,10 @@ class Instance:
return KiwiConfig.from_directory(self.directory)
+ def save_config(self, config: KiwiConfig) -> None:
+ with open(self.directory.joinpath(KIWI_CONF_NAME), "w") as file:
+ config.dump_kiwi_yml(file)
+
@property
def projects(self) -> Generator[Project, None, None]:
for project in self.config.projects:
From ed57e09d24e7cb06f10d39d9146931cbabb49b70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 20 Jan 2022 10:21:05 +0100
Subject: [PATCH 108/135] "kiwi new"
---
kiwi_scp/commands/cmd_new.py | 39 ++++++++++++++++++++
kiwi_scp/data/etc/docker-compose_default.yml | 4 ++
kiwi_scp/subcommands/new.py | 30 ---------------
3 files changed, 43 insertions(+), 30 deletions(-)
create mode 100644 kiwi_scp/commands/cmd_new.py
delete mode 100644 kiwi_scp/subcommands/new.py
diff --git a/kiwi_scp/commands/cmd_new.py b/kiwi_scp/commands/cmd_new.py
new file mode 100644
index 0000000..8fa2a35
--- /dev/null
+++ b/kiwi_scp/commands/cmd_new.py
@@ -0,0 +1,39 @@
+import os
+import shutil
+
+import click
+
+from .cmd import KiwiCommandType, KiwiCommand
+from .decorators import kiwi_command
+from .._constants import DEFAULT_DOCKER_COMPOSE_NAME, COMPOSE_FILE_NAME
+from ..config import ProjectConfig
+from ..instance import Instance
+from ..project import Project
+
+
+@kiwi_command()
+class NewCommand(KiwiCommand):
+ """Create new empty project(s) in this instance"""
+
+ type = KiwiCommandType.PROJECTS
+
+ @classmethod
+ def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
+ KiwiCommand.print_error(f"Project {project.name} already exists!")
+
+ @classmethod
+ def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
+ try:
+ os.mkdir(project_name)
+ instance.config.projects.append(ProjectConfig(
+ name=project_name,
+ enabled=False,
+ ))
+ shutil.copy(
+ DEFAULT_DOCKER_COMPOSE_NAME,
+ instance.directory.joinpath(project_name).joinpath(COMPOSE_FILE_NAME)
+ )
+ instance.save_config(instance.config)
+
+ except FileExistsError:
+ KiwiCommand.print_error(f"Project directory {project_name} already exists!")
diff --git a/kiwi_scp/data/etc/docker-compose_default.yml b/kiwi_scp/data/etc/docker-compose_default.yml
index d3cbd6d..cc6476a 100644
--- a/kiwi_scp/data/etc/docker-compose_default.yml
+++ b/kiwi_scp/data/etc/docker-compose_default.yml
@@ -10,6 +10,10 @@ networks:
name: ${KIWI_HUB_NAME}
services:
+ ######################
+ # START EDITING HERE #
+ ######################
+
# an example service
something:
# uses an image
diff --git a/kiwi_scp/subcommands/new.py b/kiwi_scp/subcommands/new.py
deleted file mode 100644
index 5fc5243..0000000
--- a/kiwi_scp/subcommands/new.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# system
-import logging
-import os
-import shutil
-
-# local
-from .._constants import DEFAULT_DOCKER_COMPOSE_NAME
-from ..subcommand import ProjectCommand
-
-
-class NewCommand(ProjectCommand):
- """kiwi new"""
-
- def __init__(self):
- super().__init__(
- 'new', num_projects='+',
- action="Creating",
- description="Create new empty project(s) in this instance"
- )
-
- def _run_project(self, runner, args, project):
- if project.exists():
- logging.error(f"Project '{project.get_name()}' exists in this instance!")
- return False
-
- else:
- os.mkdir(project.disabled_dir_name())
- shutil.copy(DEFAULT_DOCKER_COMPOSE_NAME, project.compose_file_name())
- logging.debug(f"Project '{project.get_name()}' created")
- return True
From 80181505a2e3c166c43e033bb06b0a423cb9da3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 20 Jan 2022 10:55:47 +0100
Subject: [PATCH 109/135] kiwi_help display
---
kiwi_scp/commands/cli.py | 33 ++++++++++++++++++++++++++++++
kiwi_scp/data/etc/command_help.txt | 22 --------------------
kiwi_scp/data/etc/kiwi_help.txt | 9 --------
kiwi_scp/scripts/kiwi.py | 10 ++++++++-
4 files changed, 42 insertions(+), 32 deletions(-)
delete mode 100644 kiwi_scp/data/etc/command_help.txt
delete mode 100644 kiwi_scp/data/etc/kiwi_help.txt
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 88812d3..74c947d 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -1,5 +1,6 @@
import importlib
import os
+from gettext import gettext as _
from typing import List, Optional
import click
@@ -39,3 +40,35 @@ class KiwiCLI(click.MultiCommand):
else:
raise Exception("Fail member name")
+
+ def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
+ commands = {
+ "Operation": [
+ "up", "down", "restart", "update",
+ ],
+ "Instance Management": [
+ "init", "list",
+ ],
+ "Project and Service Management": [
+ "new", "enable", "disable", "logs", "shell", "cmd",
+ ],
+ "Image Handling": [
+ "build", "pull", "push",
+ ],
+ }
+
+ # allow for 3 times the default spacing
+ cmd_names = set(self.list_commands(ctx))
+ limit = formatter.width - 6 - max(len(cmd_name) for cmd_name in cmd_names)
+
+ for purpose, cmd_list in commands.items():
+ with formatter.section(_(f"Commands for {purpose}")):
+ formatter.write_dl([
+ (cmd_name, self.get_command(ctx, cmd_name).get_short_help_str(limit))
+ for cmd_name in cmd_list
+ ])
+
+ cmd_names -= set(cmd_list)
+
+ if len(cmd_names) > 0:
+ raise Exception(f"Some commands were not registered in a group above: {cmd_names}")
diff --git a/kiwi_scp/data/etc/command_help.txt b/kiwi_scp/data/etc/command_help.txt
deleted file mode 100644
index 33c0376..0000000
--- a/kiwi_scp/data/etc/command_help.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-Commands for Operation:
- up Bring up the whole instance, a project or service(s) inside a project
- down Bring down the whole instance, a project or service(s) inside a project
- update Update the whole instance, a project or service(s) inside a project
- restart Restart the whole instance, a project or service(s) inside a project
-
-Commands for Instance Management:
- init Initialize or reconfigure kiwi-scp instance
- show Show projects in this instance, services inside a project or service(s) inside a project
- cmd Run raw docker-compose command in a project
-
-Commands for Project and Service Management:
- new Create new empty project(s) in this instance
- enable Enable project(s) in this instance
- disable Disable project(s) in this instance
- logs Show logs of a project or service(s) inside a project
- shell Spawn shell inside a service inside a project
-
-Commands for Image Handling:
- build Build images for the whole instance, a project or service(s) inside a project
- pull Pull images for the whole instance, a project or service(s) inside a project
- push Push images for the whole instance, a project or service(s) inside a project
\ No newline at end of file
diff --git a/kiwi_scp/data/etc/kiwi_help.txt b/kiwi_scp/data/etc/kiwi_help.txt
deleted file mode 100644
index 08d0224..0000000
--- a/kiwi_scp/data/etc/kiwi_help.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-kiwi is the simple tool for managing container servers.
-
-Features:
- - Group services into projects using their own docker-compose.yml
- - Bind to the local file system by using ${TARGETDIR} as volume in docker-compose.yml
- - Add instance-global config files by using ${CONFDIR} as volume in docker-compose.yml
- - Add instance-global custom values inside docker-compose.yml using config:runtime:env
- - Build service-specific, private docker images from Dockerfiles
- - Check full instances into any version control system
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
index f408986..4b00c07 100644
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -12,7 +12,15 @@ from kiwi_scp.commands import KiwiCLI
)
@click.command(cls=KiwiCLI)
def main(verbose: int) -> None:
- """kiwi is the simple tool for managing container servers."""
+ """kiwi is the simple tool for managing container servers.
+
+ \b
+ - Manage full instances using just your favorite version control system
+ - Group services into projects, each with their own docker-compose.yml
+ - Build service-specific, private docker images from Dockerfiles
+ - Make use of the local file system by referring to ${TARGETDIR}, ${TARGETROOT} and ${CONFDIR} in compose files
+ - Create your own instance-global variables for compose files using the kiwi.yml "environment" section
+ """
if verbose >= 2:
log_level = logging.DEBUG
From eb50abcf7dcaf8cb0701f5b6306b1642a560fb9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 20 Jan 2022 11:37:50 +0100
Subject: [PATCH 110/135] remove legacy
---
kiwi_scp/_constants.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 3bccffd..275fb52 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -28,10 +28,7 @@ COMPOSE_FILE_NAME = "docker-compose.yml"
# text files inside kiwi-scp "src" directory
HEADER_KIWI_CONF_NAME = f"{KIWI_ROOT}/data/etc/kiwi_header.yml"
DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/data/etc/kiwi_default.yml"
-VERSION_TAG_NAME = f"{KIWI_ROOT}/data/etc/version_tag"
DEFAULT_DOCKER_COMPOSE_NAME = f"{KIWI_ROOT}/data/etc/docker-compose_default.yml"
-KIWI_HELP_TEXT_NAME = f"{KIWI_ROOT}/data/etc/kiwi_help.txt"
-COMMAND_HELP_TEXT_NAME = f"{KIWI_ROOT}/data/etc/command_help.txt"
# special config directory in projects
CONF_DIRECTORY_NAME = 'conf'
From 29bc413f8e69dae08b77a88c2f6a8f0c5accac81 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Mon, 24 Jan 2022 16:53:19 +0100
Subject: [PATCH 111/135] RESERVED_PROJECT_NAMES
---
kiwi_scp/_constants.py | 4 ++++
kiwi_scp/commands/cmd_new.py | 6 +++++-
kiwi_scp/config.py | 12 +++++++++++-
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 275fb52..e1e54cc 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -1,5 +1,9 @@
import os
+RESERVED_PROJECT_NAMES = [
+ 'config'
+]
+
#############
# REGEX PARTS
diff --git a/kiwi_scp/commands/cmd_new.py b/kiwi_scp/commands/cmd_new.py
index 8fa2a35..f554661 100644
--- a/kiwi_scp/commands/cmd_new.py
+++ b/kiwi_scp/commands/cmd_new.py
@@ -5,7 +5,7 @@ import click
from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
-from .._constants import DEFAULT_DOCKER_COMPOSE_NAME, COMPOSE_FILE_NAME
+from .._constants import DEFAULT_DOCKER_COMPOSE_NAME, COMPOSE_FILE_NAME, RESERVED_PROJECT_NAMES
from ..config import ProjectConfig
from ..instance import Instance
from ..project import Project
@@ -23,6 +23,10 @@ class NewCommand(KiwiCommand):
@classmethod
def run_for_new_project(cls, instance: Instance, project_name: str, **kwargs) -> None:
+ if project_name in RESERVED_PROJECT_NAMES:
+ KiwiCommand.print_error(f"Project name '{project_name}' is reserved!")
+ return
+
try:
os.mkdir(project_name)
instance.config.projects.append(ProjectConfig(
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 20b01f3..3280f9a 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -5,7 +5,7 @@ from typing import Optional, Dict, List, Any, TextIO, Tuple
from pydantic import BaseModel, constr, root_validator, validator
-from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME
+from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME, RESERVED_PROJECT_NAMES
from .yaml import YAML
@@ -53,6 +53,16 @@ class ProjectConfig(BaseModel):
result["override_storage"] = self.override_storage.kiwi_dict
return result
+ @validator("name")
+ @classmethod
+ def check_project(cls, value: str) -> str:
+ """check if project name is allowed"""
+
+ if value in RESERVED_PROJECT_NAMES:
+ raise ValueError(f"Project name '{value}' is reserved!")
+
+ return value
+
@validator("override_storage", pre=True)
@classmethod
def unify_storage(cls, value) -> Dict[str, Any]:
From f926409a3f9c85dda74bdfb8aa0d5fcdc099e793 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Mon, 24 Jan 2022 17:39:55 +0100
Subject: [PATCH 112/135] rename CONFDIR -> CONFIGDIR
---
README.md | 4 ++--
example/{hello-world.project/conf => config}/html/index.html | 0
example/hello-world.project/docker-compose.yml | 4 ++--
kiwi_scp/_constants.py | 5 +++--
kiwi_scp/project.py | 2 +-
kiwi_scp/scripts/kiwi.py | 2 +-
kiwi_scp/service.py | 4 ++--
tests/test_service.py | 4 ++--
8 files changed, 13 insertions(+), 12 deletions(-)
rename example/{hello-world.project/conf => config}/html/index.html (100%)
diff --git a/README.md b/README.md
index 2b4828a..a4da112 100644
--- a/README.md
+++ b/README.md
@@ -112,11 +112,11 @@ networks:
```
-#### The `CONFDIR`
+#### The `CONFIGDIR`
Sometimes, it's convenient to re-use configuration files across projects.
For this use case, create a directory named `conf` in a project.
-Those will all be combined into a directory available as **${CONFDIR}** in your `docker-compose.yml` files.
+Those will all be combined into a directory available as **${CONFIGDIR}** in your `docker-compose.yml` files.
#### `kiwi.yml` options
diff --git a/example/hello-world.project/conf/html/index.html b/example/config/html/index.html
similarity index 100%
rename from example/hello-world.project/conf/html/index.html
rename to example/config/html/index.html
diff --git a/example/hello-world.project/docker-compose.yml b/example/hello-world.project/docker-compose.yml
index cdb4328..2cc054d 100644
--- a/example/hello-world.project/docker-compose.yml
+++ b/example/hello-world.project/docker-compose.yml
@@ -46,10 +46,10 @@ services:
- "8081:8080"
another-web:
- # Another webserver just to show off the ${CONFDIR} variable
+ # Another webserver just to show off the ${CONFIGDIR} variable
image: nginx:stable-alpine
restart: unless-stopped
ports:
- "8082:80"
volumes:
- - "${CONFDIR}/html/index.html:/usr/share/nginx/html/index.html:ro"
+ - "${CONFIGDIR}/html/index.html:/usr/share/nginx/html/index.html:ro"
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index e1e54cc..e5db944 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -34,8 +34,9 @@ HEADER_KIWI_CONF_NAME = f"{KIWI_ROOT}/data/etc/kiwi_header.yml"
DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/data/etc/kiwi_default.yml"
DEFAULT_DOCKER_COMPOSE_NAME = f"{KIWI_ROOT}/data/etc/docker-compose_default.yml"
-# special config directory in projects
-CONF_DIRECTORY_NAME = 'conf'
+# special config directory
+CONF_DIRECTORY_NAME = 'config'
+
# location for auxiliary Dockerfiles
IMAGES_DIRECTORY_NAME = f"{KIWI_ROOT}/data/images"
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index 47e0699..5b9515d 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -49,7 +49,7 @@ class Project:
"COMPOSE_PROJECT_NAME": project_name,
"KIWI_HUB_NAME": kiwi_hub_name,
"TARGETROOT": str(target_root_dir),
- "CONFDIR": str(conf_dir),
+ "CONFIGDIR": str(conf_dir),
"TARGETDIR": str(target_dir),
},
}
diff --git a/kiwi_scp/scripts/kiwi.py b/kiwi_scp/scripts/kiwi.py
index 4b00c07..6f96b5d 100644
--- a/kiwi_scp/scripts/kiwi.py
+++ b/kiwi_scp/scripts/kiwi.py
@@ -18,7 +18,7 @@ def main(verbose: int) -> None:
- Manage full instances using just your favorite version control system
- Group services into projects, each with their own docker-compose.yml
- Build service-specific, private docker images from Dockerfiles
- - Make use of the local file system by referring to ${TARGETDIR}, ${TARGETROOT} and ${CONFDIR} in compose files
+ - Make use of the local file system by referring to ${TARGETDIR}, ${TARGETROOT} and ${CONFIGDIR} in compose files
- Create your own instance-global variables for compose files using the kiwi.yml "environment" section
"""
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index c85652f..243c2a6 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -22,7 +22,7 @@ class Service:
content: CommentedMap = attr.ib()
parent: "Project" = attr.ib()
- _RE_CONFDIR = re.compile(r"^\s*\$(?:CONFDIR|{CONFDIR})/+(.*)$", flags=re.UNICODE)
+ _RE_CONFIGDIR = re.compile(r"^\s*\$(?:CONFIGDIR|{CONFIGDIR})/+(.*)$", flags=re.UNICODE)
@property
def configs(self) -> Generator[Path, None, None]:
@@ -31,7 +31,7 @@ class Service:
for volume in self.content["volumes"]:
host_part = volume.split(":")[0]
- cd_match = Service._RE_CONFDIR.match(host_part)
+ cd_match = Service._RE_CONFIGDIR.match(host_part)
if cd_match:
yield Path(cd_match.group(1))
diff --git a/tests/test_service.py b/tests/test_service.py
index 4024bdd..a5fa324 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -51,8 +51,8 @@ class TestDefault:
content=CommentedMap({
"image": "repo/image:tag",
"volumes": [
- "${CONFDIR}/some/config:/path/to/some/config",
- "$CONFDIR/other/config:/path/to/other/config",
+ "${CONFIGDIR}/some/config:/path/to/some/config",
+ "CONFIGDIR/other/config:/path/to/other/config",
]
}),
parent=None,
From 9922bcd288918fb41133a7b244c633cb80cdfe19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Mon, 24 Jan 2022 18:06:11 +0100
Subject: [PATCH 113/135] format
---
kiwi_scp/services.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/kiwi_scp/services.py b/kiwi_scp/services.py
index df8187b..94c6771 100644
--- a/kiwi_scp/services.py
+++ b/kiwi_scp/services.py
@@ -2,8 +2,8 @@ from typing import List, Generator
import attr
-from kiwi_scp.service import Service
-from kiwi_scp.yaml import YAML
+from .service import Service
+from .yaml import YAML
@attr.s
@@ -33,4 +33,4 @@ class Services:
service
for service in self.content
if service.name in service_names
- ])
\ No newline at end of file
+ ])
From 3c1f1e74cb2f908d372fd425b81c81ec58ee0f1e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Mon, 24 Jan 2022 18:07:19 +0100
Subject: [PATCH 114/135] WIP: Services.copy_configs
---
kiwi_scp/commands/cmd_update.py | 3 +++
kiwi_scp/services.py | 11 +++++++++++
2 files changed, 14 insertions(+)
diff --git a/kiwi_scp/commands/cmd_update.py b/kiwi_scp/commands/cmd_update.py
index 6b18fe0..0d0e0af 100644
--- a/kiwi_scp/commands/cmd_update.py
+++ b/kiwi_scp/commands/cmd_update.py
@@ -53,6 +53,9 @@ class UpdateCommand(KiwiCommand):
):
return
+ # services.copy_configs()
+ # return
+
ctx = get_current_context()
assert isinstance(BuildCommand, click.Command)
ctx.forward(BuildCommand)
diff --git a/kiwi_scp/services.py b/kiwi_scp/services.py
index 94c6771..0d6759b 100644
--- a/kiwi_scp/services.py
+++ b/kiwi_scp/services.py
@@ -21,6 +21,17 @@ class Services:
def __bool__(self) -> bool:
return bool(self.content)
+ # def copy_configs(self):
+ # configs = (
+ # config
+ # for service in self.content
+ # for config in service.configs
+ # )
+ #
+ # print(list(configs))
+ #
+ # # Rootkit("rsync").
+
@property
def names(self) -> Generator[str, None, None]:
return (
From b4317d50475b94563452ebd3f8edceac6608cbe0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 15:22:41 +0100
Subject: [PATCH 115/135] typo
---
tests/test_service.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_service.py b/tests/test_service.py
index a5fa324..cf82dfe 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -52,7 +52,7 @@ class TestDefault:
"image": "repo/image:tag",
"volumes": [
"${CONFIGDIR}/some/config:/path/to/some/config",
- "CONFIGDIR/other/config:/path/to/other/config",
+ "$CONFIGDIR/other/config:/path/to/other/config",
]
}),
parent=None,
From cbe4e422e06ad1e6cdfb50e61f8c00a9227b0e74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 15:26:10 +0100
Subject: [PATCH 116/135] renamed generic "parent" props
---
kiwi_scp/instance.py | 2 +-
kiwi_scp/project.py | 12 ++++++------
kiwi_scp/service.py | 9 +++++++--
kiwi_scp/services.py | 35 +++++++++++++++++++++++++----------
tests/test_project.py | 4 ++--
tests/test_service.py | 8 ++++----
tests/test_services.py | 2 +-
7 files changed, 46 insertions(+), 26 deletions(-)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 9f41977..6799280 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -27,7 +27,7 @@ class Instance:
for project in self.config.projects:
yield Project(
directory=self.directory.joinpath(project.name),
- parent=self,
+ parent_instance=self,
)
def get_projects(self, project_names: Sequence[str]) -> Dict[str, Project]:
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index 5b9515d..5f7bca9 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
@attr.s
class Project:
directory: Path = attr.ib()
- parent: "Instance" = attr.ib()
+ parent_instance: "Instance" = attr.ib()
@staticmethod
@functools.lru_cache(maxsize=10)
@@ -32,14 +32,14 @@ class Project:
@property
def config(self) -> Optional[ProjectConfig]:
- return self.parent.config.get_project_config(self.name)
+ return self.parent_instance.config.get_project_config(self.name)
@property
def process_kwargs(self) -> Dict[str, Any]:
directory: Path = self.directory
project_name: str = self.name
- kiwi_hub_name: str = self.parent.config.network.name
- target_root_dir: Path = self.parent.config.storage.directory
+ kiwi_hub_name: str = self.parent_instance.config.network.name
+ target_root_dir: Path = self.parent_instance.config.storage.directory
conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
target_dir: Path = target_root_dir.joinpath(project_name)
@@ -54,7 +54,7 @@ class Project:
},
}
- result["env"].update(self.parent.config.environment)
+ result["env"].update(self.parent_instance.config.environment)
return result
@@ -66,6 +66,6 @@ class Project:
Service(
name=name,
content=content,
- parent=self,
+ parent_project=self,
) for name, content in yml["services"].items()
])
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index 243c2a6..2631f34 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -11,6 +11,7 @@ from ruamel.yaml import CommentedMap
from .executable import COMPOSE_EXE
if TYPE_CHECKING:
+ from .instance import Instance
from .project import Project
_logger = logging.getLogger(__name__)
@@ -20,10 +21,14 @@ _logger = logging.getLogger(__name__)
class Service:
name: str = attr.ib()
content: CommentedMap = attr.ib()
- parent: "Project" = attr.ib()
+ parent_project: "Project" = attr.ib()
_RE_CONFIGDIR = re.compile(r"^\s*\$(?:CONFIGDIR|{CONFIGDIR})/+(.*)$", flags=re.UNICODE)
+ @property
+ def parent_instance(self) -> "Instance":
+ return self.parent_project.parent_instance
+
@property
def configs(self) -> Generator[Path, None, None]:
if "volumes" not in self.content:
@@ -42,7 +47,7 @@ class Service:
COMPOSE_EXE.run(
["exec", "-T", self.name, "/bin/sh", "-c", f"command -v {exe_name}"],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
- **self.parent.process_kwargs,
+ **self.parent_project.process_kwargs,
)
return True
diff --git a/kiwi_scp/services.py b/kiwi_scp/services.py
index 0d6759b..7cacab5 100644
--- a/kiwi_scp/services.py
+++ b/kiwi_scp/services.py
@@ -1,14 +1,18 @@
-from typing import List, Generator
+from pathlib import Path
+from typing import List, Generator, Optional, TYPE_CHECKING
import attr
-from .service import Service
from .yaml import YAML
+if TYPE_CHECKING:
+ from .instance import Instance
+ from .service import Service
+
@attr.s
class Services:
- content: List[Service] = attr.ib()
+ content: List["Service"] = attr.ib()
def __str__(self) -> str:
return YAML().dump({
@@ -21,14 +25,25 @@ class Services:
def __bool__(self) -> bool:
return bool(self.content)
- # def copy_configs(self):
- # configs = (
- # config
- # for service in self.content
- # for config in service.configs
- # )
+ @property
+ def parent_instance(self) -> Optional["Instance"]:
+ if not self:
+ return
+
+ return self.content[0].parent_instance
+
+ @property
+ def configs(self) -> Generator[Path, None, None]:
+ for service in self.content:
+ yield from service.configs
+
+ # def copy_configs(self) -> None:
+ # instance = self.parent_instance
#
- # print(list(configs))
+ # if instance is None:
+ # return
+ #
+ # print(list(self.configs))
#
# # Rootkit("rsync").
diff --git a/tests/test_project.py b/tests/test_project.py
index 6247950..79531ba 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -13,7 +13,7 @@ class TestDefault:
def test_example(self):
p = Project(
directory=Path("example/hello-world.project"),
- parent=None,
+ parent_instance=None,
)
ss = p.services
@@ -31,7 +31,7 @@ class TestDefault:
def test_empty(self):
p = Project(
directory=Path("nonexistent"),
- parent=None,
+ parent_instance=None,
)
with pytest.raises(FileNotFoundError) as exc_info:
diff --git a/tests/test_service.py b/tests/test_service.py
index cf82dfe..064edad 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -10,7 +10,7 @@ class TestDefault:
s = Service(
name="s",
content=CommentedMap(),
- parent=None,
+ parent_project=None,
)
assert s.name == "s"
@@ -22,7 +22,7 @@ class TestDefault:
content=CommentedMap({
"image": "repo/image:tag",
}),
- parent=None,
+ parent_project=None,
)
assert s.name == "s"
@@ -39,7 +39,7 @@ class TestDefault:
"$TARGETDIR/other/dir:/path/to/other/mountpoint",
]
}),
- parent=None,
+ parent_project=None,
)
assert s.name == "s"
@@ -55,7 +55,7 @@ class TestDefault:
"$CONFIGDIR/other/config:/path/to/other/config",
]
}),
- parent=None,
+ parent_project=None,
)
assert s.name == "s"
diff --git a/tests/test_services.py b/tests/test_services.py
index 151d242..686d6e0 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -9,7 +9,7 @@ class TestServices:
s = Service(
name="s",
content=CommentedMap(),
- parent=None,
+ parent_project=None,
)
ss = Services([s])
From a35ba188b0963979043ab770c8f58307fb75dcc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 17:57:15 +0100
Subject: [PATCH 117/135] conf-copy implemented
---
kiwi_scp/_constants.py | 9 +++---
kiwi_scp/commands/cmd_update.py | 5 +--
kiwi_scp/instance.py | 10 +++++-
kiwi_scp/project.py | 4 +--
kiwi_scp/rootkit.py | 50 +++++++++++++++++++----------
kiwi_scp/service.py | 4 ---
kiwi_scp/services.py | 57 +++++++++++++++++++++++++--------
7 files changed, 93 insertions(+), 46 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index e5db944..843e901 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -1,9 +1,5 @@
import os
-RESERVED_PROJECT_NAMES = [
- 'config'
-]
-
#############
# REGEX PARTS
@@ -40,6 +36,11 @@ CONF_DIRECTORY_NAME = 'config'
# location for auxiliary Dockerfiles
IMAGES_DIRECTORY_NAME = f"{KIWI_ROOT}/data/images"
+# prohibited project names
+RESERVED_PROJECT_NAMES = [
+ CONFIG_DIRECTORY_NAME,
+]
+
####################
# DOCKER IMAGE NAMES
diff --git a/kiwi_scp/commands/cmd_update.py b/kiwi_scp/commands/cmd_update.py
index 0d0e0af..630e8dc 100644
--- a/kiwi_scp/commands/cmd_update.py
+++ b/kiwi_scp/commands/cmd_update.py
@@ -53,15 +53,12 @@ class UpdateCommand(KiwiCommand):
):
return
- # services.copy_configs()
- # return
-
ctx = get_current_context()
assert isinstance(BuildCommand, click.Command)
ctx.forward(BuildCommand)
assert isinstance(PullCommand, click.Command)
ctx.forward(PullCommand)
- # TODO conf-copy
+ services.copy_configs()
assert isinstance(DownCommand, click.Command)
ctx.forward(DownCommand)
assert isinstance(UpCommand, click.Command)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 6799280..3ce304b 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -3,7 +3,7 @@ from typing import Generator, Dict, Sequence
import attr
-from ._constants import KIWI_CONF_NAME
+from ._constants import KIWI_CONF_NAME, CONFIG_DIRECTORY_NAME
from .config import KiwiConfig
from .project import Project
@@ -22,6 +22,14 @@ class Instance:
with open(self.directory.joinpath(KIWI_CONF_NAME), "w") as file:
config.dump_kiwi_yml(file)
+ @property
+ def config_directory(self):
+ return self.directory.joinpath(CONFIG_DIRECTORY_NAME)
+
+ @property
+ def storage_config_directory(self):
+ return self.config.storage.directory.joinpath(CONFIG_DIRECTORY_NAME)
+
@property
def projects(self) -> Generator[Project, None, None]:
for project in self.config.projects:
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index 5f7bca9..d816a7a 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional, Dict, Any
import attr
from ruamel.yaml import CommentedMap
-from ._constants import COMPOSE_FILE_NAME, CONF_DIRECTORY_NAME
+from ._constants import COMPOSE_FILE_NAME, CONFIG_DIRECTORY_NAME
from .config import ProjectConfig
from .service import Service
from .services import Services
@@ -40,7 +40,7 @@ class Project:
project_name: str = self.name
kiwi_hub_name: str = self.parent_instance.config.network.name
target_root_dir: Path = self.parent_instance.config.storage.directory
- conf_dir: Path = target_root_dir.joinpath(CONF_DIRECTORY_NAME)
+ conf_dir: Path = target_root_dir.joinpath(CONFIG_DIRECTORY_NAME)
target_dir: Path = target_root_dir.joinpath(project_name)
result: Dict[str, Any] = {
diff --git a/kiwi_scp/rootkit.py b/kiwi_scp/rootkit.py
index c1a42da..112b078 100644
--- a/kiwi_scp/rootkit.py
+++ b/kiwi_scp/rootkit.py
@@ -2,7 +2,7 @@ import functools
import logging
import subprocess
from pathlib import Path
-from typing import Optional, TypeVar, Union, List
+from typing import Optional, TypeVar, Union, Sequence, Any
import attr
@@ -11,18 +11,7 @@ from .executable import DOCKER_EXE
_logger = logging.getLogger(__name__)
-PSL = TypeVar("PSL", Union[Path, str], List[Union[Path, str]])
-
-
-def prefix_path(path: PSL, prefix: Path = Path("/mnt")) -> PSL:
- if isinstance(path, Path):
- return prefix.joinpath(path.absolute())
-
- if isinstance(path, str):
- return prefix_path(Path(path), prefix)
-
- elif isinstance(path, list):
- return [prefix_path(prefix, p) for p in path]
+ROOTKIT_PREFIX = Path("/mnt")
@attr.s
@@ -68,11 +57,38 @@ class Rootkit:
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def run(self, process_args, **kwargs) -> Optional[subprocess.CompletedProcess]:
+ any_sequence = TypeVar("any_sequence", Union[str, Path, Any], Sequence[Union[str, Path, Any]])
+
+ def parse_args(argument: any_sequence) -> any_sequence:
+ if isinstance(argument, str):
+ return argument
+
+ elif isinstance(argument, Path):
+ if argument.is_absolute():
+ argument = argument.relative_to("/")
+
+ return str(ROOTKIT_PREFIX.joinpath(argument))
+
+ elif not isinstance(argument, Sequence):
+ return str(argument)
+
+ else:
+ parsed = [parse_args(path) for path in argument]
+
+ flat = []
+ for item in parsed:
+ if not isinstance(item, list):
+ flat.append(item)
+ else:
+ flat.extend(item)
+
+ return flat
+
self.__build_image()
return DOCKER_EXE.run([
- 'run', '--rm',
- '-v', '/:/mnt',
- '-u', 'root',
+ "run", "--rm",
+ "-v", f"/:{ROOTKIT_PREFIX!s}",
+ "-u", "root",
Rootkit.__image_name(self.image_tag),
- *process_args
+ *parse_args(process_args)
], **kwargs)
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index 2631f34..c85b82f 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -25,10 +25,6 @@ class Service:
_RE_CONFIGDIR = re.compile(r"^\s*\$(?:CONFIGDIR|{CONFIGDIR})/+(.*)$", flags=re.UNICODE)
- @property
- def parent_instance(self) -> "Instance":
- return self.parent_project.parent_instance
-
@property
def configs(self) -> Generator[Path, None, None]:
if "volumes" not in self.content:
diff --git a/kiwi_scp/services.py b/kiwi_scp/services.py
index 7cacab5..f1b3455 100644
--- a/kiwi_scp/services.py
+++ b/kiwi_scp/services.py
@@ -1,12 +1,14 @@
+import subprocess
from pathlib import Path
-from typing import List, Generator, Optional, TYPE_CHECKING
+from typing import List, Generator, Optional, TYPE_CHECKING, TypeVar, Union
import attr
+from .rootkit import Rootkit
from .yaml import YAML
if TYPE_CHECKING:
- from .instance import Instance
+ from .project import Project
from .service import Service
@@ -19,33 +21,60 @@ class Services:
"services": {
service.name: service.content
for service in self.content
- }
+ },
+ "configs": [
+ str(config)
+ for config in self.configs
+ ],
}).strip()
def __bool__(self) -> bool:
return bool(self.content)
@property
- def parent_instance(self) -> Optional["Instance"]:
+ def parent_project(self) -> Optional["Project"]:
if not self:
return
- return self.content[0].parent_instance
+ return self.content[0].parent_project
@property
def configs(self) -> Generator[Path, None, None]:
for service in self.content:
yield from service.configs
- # def copy_configs(self) -> None:
- # instance = self.parent_instance
- #
- # if instance is None:
- # return
- #
- # print(list(self.configs))
- #
- # # Rootkit("rsync").
+ def copy_configs(self) -> None:
+ path_str_list = TypeVar("path_str_list", Union[Path, str], List[Union[Path, str]])
+
+ def prefix_path(path: path_str_list, prefix: Path) -> path_str_list:
+ if isinstance(path, Path):
+ return prefix.absolute().joinpath(path)
+
+ elif isinstance(path, str):
+ return prefix_path(Path(path), prefix)
+
+ elif isinstance(path, list):
+ return [prefix_path(p, prefix) for p in path]
+
+ project = self.parent_project
+
+ if project is None:
+ return
+
+ instance = project.parent_instance
+ cfgs = list(self.configs)
+
+ local_cfgs = prefix_path(cfgs, instance.config_directory)
+ storage_cfgs = prefix_path(cfgs, instance.storage_config_directory)
+ storage_dirs = [path.parent for path in storage_cfgs]
+
+ Rootkit("rsync").run([
+ "mkdir", "-p", storage_dirs
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+ Rootkit("rsync").run([
+ "rsync", "-rpt", list(zip(local_cfgs, storage_cfgs))
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@property
def names(self) -> Generator[str, None, None]:
From 36bb28ee904908c69c64a92e4b6d5fd5768fb88d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 17:57:35 +0100
Subject: [PATCH 118/135] double quote consistency
---
kiwi_scp/_constants.py | 8 ++++----
kiwi_scp/commands/cmd_shell.py | 2 +-
kiwi_scp/rootkit.py | 16 ++++++++--------
kiwi_scp/yaml.py | 2 +-
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/kiwi_scp/_constants.py b/kiwi_scp/_constants.py
index 843e901..31de4c9 100644
--- a/kiwi_scp/_constants.py
+++ b/kiwi_scp/_constants.py
@@ -18,7 +18,7 @@ RE_VARNAME = r"^[A-Za-z](?:[A-Za-z0-9\._-]*[A-Za-z0-9])$"
# location of "kiwi_scp" module
KIWI_ROOT = os.path.dirname(__file__)
# default name of kiwi-scp file
-KIWI_CONF_NAME = os.getenv('KIWI_CONF_NAME', "kiwi.yml")
+KIWI_CONF_NAME = os.getenv("KIWI_CONF_NAME", "kiwi.yml")
# default name of compose files
COMPOSE_FILE_NAME = "docker-compose.yml"
@@ -31,7 +31,7 @@ DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/data/etc/kiwi_default.yml"
DEFAULT_DOCKER_COMPOSE_NAME = f"{KIWI_ROOT}/data/etc/docker-compose_default.yml"
# special config directory
-CONF_DIRECTORY_NAME = 'config'
+CONFIG_DIRECTORY_NAME = "config"
# location for auxiliary Dockerfiles
IMAGES_DIRECTORY_NAME = f"{KIWI_ROOT}/data/images"
@@ -45,5 +45,5 @@ RESERVED_PROJECT_NAMES = [
# DOCKER IMAGE NAMES
# name for auxiliary docker images
-LOCAL_IMAGES_NAME = 'localhost/kiwi-scp/auxiliary'
-DEFAULT_IMAGE_NAME = 'alpine:latest'
+LOCAL_IMAGES_NAME = "localhost/kiwi-scp/auxiliary"
+DEFAULT_IMAGE_NAME = "alpine:latest"
diff --git a/kiwi_scp/commands/cmd_shell.py b/kiwi_scp/commands/cmd_shell.py
index 65c2f40..2d1ca23 100644
--- a/kiwi_scp/commands/cmd_shell.py
+++ b/kiwi_scp/commands/cmd_shell.py
@@ -39,7 +39,7 @@ class ShellCommand(KiwiCommand):
# shells from KiwiConfig
shells = [
*(str(path) for path in instance.config.shells),
- # as a last resort, fall back to '/bin/sh' and 'sh'
+ # as a last resort, fall back to "/bin/sh" and "sh"
"/bin/sh", "sh",
]
diff --git a/kiwi_scp/rootkit.py b/kiwi_scp/rootkit.py
index 112b078..1be6ab6 100644
--- a/kiwi_scp/rootkit.py
+++ b/kiwi_scp/rootkit.py
@@ -30,12 +30,12 @@ class Rootkit:
@functools.lru_cache(maxsize=None)
def __exists(image_tag: str) -> bool:
ps = DOCKER_EXE.run([
- 'images',
- '--filter', f"reference={Rootkit.__image_name(image_tag)}",
- '--format', '{{.Repository}}:{{.Tag}}'
+ "images",
+ "--filter", f"reference={Rootkit.__image_name(image_tag)}",
+ "--format", "{{.Repository}}:{{.Tag}}"
], stdout=subprocess.PIPE)
- return str(ps.stdout, 'utf-8').strip() == Rootkit.__image_name(image_tag)
+ return str(ps.stdout, "utf-8").strip() == Rootkit.__image_name(image_tag)
def __build_image(self) -> None:
if Rootkit.__exists(self.image_tag):
@@ -44,15 +44,15 @@ class Rootkit:
if self.image_tag is None:
_logger.info(f"Pulling image {Rootkit.__image_name(self.image_tag)}")
DOCKER_EXE.run([
- 'pull', Rootkit.__image_name(self.image_tag)
+ "pull", Rootkit.__image_name(self.image_tag)
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
_logger.info(f"Building image {Rootkit.__image_name(self.image_tag)}")
DOCKER_EXE.run([
- 'build',
- '-t', Rootkit.__image_name(self.image_tag),
- '-f', f"{IMAGES_DIRECTORY_NAME}/{self.image_tag}.Dockerfile",
+ "build",
+ "-t", Rootkit.__image_name(self.image_tag),
+ "-f", f"{IMAGES_DIRECTORY_NAME}/{self.image_tag}.Dockerfile",
f"{IMAGES_DIRECTORY_NAME}"
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
diff --git a/kiwi_scp/yaml.py b/kiwi_scp/yaml.py
index 1b8729c..5caff50 100644
--- a/kiwi_scp/yaml.py
+++ b/kiwi_scp/yaml.py
@@ -25,7 +25,7 @@ class YAML(ruamel.yaml.YAML):
@staticmethod
def _format_kiwi_yml(yml_string: str) -> str:
# insert newline before every main key
- yml_string = re.sub(r'^(\S)', r'\n\1', yml_string, flags=re.MULTILINE)
+ yml_string = re.sub(r"^(\S)", r"\n\1", yml_string, flags=re.MULTILINE)
# load header comment from file
with open(HEADER_KIWI_CONF_NAME, 'r') as stream:
From ddf7eaaee1a40ea99d5a965d6c8f58b330134d3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 17:58:22 +0100
Subject: [PATCH 119/135] remove legacy
---
kiwi_scp/subcommands/_hidden.py | 31 -------------------------------
1 file changed, 31 deletions(-)
diff --git a/kiwi_scp/subcommands/_hidden.py b/kiwi_scp/subcommands/_hidden.py
index 062495b..4751a9a 100644
--- a/kiwi_scp/subcommands/_hidden.py
+++ b/kiwi_scp/subcommands/_hidden.py
@@ -5,40 +5,9 @@ import subprocess
# local
from ..config import LoadedConfig
from ..executable import Executable
-from ..projects import Projects
-from ..rootkit import Rootkit, prefix_path_mnt
from ..subcommand import SubCommand
-class ConfCopyCommand(SubCommand):
- """kiwi conf-copy"""
-
- def __init__(self):
- super().__init__(
- 'conf-copy',
- action="Syncing all configs for", add_parser=False,
- description="Synchronize all config files to target directory"
- )
-
- def _run_instance(self, runner, args):
- conf_dirs = [
- project.conf_dir_name()
- for project in Projects.from_dir().filter_enabled()
- if project.has_configs()
- ]
-
- if conf_dirs:
- # add target directory
- conf_dirs.append(LoadedConfig.get()['runtime:storage'])
- logging.info(f"Sync directories: {conf_dirs}")
-
- Rootkit('rsync').run([
- 'rsync', '-rpt', '--delete', *prefix_path_mnt(conf_dirs)
- ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-
- return True
-
-
def _find_net(net_name):
ps = Executable('docker').run([
'network', 'ls', '--filter', f"name={net_name}", '--format', '{{.Name}}'
From 67f495d072af3e20ce13ef2c78f3a42a8fb78432 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?=
Date: Thu, 27 Jan 2022 18:06:27 +0100
Subject: [PATCH 120/135] pytest
---
tests/test_services.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_services.py b/tests/test_services.py
index 686d6e0..d01a568 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -13,4 +13,4 @@ class TestServices:
)
ss = Services([s])
- assert str(ss) == "services:\n s: {}"
+ assert str(ss) == "services:\n s: {}\nconfigs: []"
From 52d54eddad66c27d0a9f7b1f145dc37cb33b72e7 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 16:42:12 +0100
Subject: [PATCH 121/135] net-up and net-down functionality
---
kiwi_scp/commands/cmd_down.py | 3 +-
kiwi_scp/commands/cmd_up.py | 6 ++--
kiwi_scp/instance.py | 53 ++++++++++++++++++++++++++++++++
kiwi_scp/service.py | 1 -
kiwi_scp/subcommands/_hidden.py | 54 ---------------------------------
5 files changed, 57 insertions(+), 60 deletions(-)
delete mode 100644 kiwi_scp/subcommands/_hidden.py
diff --git a/kiwi_scp/commands/cmd_down.py b/kiwi_scp/commands/cmd_down.py
index 7476013..2236e17 100644
--- a/kiwi_scp/commands/cmd_down.py
+++ b/kiwi_scp/commands/cmd_down.py
@@ -36,8 +36,7 @@ class DownCommand(KiwiCommand):
return
super().run_for_instance(instance)
-
- # TODO net-down
+ instance.remove_net()
@classmethod
def run_for_project(cls, instance: Instance, project: Project, **kwargs) -> None:
diff --git a/kiwi_scp/commands/cmd_up.py b/kiwi_scp/commands/cmd_up.py
index 7c6a777..d1004ce 100644
--- a/kiwi_scp/commands/cmd_up.py
+++ b/kiwi_scp/commands/cmd_up.py
@@ -20,9 +20,6 @@ class UpCommand(KiwiCommand):
@classmethod
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
- # TODO conf-copy
- # TODO net-up
-
if not services:
if not click.confirm(
"Did not find any of those services. \n"
@@ -31,4 +28,7 @@ class UpCommand(KiwiCommand):
):
return
+ instance.create_net()
+ services.copy_configs()
+
COMPOSE_EXE.run(["up", "-d", *services.names], **project.process_kwargs)
diff --git a/kiwi_scp/instance.py b/kiwi_scp/instance.py
index 3ce304b..c40e1f7 100644
--- a/kiwi_scp/instance.py
+++ b/kiwi_scp/instance.py
@@ -1,3 +1,5 @@
+import logging
+import subprocess
from pathlib import Path
from typing import Generator, Dict, Sequence
@@ -5,8 +7,11 @@ import attr
from ._constants import KIWI_CONF_NAME, CONFIG_DIRECTORY_NAME
from .config import KiwiConfig
+from .executable import DOCKER_EXE
from .project import Project
+_logger = logging.getLogger(__name__)
+
@attr.s
class Instance:
@@ -30,6 +35,54 @@ class Instance:
def storage_config_directory(self):
return self.config.storage.directory.joinpath(CONFIG_DIRECTORY_NAME)
+ @staticmethod
+ def __find_net(net_name):
+ ps = DOCKER_EXE.run([
+ "network", "ls", "--filter", f"name={net_name}", "--format", "{{.Name}}"
+ ], stdout=subprocess.PIPE)
+
+ net_found = str(ps.stdout, 'utf-8').strip()
+
+ return net_found == net_name
+
+ def create_net(self):
+ net_name = self.config.network.name
+ net_cidr = str(self.config.network.cidr)
+
+ if self.__find_net(net_name):
+ _logger.info(f"Network '{net_name}' already exists")
+ return
+
+ try:
+ DOCKER_EXE.run([
+ "network", "create",
+ "--driver", "bridge",
+ "--internal",
+ "--subnet", net_cidr,
+ net_name
+ ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ _logger.info(f"Network '{net_name}' created")
+
+ except subprocess.CalledProcessError:
+ _logger.error(f"Error creating network '{net_name}'")
+
+ def remove_net(self):
+ net_name = self.config.network.name
+
+ if not self.__find_net(net_name):
+ _logger.info(f"Network '{net_name}' does not exist")
+ return
+
+ try:
+ DOCKER_EXE.run([
+ "network", "rm",
+ net_name
+ ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ _logger.info(f"Network '{net_name}' removed")
+
+ except subprocess.CalledProcessError:
+ _logger.error(f"Error removing network '{net_name}'")
+
@property
def projects(self) -> Generator[Project, None, None]:
for project in self.config.projects:
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index c85b82f..e2caa41 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -11,7 +11,6 @@ from ruamel.yaml import CommentedMap
from .executable import COMPOSE_EXE
if TYPE_CHECKING:
- from .instance import Instance
from .project import Project
_logger = logging.getLogger(__name__)
diff --git a/kiwi_scp/subcommands/_hidden.py b/kiwi_scp/subcommands/_hidden.py
deleted file mode 100644
index 4751a9a..0000000
--- a/kiwi_scp/subcommands/_hidden.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# system
-import logging
-import subprocess
-
-# local
-from ..config import LoadedConfig
-from ..executable import Executable
-from ..subcommand import SubCommand
-
-
-def _find_net(net_name):
- ps = Executable('docker').run([
- 'network', 'ls', '--filter', f"name={net_name}", '--format', '{{.Name}}'
- ], stdout=subprocess.PIPE)
-
- net_found = str(ps.stdout, 'utf-8').strip()
-
- return net_found == net_name
-
-
-class NetUpCommand(SubCommand):
- """kiwi net-up"""
-
- def __init__(self):
- super().__init__(
- 'net-up',
- action="Creating the local network hub for", add_parser=False,
- description="Create the local network hub for this instance"
- )
-
- def _run_instance(self, runner, args):
- config = LoadedConfig.get()
- net_name = config['network:name']
- net_cidr = config['network:cidr']
-
- if _find_net(net_name):
- logging.info(f"Network '{net_name}' already exists")
- return True
-
- try:
- Executable('docker').run([
- 'network', 'create',
- '--driver', 'bridge',
- '--internal',
- '--subnet', net_cidr,
- net_name
- ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- logging.info(f"Network '{net_name}' created")
-
- except subprocess.CalledProcessError:
- logging.error(f"Error creating network '{net_name}'")
- return False
-
- return True
From 2b0ec1cea214a4f2367b0b2aa46a544a977d00cf Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 16:44:22 +0100
Subject: [PATCH 122/135] version_str based on tag
---
dist/bump-version.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dist/bump-version.sh b/dist/bump-version.sh
index 5d57040..0fdcb4e 100755
--- a/dist/bump-version.sh
+++ b/dist/bump-version.sh
@@ -3,9 +3,9 @@
this="$(readlink -f "${0}")"
this_dir="$(dirname "${this}")"
-git_branch="$(git rev-parse --abbrev-ref HEAD)"
+# git_branch="$(git rev-parse --abbrev-ref HEAD)"
git_tag="$(git describe --abbrev=0)"
-version_str="${git_branch##*/}"
+version_str="${git_tag##*/}"
version_str="0.2.0"
sed -ri "s/(version\s*:).*$/\1 '${version_str}'/" "${this_dir}/../example/kiwi.yml"
From a787c837e7823f5821423d22041538028659a47b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 16:45:10 +0100
Subject: [PATCH 123/135] cleanup
---
kiwi_scp/commands/cmd_new.py | 2 --
kiwi_scp/service.py | 1 -
tests/test_services.py | 2 +-
3 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/kiwi_scp/commands/cmd_new.py b/kiwi_scp/commands/cmd_new.py
index f554661..0ac1394 100644
--- a/kiwi_scp/commands/cmd_new.py
+++ b/kiwi_scp/commands/cmd_new.py
@@ -1,8 +1,6 @@
import os
import shutil
-import click
-
from .cmd import KiwiCommandType, KiwiCommand
from .decorators import kiwi_command
from .._constants import DEFAULT_DOCKER_COMPOSE_NAME, COMPOSE_FILE_NAME, RESERVED_PROJECT_NAMES
diff --git a/kiwi_scp/service.py b/kiwi_scp/service.py
index e2caa41..220aa63 100644
--- a/kiwi_scp/service.py
+++ b/kiwi_scp/service.py
@@ -59,4 +59,3 @@ class Service:
elif nxt is not None:
# try next in list
_logger.info(f"Executable '{cur}' not found in container, trying '{nxt}'")
-
diff --git a/tests/test_services.py b/tests/test_services.py
index d01a568..cfb7716 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -1,7 +1,7 @@
from ruamel.yaml import CommentedMap
-from kiwi_scp.services import Services
from kiwi_scp.service import Service
+from kiwi_scp.services import Services
class TestServices:
From b76b6b59ed0351d94967bfd31bc2b880b7ba556b Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 16:46:51 +0100
Subject: [PATCH 124/135] dependencies
---
poetry.lock | 159 +++++++++++++++++++++++--------------------------
pyproject.toml | 2 +-
2 files changed, 77 insertions(+), 84 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 764a946..eecd76d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,36 +8,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
-version = "21.2.0"
+version = "21.4.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
-
-[[package]]
-name = "backports.entry-points-selectable"
-version = "1.1.1"
-description = "Compatibility shim providing selectable entry points for older implementations"
-category = "dev"
-optional = false
-python-versions = ">=2.7"
-
-[package.dependencies]
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "click"
-version = "8.0.3"
+version = "8.0.4"
description = "Composable command line interface toolkit"
category = "main"
optional = false
@@ -79,7 +64,7 @@ python-versions = ">=3.6, <3.7"
[[package]]
name = "distlib"
-version = "0.3.3"
+version = "0.3.4"
description = "Distribution utilities"
category = "dev"
optional = false
@@ -87,7 +72,7 @@ python-versions = "*"
[[package]]
name = "filelock"
-version = "3.4.0"
+version = "3.4.1"
description = "A platform independent file lock."
category = "dev"
optional = false
@@ -99,7 +84,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co
[[package]]
name = "importlib-metadata"
-version = "4.8.2"
+version = "4.8.3"
description = "Read metadata from Python packages"
category = "main"
optional = false
@@ -185,7 +170,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pydantic"
-version = "1.8.2"
+version = "1.9.0"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
@@ -201,7 +186,7 @@ email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyparsing"
-version = "3.0.6"
+version = "3.0.7"
description = "Python parsing module"
category = "dev"
optional = false
@@ -212,7 +197,7 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
-version = "6.2.5"
+version = "7.0.1"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@@ -227,10 +212,10 @@ iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
-toml = "*"
+tomli = ">=1.0.0"
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-cov"
@@ -249,14 +234,14 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale
[[package]]
name = "ruamel.yaml"
-version = "0.17.17"
+version = "0.17.21"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
python-versions = ">=3"
[package.dependencies]
-"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""}
+"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
[package.extras]
docs = ["ryd"]
@@ -288,7 +273,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
-version = "1.2.2"
+version = "1.2.3"
description = "A lil' TOML parser"
category = "dev"
optional = false
@@ -296,7 +281,7 @@ python-versions = ">=3.6"
[[package]]
name = "typing-extensions"
-version = "4.0.1"
+version = "4.1.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
@@ -304,14 +289,13 @@ python-versions = ">=3.6"
[[package]]
name = "virtualenv"
-version = "20.10.0"
+version = "20.13.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
-"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
filelock = ">=3.2,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
@@ -346,7 +330,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
-content-hash = "bdbab568cf9ce133ae55b74cc1a6939c6aff96d3c60fee5ebffd7f498a6d97a6"
+content-hash = "01f4aaa2c8fbbf76fa7442a0c64bf553fe95fd3503fde730cad71c06da957a58"
[metadata.files]
atomicwrites = [
@@ -354,16 +338,12 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
- {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
- {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
-]
-"backports.entry-points-selectable" = [
- {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"},
- {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"},
+ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
click = [
- {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
- {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
+ {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
+ {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -423,16 +403,16 @@ dataclasses = [
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
]
distlib = [
- {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
- {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
+ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
+ {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
]
filelock = [
- {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"},
- {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"},
+ {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"},
+ {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"},
- {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"},
+ {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
+ {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
]
importlib-resources = [
{file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"},
@@ -459,44 +439,57 @@ py = [
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pydantic = [
- {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
- {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
- {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
- {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
- {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
- {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
- {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
- {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
- {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
- {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
- {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
- {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
- {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
- {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
- {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
- {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
- {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
- {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
- {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
- {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
- {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
- {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
+ {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"},
+ {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"},
+ {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"},
+ {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"},
+ {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"},
+ {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"},
+ {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"},
+ {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"},
+ {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"},
+ {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"},
+ {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"},
+ {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"},
+ {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"},
+ {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"},
+ {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"},
+ {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"},
+ {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"},
+ {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"},
+ {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"},
+ {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"},
+ {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"},
+ {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"},
+ {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"},
+ {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"},
+ {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"},
+ {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"},
+ {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"},
+ {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"},
+ {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"},
+ {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"},
+ {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"},
+ {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"},
+ {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"},
+ {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"},
+ {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"},
]
pyparsing = [
- {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
- {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
+ {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
+ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
pytest = [
- {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
- {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"},
+ {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"},
]
pytest-cov = [
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
]
"ruamel.yaml" = [
- {file = "ruamel.yaml-0.17.17-py3-none-any.whl", hash = "sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f"},
- {file = "ruamel.yaml-0.17.17.tar.gz", hash = "sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be"},
+ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
+ {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
]
"ruamel.yaml.clib" = [
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
@@ -530,16 +523,16 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
- {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
- {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
+ {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
+ {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
]
typing-extensions = [
- {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
- {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
+ {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
+ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]
virtualenv = [
- {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"},
- {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"},
+ {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"},
+ {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index 866fa70..7407cf8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,7 +13,7 @@ pydantic = "^1.8.2"
wcwidth = "^0.2.5"
[tool.poetry.dev-dependencies]
-pytest = "^6.2.5"
+pytest = "^7.0.0"
pytest-cov = "^3.0.0"
toml = "^0.10.2"
virtualenv = "^20.10.0"
From aa8f0d9e6a2943a755e0d2163c978d6bdc1f5449 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 22:46:50 +0100
Subject: [PATCH 125/135] prefer "long" project configs
---
kiwi_scp/config.py | 9 ++++-----
kiwi_scp/yaml.py | 2 +-
tests/test_config.py | 7 ++++++-
3 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index 3280f9a..ad9e923 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -45,13 +45,12 @@ class ProjectConfig(BaseModel):
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
- if self.override_storage is None:
- return {self.name: self.enabled}
+ result = self.dict(exclude={"override_storage"})
- else:
- result = self.dict(exclude={"override_storage"})
+ if self.override_storage is not None:
result["override_storage"] = self.override_storage.kiwi_dict
- return result
+
+ return result
@validator("name")
@classmethod
diff --git a/kiwi_scp/yaml.py b/kiwi_scp/yaml.py
index 5caff50..2d7a606 100644
--- a/kiwi_scp/yaml.py
+++ b/kiwi_scp/yaml.py
@@ -10,7 +10,7 @@ from ._constants import HEADER_KIWI_CONF_NAME
class YAML(ruamel.yaml.YAML):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
- self.indent(offset=2)
+ self.indent(sequence=4, offset=2)
def dump(self, data, stream=None, **kwargs) -> Optional[str]:
into_str: bool = False
diff --git a/tests/test_config.py b/tests/test_config.py
index 3a344e0..33a4421 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -221,7 +221,12 @@ class TestProject:
assert p.name == "project"
assert not p.enabled
assert p.override_storage is None
- assert p.kiwi_dict == kiwi_dict
+
+ resulting_kiwi_dict = {
+ "name": "project",
+ "enabled": False,
+ }
+ assert p.kiwi_dict == resulting_kiwi_dict
def test_dict(self):
c = KiwiConfig(projects={"name": "project"})
From 261ee7bf593ebde40b6b6a2af3ff49c3392eaae1 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 22:47:19 +0100
Subject: [PATCH 126/135] rename "hello-world.project"
---
example/{hello-world.project => hello_world}/docker-compose.yml | 0
example/{hello-world.project => hello_world}/web/Dockerfile | 0
example/{hello-world.project => hello_world}/web/index.html | 0
example/kiwi.yml | 2 +-
tests/test_instance.py | 2 +-
tests/test_project.py | 2 +-
6 files changed, 3 insertions(+), 3 deletions(-)
rename example/{hello-world.project => hello_world}/docker-compose.yml (100%)
rename example/{hello-world.project => hello_world}/web/Dockerfile (100%)
rename example/{hello-world.project => hello_world}/web/index.html (100%)
diff --git a/example/hello-world.project/docker-compose.yml b/example/hello_world/docker-compose.yml
similarity index 100%
rename from example/hello-world.project/docker-compose.yml
rename to example/hello_world/docker-compose.yml
diff --git a/example/hello-world.project/web/Dockerfile b/example/hello_world/web/Dockerfile
similarity index 100%
rename from example/hello-world.project/web/Dockerfile
rename to example/hello_world/web/Dockerfile
diff --git a/example/hello-world.project/web/index.html b/example/hello_world/web/index.html
similarity index 100%
rename from example/hello-world.project/web/index.html
rename to example/hello_world/web/index.html
diff --git a/example/kiwi.yml b/example/kiwi.yml
index 581505a..a9f55fa 100644
--- a/example/kiwi.yml
+++ b/example/kiwi.yml
@@ -8,7 +8,7 @@ shells:
- /bin/bash
projects:
- - name: hello-world.project
+ - name: hello_world
enabled: true
storage:
diff --git a/tests/test_instance.py b/tests/test_instance.py
index 1a5beb3..4b7f9a5 100644
--- a/tests/test_instance.py
+++ b/tests/test_instance.py
@@ -12,7 +12,7 @@ class TestDefault:
pc = i.config.projects[0]
- assert pc.name == "hello-world.project"
+ assert pc.name == "hello_world"
def test_empty(self):
i = Instance()
diff --git a/tests/test_project.py b/tests/test_project.py
index 79531ba..8234e32 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -12,7 +12,7 @@ class TestDefault:
def test_example(self):
p = Project(
- directory=Path("example/hello-world.project"),
+ directory=Path("example/hello_world"),
parent_instance=None,
)
From b30188c8dc4c82e380cc587f2f07e2cdfdbeea4c Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Mon, 21 Feb 2022 23:35:48 +0100
Subject: [PATCH 127/135] rename docker-compose variables
---
example/hello_world/docker-compose.yml | 6 +++---
kiwi_scp/project.py | 12 ++++++------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/example/hello_world/docker-compose.yml b/example/hello_world/docker-compose.yml
index 2cc054d..33c6f1a 100644
--- a/example/hello_world/docker-compose.yml
+++ b/example/hello_world/docker-compose.yml
@@ -31,7 +31,7 @@ services:
environment:
MYSQL_ROOT_PASSWORD: changeme
volumes:
- - "${TARGETDIR}/db:/var/lib/mysql"
+ - "${KIWI_PROJECT}/db:/var/lib/mysql"
adminer:
# admin interface for databases
@@ -46,10 +46,10 @@ services:
- "8081:8080"
another-web:
- # Another webserver just to show off the ${CONFIGDIR} variable
+ # Another webserver just to show off the ${KIWI_CONFIG} variable
image: nginx:stable-alpine
restart: unless-stopped
ports:
- "8082:80"
volumes:
- - "${CONFIGDIR}/html/index.html:/usr/share/nginx/html/index.html:ro"
+ - "${KIWI_CONFIG}/html/index.html:/usr/share/nginx/html/index.html:ro"
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index d816a7a..0801dc3 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -39,18 +39,18 @@ class Project:
directory: Path = self.directory
project_name: str = self.name
kiwi_hub_name: str = self.parent_instance.config.network.name
- target_root_dir: Path = self.parent_instance.config.storage.directory
- conf_dir: Path = target_root_dir.joinpath(CONFIG_DIRECTORY_NAME)
- target_dir: Path = target_root_dir.joinpath(project_name)
+ kiwi_instance_dir: Path = self.parent_instance.config.storage.directory
+ kiwi_config_dir: Path = kiwi_instance_dir.joinpath(CONFIG_DIRECTORY_NAME)
+ kiwi_project_dir: Path = kiwi_instance_dir.joinpath(project_name)
result: Dict[str, Any] = {
"cwd": str(directory),
"env": {
"COMPOSE_PROJECT_NAME": project_name,
"KIWI_HUB_NAME": kiwi_hub_name,
- "TARGETROOT": str(target_root_dir),
- "CONFIGDIR": str(conf_dir),
- "TARGETDIR": str(target_dir),
+ "KIWI_INSTANCE": str(kiwi_instance_dir),
+ "KIWI_CONFIG": str(kiwi_config_dir),
+ "KIWI_PROJECT": str(kiwi_project_dir),
},
}
From d4eb91dae84efae9edaeeda2ada75376d3c8f1f5 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 00:02:08 +0100
Subject: [PATCH 128/135] respect override_storage
---
kiwi_scp/project.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/kiwi_scp/project.py b/kiwi_scp/project.py
index 0801dc3..a5b1bf7 100644
--- a/kiwi_scp/project.py
+++ b/kiwi_scp/project.py
@@ -43,6 +43,9 @@ class Project:
kiwi_config_dir: Path = kiwi_instance_dir.joinpath(CONFIG_DIRECTORY_NAME)
kiwi_project_dir: Path = kiwi_instance_dir.joinpath(project_name)
+ if self.config.override_storage is not None:
+ kiwi_project_dir = self.config.override_storage.directory
+
result: Dict[str, Any] = {
"cwd": str(directory),
"env": {
From 98caa876cea597e6f926321c93816893c1795131 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 00:02:28 +0100
Subject: [PATCH 129/135] README
---
README.md | 79 ++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 52 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index a4da112..59bcab7 100644
--- a/README.md
+++ b/README.md
@@ -18,24 +18,24 @@ The simple tool for managing container servers
## Installation
-A convenience installer is available as [install.sh](./install.sh) in this directory.
+A convenience installer is available as [install.sh](./dist/install.sh) in the `dist` directory.
You can `curl | sh` it using the following one-liner.
```shell script
-curl --proto '=https' --tlsv1.2 -sSf 'https://raw.githubusercontent.com/ldericher/kiwi-scp/master/install.sh' | sh
+curl --proto '=https' --tlsv1.2 -sSf 'https://raw.githubusercontent.com/ldericher/kiwi-scp/master/dist/install.sh' | sh
```
The installer downloads the `kiwi` launcher script and installs it to a location of your choice.
Please consider installing into a directory inside your `$PATH`.
-Run in a root shell or use `sudo sh` instead for system-wide installation.
+Run in a root shell or use `sudo sh` at the end instead for system-wide installation.
-You should now be able to run `kiwi init --show` and see the default configuration file.
-This downloads the latest version of the main kiwi-scp executable and sets it up for you.
+You should now be able to run `kiwi list --show` and see the default configuration file.
+This installs the latest version of the kiwi-scp package and sets it up for you.
### Adjusting environment for `kiwi`
-The `kiwi` executable depends on [Python](https://www.python.org/) 3.6 (or later) and
+The `kiwi` executable depends on [Python](https://www.python.org/) 3.6.1 (or later) and
[less](http://www.greenwoodsoftware.com/less/) being in your `$PATH`.
In some cases, notably when using a multi-version system such as
@@ -69,8 +69,8 @@ A kiwi-scp instance is a directory containing a bunch of static configuration fi
"Static" there as in "those don't change during normal service operation".
These files could be anything from actual `.conf` files to entire html-web-roots.
-Non-static, but persistent files are to be kept in a "service data directory", by default `/var/kiwi`.
-In your `docker-compose.yml` files, you can refer to that directory as **${TARGETROOT}**.
+Non-static, but persistent files are to be kept in a "service data directory", by default `/var/local/kiwi`.
+In your `docker-compose.yml` files, you can refer to that directory as **${KIWI_INSTANCE}**.
Start the current directory as a kiwi-scp instance using `kiwi up`, or stop it using `kiwi down`.
This also creates kiwi's internal hub network, which you can use as **kiwi_hub** in your `docker-compose.yml` files.
@@ -88,7 +88,7 @@ Before enabling or starting, consider editing the new project's `docker-compose.
Finally, enable it with `kiwi enable `.
You can also create, enable or (analogously) disable multiple projects in a single command.
-Each project will have its own place in the service data directory, which you can refer to as **${TARGETDIR}**.
+Each project will have its own place in the service data directory, which you can refer to as **${KIWI_PROJECT}**.
Finally, start a project using `kiwi up `.
@@ -100,7 +100,7 @@ kiwi-scp extends the logical bounds of `docker-compose` to handling multiple pro
#### The `kiwi_hub`
-With kiwi-scp, you get the internal kiwi_hub network for free.
+With kiwi-scp, you get the internal `kiwi_hub` network for free.
It allows for network communication between services in different projects.
Be aware, services only connected to the kiwi_hub can't use a port mapping!
In most cases, you will want to use this:
@@ -112,25 +112,23 @@ networks:
```
-#### The `CONFIGDIR`
+#### The `KIWI_CONFIG`
Sometimes, it's convenient to re-use configuration files across projects.
-For this use case, create a directory named `conf` in a project.
-Those will all be combined into a directory available as **${CONFIGDIR}** in your `docker-compose.yml` files.
+For this use case, create a directory named `config` in your instance.
+In your `docker-compose.yml` files, you can refer to that directory as **${KIWI_CONFIG}**.
#### `kiwi.yml` options
##### `version`
-Version of kiwi-scp to use for this instance.
-Default: Latest version.
+Version of kiwi-scp to use for this instance.
-##### `runtime:storage`
-Path of the service data directory, available as **${TARGETROOT}** in projects.
-Default: `/var/kiwi`
+Default: Version of [`master` branch](https://github.com/ldericher/kiwi-scp/tree/master).
+
+##### `shells`
+Sequence of additionally preferable shell executables when entering service containers.
-##### `runtime:shells`
-List of additionally preferable shell executables when entering service containers.
Default: `- /bin/bash`
Example:
@@ -141,17 +139,44 @@ runtime:
- /bin/fish
```
-##### `runtime:env`
-Associative array of custom variables available in projects' `docker-compose.yml` files.
-Default: `null`
+##### `projects`
+Sequence of project definitions in this instance.
+
+###### Project definition
+Defining a project in this instance. Any subdirectory with a `docker-compose.yml` might be considered a project.
+
+Format: Mapping using the keys `name`, `enabled` and `override_storage`
Example:
```yaml
-runtime:
- env:
- HELLO: "World"
- FOO: "Bar"
+- name: "hello_world"
+ enabled: true
```
+##### `environment`
+Custom variables available in projects' `docker-compose.yml` files.
+
+Format: Mapping of `KEY: "value"` pairs
+Example:
+
+```yaml
+environment:
+ HELLO: "World"
+ FOO: "Bar"
+```
+
+##### `storage`
+Configuration for the service data storage.
+
+Format: Mapping using the key `directory`
+
+###### `storage:directory`
+Path to the local service data directory, the only currently supported service data storage.
+Available as **${KIWI_INSTANCE}** in projects.
+
+Default: `/var/local/kiwi`
+
+##### `network`
+
#### For everything else, look at `kiwi --help`
#### Happy kiwi-ing!
From 66ddee35e1a9a042d07b7f6cf9b8746fea21626f Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 11:15:55 +0100
Subject: [PATCH 130/135] README
---
README.md | 56 +++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 42 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 59bcab7..f88d9c9 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ The simple tool for managing container servers
## Quick start
- Learn to use `docker` with `docker-compose`
-- Install kiwi-scp
+- Install `kiwi-scp`
- Look at [the example instance](./example)
- Look at the output of `kiwi --help`
- Start building your own instances
@@ -29,7 +29,7 @@ The installer downloads the `kiwi` launcher script and installs it to a location
Please consider installing into a directory inside your `$PATH`.
Run in a root shell or use `sudo sh` at the end instead for system-wide installation.
-You should now be able to run `kiwi list --show` and see the default configuration file.
+You should then be able to run `kiwi list --show` and see the default configuration file.
This installs the latest version of the kiwi-scp package and sets it up for you.
@@ -44,8 +44,7 @@ at login time.
In those cases, you can simply create a `.kiwi_profile` file in your home directory.
It will be sourced every time you use the `kiwi` command.
-For the aforementioned case where you installed `centos-release-scl` and `rh-python36`, your `~/.kiwi_profile` should
-contain:
+For the aforementioned case where you installed `centos-release-scl` and `rh-python36`, your `~/.kiwi_profile` should contain:
```shell script
#!/bin/sh
@@ -59,8 +58,7 @@ contain:
### Create a kiwi-scp instance
Any directory is implicitly a valid kiwi-scp instance using the default configuration.
-To prevent surprises however, you should run `kiwi init` in an empty directory and follow its directions to
-create a `kiwi.yml` before using `kiwi` more.
+To prevent surprises however, you should run `kiwi init` and follow its directions to create a `kiwi.yml` for your instance before using `kiwi` more.
### Concept
@@ -69,11 +67,11 @@ A kiwi-scp instance is a directory containing a bunch of static configuration fi
"Static" there as in "those don't change during normal service operation".
These files could be anything from actual `.conf` files to entire html-web-roots.
-Non-static, but persistent files are to be kept in a "service data directory", by default `/var/local/kiwi`.
+Non-static, persistent files are to be kept in a "service data storage", by default the directory `/var/local/kiwi`.
In your `docker-compose.yml` files, you can refer to that directory as **${KIWI_INSTANCE}**.
-Start the current directory as a kiwi-scp instance using `kiwi up`, or stop it using `kiwi down`.
-This also creates kiwi's internal hub network, which you can use as **kiwi_hub** in your `docker-compose.yml` files.
+Start the current kiwi-scp instance using `kiwi up`, or stop it using `kiwi down`.
+This also manages kiwi's internal hub network, which you can use as **kiwi_hub** in your `docker-compose.yml` files.
### Projects
@@ -143,9 +141,9 @@ runtime:
Sequence of project definitions in this instance.
###### Project definition
-Defining a project in this instance. Any subdirectory with a `docker-compose.yml` might be considered a project.
+Defining a project in this instance. Any subdirectory with a `docker-compose.yml` should be considered a project. The directory name is equivalent to the project name.
-Format: Mapping using the keys `name`, `enabled` and `override_storage`
+Format[^1]: Mapping using the keys `name` (required), `enabled` and `override_storage`
Example:
```yaml
@@ -156,7 +154,7 @@ Example:
##### `environment`
Custom variables available in projects' `docker-compose.yml` files.
-Format: Mapping of `KEY: "value"` pairs
+Format[^1]: Mapping of `KEY: "value"` pairs
Example:
```yaml
@@ -168,15 +166,45 @@ environment:
##### `storage`
Configuration for the service data storage.
-Format: Mapping using the key `directory`
+Format: Mapping using the key `directory`
+Example:
+
+```yaml
+storage:
+ directory: "/var/local/kiwi"
+```
###### `storage:directory`
Path to the local service data directory, the only currently supported service data storage.
Available as **${KIWI_INSTANCE}** in projects.
-Default: `/var/local/kiwi`
+Default: `"/var/local/kiwi"`
+
##### `network`
+Configuration for the internal `kiwi_hub` network.
+
+Format: Mapping using the keys `name` and `cidr`
+Example:
+
+```yaml
+network:
+ name: "kiwi_hub"
+ cidr: "10.22.46.0/24"
+```
+
+##### `network:name`
+Configuration for the internal `kiwi_hub` network.
+
+Default: `"kiwi_hub"`
+
+##### `network:cidr`
+[CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks) for the subnet of the internal `kiwi_hub` network.
+
+Default: `"10.22.46.0/24"`
+
#### For everything else, look at `kiwi --help`
#### Happy kiwi-ing!
+
+[^1]: This is the officially correct format. For enabling varying conventions, there are multiple accepted formats. Start trying and check with `kiwi list --show` -- if it makes sense, it will likely just work.
From 4ab770caa07205aec7f559a91d0b9e3557f612fd Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 11:16:17 +0100
Subject: [PATCH 131/135] README
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index f88d9c9..6bc8263 100644
--- a/README.md
+++ b/README.md
@@ -193,12 +193,12 @@ network:
cidr: "10.22.46.0/24"
```
-##### `network:name`
+###### `network:name`
Configuration for the internal `kiwi_hub` network.
Default: `"kiwi_hub"`
-##### `network:cidr`
+###### `network:cidr`
[CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks) for the subnet of the internal `kiwi_hub` network.
Default: `"10.22.46.0/24"`
From f75228a64f8452de86cfe1c858065c9ada21e2bb Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 11:19:12 +0100
Subject: [PATCH 132/135] README
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6bc8263..009461c 100644
--- a/README.md
+++ b/README.md
@@ -203,8 +203,8 @@ Default: `"kiwi_hub"`
Default: `"10.22.46.0/24"`
+**For everything else, look at `kiwi --help`**
-#### For everything else, look at `kiwi --help`
-#### Happy kiwi-ing!
+**Happy kiwi-ing!**
[^1]: This is the officially correct format. For enabling varying conventions, there are multiple accepted formats. Start trying and check with `kiwi list --show` -- if it makes sense, it will likely just work.
From 33cb62bc3ad9b65aadf5a4ec0359f53141764bea Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 13:17:03 +0100
Subject: [PATCH 133/135] ".coverage" excluded
---
.idea/kiwi-scp.iml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.idea/kiwi-scp.iml b/.idea/kiwi-scp.iml
index fddd591..2f08fd2 100644
--- a/.idea/kiwi-scp.iml
+++ b/.idea/kiwi-scp.iml
@@ -5,6 +5,7 @@
+
From a54d66b9e7dc8b9d1d4229f4de109aca5bcb87a6 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 13:18:35 +0100
Subject: [PATCH 134/135] Custom Exception classes
---
kiwi_scp/commands/cli.py | 37 +++++++++++++++++-----
kiwi_scp/commands/cmd.py | 6 +++-
kiwi_scp/config.py | 61 ++++++++++++++++++++++++++++++------
tests/test_config.py | 67 +++++++++++++++++++++++++---------------
4 files changed, 127 insertions(+), 44 deletions(-)
diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py
index 74c947d..ed1ceed 100644
--- a/kiwi_scp/commands/cli.py
+++ b/kiwi_scp/commands/cli.py
@@ -6,6 +6,27 @@ from typing import List, Optional
import click
+class MissingCMDObjectError(ValueError):
+ """raised if command object can't be found in its module"""
+ pass
+
+
+class CMDObjectSubclassError(TypeError):
+ """raised if a command object is not inheriting click.Command"""
+ pass
+
+
+class CMDUnregisteredError(ValueError):
+ """raised if commands have not been assigned to a command group"""
+
+ unregistered: List[str]
+
+ def __init__(self, unregistered):
+ self.unregistered = unregistered
+
+ super().__init__(f"Some commands were not registered in a group above: {unregistered!r}")
+
+
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
@@ -27,19 +48,19 @@ class KiwiCLI(click.MultiCommand):
except ImportError:
return
- member_name = f"{cmd_name.capitalize()}Command"
+ cmd_object_name = f"{cmd_name.capitalize()}Command"
- if member_name in dir(cmd_module):
- member = getattr(cmd_module, member_name)
+ if cmd_object_name in dir(cmd_module):
+ cmd_object = getattr(cmd_module, cmd_object_name)
- if isinstance(member, click.Command):
- return member
+ if isinstance(cmd_object, click.Command):
+ return cmd_object
else:
- raise Exception("Fail class")
+ raise CMDObjectSubclassError()
else:
- raise Exception("Fail member name")
+ raise MissingCMDObjectError()
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
commands = {
@@ -71,4 +92,4 @@ class KiwiCLI(click.MultiCommand):
cmd_names -= set(cmd_list)
if len(cmd_names) > 0:
- raise Exception(f"Some commands were not registered in a group above: {cmd_names}")
+ raise CMDUnregisteredError(cmd_names)
diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py
index 1da8cc4..70d20e2 100644
--- a/kiwi_scp/commands/cmd.py
+++ b/kiwi_scp/commands/cmd.py
@@ -23,6 +23,10 @@ class KiwiCommandType(Enum):
T = TypeVar("T")
+class KiwiCommandNotImplementedError(Exception):
+ pass
+
+
class KiwiCommand:
type: KiwiCommandType = KiwiCommandType.SERVICES
enabled_only: bool = False
@@ -177,4 +181,4 @@ class KiwiCommand:
@classmethod
def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services,
new_service_names: List[str], **kwargs) -> None:
- raise Exception
+ raise KiwiCommandNotImplementedError()
diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py
index ad9e923..c60f81c 100644
--- a/kiwi_scp/config.py
+++ b/kiwi_scp/config.py
@@ -9,6 +9,25 @@ from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME, RESERVED_PROJECT_
from .yaml import YAML
+class InvalidFormatError(ValueError):
+ """raised if format recognition unsuccessful"""
+
+ cls: type
+ member: Optional[str]
+ data: str
+
+ def __init__(self, cls, data, member = None):
+ self.cls = cls
+ self.data = data
+
+ if member is not None:
+ self.member = member
+ super().__init__(f"Invalid {self.cls.__name__!r}.{self.member!r} Format: {self.data!r}")
+
+ else:
+ super().__init__(f"Invalid {self.cls.__name__!r} Format: {self.data!r}")
+
+
class StorageConfig(BaseModel):
"""a storage subsection"""
@@ -31,7 +50,17 @@ class StorageConfig(BaseModel):
else:
# undefined format
- raise ValueError("Invalid Storage Format")
+ raise InvalidFormatError(cls, str(values))
+
+
+class ProjectNameReservedError(ValueError):
+ """raised if trying to create a project with a reserved name"""
+
+ name: str
+
+ def __init__(self, name):
+ self.name = name
+ super().__init__(f"Project name {self.name!r} is reserved!")
class ProjectConfig(BaseModel):
@@ -58,7 +87,7 @@ class ProjectConfig(BaseModel):
"""check if project name is allowed"""
if value in RESERVED_PROJECT_NAMES:
- raise ValueError(f"Project name '{value}' is reserved!")
+ raise ProjectNameReservedError(value)
return value
@@ -101,7 +130,7 @@ class ProjectConfig(BaseModel):
else:
# undefined format
- raise ValueError("Invalid Project Format")
+ raise InvalidFormatError(ProjectConfig, values)
class NetworkConfig(BaseModel):
@@ -120,6 +149,18 @@ class NetworkConfig(BaseModel):
}
+class MissingMemberError(ValueError):
+ """raised if class member is missing a definition"""
+
+ cls: type
+ member: str
+
+ def __init__(self, cls, member):
+ self.cls = cls
+ self.member = member
+ super().__init__(f"Member {self.cls.__name__!r}.{self.member!r} is required!")
+
+
class KiwiConfig(BaseModel):
"""represents a kiwi.yml"""
@@ -225,7 +266,7 @@ class KiwiConfig(BaseModel):
except Exception:
# undefined format
- raise ValueError("Invalid Shells Format")
+ raise InvalidFormatError(KiwiConfig, value, "shells")
@validator("projects", pre=True)
@classmethod
@@ -254,7 +295,7 @@ class KiwiConfig(BaseModel):
except Exception:
# undefined format
- raise ValueError("Invalid Projects Format")
+ raise InvalidFormatError(KiwiConfig, value, "projects")
return result
@@ -270,7 +311,7 @@ class KiwiConfig(BaseModel):
except Exception:
# undefined format
- raise ValueError("Invalid Projects Format")
+ raise InvalidFormatError(KiwiConfig, value, "projects")
@validator("environment", pre=True)
@classmethod
@@ -284,7 +325,7 @@ class KiwiConfig(BaseModel):
idx = str(var_val).find("=")
except Exception:
# undefined format
- raise ValueError("Invalid Environment Format")
+ raise InvalidFormatError(KiwiConfig, value, "environment")
if idx == -1:
# don't split, just define the variable
@@ -325,7 +366,7 @@ class KiwiConfig(BaseModel):
if value is None:
# empty storage
- raise ValueError("No Storage Given")
+ raise MissingMemberError(KiwiConfig, "storage")
elif isinstance(value, dict):
# native dict format
@@ -350,7 +391,7 @@ class KiwiConfig(BaseModel):
if value is None:
# empty network
- raise ValueError("No Network Given")
+ raise MissingMemberError(KiwiConfig, "network")
elif isinstance(value, dict):
# native dict format
@@ -358,4 +399,4 @@ class KiwiConfig(BaseModel):
else:
# undefined format
- raise ValueError("Invalid Network Format")
+ raise InvalidFormatError(KiwiConfig, value, "network")
diff --git a/tests/test_config.py b/tests/test_config.py
index 33a4421..cb3bd90 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -8,11 +8,18 @@ from kiwi_scp.config import KiwiConfig
from kiwi_scp.yaml import YAML
+class UnCoercibleError(ValueError):
+ pass
+
+
class UnCoercible:
"""A class that doesn't have a string representation"""
def __str__(self):
- raise ValueError
+ raise UnCoercibleError()
+
+ def __repr__(self) -> str:
+ return "UnCoercible()"
class TestDefault:
@@ -130,8 +137,8 @@ class TestShells:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Shells Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'shells' Format: UnCoercible()"
+ assert error["type"] == "value_error.invalidformat"
with pytest.raises(ValidationError) as exc_info:
KiwiConfig(shells=["/bin/bash", UnCoercible()])
@@ -207,8 +214,8 @@ class TestProject:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'StorageConfig' Format: '{}'"
+ assert error["type"] == "value_error.invalidformat"
def test_short(self):
kiwi_dict = {
@@ -239,6 +246,15 @@ class TestProject:
assert p.enabled
assert p.override_storage is None
+ def test_reserved_name(self):
+ with pytest.raises(ValidationError) as exc_info:
+ KiwiConfig(projects={"name": "config"})
+
+ assert len(exc_info.value.errors()) == 1
+ error = exc_info.value.errors()[0]
+ assert error["msg"] == "Project name 'config' is reserved!"
+ assert error["type"] == "value_error.projectnamereserved"
+
def test_invalid_dict(self):
with pytest.raises(ValidationError) as exc_info:
KiwiConfig(projects={
@@ -248,8 +264,9 @@ class TestProject:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Project Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'ProjectConfig' Format: " \
+ "{'random key 1': 'random value 1', 'random key 2': 'random value 2'}"
+ assert error["type"] == "value_error.invalidformat"
def test_coercible(self):
c = KiwiConfig(projects="project")
@@ -268,16 +285,16 @@ class TestProject:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Projects Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'projects' Format: ['valid', UnCoercible()]"
+ assert error["type"] == "value_error.invalidformat"
with pytest.raises(ValidationError) as exc_info:
KiwiConfig(projects=UnCoercible())
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Projects Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'projects' Format: UnCoercible()"
+ assert error["type"] == "value_error.invalidformat"
class TestEnvironment:
@@ -360,16 +377,16 @@ class TestEnvironment:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Environment Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'environment' Format: UnCoercible()"
+ assert error["type"] == "value_error.invalidformat"
with pytest.raises(ValidationError) as exc_info:
KiwiConfig(environment=["valid", UnCoercible()])
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Environment Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'environment' Format: None"
+ assert error["type"] == "value_error.invalidformat"
class TestStorage:
@@ -379,8 +396,8 @@ class TestStorage:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "No Storage Given"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Member 'KiwiConfig'.'storage' is required!"
+ assert error["type"] == "value_error.missingmember"
def test_dict(self):
kiwi_dict = {"directory": "/test/directory"}
@@ -395,8 +412,8 @@ class TestStorage:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'StorageConfig' Format: \"{'random key': 'random value'}\""
+ assert error["type"] == "value_error.invalidformat"
def test_str(self):
c = KiwiConfig(storage="/test/directory")
@@ -414,8 +431,8 @@ class TestStorage:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Storage Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'StorageConfig' Format: '{}'"
+ assert error["type"] == "value_error.invalidformat"
class TestNetwork:
@@ -425,8 +442,8 @@ class TestNetwork:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "No Network Given"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Member 'KiwiConfig'.'network' is required!"
+ assert error["type"] == "value_error.missingmember"
def test_dict(self):
kiwi_dict = {
@@ -470,5 +487,5 @@ class TestNetwork:
assert len(exc_info.value.errors()) == 1
error = exc_info.value.errors()[0]
- assert error["msg"] == "Invalid Network Format"
- assert error["type"] == "value_error"
+ assert error["msg"] == "Invalid 'KiwiConfig'.'network' Format: True"
+ assert error["type"] == "value_error.invalidformat"
From 5372db62e4dc2069de4945247f4b62c690ef22e8 Mon Sep 17 00:00:00 2001
From: ldericher <40151420+ldericher@users.noreply.github.com>
Date: Tue, 22 Feb 2022 13:24:57 +0100
Subject: [PATCH 135/135] version bump script
---
dist/bump-version.sh | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/dist/bump-version.sh b/dist/bump-version.sh
index 0fdcb4e..6022e8c 100755
--- a/dist/bump-version.sh
+++ b/dist/bump-version.sh
@@ -3,11 +3,11 @@
this="$(readlink -f "${0}")"
this_dir="$(dirname "${this}")"
-# git_branch="$(git rev-parse --abbrev-ref HEAD)"
-git_tag="$(git describe --abbrev=0)"
-version_str="${git_tag##*/}"
-version_str="0.2.0"
+git_branch="$(git rev-parse --abbrev-ref HEAD)"
+# git_tag="$(git describe --abbrev=0)"
+version_str="${git_branch##*/}"
+# version_str="0.2.0"
-sed -ri "s/(version\s*:).*$/\1 '${version_str}'/" "${this_dir}/../example/kiwi.yml"
+sed -ri "s/(version\s*:).*$/\1 ${version_str}/" "${this_dir}/../example/kiwi.yml"
sed -ri "s/(version\s*=\s*).*$/\1\"${version_str}\"/" "${this_dir}/../pyproject.toml"
sed -ri "s/(version.*=\s*).*$/\1\"${version_str}\"/" "${this_dir}/../kiwi_scp/config.py"