Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e9a542d
add a var-defaults[no-empty] check that ensures no defaults are empty
evgeni May 28, 2026
20ae1dc
add no-static-secrets ansible-lint rule
evgeni May 28, 2026
ec8d6a3
fix static default passwords in iop roles
evgeni May 28, 2026
cb30613
fix static default password in postgresql role
evgeni May 28, 2026
512917a
allow empty ca paths
evgeni May 28, 2026
687bb4f
mark ca_key_password as no-qa for static secret -- it's a path
evgeni May 29, 2026
a172c2b
fix new lint rules in foreman_development role
evgeni May 29, 2026
f3e2b69
move tests to the normal location, so they get executed
evgeni May 29, 2026
f84331a
Rename rules
evgeni Jun 1, 2026
c0019f2
Use role defaults for container images in IOP tests
ehelms Jun 8, 2026
bc7caca
add missing params
Jun 9, 2026
377474a
Fix broker inventory parsing of ruamel YAML tags
Alleny244 Jun 8, 2026
c8f3a05
test on push to stable branches
evgeni Jun 10, 2026
e8e0971
Set explicit volume mount permissions
ehelms Feb 15, 2026
d61b189
Update certificate paths to /var/lib/foremanctl/certs
ehelms Jun 11, 2026
3040e07
Set choices for flavor, so users can't select a wrong one
evgeni Jun 11, 2026
5454b1a
Fixes #525 - Add checks to health subcommand; add developer documenta…
qcjames53 May 27, 2026
5d289b7
Centralize database configuration into a single reusable list
ehelms Jun 9, 2026
c9eb27e
Add filter_plugins path to development ansible.cfg
ehelms Jun 10, 2026
099a9fe
Fixes #562 - Remove dead code from health subcommand
qcjames53 Jun 11, 2026
c3f96f0
update iop-core-kafka image tag to latest-kafka-4.2.0
pfreyburg Jun 12, 2026
e384672
Remove the duplicate HTTPS entry
amolpati30 Jun 11, 2026
3831701
Fix small typo
sjha4 Jun 11, 2026
286895b
Download CentOS Stream 10 Vagrant box directly from CentOS mirrors
evgeni Jun 12, 2026
757f936
don't load template if feature is disabled
evgeni Jun 9, 2026
a323144
Consolidate certificate vars into a single file
ehelms May 6, 2026
3f28b92
Preserve existing CA artifacts during deploy
ehelms Jun 11, 2026
008b33c
Auto-detect and normalize installer certificates
ehelms May 6, 2026
7854249
Fixes #560 - Add systemctl wants to foreman.target for httpd
qcjames53 Jun 11, 2026
ba1f1a5
Add offline backup for foremanctl
sjha4 May 11, 2026
63cdc0a
Add foremanctl restore command
Chyenne8 Jun 17, 2026
bd5f415
Add incremental backup and restore
Chyenne8 Jun 30, 2026
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
3 changes: 3 additions & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
use_default_rules: true
rulesdir:
- .ansible-lint-rules/
enable_list:
- var-naming[no-role-prefix]
exclude_paths:
Expand Down
Empty file added .ansible-lint-rules/__init__.py
Empty file.
53 changes: 53 additions & 0 deletions .ansible-lint-rules/explicit_volume_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Implementation of explicit-volume-mode rule."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any

from ansiblelint.rules import AnsibleLintRule

if TYPE_CHECKING:
from ansiblelint.file_utils import Lintable

_VOLUME_PATTERN = re.compile(r'^[^:]+:[^:]+:(rw|ro)(?:,[zZ])?$')


class ExplicitVolumeModeRule(AnsibleLintRule):
"""Volume mounts must specify explicit :rw or :ro mode."""

id = "explicit-volume-mode"
description = "Volume mounts must explicitly specify :rw or :ro mode, to ease auditing of access control."
severity = "HIGH"
tags = ["idiom"]
version_added = "custom"

def matchtask(self, task: dict[str, Any], file: Lintable | None = None) -> bool | str:
"""Return a match if any volume mount lacks explicit mode."""
action = task.get("action", {})
if action.get("__ansible_module__") != "containers.podman.podman_container":
return False

for volume_key in ("volume", "volumes"):
volume_data = action.get(volume_key)
if not volume_data:
continue

if isinstance(volume_data, str):
specs = [volume_data]
elif isinstance(volume_data, list):
specs = [s for s in volume_data if isinstance(s, str)]
else:
continue

for spec in specs:
if not _is_valid(spec):
return f"Volume mount '{spec}' missing explicit mode (:rw or :ro)"

return False


