Compare commits

...
Sign in to create a new pull request.

28 commits

Author SHA1 Message Date
Yax
9d2d099ab7 New Post 2025-04-02 20:38:29 +02:00
Yax
af6fa158fc Ignore uv.lock 2025-04-02 18:50:31 +02:00
Yax
0762185a61 Remove uv.lock 2025-04-02 18:49:20 +02:00
Yax
3957cb8738 Reduce docker image 2025-04-02 18:40:49 +02:00
Yax
22a8fd6ba8 Remove unit tests 2025-04-02 13:36:39 +02:00
Yax
7a41f3cecf Clean-up Makefile 2025-04-02 13:36:16 +02:00
Yax
145d45efeb Use slim nginx docker image and remove github actions 2025-04-01 19:21:41 +02:00
Yax
b279dd1d3a Typo
Some checks failed
docker / build (push) Has been cancelled
2025-03-30 13:27:08 +02:00
Yax
6f92874a43 New post
Some checks are pending
docker / build (push) Waiting to run
2025-03-30 13:21:46 +02:00
Yax
791f803123 Git clone from zaclys
Some checks are pending
docker / build (push) Waiting to run
2025-03-30 12:08:54 +02:00
Yax
5f86a11e88 Zaclys migration
Some checks are pending
docker / build (push) Waiting to run
2025-03-29 19:46:46 +01:00
Yax
ab2b93ce82 Docker build 2025-03-28 20:38:59 +01:00
Yax
548c495c27 Git pull from Codeberg 2025-03-23 18:08:02 +01:00
Yax
e0dcda1c47 pacman cache 2025-03-09 12:00:15 +01:00
Yax
4ab90b0993 Add redirections 2025-01-20 19:17:35 +01:00
Yax
dbb700f349 new post: analyse de logs 2025-01-11 19:36:21 +01:00
Yax
5e5a0e87fa typo 2024-12-21 22:37:46 +01:00
Yax
44c074a070 Typo 2024-12-21 21:26:10 +01:00
Yax
ddbe809a79 New Post "retour en auto-hébergement" 2024-12-21 19:42:56 +01:00
Yax
d3f7e42bf9 Fix docker init 2024-12-09 17:16:30 +01:00
Yax
9944b69815 Revert "Update default config"
This reverts commit 181ca6b6c1.
2024-12-09 16:59:47 +01:00
Yax
181ca6b6c1 Update default config 2024-12-09 16:55:43 +01:00
Yax
05db93f7cd Fix python compatibility with Alpine 2024-12-09 15:47:40 +01:00
Yax
2b9ccf13d4 Update docker image name 2024-12-09 15:17:12 +01:00
Yax
7286c3047c Remove pyinstaller config 2024-12-09 15:08:41 +01:00
Yax
dd4a6784d2
Merge pull request #6 from kianby/migrate-rye-to-uv
Replace poetry /rye with uv
2024-12-09 15:06:31 +01:00
Yax
4411b0c23b Replace poetry /rye with uv 2024-12-09 15:05:33 +01:00
Yax
ad89cefc87 new post: Nord theme 2024-09-14 19:33:59 +02:00
35 changed files with 526 additions and 791 deletions

View file

@ -1,14 +0,0 @@
name: docker
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest

4
.gitignore vendored
View file

@ -15,5 +15,5 @@ blog.sublime-project
blog.sublime-workspace
ssl/
.python-version
poetry.toml
.local
.local
uv.lock

View file

@ -1,19 +0,0 @@
language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install commonmark coverage coveralls
script:
- python -m unittest discover -bv
- coverage run --branch --source=. -m unittest discover -bv
- coverage report -m
after_success:
- coveralls

View file

@ -1,35 +1,12 @@
FROM nginx:1.19.0-alpine
FROM nginx:1.27.4-alpine-slim
RUN apk update
RUN apk add --no-cache build-base bash git python3 make tzdata curl py3-pip libressl-dev musl-dev libffi-dev python3-dev cargo
RUN curl -sSL https://install.python-poetry.org | python3 - --version 1.4.0
RUN poetry config virtualenvs.create false
# install poetry
#ENV POETRY_HOME=/opt/poetry
#RUN python3 -m venv $POETRY_HOME
#RUN $POETRY_HOME/bin/pip install --upgrade pip
#RUN $POETRY_HOME/bin/pip install setuptools_rust poetry==1.4.0
#RUN $POETRY_HOME/bin/poetry --version
RUN apk update && apk add --no-cache bash git python3 make tzdata curl py3-pip musl-locales
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && mv /root/.local/bin/uv /usr/local/bin
COPY docker/nginx.conf /etc/nginx/nginx.conf
# install locales
ENV MUSL_LOCALE_DEPS cmake make musl-dev gcc gettext-dev libintl
ENV MUSL_LOCPATH /usr/share/i18n/locales/musl
RUN apk add --no-cache \
$MUSL_LOCALE_DEPS \
&& wget https://gitlab.com/rilian-la-te/musl-locales/-/archive/master/musl-locales-master.zip \
&& unzip musl-locales-master.zip \
&& cd musl-locales-master \
&& cmake -DLOCALE_PROFILE=OFF -D CMAKE_INSTALL_PREFIX:PATH=/usr . && make && make install \
&& cd .. && rm -r musl-locales-master
# set timezone and locale
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime
RUN echo "Europe/Paris" > /etc/timezone
RUN cp /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo "Europe/Paris" > /etc/timezone
ENV TZ Europe/Paris
ENV LANG fr_FR.UTF-8
ENV LANGUAGE fr_FR.UTF-8

View file

