Skip to content

Add 3 alternative methods of installing Ascender - #234

Merged
cigamit merged 11 commits into
mainfrom
alternative
Sep 11, 2026
Merged

cigamit merged 11 commits into
mainfrom
alternative

Conversation

@cigamit

@cigamit cigamit commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

This adds 3 alternative ways of installing Ascender. While utilizing K8S is the preferred and "supported" route. This is not always optimal for everyone, especially if you just want to try it out or have a small environment. So we have added 3 alternative methods. Each one has a README that explains the method a bit better.

  • Docker Compose - docker and docker compose should already be setup on the server. Then you can just run the short setup.sh which generates the self signed cert and random passwords and then do a docker compose up -d
  • Podman - This is a podman native install (using a systemd service for Ascender). So just do a dnf install podman and then run the install.sh script, and in ~8 minutes you have a fully working Ascender install.
  • Direct Services - For those that don't want containers, this has an Ansible playbook that will directly setup postgres, valkey, etc.. and pull down the Ascender source and build it and run it all as systemd services. This takes the longest and can take upwards of 30 minutes to complete.

NOTE: I have only tested these with Rocky 9. Other EL variants may work. I have definitely not tested with Ubuntu and don't expect any of them to work on there, especially the podman or services method. Docker may.

@cigamit
cigamit requested a review from TheWitness September 10, 2026 21:58
@cigamit cigamit self-assigned this Sep 10, 2026
Copilot AI lite review requested due to automatic review settings September 10, 2026 21:58
@cigamit cigamit added the enhancement New feature or request label Sep 10, 2026
TheWitness
TheWitness previously approved these changes Sep 10, 2026

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical and moderate findings affect deployment correctness, connectivity, credential protection, and service startup.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds three non-Kubernetes Ascender installation paths: Docker Compose, Podman/systemd, and direct Rocky/EL host services.

Changes:

  • Adds Compose deployment scripts, configuration, and documentation.
  • Adds Podman-native container management and systemd integration.
  • Adds Ansible roles for PostgreSQL, Valkey, Receptor, builds, configuration, and services.
File summaries
File Description
alternative/services/setup.sh Bootstraps Ansible and runs the installation playbook.
alternative/services/roles/ascender_valkey/tasks/main.yml Installs and configures Valkey.
alternative/services/roles/ascender_valkey/handlers/main.yml Defines Valkey restart handling.
alternative/services/roles/ascender_system/tasks/main.yml Installs dependencies and configures system state.
alternative/services/roles/ascender_system/handlers/main.yml Defines system handlers.
alternative/services/roles/ascender_services/templates/ascender.service.j2 Defines the Ascender systemd unit.
alternative/services/roles/ascender_services/tasks/main.yml Runs migrations and starts services.
alternative/services/roles/ascender_services/handlers/main.yml Restarts Ascender and reloads nginx.
alternative/services/roles/ascender_receptor/tasks/main.yml Installs Receptor and initializes Podman.
alternative/services/roles/ascender_postgresql/tasks/main.yml Installs and configures PostgreSQL.
alternative/services/roles/ascender_postgresql/handlers/main.yml Defines PostgreSQL restart handling.
alternative/services/roles/ascender_defaults/defaults/main.yml Defines installation defaults.
alternative/services/roles/ascender_config/templates/supervisord.conf.j2 Configures supervised processes.
alternative/services/roles/ascender_config/templates/settings.py.j2 Defines Ascender settings.
alternative/services/roles/ascender_config/templates/receptor.conf.j2 Configures Receptor.
alternative/services/roles/ascender_config/templates/postgres.py.j2 Configures database credentials.
alternative/services/roles/ascender_config/templates/nginx.conf.j2 Defines host nginx configuration.
alternative/services/roles/ascender_config/templates/nginx-locations.conf.j2 Defines nginx routing.
alternative/services/roles/ascender_config/templates/environment.j2 Defines the service environment.
alternative/services/roles/ascender_config/templates/channels.py.j2 Configures websocket secrets.
alternative/services/roles/ascender_config/tasks/main.yml Generates application configuration and certificates.
alternative/services/roles/ascender_build/tasks/main.yml Checks out and builds Ascender.
alternative/services/requirements.yml Declares Ansible collections.
alternative/services/README.md Documents direct-services installation.
alternative/services/inventory.example Provides example inventory.
alternative/services/install.yml Orchestrates installation roles.
alternative/services/group_vars/all.yml Provides variable examples and overrides.
alternative/services/ansible.cfg Configures Ansible execution.
alternative/services/.gitignore Ignores local inventory.
alternative/podman/scripts/launch_task.sh Starts the Podman task container.
alternative/podman/scripts/init.sh Initializes shared volumes.
alternative/podman/scripts/bootstrap.sh Performs database bootstrap.
alternative/podman/receptor/launch_awx_receptor.sh Starts Receptor and nested Podman.
alternative/podman/receptor/Dockerfile Builds the Receptor sidecar image.
alternative/podman/receptor/containers.conf Configures nested Podman.
alternative/podman/README.md Documents Podman installation and operations.
alternative/podman/manage-containers.sh Manages containers, volumes, and lifecycle.
alternative/podman/install.sh Installs and starts the Podman deployment.
alternative/podman/config/valkey.conf Configures Valkey.
alternative/podman/config/settings.py Defines container settings.
alternative/podman/config/receptor.conf Configures Receptor.
alternative/podman/config/nginx.conf Configures container nginx.
alternative/podman/config/nginx-locations.conf Defines nginx routes.
alternative/podman/config/nginx-http-redirect.conf Defines HTTP redirects.
alternative/podman/config/nginx-http-insecure.conf Configures insecure HTTP mode.
alternative/podman/ascender.service Defines the Podman systemd unit.
alternative/podman/ascender.conf.example Provides Podman configuration defaults.
alternative/docker-compose/setup.sh Generates Compose secrets and certificates.
alternative/docker-compose/scripts/launch_task.sh Starts the Compose task container.
alternative/docker-compose/scripts/init.sh Initializes shared volumes.
alternative/docker-compose/scripts/bootstrap.sh Performs database bootstrap.
alternative/docker-compose/receptor/launch_awx_receptor.sh Starts Receptor and nested Podman.
alternative/docker-compose/receptor/Dockerfile Builds the Receptor sidecar image.
alternative/docker-compose/receptor/containers.conf Configures nested Podman.
alternative/docker-compose/README.md Documents Compose installation and operations.
alternative/docker-compose/docker-compose.yml Defines the Compose deployment and volumes.
alternative/docker-compose/config/valkey.conf Configures Valkey.
alternative/docker-compose/config/settings.py Defines container settings.
alternative/docker-compose/config/receptor.conf Configures Receptor.
alternative/docker-compose/config/nginx.conf Configures container nginx.
alternative/docker-compose/config/nginx-locations.conf Defines nginx routes.
alternative/docker-compose/config/nginx-http-redirect.conf Defines HTTP redirects.
alternative/docker-compose/config/nginx-http-insecure.conf Configures insecure HTTP mode.
alternative/docker-compose/certs/.gitkeep Preserves the certificate directory.
alternative/docker-compose/.gitignore Excludes generated configuration.
alternative/docker-compose/.env.example Provides Compose configuration defaults.
Review details

Suppressed comments (10)

alternative/docker-compose/docker-compose.yml:181

  • The task entrypoint creates /var/lib/awx/job_status, but this path is not backed by a shared volume and the web service has no mount for it either. Job output written by the task container is therefore unavailable after a task restart and cannot be read by the web container; add a named job-status volume and mount it in init, task, and web.
      - projects:/var/lib/awx/projects
      - job_private_data:/tmp
      - valkey_socket:/var/run/valkey
      - receptor_socket:/var/run/awx-receptor
      - rsyslog_socket:/var/run/awx-rsyslog

alternative/docker-compose/docker-compose.yml:204

  • rsyslog_spool is mounted into the unprivileged rsyslog container, but init never mounts or initializes this volume. A fresh named volume is root-owned 0755, so uid 1000 cannot create the generated rsyslog configuration/spool files and the rsyslog service will fail on first start; prepare this volume in init as well.
    volumes:
      - ./config/settings.py:/etc/tower/settings.py:ro
      - ./config/receptor.conf:/etc/receptor/receptor.conf:ro
      - valkey_socket:/var/run/valkey
      - rsyslog_socket:/var/run/awx-rsyslog
      - rsyslog_spool:/var/lib/awx/rsyslog

