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
69 changes: 69 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,74 @@ jobs:
limit-access-to-actor: true
wait-timeout-minutes: 5

satellite-tests:
runs-on: ubuntu-24.04
name: "Tests Satellite Deployment"
steps:
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Setup libvirt for Vagrant
uses: voxpupuli/setup-vagrant@v0
with:
configure_dns: true
- name: Install Ansible
run: pip install --upgrade ansible-core
- name: Setup environment
run: ./setup-environment
- name: Apply vendor overrides
run: cp -a vendor_overrides/satellite src/playbooks/_vendor_overrides

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this copied to only test the vendor supplied flavors and with that we don't want to expose satellite flavor upstream? or did i misread something

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. A "regular" (upstream) foremanctl deployment should never expose the Satellite flavor. Similarly, a Satellite deployment should not expose the Katello flavor (that's why this file fully overrides the choices).

I am still a bit torn whether I want this file to live here in the repo at the top level, or to put it as a fixture to the tests and let downstream ship an own version.

We could build foremanctl-overrides-<vendor> sub-packages from the files here, but not ship them in our repos, but then let downstreams ship them as needed. That's probably a question to @zjhuntin and @Odilhao how they want the downstream shipping to happen.

@arvind4501 arvind4501 Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about features.d implementation, where downstream package just need to add the dropin, simiar to that maybe like src/flavors.d/satellite/metadata.obsah.yml drop in could work, but i am not sure how complex is that.
Also we could teach obsah to have a choices_directory from where it should pull flavor choices and we could differ that in upstream and downstream,

these are some thoughts i had, so added them here, but i don't want to block this PR on those as PR already looks neat to me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to the extreme: should upstream also be a vendor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are several ways we could make this easier/cleaner if we teach obsah a more explicit way to recognize overrides (right now I am merely abusing the fact that includes work and support partial overrides of previously defined stuff). If you want to experiment with this, please :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to the extreme: should upstream also be a vendor?

Not on a Friday, no. 😝

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theforeman/obsah#135 this is what i had in mind

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still a bit torn whether I want this file to live here in the repo at the top level, or to put it as a fixture to the tests and let downstream ship an own version.

We could build foremanctl-overrides-<vendor> sub-packages from the files here, but not ship them in our repos, but then let downstreams ship them as needed.

Fixture vs. direct inclusion I am not sure it really matters, so maybe just start with the easiest solution.

As for the sub-package shipping, I don't think we should get into that game yet without more compelling use cases to do so.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are several ways we could make this easier/cleaner if we teach obsah a more explicit way to recognize overrides (right now I am merely abusing the fact that includes work and support partial overrides of previously defined stuff). If you want to experiment with this, please :)

Everything is a vendor, even the vendor is the vendor. We are in Inception now. https://tenor.com/0xNi.gif

We could build foremanctl-overrides- sub-packages from the files here, but not ship them in our repos, but then let downstreams ship them as needed. That's probably a question to @zjhuntin and @Odilhao how they want the downstream shipping to happen.

I don't have any problem if we need to add one override file at one .d/ folder downstream to ensure that the override is present, since we will always have one opinionated package downstream.

- name: Start VMs
run: |
./forge vms start --vms "quadlet client"
- name: Configure repositories
run: |
./forge setup-repositories
- name: Run image pull
run: |
./foremanctl pull-images
- name: Run deployment
run: |
./foremanctl deploy \
--flavor satellite \
--initial-admin-password=changeme \
--initial-organization "Foreman CI" \
--initial-location "Internet" \
--tuning development
- name: Deploy features
run: |
./foremanctl deploy \
--add-feature bmc
- 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-satellite
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-satellite
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()
Expand All @@ -474,6 +542,7 @@ jobs:
- upgrade
- migration
- foreman-proxy-content-tests
- satellite-tests
- ansible-lint
- python-lint
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions src/features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ ansible:
dependencies:
- remote-execution
dynflow:
description: dynflow
internal: true
foreman_proxy:
plugin_name: dynflow
Expand Down Expand Up @@ -90,3 +91,8 @@ registration:
plugin_name: registration
dependencies:
- templates
theme-satellite:
description: Satellite theme for Foreman
internal: true
foreman:
plugin_name: foreman_theme_satellite
24 changes: 16 additions & 8 deletions src/filter_plugins/foremanctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

__metaclass__ = type

import os
import pathlib

import yaml
Expand All @@ -15,7 +16,7 @@
with features_yaml.open() as features_file:
FEATURE_MAP = yaml.safe_load(features_file)

# load additional feature files under features.d
# load additional feature files under features.d
_features_d = _SRC_ROOT / 'features.d'
if _features_d.is_dir():
for _overlay in sorted(_features_d.glob('*.yaml')):
Expand Down Expand Up @@ -72,18 +73,25 @@ def available_foreman_plugins(_value):
def list_all_features(enabled_features, only_enabled=False):
enabled_list = []
available_list = []
list_internal = os.environ.get('FOREMANCTL_FEATURES_LIST_INTERNAL', '') == 'true'
for name, meta in FEATURE_MAP.items():
if meta.get('internal', False):
internal = meta.get('internal', False)
if internal and not list_internal:
continue
description = meta.get('description', '')
if name in enabled_features:
enabled_list.append((name, 'enabled', description))
enabled_list.append((name, 'enabled', internal, description))
elif not only_enabled:
available_list.append((name, 'available', description))

output = [f"{'FEATURE':<25} {'STATE':<12} DESCRIPTION"]
for name, state, description in enabled_list + available_list:
output.append(f"{name:<25} {state:<12} {description}")
available_list.append((name, 'available', internal, description))

if not list_internal:
output = [f"{'FEATURE':<25} {'STATE':<12} DESCRIPTION"]
for name, state, _internal, description in enabled_list + available_list:
output.append(f"{name:<25} {state:<12} {description}")
else:
output = [f"{'FEATURE':<25} {'STATE':<12} {'INTERNAL':<8} DESCRIPTION"]
for name, state, internal, description in enabled_list + available_list:
output.append(f"{name:<25} {state:<12} {internal:<8} {description}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return "\n".join(output)

Expand Down
1 change: 1 addition & 0 deletions src/playbooks/deploy/metadata.obsah.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ variables:
include:
- _flavor_features
- _flavors/katello
- _vendor_overrides/deploy
2 changes: 1 addition & 1 deletion src/roles/post_install/tasks/message.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
- name: Post install message
when: flavor in ['katello']
when: post_install_message_enabled | default(false)
ansible.builtin.debug:
msg:
- "{{ _post_install_url_msg }}"
Expand Down
1 change: 1 addition & 0 deletions src/vars/flavors/foreman-proxy-content.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
flavor_features:
- foreman-proxy
- pulp
- content/rpm
- content/deb
- content/container
Expand Down
4 changes: 4 additions & 0 deletions src/vars/flavors/katello.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
flavor_features:
- foreman
- katello
- pulp
- candlepin
- content/ansible
- content/container
- content/deb
Expand All @@ -23,3 +25,5 @@ health_checks_to_execute:
- check_foreman_tasks
- check_host_facts_count
- check_duplicate_permissions

post_install_message_enabled: true
36 changes: 36 additions & 0 deletions src/vars/flavors/satellite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
flavor_features:
- foreman
- katello
Comment thread
Gauravtalreja1 marked this conversation as resolved.
- pulp
- candlepin
- content/ansible
- content/container
- content/rpm
- ansible
- azure-rm
- google
- remote-execution
- rh-cloud
- theme-satellite

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 6 'theme-satellite|flavor_features|check_features|internal' src tests

Repository: theforeman/foremanctl

Length of output: 46583


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files ---'
git ls-files | rg '(^src/features\.|src/filter_plugins/|src/roles/checks|src/vars/flavors/satellite|src/vars/defaults|src/playbooks/deploy/_flavor_features|src/plugins|ansible|roles/.*/tasks/.*feature|checks)'

echo '--- src/features.yaml around theme-satellite ---'
sed -n '88,108p' src/features.yaml

echo '--- filter_plugins foremanctl.py ---'
sed -n '1,180p' src/filter_plugins/foremanctl.py

echo '--- checks roles files ---'
git ls-files src/roles/checks src/plugins src/**/*.yml | sort | rg 'check_.*feature|features|main\.yml|tasks/main'

echo '--- check_features references ---'
rg -n -C 5 "check_features|has_feature|theme-satellite|invalid_features|FEATURE_MAP|features.yaml|features\.yaml" src tests unit 2>/dev/null || true

echo '--- deploy plays include relevant snippets ---'
sed -n '1,120p' src/playbooks/deploy/deploy.yaml

Repository: theforeman/foremanctl

Length of output: 46013


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import os, re
root = 'src'
features = {}
for fname in ['features.yaml'] + sorted(glob_or_find('src/features.d/*.yaml') for _ in ()): # placeholder
    pass
PY

Repository: theforeman/foremanctl

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files ---'
git ls-files | rg '(^src/features\.|src/filter_plugins/|src/roles/checks|src/vars/flavors/satellite|src/vars/defaults|src/playbooks/deploy/_flavor_features|src/plugins|ansible|roles/.*/tasks/.*feature|checks)'

echo '--- src/features.yaml around theme-satellite ---'
sed -n '88,108p' src/features.yaml

echo '--- filter_plugins foremanctl.py ---'
sed -n '1,180p' src/filter_plugins/foremanctl.py

echo '--- checks roles files ---'
git ls-files src/roles/checks src/plugins src/**/*.yml | rg 'check_.*feature|features|main\.yml|tasks/main' || true

echo '--- check_features references ---'
rg -n -C 5 "check_features|has_feature|theme-satellite|invalid_features|FEATURE_MAP|features.yaml|features\.yaml" src tests 2>/dev/null || true

echo '--- deploy play snippets ---'
sed -n '1,120p' src/playbooks/deploy/deploy.yaml

echo '--- static list of feature mentions ---'
rg -n --hidden --glob '!*cache*' 'theme-satellite|has_feature|check_features|invalid_features|FEATURE_MAP' .

Repository: theforeman/foremanctl

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- full check_features role ---'
sed -n '1,120p' src/roles/check_features/tasks/main.yaml
echo '--- task includes feature.yaml ---'
sed -n '1,80p' src/roles/foreman_proxy/tasks/feature.yaml
sed -n '1,100p' src/roles/foreman_proxy/tasks/feature/ansible.yaml

echo '--- search installed/configured plugin handling by name ---'
rg -n -C 4 'foreman_theme_satellite|theme_satellite|satellite theme|Satellite theme|internal_admin|enable_plugin|plugin_enabled|foreman-installer|foreman:.*plugins' src tests docs development 2>/dev/null || true

echo '--- Python feature map behavior for theme-satellite ---'
python3 - <<'PY'
import pathlib, yaml
root = pathlib.Path('src')
FEATURE_MAP = {}
FEATURE_MAP.update(yaml.safe_load((root / 'features.yaml').read_text()))
_features_d = root / 'features.d'
if _features_d.is_dir():
    for _overlay in sorted(_features_d.glob('*.yaml')):
        FEATURE_MAP.update(yaml.safe_load(_overlay.read_text()) or {})
def get_dependencies(features):
    dependencies = set()
    def add(feature):
        if feature in dependencies:
            return
        dependencies.add(feature)
        for dependency in FEATURE_MAP.get(feature, {}).get('dependencies', []):
            add(dependency)
    for feature in features:
        add(feature)
    return dependencies
enabled = ['satellite']
print('defined:', 'theme-satellite' in FEATURE_MAP)
print('meta:', FEATURE_MAP.get('theme-satellite'))
print('filters_for:', [FEATURE_MAP.get(f, {}).get('foreman', {}).get('plugin_name') for f in ['theme-satellite'] + get_dependencies(enabled)])
PY

Repository: theforeman/foremanctl

Length of output: 33449


Condition theme-satellite on container features.

src/vars/flavors/satellite.yml enables theme-satellite unconditionally, but check_features only rejects unknown feature names, and deployment roles do not check Foreman capability before installing/configuring foreman_theme_satellite. If nightly containers lack this plugin, make the feature conditional or disable this fork so Satellite does not deploy a missing advertised feature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vars/flavors/satellite.yml` at line 15, Update the satellite.yml feature
declaration for theme-satellite so it is enabled only when the container
provides foreman_theme_satellite; otherwise remove or disable the feature for
nightly containers. Ensure check_features and deployment roles do not advertise,
install, or configure this feature when the plugin is unavailable.

Comment thread
Gauravtalreja1 marked this conversation as resolved.
- webhooks
- hammer
- foreman-proxy

@stejskalleos stejskalleos Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- bootdisk
- google
- leapp
- discovery
- openscap
- templates #foreman_templates
- virt_who

I know some of them are not ready yet, but do we have an issue to track the missing features?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my Satellite:

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would mean also actually defining those features, and people would see them, try to enable them, and complain that it has no effect.

IMHO we should not.

checks_to_execute:
- check_features
- check_hostname
- check_database_connection
- check_system_requirements
- check_podman_network_backend

health_checks_to_execute:
- check_hostname
- check_database_connection
- check_services
- check_foreman_api
- check_foreman_tasks
- check_host_facts_count
- check_duplicate_permissions

post_install_message_enabled: true
17 changes: 9 additions & 8 deletions tests/backup_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,24 @@


@pytest.fixture(scope="module")
def expected_databases(enabled_features, flavor):
def expected_databases(enabled_features):
"""
Determine expected databases based on flavor and enabled features.
Determine expected databases based on enabled features.

Note: These are the logical 'name' values from database.yml
(e.g., 'foreman', 'iop_advisor'). Dump filenames use the actual
database name via database_mapping in backup metadata.
"""
databases = []

# Katello flavor has foreman, candlepin, and pulp
if flavor == 'katello':
databases = ['foreman', 'candlepin', 'pulp']
if 'foreman' in enabled_features:
databases.append('foreman')

# Foreman-proxy-content flavor only has pulp
elif flavor == 'foreman-proxy-content':
databases = ['pulp']
if 'candlepin' in enabled_features:
databases.append('candlepin')

if 'pulp' in enabled_features:
databases.append('pulp')

# Add IOP databases if IOP feature is enabled
if 'iop' in enabled_features:
Expand Down
32 changes: 25 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
OBSAH_STATE = os.environ.get('OBSAH_STATE', '.var/lib/foremanctl')
PARAMETERS_FILE = os.path.join(OBSAH_STATE, 'parameters.yaml')
FLAVOR_TESTS_DIR = py.path.local(__file__).dirpath() / 'flavor'
FLAVOR_TESTS_DIR_OVERRIDES = {
'satellite': 'katello',
}
FOREMAN_PROXY_PORT = 8443


Expand All @@ -32,18 +35,27 @@ def features(self):
# FEATURE STATE DESCRIPTION
# $feature enabled/available $description
output = subprocess.check_output(['./foremanctl', 'features'], cwd=self._config.rootdir,
universal_newlines=True)
universal_newlines=True,
env=os.environ | {'FOREMANCTL_FEATURES_LIST_INTERNAL': 'true'})
lines = output.splitlines(keepends=False)
# feature, status, description
return [line.split(None, 2) for line in lines[1:]]
# feature, status, internal, description
return [line.split(None, 3) for line in lines[1:]]

@cached_property
def all_available_features(self):
return set(feature for feature, _status, _internal, _desc in self.features)

@cached_property
def available_features(self):
return set(feature for feature, _status, _desc in self.features)
return set(feature for feature, _status, internal, _desc in self.features if internal == '0')

@cached_property
def enabled_features(self):
return set(feature for feature, status, _desc in self.features if status == 'enabled')
return set(feature for feature, status, internal, _desc in self.features if status == 'enabled')

@cached_property
def user_enabled_features(self):
return set(feature for feature, status, internal, _desc in self.features if status == 'enabled' and internal == '0')

@cached_property
def flavor(self):
Expand All @@ -66,6 +78,11 @@ def enabled_features(pytestconfig):
return pytestconfig.user_parameters.enabled_features


@pytest.fixture(scope="module")
def user_enabled_features(pytestconfig):
return pytestconfig.user_parameters.user_enabled_features


@pytest.fixture(scope="module")
def available_features(pytestconfig):
return pytestconfig.user_parameters.available_features
Expand Down Expand Up @@ -286,7 +303,8 @@ def pytest_configure(config):

def pytest_collection_modifyitems(config, items):
active_flavor = config.user_parameters.flavor
active_flavor_dir = FLAVOR_TESTS_DIR / active_flavor
# if there is an override, use that, otherwise use the flavor verbatim
active_flavor_dir = FLAVOR_TESTS_DIR / FLAVOR_TESTS_DIR_OVERRIDES.get(active_flavor, active_flavor)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means we can't have a tests/flavor/satellite that adds satellite-specific tests. I don't think that's bad (we can place them somewhere else, if we ever have any), but wanted to call it out here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #722 I wanted to go all the way to feature-based testing anyway and reduce the flavor-based testing. I see here you already took the approach of exposing internal features (just like I did) so perhaps we can just get rid of the flavor based tests anyway?

tests/flavor/katello/postgresql_test.py already has logic that is more automated in tests/backup_test.py while tests/flavor/katello/images_test.py could also be easily written to look at enabled features.

The foreman-proxy-content are a bit more special, but that's not a concern for this particular PR.


deselected = []
selected = []
Expand Down Expand Up @@ -317,7 +335,7 @@ def pytest_collection_modifyitems(config, items):
def pytest_runtest_setup(item):
feature_markers = set(mark.args[0] for mark in item.iter_markers(name="feature"))
if feature_markers:
invalid_features = feature_markers - item.config.user_parameters.available_features
invalid_features = feature_markers - item.config.user_parameters.all_available_features
if invalid_features:
raise pytest.PytestConfigWarning(f"Invalid feature(s) {invalid_features!r} on {item}")
missing = feature_markers - item.config.user_parameters.enabled_features
Expand Down
4 changes: 2 additions & 2 deletions tests/features_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ def test_foremanctl_features(available_features):
assert feature in result.stdout, f"Expected feature '{feature}' in output"


def test_foremanctl_features_list_enabled(enabled_features):
def test_foremanctl_features_list_enabled(user_enabled_features):
command = ['./foremanctl', 'features', '--list-enabled']
result = subprocess.run(command, capture_output=True, text=True)

assert result.returncode == 0

for feature in enabled_features:
for feature in user_enabled_features:
assert feature in result.stdout, f"Expected feature '{feature}' in output"


Expand Down
1 change: 1 addition & 0 deletions tests/httpd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def test_https_pulp_auth(curl_request):
assert cmd.stdout == '200'


@pytest.mark.feature('content/python')
def test_https_pypi_endpoint(curl_request):
cmd = curl_request("pypi/test/", return_body=True)
assert cmd.succeeded
Expand Down
5 changes: 5 additions & 0 deletions vendor_overrides/satellite/deploy/metadata.obsah.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
variables:
flavor:
choices:
- satellite