Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .flake8

This file was deleted.

90 changes: 45 additions & 45 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,46 @@ repos:
args: ["--allow-missing-credentials"]
- id: detect-private-key

# Find security vulnerabilities, compliance issues, and infrastructure misconfigurations early in the development cycle of your infrastructure-as-code with KICS by Checkmarx.
# KICS stands for Keeping Infrastructure as Code Secure, it is open source and is a must-have for any cloud native project.
# https://docs.kics.io/latest/commands/
# Ignoring errors: https://docs.kics.io/latest/running-kics/#using_commands_on_scanned_files_as_comments
- repo: https://github.com/Checkmarx/kics
rev: v2.1.13
hooks:
- id: kics-scan
# TODO solve the high severity issues first
args: ["--exclude-severities", "info,low,medium"]
pass_filenames: false
always_run: true

# Gitleaks is a SAST tool for detecting and preventing hardcoded secrets like passwords, api keys, and tokens in git repos.
# Gitleaks is an easy-to-use, all-in-one solution for detecting secrets, past or present, in your code.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.28.0
hooks:
- id: gitleaks

# ShellCheck, a static analysis tool for shell scripts - https://www.shellcheck.net/
# Ignoring errors: https://www.shellcheck.net/wiki/Ignore, `.shellcheckrc`
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck

# Ruff is an extremely fast Python linter and code formatter, written in Rust.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.10
hooks:
# Linter - https://docs.astral.sh/ruff/linter/
- id: ruff-check
args: ["--fix"]
# TODO Enable after pendling MRs are merged
# Formatter - https://docs.astral.sh/ruff/formatter/
# - id: ruff-format

# Dockerfile linter, validate inline bash, written in Haskell
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
hooks:
- id: hadolint-docker
exclude: "\\.dockerignore$"

Comment thread
bubacoder marked this conversation as resolved.
# Run renovate-config-validator
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 41.1.4
hooks:
- id: renovate-config-validator
# TODO Old nodeenv (v0.13.4) does not support "lts" - remove after Ubuntu upgrade
language_version: "22.17.1"

# pre-commit git hooks to take care of Terraform configurations
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.100.0
Expand All @@ -68,41 +88,21 @@ repos:
hooks:
- id: ansible-lint
files: ^ansible/
always_run: false
entry: >
env ANSIBLE_ROLES_PATH=~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:ansible/roles
env ANSIBLE_COLLECTIONS_PATH=/usr/lib/python3/dist-packages:/usr/share/ansible/collections:/etc/ansible/collections:/opt/pipx/venvs/ansible-core/lib/python3.12/site-packages/ansible_collections:~/.ansible/collections:collections
python3 -m ansiblelint --force-color

# ShellCheck, a static analysis tool for shell scripts - https://www.shellcheck.net/
# Ignoring errors: https://www.shellcheck.net/wiki/Ignore, `.shellcheckrc`
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck

# flake8 is a python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code.
# Third-party scripts are excluded.
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks:
- id: flake8
exclude: |
(?x)^(
scripts/git-filter-repo.py|
scripts/test-colors.py
)$

# Dockerfile linter, validate inline bash, written in Haskell
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
hooks:
- id: hadolint-docker
exclude: "\\.dockerignore$"

# Run renovate-config-validator
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 41.1.4
# Find security vulnerabilities, compliance issues, and infrastructure misconfigurations early in the development cycle of your infrastructure-as-code with KICS by Checkmarx.
# KICS stands for Keeping Infrastructure as Code Secure, it is open source and is a must-have for any cloud native project.
# https://docs.kics.io/latest/commands/
# Ignoring errors: https://docs.kics.io/latest/running-kics/#using_commands_on_scanned_files_as_comments
- repo: https://github.com/Checkmarx/kics
rev: v2.1.13
hooks:
- id: renovate-config-validator
# TODO Old nodeenv (v0.13.4) does not support "lts" - remove after Ubuntu upgrade
language_version: "22.17.1"
- id: kics-scan
# TODO solve the high severity issues first
args: ["--exclude-severities", "info,low,medium"]
pass_filenames: false
always_run: false
2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"mhutchie.git-graph",
"hashicorp.terraform",
"ms-vscode.makefile-tools",
"ms-python.flake8",
"charliermarsh.ruff",
"ban.spellright"
]
}
8 changes: 1 addition & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,12 @@ task --list-all
# Run all linting and build tasks
task build