alternative/docker-compose/docker-compose.yml:144

  • The Compose deployment bind-mounts host config, scripts, and certificates with :ro but without an SELinux relabel option. On the Rocky/EL9 hosts this install targets, enforcing SELinux commonly labels the checkout so the containers cannot read these mounts; use :ro,z (or document/handle the SELinux requirement) for the shared bind mounts.
      - ./config/settings.py:/etc/tower/settings.py:ro
      - ./config/receptor.conf:/etc/receptor/receptor.conf:ro
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./config/nginx-locations.conf:/etc/nginx/ascender-locations.conf:ro
      - ./config/nginx-http-${ASCENDER_HTTP_MODE:-redirect}.conf:/etc/nginx/ascender-http.conf:ro
      - ./certs:/etc/tower/certs:ro

alternative/docker-compose/setup.sh:37

  • The generated private TLS key is made world-readable (0644). Any local user on the host can copy it and impersonate the Ascender endpoint; keep the key restricted and provide the container's nginx process access through a secret or an appropriate group/UID mapping instead.
if [ ! -f certs/ascender.crt ] || [ ! -f certs/ascender.key ]; then
    openssl req -x509 -nodes -newkey rsa:2048 -days 825 \
        -keyout certs/ascender.key -out certs/ascender.crt \
        -subj "/CN=${hostname_value}" \
        -addext "subjectAltName=DNS:${hostname_value},DNS:localhost,IP:127.0.0.1" 2>/dev/null
    # nginx runs as uid 1000 inside the container; it needs to read the key.
    chmod 644 certs/ascender.crt certs/ascender.key

alternative/podman/install.sh:80

  • systemctl enable --now does not restart an already active unit. Re-running this installer therefore replaces the scripts/configuration on disk but leaves the old containers running, contrary to the README's upgrade instructions; explicitly restart the unit after installing the files.
if [[ "${START}" == yes ]]; then
    echo "enabling and starting ascender.service (first start pulls images and migrates; follow with: journalctl -fu ascender)"
    echo "This will take a while..."
    systemctl enable --now ascender.service

alternative/podman/manage-containers.sh:361

  • The task entrypoint creates /var/lib/awx/job_status, but only /tmp is shared here and the web container has no mount for the job-status path. Job output is consequently container-local and is lost on task recreation or inaccessible to the web process; add a shared job-status volume to the task and web containers and initialize it.
    alternative/podman/manage-containers.sh:407
  • VOL_RSYSLOG_SPOOL is mounted into the unprivileged rsyslog container, but run_init never mounts or initializes it. A fresh named volume remains root-owned 0755, so uid 1000 cannot write the generated rsyslog configuration/spool files and first startup fails; include this volume in the init step.
    alternative/services/roles/ascender_build/tasks/main.yml:16
  • This command has no check-mode handling. With ./setup.sh --check, Ansible skips a command without creates/removes, leaving _commit.stdout undefined for the build decision below (and a fresh host has no checkout), so the documented check invocation fails instead of validating the install.
    alternative/services/roles/ascender_config/tasks/main.yml:114
  • creates checks only the certificate, while the following task also requires the key. If ascender.key is deleted or replaced while ascender.crt remains, OpenSSL is skipped and the play fails on the missing key during a rerun. Gate generation on both files (or otherwise manage the certificate pair atomically).
    alternative/services/roles/ascender_config/templates/postgres.py.j2:8
  • The password is inserted into a single-quoted Python literal without escaping. A documented ascender_pg_password containing ' or a backslash produces invalid postgres.py, so the service and migrations fail on the next run; render it as a JSON/Python-escaped string instead.
  • Files reviewed: 65/66 changed files
  • Comments generated: 22
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread alternative/docker-compose/config/valkey.conf
Comment thread alternative/docker-compose/receptor/Dockerfile
Comment thread alternative/docker-compose/setup.sh
Comment thread alternative/podman/config/valkey.conf
Comment thread alternative/podman/install.sh
Comment thread alternative/podman/config/nginx-locations.conf Outdated
Comment thread alternative/services/roles/ascender_config/templates/nginx-locations.conf.j2 Outdated
Comment thread alternative/services/roles/ascender_system/tasks/main.yml
Comment thread alternative/services/roles/ascender_system/tasks/main.yml

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical and moderate deployment, security, connectivity, and upgrade issues remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (18)

Previously missed (2) — in code that hasn't changed since the last review.

alternative/services/roles/ascender_config/tasks/main.yml:128

  • The creates guard only checks for the certificate. If a previous run leaves ascender.crt but loses or replaces ascender.key, this task skips generation and the later permission task does not create the missing key, so nginx validation fails instead of repairing the installation. Guard generation on the presence of both files (or stat them and regenerate when either is absent).
    alternative/services/roles/ascender_config/templates/postgres.py.j2:8
  • ascender_pg_password is an inventory-configurable value, but it is inserted into a single-quoted Python literal without escaping. A password containing a quote, backslash, or newline will make conf.d/postgres.py invalid and prevent every Ascender process from starting. Serialize the value as a Python/JSON string when rendering this template.

alternative/docker-compose/docker-compose.yml:146

  • These host bind mounts omit Docker's SELinux relabel option. On the Rocky/EL hosts this PR targets with SELinux enforcing, the containers can be denied access to the mounted settings, scripts, and certificates; the Podman implementation already uses :z for its equivalent mounts. Add ,z to every host bind mount in this Compose file (including ./scripts and ./certs), not just the files shown here.
      - ./config/settings.py:/etc/tower/settings.py:ro
      - ./config/receptor.conf:/etc/receptor/receptor.conf:ro
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./config/nginx-locations.conf:/etc/nginx/ascender-locations.conf:ro
      - ./config/nginx-http-${ASCENDER_HTTP_MODE:-redirect}.conf:/etc/nginx/ascender-http.conf:ro

alternative/docker-compose/docker-compose.yml:150

  • JOBOUTPUT_ROOT is /var/lib/awx/job_status in the mounted settings, but this service only shares projects, valkey_socket, and rsyslog_socket; launch_task.sh creates job_status only inside the task container. Job output written by the task therefore is not visible to the web container, so the UI cannot reliably serve job logs. Add a shared job-status volume, initialize it with the other shared volumes, and mount it in the web and task containers (and any execution container that writes to it).
      - projects:/var/lib/awx/projects
      - valkey_socket:/var/run/valkey
      - rsyslog_socket:/var/run/awx-rsyslog

alternative/docker-compose/docker-compose.yml:98

  • The rsyslog service mounts a fresh rsyslog_spool volume at /var/lib/awx/rsyslog, but this init container never mounts or chmods that volume. launch_awx_rsyslog.sh then runs as uid 1000 and writes rsyslog.conf there, so the first start can fail with permission denied. Include this volume in the init mounts so it receives the same group-writable permissions as the other shared volumes.
      - podman_storage:/var/lib/awx/.local/share/containers

alternative/docker-compose/receptor/launch_awx_receptor.sh:26

  • This command is documented as required to initialize rootless Podman, but || true discards every failure. If subordinate-ID or storage setup fails, receptor still starts and the install appears healthy until the first job fails. Propagate the failure or distinguish a benign already-initialized result from an actual setup error.
# Required once per storage volume for rootless podman to set up its user
# namespace state; harmless when already done.
podman system migrate >/dev/null 2>&1 || true

alternative/docker-compose/scripts/bootstrap.sh:26

  • Every createsuperuser failure is treated as proof that the user already exists. Invalid admin input or a transient database error therefore enters update_password, which can hide the original failure and leave no usable admin while bootstrap continues. Only use the password-sync fallback after verifying that the user exists; propagate other failures.
   awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
    echo "bootstrap: created admin user"
else
    # Already exists: make the password match .env, like the operator does.
    awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null

