diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ba6098 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/*.conf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4984ea6 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +NAME = netoik-cicd +VERSION = $(shell [ -d ".git" ] && git describe | sed "s/-/./g") +BRANCH = $(shell [ -d ".git" ] && git branch --show-current) + +RPM_SOURCEDIR = $(shell rpm --eval "%{_sourcedir}") + +SYSCONFDIR = $(shell rpm --eval "%{_sysconfdir}") +UNITDIR = $(shell rpm --eval "%{_unitdir}") +BINDIR = $(shell rpm --eval "%{_bindir}") +TMPDIR = $(shell rpm --eval "%{_tmppath}") + +.PHONY: build +build: bin/$(NAME)-deployer bin/$(NAME)-newtag + +bin/$(NAME)-deployer: src/deployer.sh + mkdir --parents bin + cp "$<" "$@" + +bin/$(NAME)-newtag: src/newtag.sh + mkdir --parents bin + cp "$<" "$@" + +.PHONY: name +name: + @echo "$(NAME)" + +.PHONY: version +version: + @echo "$(VERSION)" + +$(RPM_SOURCEDIR)/$(NAME)-%.tar.gz: * + git archive --format=tar.gz --output="$@" --prefix="$(NAME)-$(VERSION)/" --worktree-attributes --verbose "$(BRANCH)" + +.PHONY: tarball +tarball: $(RPM_SOURCEDIR)/$(NAME)-$(VERSION).tar.gz + +.PHONY: install +install: + install -D --no-target-directory deployer.conf.sample "$(DESTDIR)$(SYSCONFDIR)/$(NAME)/deployer.conf" + install -D --target-directory="$(DESTDIR)$(SYSCONFDIR)/$(NAME)" deployer.conf.sample + install -D --target-directory="$(DESTDIR)$(SYSCONFDIR)/profile.d" profile/$(NAME)-git.sh + install -D --target-directory="$(DESTDIR)$(UNITDIR)" systemd/$(NAME)-deployer.service + install -D --target-directory="$(DESTDIR)$(BINDIR)" bin/$(NAME)-* + install --directory "$(DESTDIR)$(TMPDIR)/$(NAME)/deployer/request" + install --directory "$(DESTDIR)$(TMPDIR)/$(NAME)/deployer/response" diff --git a/deployer.conf.sample b/deployer.conf.sample new file mode 100644 index 0000000..b3521f7 --- /dev/null +++ b/deployer.conf.sample @@ -0,0 +1,7 @@ +REQUEST_DIR="/var/tmp/netoik-cicd/deployer/request" +RESPONSE_DIR="/var/tmp/netoik-cicd/deployer/response" +REPOS_DIR="/var/gogs/repositories/samuel" +RPMS_DIR="/home/git/rpmbuild/RPMS" +RPM_ARCH="x86_64" +RPM_RELEASE="1" +RPM_DIST="el8_5" diff --git a/netoik-cicd.spec b/netoik-cicd.spec new file mode 100644 index 0000000..c8c60a2 --- /dev/null +++ b/netoik-cicd.spec @@ -0,0 +1,60 @@ +%define debug_package %{nil} + +Name: netoik-cicd +Version: %(make version) +Release: 1%{?dist} +Summary: Netoik Continuous Deployment tool + +License: GPLv3 +Source0: %{name}-%{version}.tar.gz + +BuildArch: x86_64 +BuildRequires: make +Requires: bash,rpm-build,rpmdevtools,inotify-tools + +%description +Netoik Continuous Deployment tool + +%prep +%autosetup -v + +%build +%make_build + +%install +%make_install + +%pre +# Build rpm setuptree if not already done. +runuser --login git --command "rpmdev-setuptree" + +%post +# Reload systemctl daemon and (re)start service. +systemctl daemon-reload +systemctl restart %{name}-deployer.service +systemctl enable %{name}-deployer.service + +%preun +# Stop service only if uninstalling. +if [ $1 -eq 0 ]; then + systemctl disable --now %{name}-deployer.service +fi + +%postun +# Reload systemctl daemon only if uninstalling. +if [ $1 -eq 0 ]; then + systemctl daemon-reload +fi + +%files +%attr(755, root, root) %dir %{_sysconfdir}/%{name} +%attr(644, root, root) %config(noreplace) %{_sysconfdir}/%{name}/deployer.conf +%attr(644, root, root) %{_sysconfdir}/%{name}/deployer.conf.sample +%attr(644, root, root) %{_sysconfdir}/profile.d/%{name}-git.sh +%attr(644, root, root) %{_unitdir}/%{name}-deployer.service +%attr(755, root, root) %{_bindir}/%{name}-deployer +%attr(755, root, root) %{_bindir}/%{name}-newtag +%attr(755, root, root) %dir %{_tmppath}/%{name} +%attr(755, root, root) %dir %{_tmppath}/%{name}/deployer +%attr(775, root, git) %dir %{_tmppath}/%{name}/deployer/request +%attr(775, root, root) %dir %{_tmppath}/%{name}/deployer/response diff --git a/profile/netoik-cicd-git.sh b/profile/netoik-cicd-git.sh new file mode 100644 index 0000000..ea23b90 --- /dev/null +++ b/profile/netoik-cicd-git.sh @@ -0,0 +1,6 @@ +source "/etc/netoik-cicd/deployer.conf" + +if [ "$(id --user --name)" = "git" ]; then + NETOIK_CICD_DEPLOYER_RESPONSE_DIR="$REQUEST_DIR" + NETOIK_CICD_DEPLOYER_REQUEST_DIR="$RESPONSE_DIR" +fi diff --git a/src/deployer.sh b/src/deployer.sh new file mode 100755 index 0000000..9ebaf3d --- /dev/null +++ b/src/deployer.sh @@ -0,0 +1,82 @@ +#!/usr/bin/bash +# +# This binary is made to be run by root, it expects a request from git server, deploy (install or update) +# the related tpm package and send a response to git server. + +# Exit immediately if any command fails. +set -e + +# Exit with the last non-zero fail code. +set -o pipefail + +log() { + echo -e "[DEPLOYER] $(date --rfc-3339=s) - $1" +} + +fail () { + if [ $# -eq 1 ]; then + echo "$1" 1>&2 + fi + exit 1 +} + +# Load config file. +[ $# -eq 1 ] || fail "Expecting 1 argument: config file." +source "$1" + +# Check variables in config file. +[ -d "$REQUEST_DIR" ] || fail "Directory does not exist REQUEST_DIR=$REQUEST_DIR in config file $1." +[ -d "$RESPONSE_DIR" ] || fail "Directory does not exist RESPONSE_DIR=$RESPONSE_DIR in config file $1." +[ -d "$REPOS_DIR" ] || fail "Directory does not exist REPOS_DIR=$REPOS_DIR in config file $1." +[ -d "$RPMS_DIR" ] || fail "Directory does not exist RPMS_DIR=$RPMS_DIR in config file $1." +[ -z "$RPM_ARCH" ] && fail "Empty value RPM_ARCH in config file $1." +[ -z "$RPM_RELEASE" ] && fail "Empty value RPM_RELEASE in config file $1." +[ -z "$RPM_DIST" ] && fail "Empty value RPM_DIST in config file $1." + +# First remove eventual old existing tmp files. +find "$REQUEST_DIR" -type f -delete +find "$RESPONSE_DIR" -type f -delete + +# Loop on every created request. +while read _ _ repo_name; do + + log "New request detected for repo $repo_name." + + # Read request file and remove it immediately. + repo_version=$(cat "$REQUEST_DIR/$repo_name") + rm "$REQUEST_DIR/$repo_name" + + # Check repo version not empty. + if [ -z "$repo_version" ]; then + echo -e "Content of $REQUEST_DIR/$repo_name must contain repo version but is empty\n1" > "$RESPONSE_DIR/$repo_name" + continue + fi + + # Check if repo does exist. + if [ ! -d "$REPOS_DIR/$repo_name.git" ]; then + echo -e "Repository $REPOS_DIR/$repo_name.git does not exist!\n1" > "$RESPONSE_DIR/$repo_name" + continue + fi + + # Check if repo package is already exisitng. + rpm_path="$RPMS_DIR/$RPM_ARCH/$repo_name-$repo_version-$RPM_RELEASE.$RPM_DIST.$RPM_ARCH.rpm" + log "Using rpm package at $rpm_path." + if [ ! -f "$rpm_path" ]; then + echo -e "RPM package $rpm_path does not exist!\n1" > "$RESPONSE_DIR/$repo_name" + continue + fi + + # Upgrade package if already installed. + if rpm -q "$repo_name" 1>/dev/null 2>/dev/null; then + log "Upgrade package $repo_name to v$repo_version" + output=$(rpm --upgrade --verbose --hash "$rpm_path" 2>&1) || exit_code=$? + echo -e "$output\n$exit_code" > "$RESPONSE_DIR/$repo_name" + continue + fi + + # Install package if not already installed. + log "Install package $repo_name v$repo_version." + output=$(rpm --install --verbose --hash "$rpm_path" 2>&1) || exit_code=$? + echo -e "$output\n$exit_code" > "$RESPONSE_DIR/$repo_name" + +done < <(inotifywait --monitor --event create "$REQUEST_DIR") diff --git a/src/newtag.sh b/src/newtag.sh new file mode 100755 index 0000000..5477109 --- /dev/null +++ b/src/newtag.sh @@ -0,0 +1,59 @@ +#!/usr/bin/bash +# +# This binary is made to be run by git server, it builds a rpm package and send a request to root in order +# to deploy the package on the server. + +# Exit immediately if any command fails. +set -e + +# Exit with the last non-zero exit code. +set -o pipefail + +# Name of current gitops pipeline. +pipeline="NEWTAG" + +log () { + echo -e "[$pipeline] $(date --rfc-3339=s) - $1" +} + +fail () { + if [ $# -eq 1 ]; then + echo "$1" 1>&2 + fi + exit 1 +} + +# Retrieve necessary details about package. +pkg_name=$(make name) +[ -z $pkg_name ] && fail "Empty result for target 'make name'." +pkg_version=$(make version) +[ -z $pkg_version ] && fail "Empty result for target 'make version'." + +# Make tarball with source code. +log "Make source tarball for $pkg_name v$pkg_version." +make tarball + +# Build rpm package. +log "Build rpm package." +rpmbuild -bb "$pkg_name.spec" + +# Cleanup last response. +[ -f "$NETOIK_CICD_DEPLOYER_RESPONSE_DIR/$pkg_name" ] || touch "$NETOIK_CICD_DEPLOYER_RESPONSE_DIR/$pkg_name" +sed -i "d" "$NETOIK_CICD_DEPLOYER_RESPONSE_DIR/$pkg_name" + +# Send request to deployer with a little delay in background. +log "Install or update rpm package." +echo "$pkg_version" > "$NETOIK_CICD_DEPLOYER_REQUEST_DIR/$pkg_name" + +# Wait for response from deployer. +inotifywait --timeout 600 --event modify "$NETOIK_CICD_DEPLOYER_RESPONSE_DIR/$pkg_name" >/dev/null + +# Get content of the response. +while read line; do + [ -z "$previous" ] || echo "$previous" + previous="$line" +done < "$NETOIK_CICD_DEPLOYER_RESPONSE_DIR/$pkg_name" + +# Exit now with exit code found in response. +exit_code=$(printf "%d\n" "$previous") +exit $exit_code diff --git a/systemd/netoik-cicd-deployer.service b/systemd/netoik-cicd-deployer.service new file mode 100644 index 0000000..4edb116 --- /dev/null +++ b/systemd/netoik-cicd-deployer.service @@ -0,0 +1,12 @@ +[Unit] +Description=Netoik automatic deployer +After=network.target + +[Service] +User=root +Group=root +ExecStart=/usr/bin/netoik-cicd-deployer /etc/netoik-cicd/deployer.conf +Restart=always + +[Install] +WantedBy=multi-user.target