def _is_valid(spec: str) -> bool:
if "{{" in spec:
return True
return bool(_VOLUME_PATTERN.match(spec.strip().strip("'\"")))
49 changes: 49 additions & 0 deletions .ansible-lint-rules/no_empty_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Implementation of no-empty-defaults rule."""

from __future__ import annotations

from typing import TYPE_CHECKING

from ansiblelint.rules import AnsibleLintRule
from ansiblelint.utils import parse_yaml_from_file

if TYPE_CHECKING:
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable


class EmptyDefaultsRule(AnsibleLintRule):
"""Role default variables should not have empty values."""

id = "no-empty-defaults"
description = "Role default variables must not be null or empty strings."
severity = "HIGH"
tags = ["idiom"]
version_added = "custom"

def matchyaml(self, file: Lintable) -> list[MatchError]:
"""Return matches for empty defaults in role defaults files."""
results: list[MatchError] = []

if str(file.kind) != "vars" or not file.data:
return results

if not file.role or "defaults" not in file.path.parts:
return results

meta_data = parse_yaml_from_file(str(file.path))
if not isinstance(meta_data, dict):
return results

for key, value in meta_data.items():
if value is None or value == "":
results.append(
self.create_matcherror(
message=f"Role default variable '{key}' has an empty value. Use `undef(hint='…')` to indicate defaults that need to be overriden.",
filename=file,
tag="no-empty-defaults",
data=key,
),
)

return results
60 changes: 60 additions & 0 deletions .ansible-lint-rules/no_static_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Implementation of no-static-secrets rule."""

from __future__ import annotations

from typing import TYPE_CHECKING

from ansiblelint.rules import AnsibleLintRule
from ansiblelint.text import has_jinja
from ansiblelint.utils import parse_yaml_from_file

if TYPE_CHECKING:
from ansiblelint.errors import MatchError
from ansiblelint.file_utils import Lintable

SECRET_SUFFIXES = (
"_password",
"_passwd",
"_secret",
"_token",
)


class NoStaticSecretsRule(AnsibleLintRule):
"""Variables that look like secrets must not have static default values."""

id = "no-static-secrets"
description = "Secret variables must use Jinja expressions, not static strings."
severity = "HIGH"
tags = ["security"]
version_added = "custom"

@staticmethod
def _looks_like_secret(name: str) -> bool:
return any(name.endswith(suffix) for suffix in SECRET_SUFFIXES)

def matchyaml(self, file: Lintable) -> list[MatchError]:
"""Flag secret-looking variables with static string values."""
results: list[MatchError] = []

if str(file.kind) != "vars" or not file.data:
return results

meta_data = parse_yaml_from_file(str(file.path))
if not isinstance(meta_data, dict):
return results

for key, value in meta_data.items():
if not self._looks_like_secret(str(key)):
continue
if isinstance(value, str) and not has_jinja(value):
results.append(
self.create_matcherror(
message=f"Secret variable '{key}' has a static value. Use a Jinja expression instead.",
filename=file,
tag="no-static-secrets",
data=key,
),
)

return results
85 changes: 74 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- master
- '*-stable'
pull_request:


Expand Down Expand Up @@ -57,7 +58,6 @@ jobs:
matrix:
certificate_source:
- default
- installer
security:
- none
database:
Expand All @@ -67,9 +67,6 @@ jobs:
- centos/stream10
iop:
- enabled
exclude:
- certificate_source: installer
box: centos/stream10
include:
- certificate_source: default
security: fapolicyd
Expand Down Expand Up @@ -115,10 +112,6 @@ jobs:
- name: Configure repositories
run: |
./forge setup-repositories
- name: Create installer certificates
if: contains(matrix.certificate_source, 'installer')
run: |
./forge installer-certs
- name: Create custom certificates
if: matrix.certificate_source == 'custom_server'
run: |
Expand Down Expand Up @@ -160,9 +153,6 @@ jobs:
--add-feature remote-execution \
--add-feature bmc \
${{ matrix.iop == 'enabled' && '--add-feature iop' || '' }}
- name: Run health check
run: |
./foremanctl health
- name: Run tests
run: |
./forge test --pytest-args="--certificate-source=${{ matrix.certificate_source }} --database-mode=${{ matrix.database }}"
Expand Down Expand Up @@ -311,13 +301,86 @@ jobs:
## If no one connects after 5 minutes, shut down server.
wait-timeout-minutes: 5

migration:
strategy:
fail-fast: false
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Setup libvirt for Vagrant
uses: voxpupuli/setup-vagrant@v0
- name: Install Ansible
run: pip install --upgrade ansible-core
- name: Setup environment
run: ./setup-environment
- name: Start VMs
run: |
./forge vms start --vms "quadlet client"
- name: Configure repositories
run: |
./forge setup-repositories
- name: Mock foreman-installer environment
run: |
./forge mock-installer
- name: Run image pull
run: |
./foremanctl pull-images
- name: Run migration preview
run: |
mkdir -p .var/lib/foremanctl
./foremanctl migrate
- name: Run migration
run: |
./foremanctl migrate --apply
- name: Run deployment
run: |
./foremanctl deploy \
--tuning development \
--add-feature hammer \
--add-feature foreman-proxy \
--add-feature azure-rm \
--add-feature google \
--add-feature remote-execution
- name: Run tests
run: |
./forge test
- name: Run smoker
run: |
./forge smoker
- name: Archive smoker report
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: smoker-migration
path: "/home/runner/smoker/report/"
- name: Generate sos reports
if: ${{ always() }}
run: ./forge sos
- name: Archive sos reports
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: sosreport-migration
path: sos/
- name: Setup upterm session
if: ${{ failure() }}
uses: owenthereal/action-upterm@v1
with:
limit-access-to-actor: true
wait-timeout-minutes: 5