alternative/podman/manage-containers.sh:98

  • JOBOUTPUT_ROOT is /var/lib/awx/job_status, but the Podman layout defines only VOL_JOBDATA for /tmp; the task entrypoint creates job_status in its own container filesystem and the web container never receives it. This loses the shared job output that the UI needs. Add a persistent shared job-status volume and mount/initialize it for the relevant containers.
    alternative/podman/manage-containers.sh:262
  • The rsyslog container mounts VOL_RSYSLOG_SPOOL at /var/lib/awx/rsyslog, but run_init does not mount that volume, so the init script cannot make it group-writable. The unprivileged launch_awx_rsyslog.sh subsequently creates rsyslog.conf in the fresh root-owned volume and fails on the first start. Mount the spool volume in this init command as well.
    alternative/podman/receptor/launch_awx_receptor.sh:26
  • This command is documented as required to initialize rootless Podman, but || true discards every failure. If subordinate-ID or storage setup fails, receptor still starts and the install appears healthy until the first job fails. Propagate the failure or distinguish a benign already-initialized result from an actual setup error.
    alternative/podman/scripts/bootstrap.sh:26
  • Every createsuperuser failure is treated as proof that the user already exists. Invalid admin input or a transient database error therefore enters update_password, which can hide the original failure and leave no usable admin while bootstrap continues. Only use the password-sync fallback after verifying that the user exists; propagate other failures.
    alternative/services/install.yml:8
  • This new infrastructure-installation playbook does not attach the required owner and repo tags/metadata to the resources it creates. Add those tags consistently so installations from this path can be attributed and discovered.
    alternative/services/roles/ascender_build/tasks/main.yml:58
  • On an upgrade, ascender.service is still running while this task force-reinstalls packages into its live virtualenv; the restart handler is deferred until the end of a later role. Old processes can therefore run while their dependencies are replaced, and migrations can run before those processes are restarted, creating mixed-code requests and migration failures. Stop the service before changing the venv (or switch to a versioned venv atomically) and start it only after the migration/configuration sequence.
    alternative/services/roles/ascender_config/templates/channels.py.j2:2
  • The websocket secret is also interpolated into a single-quoted Python literal without escaping. Although generated secrets are alphanumeric, the documented variable can be overridden; a quote, backslash, or newline then breaks channels.py and prevents Ascender from starting. Use a JSON/Python-escaped rendering here.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This starts a plaintext Receptor TCP listener on the host by default. The playbook only opens 80/443 in firewalld, but firewalld may be disabled or ascender_manage_firewalld may be false, in which case port 27199 is reachable without TLS/authentication. Bind the listener to loopback by default or make it opt-in behind a TLS/peer configuration before exposing it.
    alternative/services/roles/ascender_receptor/tasks/main.yml:6
  • The downloaded archive contains the executable that will run as the awx service user, but get_url does not verify it. The v1.6.8 release publishes checksums.txt; without validating the selected asset's SHA-256, a compromised release path or proxy can replace the binary and gain code execution in the Ascender service.
    alternative/services/roles/ascender_services/tasks/main.yml:19
  • On an upgrade, the build role notifies restart ascender, but that handler is not flushed until line 89. Therefore the currently running old Ascender processes remain online while this task applies the new code's migrations, which can expose an incompatible schema to the old workers and cause failed requests or jobs. Stop/restart the service before migrating (while still handling the first-install case) rather than deferring the restart until after the migration.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • This Node-based build path installs npm and documents it as the prerequisite, but the supported package-manager path should use pnpm. Provision/configure pnpm and ensure the UI build invokes it so dependency resolution is consistent with the rest of the project.
  • Files reviewed: 65/66 changed files
  • Comments generated: 6
  • Review effort level: Lite

Comment thread alternative/docker-compose/config/settings.py Outdated
Comment thread alternative/docker-compose/docker-compose.yml
Comment thread alternative/docker-compose/setup.sh Outdated
Comment thread alternative/podman/config/settings.py Outdated
Comment thread alternative/podman/manage-containers.sh
Comment thread alternative/services/roles/ascender_defaults/defaults/main.yml Outdated
Copilot AI review requested due to automatic review settings September 11, 2026 01:58

Copilot AI left a comment

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.

🟡 Changes recommended

Critical and moderate findings remain unresolved, including rollback-induced outages, sub-ID overlap validation, bootstrap failures, and configuration issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (11)

Previously missed (2) — in code that hasn't changed since the last review.

alternative/services/roles/ascender_config/templates/postgres.py.j2:8

  • The configured database password is inserted into a single-quoted Python literal without escaping. A password containing a quote, backslash, or newline produces invalid or altered settings and prevents the service from starting. Render a Python/JSON-escaped string instead.
    alternative/services/README.md:80
  • The PostgreSQL role leaves the default local Unix-socket rule in place and only adds password-authenticated TCP rules. Running pg_dump -U ascender ascender as the operator therefore uses peer authentication and normally fails because the OS user is not ascender. Document a TCP dump with the stored password or run it as the postgres OS user.

alternative/docker-compose/docker-compose.yml:18

  • This new Compose infrastructure has no owner or repo metadata labels. Add the required ownership and repository tags/labels to the managed services (and equivalent metadata to the other alternative-install manifests) so deployed resources can be attributed.
name: ascender

alternative/docker-compose/scripts/bootstrap.sh:26

  • This fallback runs for every nonzero createsuperuser exit, not only the duplicate-user case, while stderr is discarded. Invalid input, a database failure, or another bootstrap error is therefore reported as "user exists" and can be hidden behind a misleading update_password failure. Preserve the original error and only call update_password when the duplicate condition is explicitly detected.
else
    # Already exists: make the password match .env, like the operator does.
    awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null

alternative/docker-compose/setup.sh:14

  • The README's quick-start sequence runs this script with sudo and then invokes docker compose as the normal user. When .env is first created by sudo, this mode makes it root-only, so Compose cannot read the generated credentials and the next command fails with missing variables. Preserve the invoking user's ownership/read access (or run the Compose command as root) while still protecting the file.
chmod 0600 .env

alternative/podman/install.sh:71

  • On a re-run after an operator replaces either certificate file, this conditional skips the permission repair. A typical root-owned 0600 private key then remains unreadable to the web container (uid 1000/gid 0), so nginx cannot start even though the README says replacing the certs and restarting is supported. Keep the ownership/mode repair outside the generation guard.
if [[ ! -f "${CONFDIR}/certs/ascender.crt" || ! -f "${CONFDIR}/certs/ascender.key" ]]; then
    openssl req -x509 -nodes -newkey rsa:2048 -days 825 \
        -keyout "${CONFDIR}/certs/ascender.key" -out "${CONFDIR}/certs/ascender.crt" \
        -subj "/CN=${hostname_value}" \
        -addext "subjectAltName=DNS:${hostname_value},DNS:localhost,IP:127.0.0.1" 2>/dev/null
    # nginx runs as uid 1000, gid 0 inside the container: give the group (0)
    # read access and keep the private key out of reach of other local users.
    chmod 0644 "${CONFDIR}/certs/ascender.crt"
    chown root:0 "${CONFDIR}/certs/ascender.key"
    chmod 0640 "${CONFDIR}/certs/ascender.key"

alternative/podman/scripts/bootstrap.sh:26

  • This fallback runs for every nonzero createsuperuser exit, not only the duplicate-user case, while stderr is discarded. Invalid input, a database failure, or another bootstrap error is therefore reported as "user exists" and can be hidden behind a misleading update_password failure. Preserve the original error and only call update_password when the duplicate condition is explicitly detected.
    alternative/services/README.md:87
  • The documented supervisorctl command will not resolve on a fresh install: supervisor is installed only inside {{ ascender_venv }} and is not symlinked into /usr/bin, while sudo typically resets PATH. Invoke the virtualenv's supervisorctl (or set PATH) so this operations command works.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This enables a listener on the direct-services host's network interfaces, but the default configuration has no TLS or peer authentication. Because firewalld is optional, port 27199 can be reachable by untrusted hosts and accept unauthenticated receptor peers. Bind it locally or make the listener opt-in only after TLS/authentication is configured.
    alternative/services/roles/ascender_receptor/tasks/main.yml:6
  • The receptor executable is downloaded and installed as root without any integrity check. The pinned GitHub release publishes checksums, so a replaced or corrupted asset would be trusted and run by the system service; verify the version/architecture asset against its release checksum before unpacking it.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • This installation path explicitly installs npm and the README advertises npm as a build prerequisite, but the repository's Node tooling requirement is to use pnpm. Replace the npm dependency/build path with pnpm (including whatever the Ascender UI build invokes) so this alternative does not introduce npm.
  • Files reviewed: 66/67 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread alternative/podman/manage-containers.sh Outdated
Comment thread alternative/services/roles/ascender_system/tasks/subids.yml Outdated
Copilot AI review requested due to automatic review settings September 11, 2026 02:34

Copilot AI left a comment

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.

🟡 Changes recommended

One or more issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (26)

Previously missed (4) — in code that hasn't changed since the last review.

