mirror of
https://github.com/yavook/kiwi-scp.git
synced 2024-11-21 20:33:00 +00:00
Merge branch 'release/0.0.1'
This commit is contained in:
commit
1e010d0761
57 changed files with 2273 additions and 180 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__pycache__/
|
4
.idea/.gitignore
vendored
Normal file
4
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
|
7
.idea/inspectionProfiles/profiles_settings.xml
Normal file
7
.idea/inspectionProfiles/profiles_settings.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
11
.idea/kiwi-config.iml
Normal file
11
.idea/kiwi-config.iml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Pipenv (kiwi-config)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Pipenv (kiwi-config)" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/kiwi-config.iml" filepath="$PROJECT_DIR$/.idea/kiwi-config.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jörn-Michael Miehe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
175
Makefile
175
Makefile
|
@ -1,175 +0,0 @@
|
|||
#########
|
||||
# CONFIGS
|
||||
|
||||
CONF_WILDC:=$(wildcard $(PWD)/*.conf)
|
||||
CONF_SOURCE:=$(patsubst %,. %;,$(CONF_WILDC))
|
||||
|
||||
# extraction of env variables from *.conf files
|
||||
confvalue=$(shell $(CONF_SOURCE) echo -n $${$(1)})
|
||||
|
||||
# docker network name
|
||||
CONF_DOCKERNET:=$(call confvalue,DOCKERNET)
|
||||
ifeq ($(CONF_DOCKERNET),)
|
||||
$(error DOCKERNET not set in $(CONF_WILDC))
|
||||
endif
|
||||
|
||||
# persistent data directory
|
||||
CONF_TARGETROOT:=$(call confvalue,TARGETROOT)
|
||||
ifeq ($(CONF_TARGETROOT),)
|
||||
$(error TARGETROOT not set in $(CONF_WILDC))
|
||||
endif
|
||||
|
||||
# suffix for project directories
|
||||
PROJ_SUFFX:=$(call confvalue,SUFFIX_PROJECT)
|
||||
ifeq ($(PROJ_SUFFX),)
|
||||
$(error SUFFIX_PROJECT not set in $(CONF_WILDC))
|
||||
endif
|
||||
|
||||
# suffix for disabled project directories
|
||||
DOWN_SUFFX:=$(call confvalue,SUFFIX_DOWN)
|
||||
ifeq ($(DOWN_SUFFX),)
|
||||
$(error SUFFIX_DOWN not set in $(CONF_WILDC))
|
||||
endif
|
||||
|
||||
#########
|
||||
# CONSTANTS
|
||||
|
||||
# file to store docker network cidr
|
||||
FILE_DOCKERNET:=$(CONF_TARGETROOT)/up-$(CONF_DOCKERNET)
|
||||
|
||||
# project directory handling
|
||||
PROJ_WILDC:=$(wildcard *$(PROJ_SUFFX))
|
||||
PROJ_NAMES:=$(basename $(PROJ_WILDC))
|
||||
|
||||
#########
|
||||
# FUNCTIONS
|
||||
|
||||
# different complexities of commands with root privileges
|
||||
# - in project directory
|
||||
projsudo=cd "$<"; sudo bash -c "$(1)"
|
||||
# - additionally with sourced *.conf files
|
||||
confprojsudo=$(call projsudo,$(CONF_SOURCE) $(1))
|
||||
# - only for compose: additionally with COMPOSE_PROJECT_NAME, CONFDIR and TARGETDIR set
|
||||
sudocompose=$(call confprojsudo,COMPOSE_PROJECT_NAME="$(basename $<)" CONFDIR="$(CONF_TARGETROOT)/conf" TARGETDIR="$(CONF_TARGETROOT)/$<" docker-compose $(1))
|
||||
|
||||
#########
|
||||
# TARGETS
|
||||
|
||||
# default target
|
||||
.PHONY: all
|
||||
all: purge-conf up
|
||||
|
||||
#########
|
||||
# manage the docker network (container name local DNS)
|
||||
$(FILE_DOCKERNET):
|
||||
sudo docker network create "$(CONF_DOCKERNET)" ||:
|
||||
sudo mkdir -p "$(CONF_TARGETROOT)"
|
||||
sudo chmod 700 "$(CONF_TARGETROOT)"
|
||||
sudo docker network inspect -f '{{(index .IPAM.Config 0).Subnet}}' "$(CONF_DOCKERNET)" | sudo tee "$@"
|
||||
|
||||
.PHONY: net-up
|
||||
net-up: $(FILE_DOCKERNET)
|
||||
|
||||
.PHONY: net-down
|
||||
net-down: down
|
||||
sudo docker network rm $(CONF_DOCKERNET)
|
||||
sudo rm $(FILE_DOCKERNET)
|
||||
|
||||
#########
|
||||
# sync project config directory to variable folder
|
||||
.PHONY: %-copyconf
|
||||
%-copyconf: %$(PROJ_SUFFX)
|
||||
@if [ -d "$</conf" ]; then \
|
||||
sudo rsync -r "$</conf" "$(CONF_TARGETROOT)"; \
|
||||
echo "Synced '$</conf' to '$(CONF_TARGETROOT)'"; \
|
||||
fi
|
||||
|
||||
.PHONY: purge-conf
|
||||
purge-conf:
|
||||
sudo rm -rf "$(CONF_TARGETROOT)/conf"
|
||||
|
||||
#########
|
||||
# manage all projects
|
||||
.PHONY: up down update
|
||||
up: net-up $(patsubst %,%-copyconf,$(PROJ_NAMES)) $(patsubst %,%-up,$(PROJ_NAMES))
|
||||
down: $(patsubst %,%-down,$(PROJ_NAMES))
|
||||
update: $(patsubst %,%-update,$(PROJ_NAMES))
|
||||
|
||||
#########
|
||||
# manage single project
|
||||
.PHONY: %-up
|
||||
%-up: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,up -d $(x))
|
||||
|
||||
.PHONY: %-down
|
||||
ifeq ($(x),)
|
||||
%-down: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,down)
|
||||
else
|
||||
%-down: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,stop $(x))
|
||||
$(call sudocompose,rm -f $(x))
|
||||
endif
|
||||
|
||||
.PHONY: %-pull
|
||||
%-pull: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,pull $(x))
|
||||
|
||||
.PHONY: %-build
|
||||
%-build: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,build --pull $(x))
|
||||
|
||||
.PHONY: %-logs
|
||||
%-logs: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,logs -t $(x)) 2>/dev/null | less -R +G
|
||||
|
||||
.PHONY: %-logf
|
||||
%-logf: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,logs -tf --tail=10 $(x)) ||:
|
||||
|
||||
s?=bash
|
||||
.PHONY: %-sh
|
||||
%-sh: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,exec $(x) $(s)) ||:
|
||||
|
||||
# enabling and disabling
|
||||
.PHONY: %-enable %-disable
|
||||
%-enable: %$(PROJ_SUFFX)$(DOWN_SUFFX)
|
||||
mv "$<" "$(basename $<)"
|
||||
%-disable: %$(PROJ_SUFFX)
|
||||
mv "$<" "$<$(DOWN_SUFFX)"
|
||||
|
||||
# Combinations
|
||||
.PHONY: %-update
|
||||
%-update: %$(PROJ_SUFFX) %-build %-pull
|
||||
$(MAKE) $(basename $<)-up
|
||||
|
||||
# Arbitrary compose command
|
||||
.PHONY: %-cmd
|
||||
%-cmd: %$(PROJ_SUFFX)
|
||||
$(call sudocompose,$(x))
|
||||
|
||||
#########
|
||||
# project creation
|
||||
.PHONY: %-new
|
||||
%-new:
|
||||
$(eval proj_dir:=$(patsubst %-new,%$(PROJ_SUFFX)$(DOWN_SUFFX),$@))
|
||||
mkdir $(proj_dir)
|
||||
$(eval export COMPOSEFILE)
|
||||
echo -e "$$COMPOSEFILE" > $(proj_dir)/docker-compose.yml
|
||||
|
||||
# default compose file
|
||||
define COMPOSEFILE
|
||||
version: "3"
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: $$DOCKERNET
|
||||
|
||||
services:
|
||||
something:
|
||||
image: maintainer/repo:tag
|
||||
restart: unless-stopped
|
||||
[...]
|
||||
endef
|
12
Pipfile
Normal file
12
Pipfile
Normal file
|
@ -0,0 +1,12 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
pyyaml = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
38
Pipfile.lock
generated
Normal file
38
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d518a36ed441568acff15b0a3c4b536738a55fb68801cdd682045be04d29954a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
|
||||
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
|
||||
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
|
||||
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
|
||||
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
|
||||
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
|
||||
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
|
||||
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
|
||||
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
|
||||
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
23
README.md
Normal file
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# kiwi-config
|
||||
|
||||
The simple tool for managing container servers
|
||||
|
||||
## Quick start
|
||||
|
||||
- Learn to use `docker` with `docker-compose`
|
||||
- Install kiwi-config
|
||||
- Look at [the example instance](../example)
|
||||
- Look at the output of `kiwi --help`
|
||||
- Start building your own instances
|
||||
|
||||
## Installation
|
||||
|
||||
```shell script
|
||||
curl 'https://raw.githubusercontent.com/ldericher/kiwi-config/master/install.sh' | sh
|
||||
```
|
||||
|
||||
That script checks for the basic dependencies of the `kiwi` command, then downloads the main script and installs it to a location of your choice. Please consider installing `kiwi` into a directory inside your $PATH.
|
||||
|
||||
## Get started
|
||||
|
||||
TODO
|
|
@ -1,5 +0,0 @@
|
|||
export SUFFIX_PROJECT=.project
|
||||
export SUFFIX_DOWN=.down
|
||||
|
||||
export DOCKERNET=kiwinet
|
||||
export TARGETROOT=/var/kiwi
|
24
example/hello-world.project/conf/html/index.html
Normal file
24
example/hello-world.project/conf/html/index.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Hello kiwi #2!</title>
|
||||
<style>
|
||||
body {
|
||||
width: 50em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Another Hello World!</h1>
|
||||
|
||||
<p>This service uses an off-the-shelf image and the conf-directory feature of kiwi-config.</p>
|
||||
|
||||
<p><em>Greetings, nginx.</em></p>
|
||||
</body>
|
||||
|
||||
</html>
|
55
example/hello-world.project/docker-compose.yml
Normal file
55
example/hello-world.project/docker-compose.yml
Normal file
|
@ -0,0 +1,55 @@
|
|||
version: "2"
|
||||
|
||||
networks:
|
||||
# reachable from outside
|
||||
default:
|
||||
driver: bridge
|
||||
# interconnects projects
|
||||
kiwi_hub:
|
||||
external:
|
||||
name: ${KIWI_HUB_NAME}
|
||||
|
||||
services:
|
||||
# simple loop producing (rather boring) logs
|
||||
greeter:
|
||||
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:
|
||||
build: web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
|
||||
# internal mariadb (mysql) instance with persistent storage
|
||||
db:
|
||||
image: mariadb:10
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- kiwi_hub
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: changeme
|
||||
volumes:
|
||||
- "${TARGETDIR}/db:/var/lib/mysql"
|
||||
|
||||
# admin interface for databases
|
||||
adminer:
|
||||
image: adminer:standalone
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
- kiwi_hub
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8081:8080"
|
||||
|
||||
# Another webserver just to show off the ${CONFDIR} variable
|
||||
another-web:
|
||||
image: nginx:stable-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:80"
|
||||
volumes:
|
||||
- "${CONFDIR}/html/index.html:/usr/share/nginx/html/index.html:ro"
|
2
example/hello-world.project/web/Dockerfile
Normal file
2
example/hello-world.project/web/Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
FROM nginx:stable-alpine
|
||||
COPY index.html /usr/share/nginx/html
|
31
example/hello-world.project/web/index.html
Normal file
31
example/hello-world.project/web/index.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Hello kiwi!</title>
|
||||
<style>
|
||||
body {
|
||||
width: 50em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hello World!</h1>
|
||||
<h2>kiwi-config works.</h2>
|
||||
|
||||
<p>This is an example service on a custom image assembled by kiwi-config.<br /> But wait, there's more!</p>
|
||||
|
||||
<a href="//localhost:8081"><h3>Adminer</h3></a>
|
||||
<p>There's a mySQL database included in this project. Login with user <q>root</q> and password <q>changeme</q>.</p>
|
||||
|
||||
<a href="//localhost:8082"><h3>Another hello world</h3></a>
|
||||
<p>Another web server, built in a different way.</p>
|
||||
|
||||
<p><em>Greetings, nginx.</em></p>
|
||||
</body>
|
||||
|
||||
</html>
|
19
example/kiwi.yml
Normal file
19
example/kiwi.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
######################################
|
||||
# kiwi-config instance configuration #
|
||||
######################################
|
||||
|
||||
version: '0.0.1'
|
||||
|
||||
runtime:
|
||||
storage: /tmp/kiwi
|
||||
shells:
|
||||
- /bin/bash
|
||||
env: null
|
||||
|
||||
markers:
|
||||
project: .project
|
||||
disabled: .disabled
|
||||
|
||||
network:
|
||||
name: kiwi_hub
|
||||
cidr: 10.22.46.0/24
|
85
install.sh
Executable file
85
install.sh
Executable file
|
@ -0,0 +1,85 @@
|
|||
#!/bin/sh
|
||||
|
||||
#############
|
||||
# CONSTANTS #
|
||||
#############
|
||||
|
||||
# dependencies to run kiwi-config
|
||||
KIWI_DEPS="bash python3 pipenv less"
|
||||
|
||||
##########
|
||||
# CHECKS #
|
||||
##########
|
||||
|
||||
printf "checking dependencies ... "
|
||||
|
||||
for dep in ${KIWI_DEPS}; do
|
||||
printf "%s, " "${dep}"
|
||||
|
||||
if ! command -v "${dep}" >/dev/null 2>/dev/null; then
|
||||
echo
|
||||
echo "Dependency '${dep}' not found, please install!" >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "OK"
|
||||
|
||||
########
|
||||
# MAIN #
|
||||
########
|
||||
|
||||
# prompt for installation directory
|
||||
install_dir_default="/usr/local/bin"
|
||||
valid="no"
|
||||
|
||||
while [ "${valid}" = "no" ]; do
|
||||
printf "Select installation directory [Default: '%s']: " "${install_dir_default}"
|
||||
read install_dir
|
||||
install_dir="${install_dir:-${install_dir_default}}"
|
||||
|
||||
# check
|
||||
if [ -d "${install_dir}" ]; then
|
||||
valid="yes"
|
||||
|
||||
else
|
||||
printf "Install directory doesn't exist. Try creating? [Y|n] "
|
||||
read yesno
|
||||
if [ ! "${yesno}" = "N" ] || [ ! "${yesno}" = "n" ]; then
|
||||
|
||||
# check creation failure
|
||||
if mkdir -p "${install_dir}"; then
|
||||
valid="yes"
|
||||
|
||||
else
|
||||
echo "Invalid install directory." >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# start actual installation
|
||||
printf "Installing into '%s' ... " "${install_dir}"
|
||||
|
||||
# install "kiwi" script
|
||||
uri="https://raw.githubusercontent.com/ldericher/kiwi-config/master/kiwi"
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
if ! curl -o "${tmp_file}" "${uri}" >/dev/null 2>/dev/null; then
|
||||
rm "${tmp_file}"
|
||||
echo "Download 'kiwi' failed!" >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! install -m 0755 "${tmp_file}" "${install_dir}/kiwi"; then
|
||||
rm "${tmp_file}"
|
||||
echo "Install 'kiwi' failed!" >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "${tmp_file}"
|
||||
|
||||
# finalization
|
||||
echo "OK"
|
||||
exit 0
|
125
kiwi
Executable file
125
kiwi
Executable file
|
@ -0,0 +1,125 @@
|
|||
#!/bin/bash
|
||||
|
||||
#############
|
||||
# CONSTANTS #
|
||||
#############
|
||||
|
||||
# base config filename
|
||||
KIWI_CONF_NAME="kiwi.yml"
|
||||
# version tag filename
|
||||
KIWI_VERSION_TAG="etc/version_tag"
|
||||
|
||||
# dependencies to run kiwi-config
|
||||
KIWI_DEPS=(docker docker-compose)
|
||||
# base install dir
|
||||
KIWI_BASEDIR="${HOME}/.local/lib/kiwi-config"
|
||||
# per-user env setup script
|
||||
KIWI_ENVFILE="${HOME}/.kiwienv"
|
||||
|
||||
# repository uri
|
||||
KIWI_REPO="https://github.com/ldericher/kiwi-config"
|
||||
# use latest version by default
|
||||
KIWI_VERSION="master"
|
||||
|
||||
###################
|
||||
# DYNAMIC STRINGS #
|
||||
###################
|
||||
|
||||
# directory of correct installation
|
||||
function kiwi_installdir() {
|
||||
echo "${KIWI_BASEDIR}/${KIWI_VERSION}"
|
||||
}
|
||||
|
||||
# src directory in installed version
|
||||
function kiwi_root() {
|
||||
echo "$(kiwi_installdir)/src"
|
||||
}
|
||||
|
||||
# main script in installed version
|
||||
function kiwi_executable() {
|
||||
echo "$(kiwi_root)/kiwi-config.py"
|
||||
}
|
||||
|
||||
# cache current work directory
|
||||
WORKDIR="$(pwd)"
|
||||
|
||||
##################
|
||||
# PER-USER SETUP #
|
||||
##################
|
||||
|
||||
# add in environment setup
|
||||
if [ -f "${KIWI_ENVFILE}" ]; then
|
||||
# shellcheck source=$HOME/.kiwienv
|
||||
source "${KIWI_ENVFILE}"
|
||||
fi
|
||||
|
||||
##########
|
||||
# CHECKS #
|
||||
##########
|
||||
|
||||
for dep in "${KIWI_DEPS[@]}"; do
|
||||
if ! command -v "${dep}" &>/dev/null; then
|
||||
echo "Dependency '${dep}' not found, please install!" >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
########
|
||||
# MAIN #
|
||||
########
|
||||
|
||||
# check if pwd is a kiwi folder
|
||||
if [ -f "./${KIWI_CONF_NAME}" ]; then
|
||||
# determine needed kiwi-config 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
|
||||
|
||||
# install if kiwi-config not found
|
||||
if [ ! -x "$(kiwi_executable)" ]; then
|
||||
echo -n "Installing kiwi-config v${KIWI_VERSION} into ${KIWI_BASEDIR} ... "
|
||||
|
||||
# switch to temp dir
|
||||
tmpdir=$(mktemp -d)
|
||||
cd "${tmpdir}" || :
|
||||
|
||||
# download archive
|
||||
curl -o "kiwi-config.zip" "${KIWI_REPO}/archive/${KIWI_VERSION}.zip"
|
||||
unzip "kiwi-config.zip"
|
||||
|
||||
# read archive version tag
|
||||
cd "kiwi-config-${KIWI_VERSION}" || :
|
||||
KIWI_VERSION=$(cat "./src/${KIWI_VERSION_TAG}")
|
||||
|
||||
# install archive
|
||||
mkdir -p "$(kiwi_installdir)"
|
||||
mv "src" "Pipfile" "Pipfile.lock" "$(kiwi_installdir)/"
|
||||
|
||||
# discard temp dir
|
||||
cd "${WORKDIR}" || :
|
||||
rm -rf "${tmpdir}"
|
||||
|
||||
echo "OK"
|
||||
fi
|
||||
|
||||
# check virtualenv
|
||||
cd "$(kiwi_installdir)" || :
|
||||
if ! pipenv --venv &>/dev/null; then
|
||||
# install virtualenv
|
||||
echo -n "Preparing virtualenv ... "
|
||||
pipenv sync &>/dev/null
|
||||
echo "OK"
|
||||
fi
|
||||
|
||||
# go back to the original work directory
|
||||
cd "${WORKDIR}" || :
|
||||
|
||||
# setup main environment
|
||||
KIWI_ROOT="$(kiwi_root)"
|
||||
|
||||
export KIWI_ROOT
|
||||
export KIWI_CONF_NAME
|
||||
export PIPENV_VERBOSITY=-1
|
||||
|
||||
# run main script
|
||||
exec pipenv run "$(kiwi_executable)" "${@}"
|
23
src/etc/command_help.txt
Normal file
23
src/etc/command_help.txt
Normal file
|
@ -0,0 +1,23 @@
|
|||
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
|
||||
clean Cleanly sync all configs to target folder, then relaunch affected projects
|
||||
purge Remove all running docker artifacts of this instance
|
||||
|
||||
Commands for Instance Management:
|
||||
config Create or configure kiwi-config instance
|
||||
inspect Inspect 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
|
22
src/etc/docker-compose_default.yml
Normal file
22
src/etc/docker-compose_default.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
version: "2"
|
||||
|
||||
networks:
|
||||
# reachable from outside
|
||||
default:
|
||||
driver: bridge
|
||||
# interconnects projects
|
||||
kiwi_hub:
|
||||
external:
|
||||
name: ${KIWI_HUB_NAME}
|
||||
|
||||
services:
|
||||
# an example service
|
||||
something:
|
||||
# uses an image
|
||||
image: maintainer/repo:tag
|
||||
# will get restarted
|
||||
restart: unless-stopped
|
||||
# is also connected to the instance hub
|
||||
networks:
|
||||
- default
|
||||
- kiwi_hub
|
12
src/etc/kiwi_default.yml
Normal file
12
src/etc/kiwi_default.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
version:
|
||||
runtime:
|
||||
storage: /var/kiwi
|
||||
shells:
|
||||
- /bin/bash
|
||||
env: null
|
||||
markers:
|
||||
project: .project
|
||||
disabled: .disabled
|
||||
network:
|
||||
name: kiwi_hub
|
||||
cidr: 10.22.46.0/24
|
3
src/etc/kiwi_header.yml
Normal file
3
src/etc/kiwi_header.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
######################################
|
||||
# kiwi-config instance configuration #
|
||||
######################################
|
9
src/etc/kiwi_help.txt
Normal file
9
src/etc/kiwi_help.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
kiwi-config is the tool for container server management.
|
||||
|
||||
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
|
1
src/etc/version_tag
Normal file
1
src/etc/version_tag
Normal file
|
@ -0,0 +1 @@
|
|||
0.0.1
|
2
src/images/rsync.Dockerfile
Normal file
2
src/images/rsync.Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
FROM alpine:latest
|
||||
RUN apk --no-cache add rsync
|
39
src/kiwi-config.py
Executable file
39
src/kiwi-config.py
Executable file
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# system
|
||||
import logging
|
||||
|
||||
# local
|
||||
import kiwi
|
||||
|
||||
|
||||
def set_verbosity(logger, handler, verbosity):
|
||||
"""set logging default verbosity level and format"""
|
||||
|
||||
if verbosity >= 2:
|
||||
log_level = logging.DEBUG
|
||||
log_format = "[%(asctime)s] %(levelname)s @ %(filename)s:%(funcName)s:%(lineno)d: %(message)s"
|
||||
elif verbosity >= 1:
|
||||
log_level = logging.INFO
|
||||
log_format = "[%(asctime)s] %(levelname)s: %(message)s"
|
||||
else:
|
||||
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.verbosity())
|
||||
|
||||
# run the app
|
||||
if not kiwi.run():
|
||||
quit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
20
src/kiwi/__init__.py
Normal file
20
src/kiwi/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# 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'
|
||||
]
|
36
src/kiwi/_constants.py
Normal file
36
src/kiwi/_constants.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# system
|
||||
import os
|
||||
|
||||
|
||||
#############
|
||||
# ENVIRONMENT
|
||||
|
||||
# location of "src" directory to use
|
||||
KIWI_ROOT = os.getenv('KIWI_ROOT', ".")
|
||||
# default name of kiwi-config file
|
||||
KIWI_CONF_NAME = os.getenv('KIWI_CONF_NAME', "kiwi.yml")
|
||||
|
||||
|
||||
############
|
||||
# FILE NAMES
|
||||
|
||||
# text files inside kiwi-config "src" directory
|
||||
HEADER_KIWI_CONF_NAME = f"{KIWI_ROOT}/etc/kiwi_header.yml"
|
||||
DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/etc/kiwi_default.yml"
|
||||
VERSION_TAG_NAME = f"{KIWI_ROOT}/etc/version_tag"
|
||||
DEFAULT_DOCKER_COMPOSE_NAME = f"{KIWI_ROOT}/etc/docker-compose_default.yml"
|
||||
KIWI_HELP_TEXT_NAME = f"{KIWI_ROOT}/etc/kiwi_help.txt"
|
||||
COMMAND_HELP_TEXT_NAME = f"{KIWI_ROOT}/etc/command_help.txt"
|
||||
|
||||
# special config directory in projects
|
||||
CONF_DIRECTORY_NAME = 'conf'
|
||||
# location for auxiliary Dockerfiles
|
||||
IMAGES_DIRECTORY_NAME = f"{KIWI_ROOT}/images"
|
||||
|
||||
|
||||
####################
|
||||
# DOCKER IMAGE NAMES
|
||||
|
||||
# name for auxiliary docker images
|
||||
LOCAL_IMAGES_NAME = 'localhost/kiwi-config/auxiliary'
|
||||
DEFAULT_IMAGE_NAME = 'alpine:latest'
|
157
src/kiwi/config.py
Normal file
157
src/kiwi/config.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
# system
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
|
||||
# local
|
||||
from ._constants import KIWI_CONF_NAME, HEADER_KIWI_CONF_NAME, DEFAULT_KIWI_CONF_NAME, VERSION_TAG_NAME
|
||||
|
||||
|
||||
class Config:
|
||||
"""represents a kiwi.yml"""
|
||||
|
||||
__yml_content = {}
|
||||
__keys = {
|
||||
'version': "kiwi-config version to use in this instance",
|
||||
|
||||
'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
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
if cls.__instance is None:
|
||||
# create singleton
|
||||
cls.__instance = cls()._update_from_file(DEFAULT_KIWI_CONF_NAME)
|
||||
|
||||
# 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 = {}
|
||||
|
||||
@classmethod
|
||||
def get(cls, directory='.'):
|
||||
if directory not in LoadedConfig.__instances:
|
||||
# create singleton for new path
|
||||
result = DefaultConfig.get()
|
||||
|
||||
# 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.")
|
||||
|
||||
LoadedConfig.__instances[directory] = result
|
||||
|
||||
# return singleton
|
||||
return LoadedConfig.__instances[directory]
|
77
src/kiwi/executable.py
Normal file
77
src/kiwi/executable.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
# system
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# local
|
||||
from .config import LoadedConfig
|
||||
|
||||
|
||||
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!")
|
||||
|
||||
|
||||
class Executable:
|
||||
class __Executable:
|
||||
__exe_path = None
|
||||
|
||||
def __init__(self, exe_name):
|
||||
self.__exe_path = _find_exe_file(exe_name)
|
||||
|
||||
def __build_cmd(self, args, kwargs):
|
||||
cmd = [self.__exe_path, *args]
|
||||
|
||||
logging.debug(f"Executable cmd{cmd}, kwargs{kwargs}")
|
||||
return cmd
|
||||
|
||||
def run(self, process_args, **kwargs):
|
||||
return subprocess.run(
|
||||
self.__build_cmd(process_args, kwargs),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def Popen(self, process_args, **kwargs):
|
||||
return subprocess.Popen(
|
||||
self.__build_cmd(process_args, kwargs),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def run_less(self, process_args, **kwargs):
|
||||
kwargs['stdout'] = subprocess.PIPE
|
||||
kwargs['stderr'] = subprocess.DEVNULL
|
||||
|
||||
process = self.Popen(
|
||||
process_args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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)
|
37
src/kiwi/misc.py
Normal file
37
src/kiwi/misc.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
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 PROGRESS', '!')}\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'
|
66
src/kiwi/parser.py
Normal file
66
src/kiwi/parser.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# 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)
|
133
src/kiwi/project.py
Normal file
133
src/kiwi/project.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from .executable import Executable
|
||||
|
||||
from ._constants import CONF_DIRECTORY_NAME
|
||||
from .config import LoadedConfig
|
||||
|
||||
|
||||
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'],
|
||||
'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
|
83
src/kiwi/projects.py
Normal file
83
src/kiwi/projects.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
import os
|
||||
|
||||
from kiwi.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
|
86
src/kiwi/rootkit.py
Normal file
86
src/kiwi/rootkit.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# system
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# local
|
||||
from ._constants import IMAGES_DIRECTORY_NAME, LOCAL_IMAGES_NAME, DEFAULT_IMAGE_NAME
|
||||
from .executable import Executable
|
||||
|
||||
|
||||
def _prefix_path(prefix, path):
|
||||
if isinstance(path, str):
|
||||
abs_path = os.path.abspath(path)
|
||||
return os.path.realpath(f"{prefix}/{abs_path}")
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Rootkit:
|
||||
class __Rootkit:
|
||||
__image_tag = None
|
||||
|
||||
def __init__(self, image_tag=None):
|
||||
self.__image_tag = image_tag
|
||||
|
||||
def __exists(self):
|
||||
ps = Executable('docker').run([
|
||||
'images',
|
||||
'--filter', f"reference={_image_name(self.__image_tag)}",
|
||||
'--format', '{{.Repository}}:{{.Tag}}'
|
||||
], stdout=subprocess.PIPE)
|
||||
|
||||
return str(ps.stdout, 'utf-8').strip() == _image_name(self.__image_tag)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
73
src/kiwi/runner.py
Normal file
73
src/kiwi/runner.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
# 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)
|
128
src/kiwi/subcommand.py
Normal file
128
src/kiwi/subcommand.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
# 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-config 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)
|
42
src/kiwi/subcommands/__init__.py
Normal file
42
src/kiwi/subcommands/__init__.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# local
|
||||
from ._hidden import ConfCopyCommand, ConfPurgeCommand, NetUpCommand
|
||||
|
||||
from .build import BuildCommand
|
||||
from .clean import CleanCommand
|
||||
from .cmd import CmdCommand
|
||||
from .config import ConfigCommand
|
||||
from .disable import DisableCommand
|
||||
from .down import DownCommand
|
||||
from .enable import EnableCommand
|
||||
from .inspect import InspectCommand
|
||||
from .logs import LogsCommand
|
||||
from .new import NewCommand
|
||||
from .pull import PullCommand
|
||||
from .purge import PurgeCommand
|
||||
from .push import PushCommand
|
||||
from .shell import ShellCommand
|
||||
from .up import UpCommand
|
||||
from .update import UpdateCommand
|
||||
|
||||
__all__ = [
|
||||
'ConfCopyCommand',
|
||||
'ConfPurgeCommand',
|
||||
'NetUpCommand',
|
||||
|
||||
'BuildCommand',
|
||||
'CleanCommand',
|
||||
'CmdCommand',
|
||||
'ConfigCommand',
|
||||
'DisableCommand',
|
||||
'DownCommand',
|
||||
'EnableCommand',
|
||||
'InspectCommand',
|
||||
'LogsCommand',
|
||||
'NewCommand',
|
||||
'PullCommand',
|
||||
'PurgeCommand',
|
||||
'PushCommand',
|
||||
'ShellCommand',
|
||||
'UpCommand',
|
||||
'UpdateCommand',
|
||||
]
|
106
src/kiwi/subcommands/_hidden.py
Normal file
106
src/kiwi/subcommands/_hidden.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# system
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
# local
|
||||
from .._constants import CONF_DIRECTORY_NAME
|
||||
from ..executable import Executable
|
||||
from ..subcommand import SubCommand
|
||||
from ..config import LoadedConfig
|
||||
from ..projects import Projects
|
||||
from ..rootkit import Rootkit, prefix_path_mnt
|
||||
|
||||
|
||||
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 conf_dirs:
|
||||
# add target directory
|
||||
conf_dirs.append(LoadedConfig.get()['runtime:storage'])
|
||||
logging.info(f"Sync directories: {conf_dirs}")
|
||||
|
||||
Rootkit('rsync').run([
|
||||
'rsync', '-r', *prefix_path_mnt(conf_dirs)
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ConfPurgeCommand(SubCommand):
|
||||
"""kiwi conf-purge"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'conf-purge',
|
||||
action="Removing all configs for", add_parser=False,
|
||||
description="Remove all config files in target directory"
|
||||
)
|
||||
|
||||
def _run_instance(self, runner, args):
|
||||
conf_target = f"{LoadedConfig.get()['runtime:storage']}/{CONF_DIRECTORY_NAME}"
|
||||
logging.info(f"Purging directories: {conf_target}")
|
||||
|
||||
Rootkit().run([
|
||||
'rm', '-rf', prefix_path_mnt(conf_target)
|
||||
], 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}}'
|
||||
], 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
|
18
src/kiwi/subcommands/build.py
Normal file
18
src/kiwi/subcommands/build.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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
|
37
src/kiwi/subcommands/clean.py
Normal file
37
src/kiwi/subcommands/clean.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from ..projects import Projects
|
||||
from ..subcommand import SubCommand
|
||||
|
||||
|
||||
class CleanCommand(SubCommand):
|
||||
"""kiwi clean"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'clean',
|
||||
action="Cleaning all configs for",
|
||||
description="Cleanly sync all configs to target folder, then relaunch affected projects"
|
||||
)
|
||||
|
||||
def _run_instance(self, runner, args):
|
||||
result = True
|
||||
|
||||
affected_projects = [
|
||||
project.get_name()
|
||||
for project in Projects.from_dir()
|
||||
if project.has_configs()
|
||||
]
|
||||
|
||||
for project_name in affected_projects:
|
||||
args.projects = project_name
|
||||
result &= runner.run('down')
|
||||
|
||||
# cleanly sync configs
|
||||
result &= runner.run('conf-purge')
|
||||
result &= runner.run('conf-copy')
|
||||
|
||||
# bring projects back up
|
||||
for project_name in affected_projects:
|
||||
args.projects = project_name
|
||||
result &= runner.run('up')
|
||||
|
||||
return result
|
40
src/kiwi/subcommands/cmd.py
Normal file
40
src/kiwi/subcommands/cmd.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 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
|
64
src/kiwi/subcommands/config.py
Normal file
64
src/kiwi/subcommands/config.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# system
|
||||
import logging
|
||||
import os
|
||||
|
||||
# local
|
||||
from .._constants import KIWI_CONF_NAME
|
||||
from ..subcommand import SubCommand
|
||||
from ..config import DefaultConfig, LoadedConfig
|
||||
|
||||
|
||||
class ConfigCommand(SubCommand):
|
||||
"""kiwi config"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'config',
|
||||
action=f"Configuring '{KIWI_CONF_NAME}' in",
|
||||
description="Create or configure kiwi-config 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
|
16
src/kiwi/subcommands/disable.py
Normal file
16
src/kiwi/subcommands/disable.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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()
|
35
src/kiwi/subcommands/down.py
Normal file
35
src/kiwi/subcommands/down.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# local
|
||||
from ..subcommand import ServiceCommand
|
||||
from ..misc import are_you_sure
|
||||
|
||||
|
||||
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):
|
||||
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",
|
||||
]):
|
||||
return super()._run_instance(runner, args)
|
||||
|
||||
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
|
16
src/kiwi/subcommands/enable.py
Normal file
16
src/kiwi/subcommands/enable.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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()
|
98
src/kiwi/subcommands/inspect.py
Normal file
98
src/kiwi/subcommands/inspect.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
# system
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
|
||||
# local
|
||||
from ..subcommand import ServiceCommand
|
||||
from ..project import Project
|
||||
from ..projects import Projects
|
||||
|
||||
|
||||
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 InspectCommand(ServiceCommand):
|
||||
"""kiwi inspect"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'inspect', num_projects='?', num_services='*',
|
||||
action="Inspecting",
|
||||
description="Inspect projects in this instance, services inside a project or service(s) inside a project"
|
||||
)
|
||||
|
||||
def _run_instance(self, runner, args):
|
||||
print(f"kiwi-config 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
|
40
src/kiwi/subcommands/logs.py
Normal file
40
src/kiwi/subcommands/logs.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 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
|
30
src/kiwi/subcommands/new.py
Normal file
30
src/kiwi/subcommands/new.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# 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
|
18
src/kiwi/subcommands/pull.py
Normal file
18
src/kiwi/subcommands/pull.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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
|
45
src/kiwi/subcommands/purge.py
Normal file
45
src/kiwi/subcommands/purge.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# system
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
# local
|
||||
from ._hidden import _find_net
|
||||
from ..subcommand import SubCommand
|
||||
from ..config import LoadedConfig
|
||||
from ..executable import Executable
|
||||
from ..misc import are_you_sure
|
||||
|
||||
|
||||
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
|
18
src/kiwi/subcommands/push.py
Normal file
18
src/kiwi/subcommands/push.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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
|
104
src/kiwi/subcommands/shell.py
Normal file
104
src/kiwi/subcommands/shell.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
# system
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
# local
|
||||
from ..subcommand import ServiceCommand
|
||||
from ..config import LoadedConfig
|
||||
|
||||
|
||||
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
|
26
src/kiwi/subcommands/up.py
Normal file
26
src/kiwi/subcommands/up.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# 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
|
37
src/kiwi/subcommands/update.py
Normal file
37
src/kiwi/subcommands/update.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# local
|
||||
from ..subcommand import ServiceCommand
|
||||
from ..misc import are_you_sure
|
||||
|
||||
|
||||
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
|
Loading…
Reference in a new issue