# A dummy job that you can mark as a required check instead of each individual test
test-suite:
if: always()
needs:
- tests
- devel-tests
- upgrade
- migration
- ansible-lint
- python-lint
runs-on: ubuntu-latest
Expand Down
40 changes: 22 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# AI Development Guide

This file provides guidance to AI tools and agents when working with code in this repository. It is intentionally tool-agnostic -- it works with Claude Code, Cursor, GitHub Copilot, and any other AI assistant.

## Project Overview
## Foremanctl

foremanctl is a deployment tool for Foreman and Katello using Podman quadlets and Ansible. It provides a command-line interface built on top of `obsah` (an Ansible wrapper) for managing containerized Foreman deployments.

Expand Down Expand Up @@ -126,8 +122,8 @@ Roles in `src/roles/` correspond to services and deployment stages:
- Service roles: `foreman`, `pulp`, `candlepin`, `postgresql`, `redis`, `httpd`
- Feature roles: `hammer`, `foreman_proxy`
- Infrastructure: `certificates`, `systemd_target`
- Checks: `check_hostname`, `check_database_connection`, `check_system_requirements`, `check_subuid_subgid`
- Lifecycle: `pre_install`, `post_install`
- Checks: `check_*` roles; see [documentation](docs/developer/checks.md) for more info

### Configuration System

Expand Down Expand Up @@ -170,15 +166,23 @@ Tests use pytest with testinfra for infrastructure testing. Key fixtures in `tes

Test files follow the pattern `tests/<component>_test.py` and use testinfra's server fixture to execute commands and check system state.

## Developer Documentation

- [How to Add a Feature](docs/developer/how-to-add-a-feature.md) - end-to-end feature development
- [Playbooks and Roles](docs/developer/playbooks-and-roles.md) - playbook structure, naming, metadata
- [Testing](docs/developer/testing.md) - test infrastructure, fixtures, patterns
- [Deployment Design](docs/developer/deployment.md) -- deployment architecture
- [Container Image Builds](docs/developer/container-image-builds.md) - image naming, registries
- [Development Environment](docs/developer/development-environment.md) - dev setup with git Foreman

## Git Workflow

Main branch: `master`
## Additional Documentation

Developer docs:
- [Check roles](docs/developer/checks.md) - How to integrate check roles; update as checks are created/modified
- [Container Image Builds](docs/developer/container-image-builds.md) - Info on image naming, registries
- [Deployment Architecture](docs/developer/deployment.md)
- [Development Environment](docs/developer/development-environment.md) - Dev environment setup with Foreman from source
- [How to Add a Feature](docs/developer/how-to-add-a-feature.md) - End-to-end feature development
- [Playbooks and Roles](docs/developer/playbooks-and-roles.md) - Playbook structure, naming, metadata
- [Testing](docs/developer/testing.md) - Additional info on test infrastructure, fixtures, patterns

User docs:
- [Certificates](docs/user/certificates.md) - Overview of certificate sources
- [Parameters](docs/user/parameters.md) - Map of Foreman installation parameters; update as parameters are created/modified

- [CONTRIBUTING](CONTRIBUTING.md) - How to contribute
- [Development](DEVELOPMENT.md) - Foremanctl development overview
- [IOP](docs/iop.md) - Overview of insights on premise
- [Migration Guide](docs/migration-guide.md) - Migrating from foreman-installer to foremanctl
- [Release](RELEASE.md) - Info on Foremanctl releases
3 changes: 3 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Vagrant.configure("2") do |config|

config.vm.define "quadlet" do |override|
override.vm.box = ENV.fetch("FOREMANCTL_BASE_BOX", "centos/stream9")
if override.vm.box == "centos/stream10"
override.vm.box_url = "https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-Vagrant-10-latest.x86_64.vagrant-libvirt.box"
end
override.vm.hostname = "quadlet.#{DOMAIN}"

override.vm.provider "libvirt" do |libvirt, provider|
Expand Down
1 change: 1 addition & 0 deletions development/ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ host_key_checking = False
stdout_callback=debug
stderr_callback=debug
roles_path = ./roles:../src/roles
filter_plugins = ../src/filter_plugins
display_skipped_hosts = no
Loading