alternative/docker-compose/docker-compose.yml:98

  • The rsyslog_spool named volume is mounted into the rsyslog container later, but it is not mounted into init, and init.sh never prepares /var/lib/awx/rsyslog. On a fresh install this leaves the volume root-owned while rsyslog runs as UID 1000, so its spool/config writes can fail. Mount this volume in the init service and add the path to the ownership loop (and mirror the fix in the Podman installer).
    alternative/podman/manage-containers.sh:262
  • VOL_RSYSLOG_SPOOL is mounted into ascender-rsyslog, but run_init does not mount that volume and init.sh does not prepare /var/lib/awx/rsyslog. A new install therefore leaves this volume root-owned while rsyslog runs as UID 1000, which can prevent spool/config writes. Include the volume in init and initialize its ownership.
    alternative/services/roles/ascender_config/tasks/main.yml:128
  • creates checks only the certificate. If an install is interrupted after the certificate is written but before the key is created (or the key is later removed), this task is skipped and nginx is rendered with a missing key, so the installation cannot start. Check that both files exist before skipping generation, or regenerate when either is absent.
    alternative/services/roles/ascender_config/templates/postgres.py.j2:8
  • The password is inserted into a single-quoted Python literal without escaping. A user-supplied ascender_pg_password containing an apostrophe, backslash, or newline produces invalid settings and prevents every management/service command from starting. Serialize the value as a Python/JSON string rather than interpolating it into quotes.

alternative/docker-compose/config/nginx-locations.conf:29

  • $server_port is the container's listener (8052/8053), not the published ASCENDER_HTTP(S)_PORT on the host. Because settings.py enables USE_X_FORWARDED_PORT, Django receives 8052/8053 and can generate absolute URLs or redirects to an unreachable internal port, especially when the host uses a non-default port. Render the external port into this config or pass a correctly trusted forwarded-port value.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;

alternative/docker-compose/docker-compose.yml:75

  • These Docker bind mounts omit the :z/:Z SELinux relabel option. On the Rocky/EL9 and Fedora hosts described by this install, an enforcing SELinux policy can leave the unprivileged containers unable to read the mounted configs, scripts, and certificates, causing startup to fail. Add the appropriate shared relabel suffix to every host bind mount in this Compose file.
      - ./config/valkey.conf:/usr/local/etc/valkey/valkey.conf:ro

alternative/docker-compose/receptor/containers.conf:6

  • userns="host" makes the rootless job containers use the sidecar's user namespace, so container root is the unprivileged awx UID rather than root in a subordinate namespace. Execution environments normally run Ansible as root; with this default, jobs that need root-owned paths or package installation will run without the expected privileges. Let rootless Podman create its normal user namespace instead; sharing UID 1000 files does not require this override.
userns="host"

alternative/docker-compose/scripts/bootstrap.sh:26

  • The admin password is passed in --password, so despite the surrounding redirection and the generated env file, it remains visible in the container process command line while awx-manage runs. Use a supported stdin, file, or environment-based secret input instead of placing it in argv.
    awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null

alternative/docker-compose/scripts/bootstrap.sh:10

  • The migration container is configured to run as 1000:0, so this fallback cannot append to root-owned /etc/passwd when the UID has no entry. Since set -e is enabled only after the block, the failure is silently ignored and the bootstrap may then fail when Python resolves the current user. Establish the passwd entry in the image or from a root init step before running this script.
if [ "$(id -u)" -ge 500 ]; then
    echo "awx:x:$(id -u):$(id -g):,,,:/var/lib/awx:/bin/bash" >> /etc/passwd
fi

alternative/docker-compose/scripts/launch_task.sh:9

  • This container is configured to run as 1000:0, so when that UID is absent from /etc/passwd the redirection is attempted by an unprivileged process and fails. Because set -e is enabled only afterward, the failure is ignored and the script continues without the account entry that Python/user lookups may require. Create the entry in the image or from a root init step before dropping privileges instead of appending it here.
if [ "$(id -u)" -ge 500 ]; then
    echo "awx:x:$(id -u):$(id -g):,,,:/var/lib/awx:/bin/bash" >> /etc/passwd
fi

alternative/docker-compose/setup.sh:61

  • This rewrites config/nginx-http-redirect.conf, which is a tracked file in the install directory, just to encode the host port. Running setup from a checkout leaves a persistent Git modification and can carry a stale generated port into another install. Generate a runtime config or interpolate the port without mutating the repository file.
sed -i -E "s#^(return 301 https://\\\$host)(:[0-9]+)?(\\\$request_uri;)#\\1${https_suffix}\\3#" \
    config/nginx-http-redirect.conf

alternative/podman/config/nginx-locations.conf:29

  • $server_port is the container's listener (8052/8053), not the published ASCENDER_HTTP(S)_PORT on the host. Because settings.py enables USE_X_FORWARDED_PORT, Django receives 8052/8053 and can generate absolute URLs or redirects to an unreachable internal port, especially when the host uses a non-default port. Render the external port into this config or pass a correctly trusted forwarded-port value.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;

alternative/podman/install.sh:66

  • This guard also controls the certificate permission fix below. If an operator replaces an existing certificate/key pair as documented, neither file is missing and the key can remain root-owned 0600, which uid 1000/gid 0 in the web container cannot read. Keep generation conditional, but apply the ownership and mode normalization on every install.
    alternative/podman/manage-containers.sh:475
  • The existing containers are removed before image pulls and the receptor build run. If a registry outage or build failure occurs during an upgrade, the EXIT trap only stops the partially created stack; it cannot restore the old containers, leaving the service down. Pull/build the new images before tearing down the currently running stack, or retain a rollback path.
    alternative/podman/scripts/bootstrap.sh:26
  • The admin password is passed in --password, so despite the generated env file it remains visible in the container process command line while awx-manage runs. Use a supported stdin, file, or environment-based secret input instead of placing it in argv.
    alternative/podman/scripts/bootstrap.sh:10
  • The migration container is configured to run as 1000:0, so this fallback cannot append to root-owned /etc/passwd when the UID has no entry. Since set -e is enabled only after the block, the failure is silently ignored and the bootstrap may then fail when Python resolves the current user. Establish the passwd entry in the image or from a root init step before running this script.
    alternative/podman/scripts/launch_task.sh:9
  • This container is configured to run as 1000:0, so when that UID is absent from /etc/passwd the redirection is attempted by an unprivileged process and fails. Because set -e is enabled only afterward, the failure is ignored and the script continues without the account entry that Python/user lookups may require. Create the entry in the image or from a root init step before dropping privileges instead of appending it here.
    alternative/services/README.md:80
  • The PostgreSQL role leaves the default local peer rule in pg_hba.conf and adds password authentication only for TCP. Running this documented command as the usual root shell therefore attempts local peer authentication as root, not database user ascender, and fails before producing a backup. Use a command that runs as the postgres OS user or explicitly supplies the TCP/password authentication path.
    alternative/services/README.md:87
  • The supervisor package is installed only inside {{ ascender_venv }} by the build role, and no supervisorctl executable is linked into a system PATH. Therefore this documented command returns command not found on a normal host; invoke the venv's supervisorctl (or install/link it system-wide).
    alternative/services/README.md:47
  • The documented ./setup.sh --check does not work on a clean host: check mode does not create the generated secret files, but ascender_config immediately slurps them and later roles also assume the checked-out source/configuration exists. Either make the playbook check-mode safe by guarding those reads and dependent tasks, or remove this claim and document that check mode is only useful after an installation.
    alternative/services/roles/ascender_config/templates/channels.py.j2:2
  • The websocket secret is inserted into a single-quoted Python literal without escaping. Although generated values are simple, an explicitly configured secret containing Python string characters can make channels.py invalid and stop the application. Render it as a serialized string instead of embedding it inside quotes.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This enables a plain TCP listener on the host's default bind address, while the playbook only opens 80/443 in firewalld. If firewalld is disabled or another network path permits access, port 27199 is exposed despite the comment and has no TLS or peer restriction. Keep the listener disabled/loopback-bound by default and require an explicit TLS configuration before enabling remote peering.
    alternative/services/roles/ascender_receptor/tasks/main.yml:6
  • This downloads and installs an executable release artifact without a checksum. A compromised or replaced GitHub asset would be accepted silently and then run as the Ascender service user. Pin a release checksum (or verify a signed checksum manifest) before unpacking the binary.
    alternative/services/roles/ascender_services/tasks/main.yml:33
  • no_log hides this task from Ansible output, but the effective admin password is still placed in --password and is observable in the process command line while awx-manage runs. Use a supported stdin, file, or environment-based secret input instead of passing the password in argv.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • The UI build is Node-based, but this installs npm; the repository toolchain requires pnpm instead. Install/configure pnpm and ensure the make build path invokes it so the direct-services installation uses the supported package manager.
    alternative/services/roles/ascender_system/tasks/subids.yml:53
  • ascender_subid_start is written verbatim, while it is only coerced with | int for overlap calculations; _subid_count has the same gap. Values such as ascender_subid_start=abc or ascender_subid_count=0 can pass the overlap assertion and produce an invalid /etc/subuid//etc/subgid entry, with rootless Podman failing later. Validate that both values are positive decimal ranges before selecting and writing them.
  • Files reviewed: 67/68 changed files
  • Comments generated: 7
  • Review effort level: Lite