@ -1,5 +1,6 @@
# Makefile
#
# Makefile
.PHONY: build
# if a file .local exists run site locally
ifeq ($(wildcard .local),)
@ -8,7 +9,7 @@ else
TARGET = site_local
endif
site: $(TARGET)
site: site_local
echo $(TARGET)
site_remote:
@ -17,43 +18,14 @@ site_remote:
systemctl reload nginx
site_local:
rye run python makesite.py --params params-local.json
uv run python makesite.py --params params-local.json
cd _site && python -m SimpleHTTPServer 2> /dev/null || python3 -m http.server
dock: site_local
$(shell docker start --interactive bloglocal || docker run --name bloglocal -p 80:80 -p 443:443 -v `pwd`/_site:/usr/share/nginx/html:ro -v `pwd`/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v `pwd`/nginx/dhparam.pem:/etc/nginx/dhparam.pem:ro -v `pwd`/ssl:/etc/nginx/ssl:ro nginx)
# docker build
build:
docker build -t source.madyanne.fr/yax/blog .
# docker publish
publish:
docker push source.madyanne.fr/yax/blog
undock:
docker stop bloglocal
certs:
mkdir -p ssl
cd ssl ; wget -N https://traefik.me/cert.pem
cd ssl ; wget -N https://traefik.me/chain.pem
cd ssl ; wget -N https://traefik.me/fullchain.pem
cd ssl ; wget -N https://traefik.me/privkey.pem
venv2:
virtualenv ~/.venv/makesite
echo . ~/.venv/makesite/bin/activate > venv
. ./venv && pip install commonmark coverage
venv: FORCE
python3 -m venv ~/.venv/makesite
echo . ~/.venv/makesite/bin/activate > venv
. ./venv && pip install commonmark coverage
test: FORCE
. ./venv && python -m unittest -bv
coverage:
. ./venv && coverage run --branch --source=. -m unittest discover -bv; :
. ./venv && coverage report -m
. ./venv && coverage html
clean:
find . -name "__pycache__" -exec rm -r {} +
find . -name "*.pyc" -exec rm {} +
rm -rf .coverage htmlcov
FORCE:

View file

@ -7,3 +7,4 @@ This static blog generator code is under MIT license.
"Blog du Yax" content is under [CC-BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/3.0)
Moved as private repository to gitea.zaclys.net on 2025-03-29.

View file

@ -1,2 +0,0 @@
#!/bin/sh
pyinstaller makesite.py --onefile

View file

@ -1,20 +1,19 @@
#!/bin/bash
#export POETRY_HOME=/opt/poetry
python -V
# clone and build blog
cd /
rm -rf /blog
git clone https://github.com/kianby/blog.git
git clone https://gitea.zaclys.com/yannic/blog.git
cd /blog
#$POETRY_HOME/bin/poetry install
#$POETRY_HOME/bin/poetry run make
poetry install
poetry run make
uv python pin 3.12.9
uv sync
uv run python ./makesite.py
# nginx serve
#nginx -g 'daemon off;'
nginx
# exit on change in stacosys or Git repo
$POETRY_HOME/bin/poetry run python3 monitor.py
uv run python monitor.py

View file

@ -45,6 +45,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# redirections to preserve inbound links
rewrite ^/migration-du-blog-sous-pelican.html$ https://blogduyax.madyanne.fr/2013/migration-du-blog-sous-pelican/ redirect;
rewrite ^/2017/nextcloud-securite/$ https://blogduyax.madyanne.fr/2017/securite-des-donnees-focus-sur-nextcloud/ redirect;
rewrite ^/installation-de-shinken.html$ https://blogduyax.madyanne.fr/2014/installation-de-shinken/ redirect;
rewrite ^/retour-dexperience-ubuntu-touch.html$ https://blogduyax.madyanne.fr/2015/retour-dexperience-ubuntu-touch/ redirect;
rewrite ^/haute-disponibilite-avec-corosync-et-pacemaker.html$ https://blogduyax.madyanne.fr/2013/haute-disponibilite-avec-corosync-et-pacemaker/ redirect;
rewrite ^/feeds/all.atom.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
rewrite ^/feed.xml$ https://blogduyax.madyanne.fr/rss.xml redirect;
}
}

View file