# Run linting tools (via pre-commit)
# Run linting tools on all files (via pre-commit)
task lint

# Open TaskUI (terminal UI for executing tasks)
task ui

# Update pre-commit hooks
task pre-commit-update

# Run renovatebot locally
task renovate

# Clear generated content and cache data
task clean
```
Expand Down
7 changes: 6 additions & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ tasks:

# See https://pre-commit.com for more information
lint:
desc: Run linting tools (via pre-commit)
desc: Run linting tools on all files (via pre-commit)
cmds:
- pre-commit run --all-files

lint-stashed:
desc: Run linting tools on stashed files (via pre-commit)
cmds:
- pre-commit run

git-rebase-fixup:
desc: Interactively rebase current branch to origin/main (stash, rebase, pop)
aliases: [rebase]
Expand Down
65 changes: 28 additions & 37 deletions docker/labctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
Uses YAML configuration to manage Docker services.
"""

import os
import sys
import argparse
import socket
import yaml
import logging
import os
import socket
import subprocess
import sys
from pathlib import Path
from typing import Dict, Optional

import yaml

# Configure logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)

# Global variables
host_config_dir: Optional[Path] = None
docker_stacks_dir: Path = Path(__file__).parent.absolute()


Expand All @@ -32,12 +31,11 @@ def create_network_if_missing(network_name: str) -> None:
docker(["network", "create", "--driver", "bridge", network_name])


def create_localhost_link() -> None:
def create_localhost_link(docker_config_dir: Path) -> None:
"""Create 'localhost' symlink in the parent directory."""
parent_dir = Path(host_config_dir).parent
hostname = socket.gethostname()
localhost_link = parent_dir / "localhost"
hostname_dir = parent_dir / hostname
localhost_link = docker_config_dir / "localhost"
hostname_dir = docker_config_dir / hostname

if hostname_dir.exists() and hostname_dir.is_dir():
# Create or update the localhost symlink
Expand All @@ -54,14 +52,14 @@ def create_localhost_link() -> None:
logger.error(f"Error creating localhost symlink: {e}")


def get_compose_file(stack_dir: str, service_name: str) -> str:
def get_compose_file(stack_dir: Path, service_name: str) -> Path:
"""Get the yaml file path for a service."""
return str(Path(stack_dir) / f"{service_name}.yaml")
return stack_dir / f"{service_name}.yaml"

Comment thread
bubacoder marked this conversation as resolved.

def has_build_directive(compose_file: str) -> bool:
def has_build_directive(compose_file: Path) -> bool:
"""Check if the service uses a build directive."""
with open(compose_file, "r") as f:
with open(compose_file) as f:
yaml_content = yaml.safe_load(f)
if yaml_content and 'services' in yaml_content:
for service_config in yaml_content['services'].values():
Expand All @@ -70,7 +68,7 @@ def has_build_directive(compose_file: str) -> bool:
return False


def get_env_file_args(service_name: str) -> list[str]:
def get_env_file_args(host_config_dir: Path, service_name: str) -> list[str]:
"""Get environment file arguments for Docker Compose with normalized paths."""
env_paths = [
host_config_dir.parent / ".env", # Common .env file in config/docker
Expand All @@ -81,26 +79,26 @@ def get_env_file_args(service_name: str) -> list[str]:

args = []
for file in env_paths:
norm_file = os.path.normpath(file)
if os.path.isfile(norm_file):
args.extend(["--env-file", norm_file])
absolute_path = file.resolve()
if absolute_path.is_file():
args.extend(["--env-file", str(absolute_path)])
return args


def docker(cmd: list[str], env=None, stdin=None, stdout=None, stderr=None) -> None:
subprocess.run(["docker"] + cmd, env=env, stdin=stdin, stdout=stdout, stderr=stderr, check=True)


def docker_command(stack_dir: str, service_name: str, action: str) -> None:
def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, action: str) -> None:
"""Execute Docker Compose command for a service."""
print() # empty line for separation

compose_file = get_compose_file(stack_dir, service_name)
if not os.path.exists(compose_file):
if not compose_file.exists():
logger.error(f"Compose file not found: {compose_file}")
return

env_file_args = get_env_file_args(service_name)
env_file_args = get_env_file_args(host_config_dir, service_name)

# Handle pull operations
if action in ["update", "pull"]:
Expand Down Expand Up @@ -133,18 +131,18 @@ def docker_command(stack_dir: str, service_name: str, action: str) -> None:
docker(["compose", "-f", compose_file, *env_file_args, "up", "--detach", "--force-recreate"])


def load_services_config(config_file: str) -> Dict:
def load_services_config(config_file: str) -> dict:
"""Load services configuration from YAML file."""
try:
with open(config_file, 'r') as file:
with open(config_file) as file:
config = yaml.safe_load(file)
return config
except Exception as e:
logger.error(f"Error loading configuration file {config_file}: {e}")
sys.exit(1)


def process_services(config: Dict, state_override: Optional[str] = None) -> None:
def process_services(host_config_dir: Path, config: dict, state_override: str | None = None) -> None:
"""Process services based on the configuration."""
if not config or 'services' not in config:
logger.error("Error: Invalid configuration format. 'services' key not found.")
Expand Down Expand Up @@ -174,21 +172,18 @@ def process_services(config: Dict, state_override: Optional[str] = None) -> None
logger.warning(f"Unknown state '{state}' for service {category}/{name}")
continue

stack_dir = os.path.join(docker_stacks_dir, category)
docker_command(stack_dir, name, state)
docker_command(host_config_dir, docker_stacks_dir / category, name, state)


def get_host_config_dir() -> Path:
hostname = socket.gethostname().lower()
script_dir = Path(__file__).parent.absolute()
config_dir = script_dir.parent / "config" / "docker"
return config_dir / hostname
docker_config_dir = script_dir.parent / "config" / "docker"
return docker_config_dir / hostname


def cmd_config_apply(args) -> None:
"""Apply configuration to Docker services."""
global host_config_dir

if args.config:
config_file = args.config
host_config_dir = Path(config_file).parent
Expand All @@ -199,11 +194,11 @@ def cmd_config_apply(args) -> None:

logger.info("Init...")
config = load_services_config(config_file)
create_localhost_link()
create_localhost_link(host_config_dir.parent)
create_network_if_missing("proxy")

# Process services with optional mode override
process_services(config, args.mode)
process_services(host_config_dir, config, args.mode)

# Cleanup old images if enabled in config and in update mode
if args.mode == 'update':
Expand All @@ -214,9 +209,6 @@ def cmd_config_apply(args) -> None:

def cmd_service(args) -> None:
"""Manage individual Docker services."""
global host_config_dir
host_config_dir = get_host_config_dir()

if not args.name:
logger.error("Service name is required")
sys.exit(1)
Expand All @@ -232,8 +224,7 @@ def cmd_service(args) -> None:
# Everything before the last part is the category path
category_path = '/'.join(name_parts[:-1])

stack_dir = os.path.join(docker_stacks_dir, category_path)
docker_command(stack_dir, service_name, args.operation)
docker_command(get_host_config_dir(), docker_stacks_dir / category_path, service_name, args.operation)


def main() -> None:
Expand Down
Loading