Comment thread alternative/docker-compose/config/settings.py Outdated
Comment thread alternative/docker-compose/receptor/Dockerfile
Comment thread alternative/podman/config/settings.py Outdated
Comment thread alternative/podman/manage-containers.sh Outdated
Comment thread alternative/podman/receptor/Dockerfile
Comment thread alternative/services/roles/ascender_config/templates/settings.py.j2
Comment thread alternative/services/roles/ascender_services/tasks/main.yml
Copilot AI review requested due to automatic review settings September 11, 2026 05:09

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical startup failures and configuration issues must be addressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (17)

alternative/docker-compose/config/nginx-locations.conf:29

  • $server_port is the port nginx listens on inside the container (8052/8053), not the published host port. With USE_X_FORWARDED_PORT = True and the documented 80/443 or custom mappings, Django will therefore see the wrong port when building absolute URLs and redirects. Pass the configured external port instead of the internal $server_port.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;

alternative/docker-compose/receptor/Dockerfile:30

  • These edits assume uncommented storage.conf keys, but the EL9 defaults use # mount_program, # mountopt, and a commented # additionalimagestores = [ array. The first substitution misses the space, the mountopt substitution misses the comment, and the next expression appends an uncommented array element without uncommenting the array; the resulting config either lacks the required overlay settings or is invalid TOML, so the validation below aborts the receptor image build. Rewrite the relevant [storage.options]/[storage.options.overlay] entries explicitly for the supported base image.
RUN sed -i -e 's|^#mount_program|mount_program|g' \
           -e '/additionalimage.*/a "/var/lib/shared",' \
           -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' \

alternative/docker-compose/scripts/bootstrap.sh:26

  • createsuperuser can fail for reasons other than an existing username, but this branch suppresses stderr and treats every nonzero exit as “already exists.” A bad email, database failure, or other bootstrap error is then hidden and reported as an unrelated password-sync failure. Only perform update_password for the specific already-exists result; otherwise preserve the original failure.
if DJANGO_SUPERUSER_PASSWORD="${ASCENDER_ADMIN_PASSWORD}" \
   awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
    echo "bootstrap: created admin user"
else
    # Already exists: make the password match .env, like the operator does.
    awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null

alternative/docker-compose/setup.sh:61

  • The services path rewrites the redirect file only when setup.sh runs. If a user later changes the documented ASCENDER_HTTPS_PORT in .env and runs docker compose up, the port mapping changes but this mounted config still redirects to the old port. Generate the redirect from the current settings on each start, or make the configuration workflow require rerunning this setup step.
sed -i -E "s#^(return 301 https://\\\$host)(:[0-9]+)?(\\\$request_uri;)#\\1${https_suffix}\\3#" \
    config/nginx-http-redirect.conf

alternative/podman/config/nginx-locations.conf:29

  • This has the same port-forwarding problem as the Compose configuration: $server_port is 8052/8053 inside the container, while USE_X_FORWARDED_PORT needs the externally published port. The default HTTPS mapping alone is enough to make Django receive 8053 instead of 443, and custom mappings are also documented. Pass the host port into nginx rather than forwarding this internal value.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;

alternative/podman/install.sh:66

  • Certificate permissions are fixed only inside the self-signed-generation branch. The Podman README tells operators to replace these files with their own certificate and restart the service; a normal root-owned 0600 private key then remains unreadable to nginx running as uid 1000/gid 0, causing the web container to fail. Apply the ownership and mode normalization on every install, not only when generating a new certificate.
    alternative/podman/receptor/Dockerfile:30
  • These edits assume uncommented storage.conf keys, but the EL9 defaults use # mount_program, # mountopt, and a commented # additionalimagestores = [ array. The first substitution misses the space, the mountopt substitution misses the comment, and the next expression appends an uncommented array element without uncommenting the array; the resulting config either lacks the required overlay settings or is invalid TOML, so the validation below aborts the receptor image build. Rewrite the relevant [storage.options]/[storage.options.overlay] entries explicitly for the supported base image.
    alternative/podman/scripts/bootstrap.sh:26
  • This Podman bootstrap duplicates the Compose error-handling bug: every createsuperuser failure is assumed to mean that the user already exists, and stderr is discarded. Configuration or database failures can therefore be masked as a misleading update_password error. Restrict the fallback to the specific already-exists case and propagate other failures.
    alternative/services/.gitignore:1
  • setup.sh explicitly accepts inventory.yml, but only inventory is ignored. Users following that supported path can put host variables or passwords in inventory.yml and accidentally commit them. Ignore both inventory filenames (or use a matching pattern).
    alternative/services/README.md:84
  • The documented upgrade backup command uses the local Unix socket, but the role leaves local connections on peer authentication and only adds password authentication for 127.0.0.1. A normal operator therefore cannot run this command as OS user ascender; use the postgres OS account or specify the TCP host and password.
    alternative/services/roles/ascender_build/tasks/main.yml:32
  • The setup wrapper explicitly advertises ./setup.sh --check, but in Ansible check mode the command task that populates _commit is skipped, so _commit.stdout is absent here. This set-fact therefore fails while evaluating the build decision (and the services role has similar assumptions about skipped command results), making the documented check mode unusable. Guard skipped results or implement check-mode handling before advertising this option.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This creates a TCP Receptor listener without TLS/authentication or an explicit bind address. The system role only opens HTTP/HTTPS when an already-running firewalld is present and does not install one, so a host without that firewall can expose port 27199 to the network and accept unauthenticated peer traffic. Bind the listener to loopback by default, or require an explicitly configured authenticated TLS listener and firewall rule before enabling remote peering.
    alternative/services/roles/ascender_receptor/tasks/main.yml:37
  • loginctl enable-linger only arranges for a user manager/runtime directory at boot; it does not guarantee /run/user/{{ ascender_uid }} exists during this play or order the later system service behind it. The next role invokes podman system migrate with that XDG_RUNTIME_DIR, and ascender.service exports the same path, so a fresh install or boot can fail rootless Podman with a missing runtime directory. Create/manage the runtime directory and add the required ordering before starting Podman.
    alternative/services/roles/ascender_receptor/tasks/main.yml:5
  • This downloads an executable archive that is then installed as /usr/local/bin/receptor without verifying its contents. A replaced release asset or compromised download path would be accepted and executed as the Ascender service user. Pin and verify the release's published SHA-256 checksum for each supported version/architecture before unarchiving.
    alternative/services/roles/ascender_system/tasks/main.yml:85
  • The README advertises ./setup.sh --check, but on a fresh host the user task is only simulated in check mode and _awx_user.uid is not guaranteed to exist. This set_fact then fails while evaluating the missing attribute, so the advertised dry run cannot complete. Handle check mode for this fact and the later build/command tasks, or remove the --check claim.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • This direct install explicitly provisions npm for the UI build. Node-based code and workflows in this project are required to use pnpm; provision a pinned pnpm toolchain and ensure the source build invokes pnpm instead of npm.
    alternative/services/setup.sh:79
  • The documented inventory runs the playbook as root, but this bootstrap path invokes sudo unconditionally when Ansible is missing. On a minimal Rocky host where sudo is not installed, running ./setup.sh as root cannot install ansible-core and fails before the playbook can install its prerequisites. Use an empty sudo prefix for root and invoke sudo only for non-root callers.
  • Files reviewed: 67/68 changed files
  • Comments generated: 5
  • Review effort level: Lite

Comment thread alternative/docker-compose/scripts/bootstrap.sh
Comment thread alternative/docker-compose/scripts/launch_task.sh
Comment thread alternative/podman/scripts/bootstrap.sh
Comment thread alternative/podman/scripts/launch_task.sh
Comment thread alternative/services/roles/ascender_config/templates/postgres.py.j2 Outdated
Copilot AI review requested due to automatic review settings September 11, 2026 06:01

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical and moderate installation, runtime, and security issues remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (12)