@ -1,44 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['makesite.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='makesite',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -12,6 +12,10 @@ Supprimer les dépendances des paquets orphelins
pacman -Rsn $(pacman -Qtdq)
Nettoyage du cache pacman des archives de +90 jours
find ./ -maxdepth 1 -type f -mtime +90 -print0 | xargs -0 sudo /bin/rm -f
## *Downgrader* des paquets
Récupérer la liste des upgraded ([source](https://wiki.archlinux.org/title/Downgrading_packages))
@ -36,6 +40,7 @@ for i in $(cat /tmp/packages); do sudo pacman --noconfirm -U "$i"*.zst; done
(potentiel problème d'ordre, réarranger le fichier /tmp/packages en fonction des dépendances entre les paquets)
# Fedora
Historique des transactions **dnf**

View file

@ -0,0 +1,11 @@
<!-- title: Thème : cap au Nord -->
<!-- category: GNU/Linux -->
Je ne pratique pas le *ricing* et je me contente d'un thème par défaut sur mes environnements de bureau, que ce soit Gnome, Mate ou XFCE, surtout que les thèmes fournis par défaut sont généralement bien soignés de nos jours. Mais il y a un domaine où j'ai mes exigences, c'est le terminal : c'est important de ne pas s'esquinter les yeux. Or une palette de couleur mal choisie peut compliquer la vie avec la coloration syntaxique à gogo. D'abord séduit par le classieux Solaris Dark je me suis tourné depuis deux ans vers le thème Nord. Moins contrasté et très homogène, je l'ai progressivement intégré à mes applications quotidiennes : Alacritty, Tmux, NeoVim, IntelliJ, Firefox, la page de recherche DuckDuckGo, le thème GTK. Bref je ne fais pas de *ricing* mais j'ai déjà abordé quelques applications de bureau.
Mes sources d'informations sont les suivantes :
- Sven Greb, celui par qui [tout a commencé](https://github.com/orgs/nordtheme/discussions/183) et son site officiel [NordTheme](https://www.nordtheme.com/)
- la communauté sur Reddit : [r/nordtheme](https://www.reddit.com/r/nordtheme/)
et la configuration de mes postes (toujours gérée par [ChezMoi](https://www.chezmoi.io/)) est sur [mon GitHub](https://github.com/kianby/dotfiles) et répliquée sur [mon Gitea](https://source.madyanne.fr/yax/dotfiles).

View file

@ -0,0 +1,22 @@
<!-- title: Retour en auto-hébergement -->
<!-- category: Hébergement -->
Après une dizaine d'années à héberger mes services chez différents hébergeurs je reviens à de l'auto-hébergement sur du matériel à la maison. Et quelle évolution au niveau matériel : en 2013 c'était un portable Céléron peut véloce dans un placard. En cette fin d'année j'ai acquis un mini-pc sur les conseils avisés de [MiniMachines](https://www.minimachines.net) : un Beelink Mini S12 Pro: pour la taille d'un Rubik's Cube on a un processeur Intel N100 (de la famille Alder Lake) appuyé par 16 Go de RAM et 512 Go de stockage en M.2. Ça coûte un peu plus de 200 euros et c'est très raisonnable par rapport à ce qu'on obtient en retour : une machine avec une consommation électrique sobre, quasi-silencieuse et des performances suffisantes pour des usages standards en auto-hébergement.
Pour moi l'objectif est double :
- financier d'abord avec l'opportunité de réduire mes factures bien que je sois très satisfait de mes services externes : résilier le cloud Infomaniak et le petit VPS me fera économiser d'une bonne centaine d'euros par an.
- apprendre en s'amusant : refaire de l'administration système, découvrir des nouveaux services à héberger, ça ouvre plein de possibilités.
Joli projet sur le papier mais moins simple qu'en 2012 car l'Internet est sacrément plus hostile et une grosse réflexion sur la sécurité s'est imposée avant douvrir quoi que ce soit sur Internet. Finalement la solution sera basée sur l'hyperviseur Proxmox qui apporte une souplesse sur les types de déploiement en permettant de mixer des conteneurs LXC et des machines virtuelles KVM et d'apporter une brique de sécurité avec un pare-feu à multiple niveaux. Le but étant d'isoler autant que possible les parties exposées du réseau domestique. Les machines virtuelles [regrouperont des services Docker](https://github.com/kianby/selfhosting/tree/config-vm1) exposés indirectement par un proxy NginX.
![Proxmox](/images/2024/proxmox.svg)
Le proxy NginX est directement exposé sur Internet via une redirection des ports HTTP / HTTPs depuis la box internet. C'est un container LXC Alpine avec une installation de [Nginx Proxy Manager](https://nginxproxymanager.com/) modifiée pour que les services ne s'exécutent par avec le super-utilisateur *root*. Le minimum de paquets est installé (surtout pas de service SSHD ni de SSH) et il s'administre par l'interface Web de Nginx Proxy Manager depuis le réseau local qui n'est évidemment pas exposée à l'extérieur. Il a deux cartes réseau virtuelles : une adresse sur le réseau local et l'autre sur le réseau privé constitué des machines virtuelles exécutant les services. Le proxy sert aussi de passerelle de sortie aux machines virtuelles : le routage est activé entre les deux interfaces et l'interface du réseau privé est *bridgée* sur l'interface réseau locale.
Les machines virtuelles exécutant les services appartiennent au réseau privé et le pare-feu de l'hyperviseur bloque le trafic pour qu'elles ne puisse communiquer qu'avec la passerelle. En cas de compromission elles n'ont accès ni au réseau local ni au bastion. Chaque machine virtuelle est accessible par SSH depuis le bastion, qui est un simple conteneur LXC Alpine avec deux cartes réseau qui permet par rebond d'accéder aux machines virtuelles depuis un PC du réseau local. Excepté la console Web de Proxmox c'est le seul moyen d'accéder aux machines virtuelles. Les accès SSH sont protégés par échange de clefs (aucun accès autorisé par mot de passe) et seul le bastion est autorisé. Corollaire : n'importe quelle machine du réseau local ayant un accès SSH au bastion peut accéder aux machines virtuelles.
![Archi réseau](/images/2024/archi-lan.svg)
Voilà c'est sûrement perfectible mais j'ai jugé la solution suffisamment sécurisée pour la mettre en service. La machine "vm2" avec Nextcloud et Immlich est encore un projet mais la machine "vm1" exécutant tous mes services de base est déjà opérationnelle.

View file

@ -0,0 +1,27 @@
<!-- title: Analyse de logs -->
<!-- category: Hébergement -->
Après quelques semaines [sur ma nouvelle installation de serveur](/2024/retour-en-auto-hebergement/) j'ai eu besoin de visibilité sur les visites pour estimer si la sécurité devait être renforcée. De longue date j'avais mis en favori l'outil [GoAccess](https://goaccess.io) et il semblait correspondre à ce que je cherchais, à savoir un outil passif de génération de rapports basé sur les logs d'accès du serveur HTTP : pas d'analyse des IP source mais des statistiques d'accès par site, une répartition des *User Agent* utilisés avec une reconnaissance des bots (ou crawlers) qui représentent le plus gros du trafic, les erreurs HTTP, les URI les plus recherchées ...
A ma grande satisfaction j'ai mis en place GoAccess en moins de deux heures avec deux étapes :
1. l'installation sur le container LXC Alpine. GoAccess est écrit en langage C et a peu de dépendances pour la compilation. Avec deux ou trois dépendances de librairies ajoutées, la compilation sur le container a été simple.
2. le travail principal consiste à décrire le format du log à analyser dans le formalisme de GoAccess
Nginx Proxy Manager utilise un format de log commun pour tous les proxy ce qui m'a simplifié la tâche. La documentation de GoAccess est exemplaire, notamment le formalisme des logs (https://goaccess.io/man#custom-log).
Pour ce format de log Nginx :
```
log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"';
```
j'ai défini ces paramètres de lancement :
```
--datetime-format='%d/%b/%Y:%H:%M:%S %z'
--log-format='[%x] - %^ %s - %m %^ %v "%U" [Client %h] [Length %b] [Gzip %^] [Sent-to %^] "%u" "%R"'
```
GoAccess peut générer son rapport HTML à partir d'une liste de fichiers de logs à la demande ou en temps-réel avec un lancement en tâche de fond (option --real-time-html --daemonize). L'option temps-réel est sympa avec une web socket qui rafraîchit la page du navigateur automatiquement mais c'est consommateur en CPU sur ce type d'installation en mini-pc où l'un des objectifs est la sobriété énergétique. J'ai préféré opter pour une génération horaire par une tâche planifiée avec CRON.
GoAccess répond à mon besoin et j'ai commencé à analyser les données pour préparer une phase de renforcement de la sécurité.

View file

@ -0,0 +1,8 @@
<!-- title: Est-ce que tout est politique ? -->
<!-- category: Humeur -->
Le retour en auto-hébergement avec [l'achat d'un mini-pc](/2024/retour-en-auto-hebergement/) a été ma meilleure idée de fin d'année. Et ce n'est pas suffisant car j'ai encore beaucoup de dépendance à des services U.S. Même si je voyais d'un œil inquiet la réélection de Donald (pas le canard rigolo qui a bercé mon enfance... l'autre), jamais je n'aurais imaginé le système fasciste qui se met en place depuis le début d'année. Naïf, je pensais qu'il y avait des garde-fous, qu'on ne pouvait pas poignarder un système démocratique sans qu'une instance siffle un arrêt de jeu, mais je me trompais lourdement. Je ne rentrerais pas dans le détail des mesures appliquées par ce nouveau gouvernement sinon cet article va devenir trop triste ; j'ai juste le cœur qui saigne pour tous ceux qui en souffrent, sûrement une vilaine faiblesse de mon côté woke ;-)
Depuis tant d'années nous baignons dans ce monde du partage communautaire et multiculturel autour de Linux, des projets libres et opensource. Alors est-ce qu'on fait de la politique ? Pas forcément consciemment mais nous essayons de vivre en accord avec nos valeur et nos goûts reflètent une vision du monde (pas tel qu'il est malheureusement mais plutôt comme nous le rêvons). Le durcissement des gouvernements au détriment de l'humain (en piétinant d'abord les minorités) et de la nature, le repli en mode écureuil (tiens si je prends le Groenland je pourrais assurer le même train de vie pendant encore quelques dizaines d'année et faire croire que tout va bien ?) nous expose peu à peu. Si on n'est pas d'accord avec cela car nous avons d'autres valeurs plus humanistes alors je crains que nous ayons une posture politique à l'insu de nous-mêmes même :-) Le fait qu'on ait jamais voté ou qu'on soit fidèle aux urnes ne change rien à l'affaire : il y a une majorité silencieuse qui n'approuve pas, ne défile pas, du moins j'aime à le croire. Quelle est la limite qui fait basculer de l'indignation à l'action, je me pose la question... à moi-même en premier lieu.
L'année va être intéressante mais pas forcément joyeuse. Prenez soin de vous, mentalement aussi, car la période est malsaine.

View file

@ -0,0 +1,20 @@
<!-- title: Migration Git et optimisation Docker -->
<!-- category: Hébergement -->
J'ai quitté GitHub (entre autres) ! Enfin presque car je n'ai pas encore recensé les sites où mon compte GitHub sert d'authentification mais j'ai fait le plus dur : supprimer tous mes projets et ne migrer que ceux encore actifs ; un peu dur car supprimer des projets, mêmes archivés, pour un développeur c'est un crève-cœur mais le spleen est vite passé avec l'engouement de repartir à zéro sur une plate-forme plus conforme à mes valeurs, la forge de [Zaclys](https://www.zaclys.com) et je les remercie grandement d'avoir ajouté ce service à leur offre "Famille".
Après migration, il me reste quatre projets actifs (hé oui il ne reste que ça) à savoir mes configurations (dotfiles), le déploiement par docker compose (selfhosting), le gestionnaire de commentaires (stacosys) et le blog. Vous avez noté la subtilité pour être retrouvé sur la [forge de Zaclys](https://gitea.zaclys.com) sans donner explicitement le lien de mon espace dans l'article ;-)
Sur mon infrastructure domestique (le mini-pc Beelink propulsé par l'hyperviseur Proxmox) j'ai déjà une instance Gitea qui réplique mes projets à l'extérieur, ça me garantit d'avoir une copie à jour en cas de gros pépin. Bref après avoir migré mes projets je me suis trouvé devant la problématique de construire et publier les images Docker de stacosys et du blog puisque cette tâche était dévolue aux actions GitHub. Je ne sais pas si la forge Zaclys propose un équivalent mais je trouve inefficace de publier ailleurs des images prêtes à l'emploi qui ne servent qu'à moi, pour ensuite les rapatrier en local. En plus dans la foulée de GitHub j'avais bien apprécié de supprimer mon compte Docker Hub enfin désactiver car la suppression ne semble pas possible :-(
En cherchant une alternative pour construire mes images localement et les transférer vers mon infra, j'ai pensé à Gitea qui a aussi la fonction de "docker registry" appelée *packages*. Je suis donc parti là dessus, une publication à la demande, pas de CI ; vu le peu d'activité des projets ça fait parfaitement l'affaire.
> C'est quand on paye de sa poche qu'on prend conscience de la valeur des choses
Après avoir mis ça en place... un changement d'URL dans 2 projets j'ai trouvé assez long la publication de l'image du blog dans Gitea alors que tout est sur le LAN. Et pour cause, elle faisait 1,2 Go alors que celle de stacosys tient dans 74 Mo. Évidemment, en tout automatisé dans une CI ça ne m'avait pas alerté auparavant. Me voilà donc à m'intéresser à un génial outil d'analyse d'images [Dive](https://github.com/wagoodman/dive) qu'on peut même exécuter en container,
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock docker.io/wagoodman/dive"
à lire un peu de littérature sur les build multi-stages et à analyser où ça pêche... Bon, ça pêche à plusieurs endroits : trop de couches, installation d'outils de développement pour construire les locales MUSL de la distribution Alpine alors que sur les versions récentes elles sont proposés dans les dépôts. Après une phase d'optimisation j'ai pu ramener l'image à 128 Mo, honnête à mon sens pour un container avec Nginx, Python et des traitements Bash qui font du "git clone".

View file

@ -1,28 +1,19 @@
[project]
name = "blog"
version = "1.1"
version = "1.2"
description = "Blog du Yax"
readme = "README.md"
authors = [
{ name = "Yax" }
]
readme = "README.md"
requires-python = ">= 3.8"
requires-python = ">=3.12.9"
dependencies = [
"requests>=2.31.0",
"pygments>=2.17.1",
"mistune>=3.0.2",
"pygments>=2.18.0",
"requests>=2.32.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = [
"black>=23.10.1",
"pyinstaller>=6.1.0",
[dependency-groups]
dev = [
"black>=24.10.0",
]
[tool.hatch.metadata]
allow-direct-references = true

View file

@ -1,29 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
-e file:.
altgraph==0.17.4
black==23.11.0
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
idna==3.4
mistune==3.0.2
mypy-extensions==1.0.0
packaging==23.2
pathspec==0.11.2
platformdirs==4.0.0
pygments==2.17.1
pyinstaller==6.2.0
pyinstaller-hooks-contrib==2023.10
requests==2.31.0
tomli==2.0.1
typing-extensions==4.8.0
urllib3==2.1.0
# The following packages are considered to be unsafe in a requirements file:
setuptools==68.2.2

View file

@ -1,16 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
-e file:.
certifi==2023.11.17
charset-normalizer==3.3.2
idna==3.4
mistune==3.0.2
pygments==2.17.1
requests==2.31.0
urllib3==2.1.0

View file

@ -0,0 +1,47 @@
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
dmz: {
nginx proxy: {
icon: https://icons.terrastruct.com/dev%2Fnginx.svg
shape: rectangle
}
}
private network: {
label.near: bottom-center
virtual machines: {
icon: https://icons.terrastruct.com/gcp%2FProducts%20and%20services%2FNetworking%2FVirtual%20Private%20Cloud.svg
style.multiple: true
containers: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
style.multiple: true
}
}
}
lan: {
bastion: {
icon: https://icons.terrastruct.com/dev%2Fssh.svg
}
devices: {
icon: https://icons.terrastruct.com/tech%2Flaptop.svg
style.multiple: true
}
}
internet box: {
icon: https://icons.terrastruct.com/tech%2Frouter.svg
width: 130
}
internet box -> dmz.nginx proxy: http/https
lan.bastion -> private network.virtual machines: ssh
dmz.nginx proxy -> private network.virtual machines: http

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1,76 @@
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 4
}
}
Proxmox: {
icon: https://www.svgrepo.com/download/331552/proxmox.svg
label: "Hyperviseur Proxmox"
Containers: {
label: "Containers LXC"
Nginx_Proxy: {
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
label: "Nginx Proxy"
}
Bastion: {
icon: https://upload.wikimedia.org/wikipedia/commons/d/dd/Linux_Containers_logo.svg
label: "Bastion"
}
}
vm1: {
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
label: "vm1"
Heimdall: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Heimdall"
}
Blog: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Blog"
}
Selfoss: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Selfoss"
}
Wallabag: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Wallabag"
}
Shaarli: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Shaarli"
}
}
vm2: {
icon : https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Kvmbanner-logo2_1.png/320px-Kvmbanner-logo2_1.png
label: "vm2"
style: {
stroke-dash: 3
}
Nextcloud: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Nextcloud"
}
Immich: {
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
label: "Immich"
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 177 KiB

View file

View file

@ -1,16 +0,0 @@
import os
import tempfile
import shutil
def temppath(*paths):
return os.path.join(tempfile.gettempdir(), *paths)
def move(src, dst):
if os.path.isfile(dst):
os.remove(dst)
elif os.path.isdir(dst):
shutil.rmtree(dst)
if os.path.exists(src):
os.rename(src, dst)

View file

@ -1,104 +0,0 @@
import unittest
import shutil
import os
import makesite
from test import path
class ContentTest(unittest.TestCase):
def setUp(self):
self.blog_path = path.temppath('blog')
self.undated_path = os.path.join(self.blog_path, 'foo.txt')
self.dated_path = os.path.join(self.blog_path, '2018-01-01-foo.txt')
self.normal_post_path = os.path.join(self.blog_path, 'baz.txt')
self.md_post_path = os.path.join(self.blog_path, 'qux.md')
self.no_md_post_path = os.path.join(self.blog_path, 'qux.txt')
os.makedirs(self.blog_path)
with open(self.undated_path, 'w') as f:
f.write('hello world')
with open(self.dated_path, 'w') as f:
f.write('hello world')
with open(self.normal_post_path, 'w') as f:
f.write('<!-- a: 1 -->\n<!-- b: 2 -->\nFoo')
with open(self.md_post_path, 'w') as f:
f.write('*Foo*')
with open(self.no_md_post_path, 'w') as f:
f.write('*Foo*')
def tearDown(self):
shutil.rmtree(self.blog_path)
# Rudimentary mock because unittest.mock is unavailable in Python 2.7.
def mock(self, *args):
self.mock_args = args
def test_content_content(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['content'], 'hello world')
def test_content_date(self):
content = makesite.read_content(self.dated_path)
self.assertEqual(content['date'], '2018-01-01')
def test_content_date_missing(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['date'], '1970-01-01')
def test_content_slug_dated(self):
content = makesite.read_content(self.dated_path)
self.assertEqual(content['slug'], 'foo')
def test_content_slug_undated(self):
content = makesite.read_content(self.undated_path)
self.assertEqual(content['slug'], 'foo')
def test_content_headers(self):
content = makesite.read_content(self.normal_post_path)
self.assertEqual(content['a'], '1')
self.assertEqual(content['b'], '2')
self.assertEqual(content['content'], 'Foo')
def test_markdown_rendering(self):
content = makesite.read_content(self.md_post_path)
self.assertEqual(content['content'], '<p><em>Foo</em></p>\n')
def test_markdown_import_error(self):
makesite._test = 'ImportError'
original_log = makesite.log
makesite.log = self.mock
self.mock_args = None
content = makesite.read_content(self.md_post_path)
makesite._test = None
makesite.log = original_log
self.assertEqual(content['content'], '*Foo*')
self.assertEqual(self.mock_args,
('WARNING: Cannot render Markdown in {}: {}',
self.md_post_path, 'Error forced by test'))
def test_no_markdown_rendering(self):
content = makesite.read_content(self.no_md_post_path)
self.assertEqual(content['content'], '*Foo*')
def test_no_markdown_import_error(self):
makesite._test = 'ImportError'
original_log = makesite.log
makesite.log = self.mock
self.mock_args = None
content = makesite.read_content(self.no_md_post_path)
makesite._test = None
makesite.log = original_log
self.assertEqual(content['content'], '*Foo*')
self.assertIsNone(self.mock_args)

View file

@ -1,39 +0,0 @@
import unittest
import os
import shutil
import makesite
from test import path
class FileIOTest(unittest.TestCase):
"""Tests for file I/O functions."""
def test_fread(self):
text = 'foo\nbar\n'
filepath = path.temppath('foo.txt')
with open(filepath, 'w') as f:
f.write(text)
text_read = makesite.fread(filepath)
os.remove(filepath)
self.assertEqual(text_read, text)
def test_fwrite(self):
text = 'baz\nqux\n'
filepath = path.temppath('foo.txt')
makesite.fwrite(filepath, text)
with open(filepath) as f:
text_read = f.read()
os.remove(filepath)
self.assertEqual(text_read, text)
def test_fwrite_makedir(self):
text = 'baz\nqux\n'
dirpath = path.temppath('foo', 'bar')
filepath = os.path.join(dirpath, 'foo.txt')
makesite.fwrite(filepath, text)
with open(filepath) as f:
text_read = f.read()
self.assertTrue(os.path.isdir(dirpath))
shutil.rmtree(path.temppath('foo'))
self.assertEqual(text_read, text)

View file

@ -1,43 +0,0 @@
import unittest
import makesite
class HeaderTest(unittest.TestCase):
"""Tests for read_headers() function."""
def test_single_header(self):
text = '<!-- key1: val1 -->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('key1', 'val1', 19)])
def test_multiple_headers(self):
text = '<!-- key1: val1 -->\n<!-- key2: val2-->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('key1', 'val1', 20), ('key2', 'val2', 38)])
def test_headers_and_text(self):
text = '<!-- a: 1 -->\n<!-- b: 2 -->\nFoo\n<!-- c: 3 -->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 14), ('b', '2', 28)])
def test_headers_and_blank_line(self):
text = '<!-- a: 1 -->\n<!-- b: 2 -->\n\n<!-- c: 3 -->\n'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 14),
('b', '2', 29),
('c', '3', 43)])
def test_multiline_header(self):
text = '<!--\na: 1 --><!-- b:\n2 -->\n<!-- c: 3\n-->'
headers = list(makesite.read_headers(text))
self.assertEqual(headers, [('a', '1', 13),
('b', '2', 27),
('c', '3', 40)])
def test_no_header(self):
headers = list(makesite.read_headers('Foo'))
self.assertEqual(headers, [])
def test_empty_string(self):
headers = list(makesite.read_headers(''))
self.assertEqual(headers, [])

View file

@ -1,46 +0,0 @@
import unittest
import shutil
import os
import makesite
from test import path
class PagesTest(unittest.TestCase):
def setUp(self):
self.site_path = path.temppath('site')
def tearDown(self):
shutil.rmtree(self.site_path)
def test_list(self):
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
dst = os.path.join(self.site_path, 'list.txt')
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout)
with open(os.path.join(self.site_path, 'list.txt')) as f:
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')
def test_list_params(self):
posts = [{'content': 'Foo', 'title': 'foo'},
{'content': 'Bar', 'title': 'bar'}]
dst = os.path.join(self.site_path, 'list.txt')
list_layout = '<div>{{ key }}:{{ title }}:{{ content }}</div>'
item_layout = '<p>{{ key }}:{{ title }}:{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout,
key='val', title='lorem')
with open(os.path.join(self.site_path, 'list.txt')) as f:
text = f.read()
self.assertEqual(text,
'<div>val:lorem:<p>val:foo:Foo</p><p>val:bar:Bar</p></div>')
def test_dst_params(self):
posts = [{'content': 'Foo'}, {'content': 'Bar'}]
dst = os.path.join(self.site_path, '{{ key }}.txt')
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ content }}</p>'
makesite.make_list(posts, dst, list_layout, item_layout, key='val')
expected_path = os.path.join(self.site_path, 'val.txt')
self.assertTrue(os.path.isfile(expected_path))
with open(expected_path) as f:
self.assertEqual(f.read(), '<div><p>Foo</p><p>Bar</p></div>')

View file

@ -1,73 +0,0 @@
import unittest
import makesite
import os
import shutil
import json
from test import path
class MainTest(unittest.TestCase):
def setUp(self):
path.move('_site', '_site.backup')
path.move('params.json', 'params.json.backup')
def tearDown(self):
path.move('_site.backup', '_site')
path.move('params.json.backup', 'params')
def test_site_missing(self):
makesite.main()
def test_site_exists(self):
os.mkdir('_site')
with open('_site/foo.txt', 'w') as f:
f.write('foo')
self.assertTrue(os.path.isfile('_site/foo.txt'))
makesite.main()
self.assertFalse(os.path.isfile('_site/foo.txt'))
def test_default_params(self):
makesite.main()
with open('_site/blog/proin-quam/index.html') as f:
s1 = f.read()
with open('_site/blog/rss.xml') as f:
s2 = f.read()
shutil.rmtree('_site')
self.assertIn('<a href="/">Home</a>', s1)
self.assertIn('<title>Proin Quam - Lorem Ipsum</title>', s1)
self.assertIn('Published on 2018-01-01 by <b>Admin</b>', s1)
self.assertIn('<link>http://localhost:8000/</link>', s2)
self.assertIn('<link>http://localhost:8000/blog/proin-quam/</link>', s2)
def test_json_params(self):
params = {
'base_path': '/base',
'subtitle': 'Foo',
'author': 'Bar',
'site_url': 'http://localhost/base'
}
with open('params.json', 'w') as f:
json.dump(params, f)
makesite.main()
with open('_site/blog/proin-quam/index.html') as f:
s1 = f.read()
with open('_site/blog/rss.xml') as f:
s2 = f.read()
shutil.rmtree('_site')
self.assertIn('<a href="/base/">Home</a>', s1)
self.assertIn('<title>Proin Quam - Foo</title>', s1)
self.assertIn('Published on 2018-01-01 by <b>Bar</b>', s1)
self.assertIn('<link>http://localhost/base/</link>', s2)
self.assertIn('<link>http://localhost/base/blog/proin-quam/</link>', s2)

View file

@ -1,127 +0,0 @@
import unittest
import os
import shutil
import makesite
from test import path
class PagesTest(unittest.TestCase):
def setUp(self):
self.blog_path = path.temppath('blog')
self.site_path = path.temppath('site')
os.makedirs(self.blog_path)
with open(os.path.join(self.blog_path, 'foo.txt'), 'w') as f:
f.write('Foo')
with open(os.path.join(self.blog_path, 'bar.txt'), 'w') as f:
f.write('Bar')
with open(os.path.join(self.blog_path, '2018-01-01-foo.txt'), 'w') as f:
f.write('Foo')
with open(os.path.join(self.blog_path, '2018-01-02-bar.txt'), 'w') as f:
f.write('Bar')
with open(os.path.join(self.blog_path, 'header-foo.txt'), 'w') as f:
f.write('<!-- tag: foo -->Foo')
with open(os.path.join(self.blog_path, 'header-bar.txt'), 'w') as f:
f.write('<!-- title: bar -->Bar')
with open(os.path.join(self.blog_path, 'placeholder-foo.txt'), 'w') as f:
f.write('<!-- title: foo -->{{ title }}:{{ author }}:Foo')
with open(os.path.join(self.blog_path, 'placeholder-bar.txt'), 'w') as f:
f.write('<!-- title: bar --><!-- render: yes -->{{ title }}:{{ author }}:Bar')
def tearDown(self):
shutil.rmtree(self.blog_path)
shutil.rmtree(self.site_path)
def test_pages_undated(self):
src = os.path.join(self.blog_path, '[fb]*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>Bar</div>')
def test_pages_dated(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>Bar</div>')
def test_pages_layout_params(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ slug }}:{{ title }}:{{ date }}:{{ content }}</div>'
makesite.make_pages(src, dst, tpl, title='Lorem')
with open(os.path.join(self.site_path, 'foo.txt')) as f:
self.assertEqual(f.read(), '<div>foo:Lorem:2018-01-01:Foo</div>')
with open(os.path.join(self.site_path, 'bar.txt')) as f:
self.assertEqual(f.read(), '<div>bar:Lorem:2018-01-02:Bar</div>')
def test_pages_return_value(self):
src = os.path.join(self.blog_path, '2*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
posts = makesite.make_pages(src, dst, tpl)
self.assertEqual(len(posts), 2)
self.assertEqual(posts[0]['date'], '2018-01-02')
self.assertEqual(posts[1]['date'], '2018-01-01')
def test_content_header_params(self):
# Test that header params from one post is not used in another
# post.
src = os.path.join(self.blog_path, 'header*.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '{{ title }}:{{ tag }}:{{ content }}'
makesite.make_pages(src, dst, tpl)
with open(os.path.join(self.site_path, 'header-foo.txt')) as f:
self.assertEqual(f.read(), '{{ title }}:foo:Foo')
with open(os.path.join(self.site_path, 'header-bar.txt')) as f:
self.assertEqual(f.read(), 'bar:{{ tag }}:Bar')
def test_content_no_rendering(self):
# Test that placeholders are not populated in content rendering
# by default.
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin')
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
self.assertEqual(f.read(), '<div>{{ title }}:{{ author }}:Foo</div>')
def test_content_rendering_via_kwargs(self):
# Test that placeholders are populated in content rendering when
# requested in make_pages.
src = os.path.join(self.blog_path, 'placeholder-foo.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin', render='yes')
with open(os.path.join(self.site_path, 'placeholder-foo.txt')) as f:
self.assertEqual(f.read(), '<div>foo:Admin:Foo</div>')
def test_content_rendering_via_header(self):
# Test that placeholders are populated in content rendering when
# requested in content header.
src = os.path.join(self.blog_path, 'placeholder-bar.txt')
dst = os.path.join(self.site_path, '{{ slug }}.txt')
tpl = '<div>{{ content }}</div>'
makesite.make_pages(src, dst, tpl, author='Admin')
with open(os.path.join(self.site_path, 'placeholder-bar.txt')) as f:
self.assertEqual(f.read(), '<div>bar:Admin:Bar</div>')
def test_rendered_content_in_summary(self):
# Test that placeholders are populated in summary if and only if
# content rendering is enabled.
src = os.path.join(self.blog_path, 'placeholder*.txt')
post_dst = os.path.join(self.site_path, '{{ slug }}.txt')
list_dst = os.path.join(self.site_path, 'list.txt')
post_layout = ''
list_layout = '<div>{{ content }}</div>'
item_layout = '<p>{{ summary }}</p>'
posts = makesite.make_pages(src, post_dst, post_layout, author='Admin')
makesite.make_list(posts, list_dst, list_layout, item_layout)
with open(os.path.join(self.site_path, 'list.txt')) as f:
self.assertEqual(f.read(), '<div><p>{{ title }}:{{ author }}:Foo</p><p>bar:Admin:Bar</p></div>')

View file

@ -1,78 +0,0 @@
import unittest
import os
import shutil
from test import path
class PathTest(unittest.TestCase):
def test_temppath(self):
self.assertTrue(path.temppath())
def test_move_existing_file(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
with open(src, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertTrue(os.path.isfile(dst))
with open(dst) as f:
text = f.read()
os.remove(dst)
self.assertEqual(text, 'foo')
def test_move_missing_file(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertFalse(os.path.isfile(dst))
def test_move_file_cleanup(self):
src = os.path.join(path.temppath(), 'foo.txt')
dst = os.path.join(path.temppath(), 'bar.txt')
with open(dst, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isfile(src))
self.assertFalse(os.path.isfile(dst))
def test_move_existing_dir(self):
src = os.path.join(path.temppath(), 'foo')
srcf = os.path.join(src, 'foo.txt')
dst = os.path.join(path.temppath(), 'bar')
dstf = os.path.join(dst, 'foo.txt')
os.makedirs(src)
with open(srcf, 'w') as f:
f.write('foo')
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertTrue(os.path.isdir(dst))
with open(dstf) as f:
text = f.read()
shutil.rmtree(dst)
self.assertEqual(text, 'foo')
def test_move_missing_dir(self):
src = os.path.join(path.temppath(), 'foo')
dst = os.path.join(path.temppath(), 'bar')
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertFalse(os.path.isdir(dst))
def test_move_dir_cleanup(self):
src = os.path.join(path.temppath(), 'foo')
dst = os.path.join(path.temppath(), 'bar')
os.makedirs(dst)
path.move(src, dst)
self.assertFalse(os.path.isdir(src))
self.assertFalse(os.path.isdir(dst))

View file

@ -1,25 +0,0 @@
import unittest
import makesite
class RenderTest(unittest.TestCase):
"""Tests for render() function."""
def test_oneline_template(self):
tpl = 'foo {{ key1 }} baz {{ key2 }}'
out = makesite.render(tpl, key1='bar', key2='qux')
self.assertEqual(out, 'foo bar baz qux')
def test_multiline_template(self):
tpl = 'foo {{ key1 }}\nbaz {{ key1 }}'
out = makesite.render(tpl, key1='bar')
self.assertEqual(out, 'foo bar\nbaz bar')
def test_repeated_key(self):
tpl = 'foo {{ key1 }} baz {{ key1 }}'
out = makesite.render(tpl, key1='bar')
self.assertEqual(out, 'foo bar baz bar')
def test_multiline_placeholder(self):
tpl = 'foo {{\nkey1\n}} baz {{\nkey2\n}}'
out = makesite.render(tpl, key1='bar', key2='qux')
self.assertEqual(out, 'foo bar baz qux')

View file

@ -1,13 +0,0 @@
import unittest
import makesite
class RFC822DateTest(unittest.TestCase):
def test_epoch(self):
self.assertEqual(makesite.rfc_2822_format('1970-01-01'),
'Thu, 01 Jan 1970 00:00:00 +0000')
def test_2018_06_16(self):
self.assertEqual(makesite.rfc_2822_format('2018-06-16'),
'Sat, 16 Jun 2018 00:00:00 +0000')

View file

@ -1,9 +0,0 @@
import unittest
import makesite
class TruncateTest(unittest.TestCase):
def test_truncate(self):
long_text = ' \n'.join('word' + str(i) for i in range(50))
expected_text = ' '.join('word' + str(i) for i in range(25))
self.assertEqual(makesite.truncate(long_text), expected_text)