Previously missed (3) — in code that hasn't changed since the last review.

alternative/podman/install.sh:83

  • Re-running install.sh overwrites the manager/configuration files and then calls systemctl enable --now on a unit that is usually already active. --now does not re-execute an active oneshot service, so the running containers keep the old image/config despite the README promising that reruns upgrade them. Stop/start or restart the unit after installing the files.
    alternative/services/roles/ascender_system/tasks/subids.yml:51
  • When both files already contain an awx entry, this logic always prefers /etc/subuid and later writes that range into /etc/subgid. If the existing UID and GID mappings differ, the play silently changes the user's GID range and can orphan rootless Podman storage, contradicting the claim that an existing range is kept. Detect inconsistent existing mappings and fail with repair instructions instead of choosing the UID entry.
    alternative/services/setup.sh:44
  • This preflight only checks whether the collection plugin names are present, not whether their versions satisfy requirements.yml. A host with an older community.postgresql or community.general collection therefore skips installation and can run incompatible modules despite the declared minimums. Validate installed versions or always install/upgrade from the requirements file.

alternative/docker-compose/scripts/bootstrap.sh:15

  • The database readiness loop has no deadline. A wrong external host, blocked port, or permanently failed PostgreSQL leaves the migration container retrying forever; Compose reports the container as started while the dependent services never become available. Bound the wait and fail with a diagnostic after a defined timeout.
echo "bootstrap: waiting for PostgreSQL at ${DATABASE_HOST}:${DATABASE_PORT}"
until pg_isready -q -h "${DATABASE_HOST}" -p "${DATABASE_PORT}" -U "${DATABASE_USER}"; do
    sleep 2
done

alternative/docker-compose/scripts/bootstrap.sh:25

  • Any createsuperuser failure is treated as proof that the user already exists because stderr is discarded and the else branch unconditionally runs update_password. For failures such as invalid input or a database/application error, this can hide the real cause and can complete without creating the requested admin user; only fall back for the known already-exists result and propagate all other errors.
if DJANGO_SUPERUSER_PASSWORD="${ASCENDER_ADMIN_PASSWORD}" \
   awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
    echo "bootstrap: created admin user"
else
    # Already exists: make the password match .env, like the operator does.

alternative/podman/install.sh:62

  • The certificate permission fix is inside the certificate-generation branch. If an operator follows the README and replaces an existing key, rerunning install.sh leaves a typical root:root 0600 key unreadable to nginx's uid 1000/gid 0, so the service cannot restart. Apply the ownership and mode normalization on every install, not only when generating a certificate.
    alternative/podman/scripts/bootstrap.sh:15
  • The database readiness loop has no deadline. A wrong external host, blocked port, or permanently failed PostgreSQL leaves systemctl start ascender blocked until the unit's one-hour start timeout, instead of failing promptly with a diagnostic. Bound the wait and report the database endpoint after a defined timeout.
    alternative/podman/scripts/bootstrap.sh:25
  • Any createsuperuser failure is treated as proof that the user already exists because stderr is discarded and the else branch unconditionally runs update_password. For failures such as invalid input or a database/application error, this can hide the real cause and can complete without creating the requested admin user; only fall back for the known already-exists result and propagate all other errors.
    alternative/services/requirements.yml:8
  • These lower bounds allow ansible-galaxy to install an untested future collection release, which can require a newer ansible-core or change module behavior and make this installer stop working on the documented Ansible 2.15+ target. The main repository pins the corresponding collections exactly in playbooks/group_vars/all.yml:343-375; pin the versions tested by this alternative as well.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This enables a cleartext Receptor TCP listener on the host, while firewalld is optional and a missing or inactive firewall is deliberately tolerated. On an EL host without an active firewall, port 27199 is therefore reachable despite the comment, without TLS protection. Bind the listener locally by default, or require an explicit TLS/firewall configuration before enabling it.
    alternative/services/roles/ascender_receptor/tasks/main.yml:5
  • This downloads and installs an executable without verifying a checksum or release signature. A compromised release asset or delivery path could therefore replace receptor and gain code execution as the service user (and access to the host's job runtime). Pin a version-specific checksum or verify the release signature before unpacking.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • The direct-service build installs and documents npm for the Node-based UI build, but the repository's coding guideline requires pnpm for Node code and workflows. Provision/enable pnpm and make the build invoke it instead of adding npm as the required package.
  • Files reviewed: 67/68 changed files
  • Comments generated: 8
  • Review effort level: Lite

Comment thread alternative/docker-compose/config/settings.py
Comment thread alternative/docker-compose/docker-compose.yml
Comment thread alternative/docker-compose/scripts/init.sh
Comment thread alternative/podman/config/settings.py
Comment thread alternative/podman/manage-containers.sh
Comment thread alternative/podman/scripts/init.sh
Comment thread alternative/services/roles/ascender_build/tasks/main.yml
Comment thread alternative/services/roles/ascender_receptor/tasks/main.yml
Copilot AI review requested due to automatic review settings September 11, 2026 16:17

Copilot AI left a comment

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.

🟡 Changes recommended

Critical installation and credential-handling issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (11)

Previously missed (3) — in code that hasn't changed since the last review.

alternative/docker-compose/scripts/bootstrap.sh:28

  • Any nonzero createsuperuser exit is treated as “already exists” because stderr is discarded and the else branch is unconditional. Invalid admin input or another database error will therefore be misreported and can be masked by a subsequent password-update attempt; only take that branch when the error explicitly indicates an existing username, and surface other failures.
    alternative/podman/install.sh:74
  • Certificate permissions are normalized only inside the branch that generates a new certificate. The README tells operators to replace the certificate/key and run systemctl restart ascender; a replacement key with the usual root:root 0600 permissions then remains unreadable by nginx uid 1000/gid 0, so the service fails. Apply these chmod/chown operations on every install, not only when generating the files.
    alternative/podman/scripts/bootstrap.sh:28
  • Any nonzero createsuperuser exit is treated as “already exists” because stderr is discarded and the else branch is unconditional. Invalid admin input or another database error will therefore be misreported and can be masked by a subsequent password-update attempt; only take that branch when the error explicitly indicates an existing username, and surface other failures.

alternative/docker-compose/docker-compose.yml:143

  • These host bind mounts have no :z relabel option. On the Rocky/EL9 SELinux-enforcing hosts this installation targets, the containers can be denied access to the repository files (including the certificate), so the stack may not start. Add a shared SELinux relabel (:z) to every ./... bind mount in this Compose file, or explicitly document the required manual relabeling.
      - ./config/settings.py:/etc/tower/settings.py:ro

alternative/services/.gitignore:1

  • setup.sh explicitly prefers inventory.yml when it exists, but this ignore file only excludes inventory. A YAML inventory can contain the same admin/database secrets as the ignored file and would then be easy to commit accidentally; ignore both local inventory filenames.
    alternative/services/install.yml:8
  • This new Ansible infrastructure entry point does not declare the required owner and repo tags/metadata. Add the repository's owner/repository attribution to the managed infrastructure (or the play/resource metadata) so deployments remain attributable.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • This starts an unauthenticated Receptor TCP listener on the host's interfaces by default. The firewalld role is optional and tolerates a missing or inactive firewall, so on a host without an active firewall port 27199 becomes remotely reachable even though the comment only warns not to open it; gate the listener behind explicit TLS/mesh configuration or bind it to loopback until it is secured.
    alternative/services/roles/ascender_postgresql/tasks/main.yml:69
  • The database password is returned by slurp and copied into a fact without no_log; verbose Ansible callbacks can expose both the base64 file content and the effective secret. Mark these secret-handling tasks no_log: true, as the database role task below already does.
    alternative/services/roles/ascender_system/tasks/main.yml:8
  • The README advertises RHEL 9, but crb is the Rocky/Alma repository ID; a registered RHEL 9 host exposes codeready-builder-for-rhel-9-<arch>-rpms instead. This command exits before package installation on RHEL, so either select the platform-specific repository ID or remove RHEL from the documented support.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • This direct-services path installs npm and documents it as the UI build prerequisite, but the repository's Node guidance requires pnpm for Node-based builds. Install/use pnpm instead and update the prerequisite/build invocation so this installation does not introduce a second package-manager path.
    alternative/services/setup.sh:21
  • This branch installs Ansible on Debian/Ubuntu, but every role in this playbook uses EL-specific dnf packages and module commands, so the run is guaranteed to fail after making a partial system change. Reject Debian/Ubuntu here (or provide a separate implementation) instead of advertising them as installable.
  • Files reviewed: 67/68 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread alternative/docker-compose/docker-compose.yml
Comment thread alternative/docker-compose/setup.sh
Comment thread alternative/podman/manage-containers.sh
Comment thread alternative/services/roles/ascender_postgresql/tasks/main.yml
Copilot AI review requested due to automatic review settings September 11, 2026 16:55

Copilot AI left a comment

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.

🟡 Changes recommended

Critical and moderate findings remain in the installation paths, including security and deployment-runtime issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (14)

alternative/docker-compose/config/nginx-locations.conf:30

  • Because this location uses uwsgi_pass, proxy_set_header is not applied to the uWSGI request. Without forwarding HTTP_X_FORWARDED_PROTO, Django can treat HTTPS requests as HTTP, which breaks secure URL/redirect and CSRF behavior behind this TLS terminator. Forward the scheme with uwsgi_param.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
    proxy_set_header X-Forwarded-Port $server_port;

alternative/docker-compose/docker-compose.yml:151

  • The web container handles the external-logging API, whose upstream SettingLoggingTest reads /var/lib/awx/rsyslog/rsyslog.conf, while the rsyslog container's configurer writes that file on the rsyslog_spool volume. Without this volume here, the UI validates an image-local/stale configuration instead of the active one. Mount rsyslog_spool into web as well.
      - rsyslog_socket:/var/run/awx-rsyslog

alternative/docker-compose/docker-compose.yml:99

  • rsyslog_spool is mounted by the rsyslog service below, but not by this root init container, so a fresh spool is never prepared. The image's starter rsyslog.conf is root-owned while launch_awx_rsyslog.sh overwrites it as UID 1000; the rsyslog container can therefore fail on its first start. Mount this volume in init and normalize both the directory and starter-file permissions.
      - podman_storage:/var/lib/awx/.local/share/containers

alternative/podman/config/nginx-locations.conf:30

  • Because this location uses uwsgi_pass, proxy_set_header is not applied to the uWSGI request. Without forwarding HTTP_X_FORWARDED_PROTO, Django can treat HTTPS requests as HTTP, which breaks secure URL/redirect and CSRF behavior behind this TLS terminator. Forward the scheme with uwsgi_param.
    uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
    proxy_set_header X-Forwarded-Port $server_port;

alternative/podman/manage-containers.sh:396

  • The web container handles the external-logging API, whose upstream SettingLoggingTest reads /var/lib/awx/rsyslog/rsyslog.conf, while the rsyslog container's configurer writes that file on VOL_RSYSLOG_SPOOL. Without this mount, the UI validates an image-local/stale configuration instead of the active one. Mount VOL_RSYSLOG_SPOOL into ascender-web too.
    alternative/podman/manage-containers.sh:267
  • VOL_RSYSLOG_SPOOL is mounted by run_rsyslog, but not by this root init container, so a fresh spool is never prepared. The image's starter rsyslog.conf is root-owned while launch_awx_rsyslog.sh overwrites it as UID 1000; the rsyslog container can therefore fail on its first start. Mount this volume in run_init and normalize both the directory and starter-file permissions.
    alternative/services/README.md:84
  • This backup command uses the local Unix socket and requests the ascender role. When run as the root/operator user, the default peer authentication compares the OS user and fails instead of using the database password configured by this play. Run the dump as the postgres OS user or connect to 127.0.0.1 with the generated password.
    alternative/services/roles/ascender_config/tasks/main.yml:128
  • The creates guard checks only the certificate, but the next task requires both the certificate and key. If a partial or custom install leaves ascender.crt without ascender.key, generation is skipped and the play fails at Certificate permissions, so rerunning cannot repair that state. Gate generation on both files with an explicit existence check.
    alternative/services/roles/ascender_config/templates/nginx-locations.conf.j2:27
  • Forward the scheme as a uWSGI parameter as well. This TLS-terminating nginx config currently only sets the forwarded port, so Django can treat HTTPS requests as HTTP, breaking secure URL/redirect and CSRF behavior. Add uwsgi_param HTTP_X_FORWARDED_PROTO $scheme;.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:13
  • This listener has neither a bind address nor TLS. Receptor defaults bindaddr to 0.0.0.0, so the direct-services install exposes a plaintext peer endpoint on every interface, while the firewalld tasks only manage ports 80/443. Bind it to loopback by default, or make a TLS-configured listener and explicit firewall/peer setup mandatory before exposing it.
    alternative/services/roles/ascender_receptor/tasks/main.yml:6
  • The receptor archive is downloaded over HTTPS but never checked against the release checksum. A compromised or replaced artifact would therefore be unpacked and installed as the service's executable; the v1.6.8 release publishes checksums.txt, so fetch and verify the expected per-architecture SHA-256 (or pin a trusted digest) before unarchiving.
    alternative/services/roles/ascender_receptor/tasks/main.yml:6
  • The playbook writes the archive under /usr/local/src without creating that directory. A minimal Rocky 9 host can lack /usr/local/src, so the first get_url task fails before Receptor is installed; create the download directory first.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • This direct-services path installs npm, but the Node build toolchain for this project is required to use pnpm. Since the build role delegates the UI build to the source Makefile, this package choice makes the installer select the wrong package manager; install pnpm and pass the supported package-manager override through the build instead.
    alternative/services/roles/ascender_system/tasks/subids.yml:68
  • When awx already has entries in both subordinate-id files but the ranges differ, this selection prefers the /etc/subuid range and the later lineinfile task silently rewrites /etc/subgid to match it. That changes an existing user-namespace mapping and can invalidate the user's rootless Podman storage, contrary to the comment that existing ranges are kept; detect and reject mismatched existing ranges (or otherwise handle the migration explicitly) before writing either file.
  • Files reviewed: 67/68 changed files
  • Comments generated: 5
  • Review effort level: Lite

Comment thread alternative/docker-compose/config/settings.py
Comment thread alternative/podman/config/settings.py
Comment thread alternative/podman/install.sh
Comment thread alternative/services/roles/ascender_config/templates/settings.py.j2
Comment thread alternative/services/roles/ascender_defaults/defaults/main.yml
Copilot AI review requested due to automatic review settings September 11, 2026 18:27

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved moderate and critical findings affect security and installation reliability.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (13)

Previously missed (2) — in code that hasn't changed since the last review.

alternative/docker-compose/docker-compose.yml:76

  • These host bind mounts are not given an SELinux relabel option. On an enforcing EL host, Docker leaves the files with their host labels and the containers can be denied access to the mounted settings, scripts, certificates, and Valkey config, so the documented setup fails. Add :z (or an equivalent host labeling step) to every host bind mount in this Compose file, not just the named volumes.
    alternative/services/setup.sh:105
  • This presence-only check ignores the version constraints in requirements.yml. If an older collection already exposes these four plugins, setup skips installation and the play can still run with an unsupported collection version. Install from requirements.yml (or compare the installed versions) unconditionally instead of treating plugin presence as sufficient.

alternative/docker-compose/config/settings.py:32

  • ALLOWED_HOSTS = ['*'] disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin; the comment above explicitly acknowledges this risk. Build the list from the configured hostname and aliases (plus the local health-check addresses) instead of accepting every host.
ALLOWED_HOSTS = ['*']

alternative/docker-compose/docker-compose.yml:50

  • This new Compose infrastructure has no owner or repo metadata labels on its managed services/volumes, so deployed resources cannot be attributed to an owner or repository. Add the required labels using the repository's canonical values.
services:

  postgres:
    image: ${POSTGRES_IMAGE:-quay.io/sclorg/postgresql-15-c9s}

alternative/podman/config/settings.py:32

  • ALLOWED_HOSTS = ['*'] disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin. Build the list from ASCENDER_HOSTNAME and configured aliases (plus the local health-check addresses) instead of accepting every host.
ALLOWED_HOSTS = ['*']

alternative/services/roles/ascender_config/tasks/main.yml:128

  • The certificate generation is skipped whenever ascender.crt exists, even if ascender.key is missing. In that partial-file state the following permission task cannot recreate the key and nginx will fail to start; gate this task on both certificate files existing (as the Compose and Podman installers do) so a damaged generated certificate can recover.
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • With no bind address, Receptor's TCP listener defaults to 0.0.0.0. Because ascender_manage_firewalld permits firewalld to be absent or disabled, a host without an effective firewall exposes unauthenticated port 27199 despite the comment saying it is not exposed; bind to loopback by default or make remote peering opt-in with TLS and an explicit firewall rule.
    alternative/services/roles/ascender_config/templates/settings.py.j2:21
  • ALLOWED_HOSTS = ['*'] disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin. Build the list from ascender_hostname and ascender_allowed_hosts (plus the local health-check addresses) instead of accepting every host.
    alternative/services/roles/ascender_postgresql/tasks/main.yml:5
  • The PostgreSQL stream is enabled without first resetting any currently enabled stream. On a host that already has another EL9 PostgreSQL stream selected, dnf module enable postgresql:15 fails instead of converging, so the advertised rerun/install path breaks. Reset the module stream before enabling version 15, just as needed for the Node.js stream.
    alternative/services/roles/ascender_receptor/tasks/main.yml:5
  • This downloads an executable as root and installs it into /usr/local/bin without verifying a checksum. If the GitHub asset or the connection is compromised, the playbook will execute the altered binary as the awx service; pin an official SHA-256 checksum for each supported receptor version and pass it to get_url.
    alternative/services/roles/ascender_receptor/tasks/main.yml:37
  • This runs podman system migrate immediately after enabling linger, but lingering only arranges for systemd to create /run/user/<uid> when the user manager starts (typically at boot). On a fresh host the configured XDG_RUNTIME_DIR can therefore be absent and rootless Podman fails during the first play. Create the runtime directory or start the user manager before this command.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • The direct-services path provisions npm, and the subsequent make sdist uses the source tree's default NPM_BIN=npm for the UI build. This introduces an npm-based Node build into the new installation method; use pnpm for the UI build and install/configure the corresponding tool instead.
    alternative/services/roles/ascender_system/tasks/main.yml:21
  • Enabling a module stream without resetting the existing Node.js stream fails when the EL9 host already has another stream enabled, which is common on reruns or preconfigured hosts. Reset the stream before enabling nodejs:22, as the source image build does, so the service installation is repeatable.
  • Files reviewed: 67/68 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread alternative/services/roles/ascender_services/tasks/main.yml Outdated
Copilot AI review requested due to automatic review settings September 11, 2026 18:49

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical and moderate findings affect installation, startup, persistence, security, and service initialization.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (14)

alternative/docker-compose/config/settings.py:13

  • JOBOUTPUT_ROOT is set to /var/lib/awx/job_status, but the task script creates that directory only in the task container and neither the task nor web service mounts a shared volume there. Job output/artifacts therefore remain private to the task container and disappear when it is recreated, while the web process cannot read them. Add a persistent shared job-status volume to the relevant services and initialize its ownership.
STATIC_ROOT = '/var/lib/awx/public/static'
PROJECTS_ROOT = '/var/lib/awx/projects'
JOBOUTPUT_ROOT = '/var/lib/awx/job_status'

alternative/docker-compose/config/settings.py:32

  • ALLOWED_HOSTS = ['*'] disables Django's Host-header validation for the public HTTP/HTTPS endpoints. An attacker can send arbitrary Host headers and influence host-derived absolute URLs; the surrounding comment already acknowledges this for TOWER_URL_BASE. Build this list from ASCENDER_HOSTNAME and _extra_hosts instead, or require a trusted proxy to validate Host.
ALLOWED_HOSTS = ['*']

alternative/docker-compose/docker-compose.yml:99

  • The rsyslog service mounts rsyslog_spool at /var/lib/awx/rsyslog, but this init container does not mount that volume, so the ownership loop cannot prepare it. A fresh named volume remains root-owned while rsyslog runs as 1000:0 and cannot create its spool/queue files. Mount rsyslog_spool here and add its path to init.sh's ownership loop.
    volumes:
      - ./scripts:/opt/ascender:ro
      - projects:/var/lib/awx/projects
      - job_private_data:/tmp
      - receptor_socket:/var/run/awx-receptor
      - rsyslog_socket:/var/run/awx-rsyslog
      - podman_storage:/var/lib/awx/.local/share/containers

alternative/docker-compose/docker-compose.yml:49

  • This Compose IaC declares the deployment without any owner or repo labels on its services, so the resulting containers cannot be attributed to their owner and repository. Add those required labels through the service anchors or each service definition.
services:

  postgres:

alternative/podman/config/settings.py:13

  • JOBOUTPUT_ROOT is set to /var/lib/awx/job_status, but run_task and run_web do not mount a shared volume at that path; launch_task.sh only creates it in the task container's writable layer. Job output/artifacts are consequently unavailable to the web process and are lost whenever the task container is recreated. Add a persistent shared job-status volume to both containers and initialize it.
STATIC_ROOT = '/var/lib/awx/public/static'
PROJECTS_ROOT = '/var/lib/awx/projects'
JOBOUTPUT_ROOT = '/var/lib/awx/job_status'

alternative/podman/config/settings.py:32

  • ALLOWED_HOSTS = ['*'] disables Django's Host-header validation for the public HTTP/HTTPS endpoints. An attacker can send arbitrary Host headers and influence host-derived absolute URLs. Build this list from ASCENDER_HOSTNAME and _extra_hosts instead, or require a trusted proxy to validate Host.
ALLOWED_HOSTS = ['*']

alternative/podman/manage-containers.sh:277

  • run_rsyslog later mounts VOL_RSYSLOG_SPOOL at /var/lib/awx/rsyslog, but run_init never mounts that volume, so the ownership setup cannot make it writable. On a fresh install the volume stays root-owned while rsyslog runs as 1000:0 and cannot create spool/queue files. Mount this volume in run_init and add its path to scripts/init.sh.
    alternative/services/README.md:90
  • supervisorctl is installed by the build into /var/lib/awx/venv/awx/bin, and this role only exposes awx-manage and awx-python in /usr/bin. The documented command therefore normally fails with command not found; invoke the venv binary (or install a global wrapper).
    alternative/services/README.md:84
  • The documented backup command uses the local Unix socket, but the role leaves local pg_hba.conf authentication as peer; a root/operator shell therefore cannot authenticate as database user ascender. Use the postgres OS account or specify TCP/password (for example, sudo -u postgres pg_dump -d ascender).
    alternative/services/roles/ascender_config/templates/receptor.conf.j2:12
  • The direct-services play always starts a plaintext TCP listener on port 27199, and the optional firewalld management does not guarantee that this port is blocked when firewalld is disabled or unavailable. A host in that state exposes the Receptor endpoint without TLS; bind it to loopback by default or make the listener/TLS configuration opt-in and only expose it when secured.
    alternative/services/roles/ascender_config/templates/settings.py.j2:21
  • ALLOWED_HOSTS = ['*'] disables Django's Host-header validation for the public HTTP/HTTPS endpoints, allowing attacker-controlled Host values to reach host-derived URL generation. Use the configured hostname and aliases as the allowlist instead, rather than accepting arbitrary hosts.
    alternative/services/roles/ascender_defaults/defaults/main.yml:10
  • The fallback maps every architecture other than aarch64 to the x86_64 amd64 archive. On hosts such as ppc64le or s390x, the download and extraction can succeed but the installed Receptor binary cannot execute, causing a late service failure; map only supported architectures and fail before downloading when there is no matching archive.
    alternative/services/roles/ascender_receptor/tasks/main.yml:5
  • get_url does not create parent directories, and a minimal Rocky 9 host does not reliably contain /usr/local/src; the first run can therefore fail before the receptor tarball is downloaded. Create /usr/local/src with ansible.builtin.file before this task.
    alternative/services/roles/ascender_system/tasks/main.yml:41
  • The service install relies on the Node toolchain for the UI build but installs npm (and the README documents npm access). This repository's Node build convention requires pnpm; install/configure pnpm and ensure the UI build uses it instead of npm.
  • Files reviewed: 67/68 changed files
  • Comments generated: 7
  • Review effort level: Lite

Comment thread alternative/docker-compose/receptor/launch_awx_receptor.sh
Comment thread alternative/docker-compose/scripts/launch_task.sh
Comment thread alternative/docker-compose/setup.sh
Comment thread alternative/podman/install.sh
Comment thread alternative/podman/receptor/launch_awx_receptor.sh
Comment thread alternative/services/install.yml
Comment thread alternative/services/setup.sh
@cigamit
cigamit merged commit 956b7a5 into main Sep 11, 2026
1 check passed
@cigamit
cigamit deleted the alternative branch September 11, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Development

Successfully merging this pull request may close these issues.

3 participants