Skip to content

Fixes for OFE + Apptainer modules - #6047

Open
scott-nag wants to merge 16 commits into
GoogleCloudPlatform:developfrom
nagconsulting:develop
Open

Fixes for OFE + Apptainer modules#6047
scott-nag wants to merge 16 commits into
GoogleCloudPlatform:developfrom
nagconsulting:develop

Conversation

@scott-nag

Copy link
Copy Markdown
Collaborator

1. OFE baseline fixes

Job accounting: ghpcfe_c2daemon.py now falls back to sacct when a Slurm job drops out of squeue, so completed/failed jobs still get final state, start/end times recorded instead of going untracked.

Dependency fix: adds missing PyYAML to the c2_daemon's installed Python packages.

Template fixes: converts blueprint Jinja templates (artifact_registry_config, cloudsql_config, cluster_config, filesystem_config, partition_config) from {# #} comments to {% comment %} blocks, and adds a missing network_storage output wiring in filesystem_config.yaml.j2.

2. Apptainer container modules

New modules:community/modules/container/apptainer-runtime and apptainer-app for staging/running Apptainer/Singularity SIF images pulled from Google Artifact Registry on Cluster Toolkit VMs.

New example blueprint: apptainer-artifact-registry-openfoam.yaml demonstrating both modules with an Artifact Registry REMOTE_REPOSITORY pull-through mirror of Docker Hub.

Bug fix bundled in: artifact-registry's registry_url output was pointing at deployment_name instead of the actual generated repository_id referencing a repository that doesn't exist. Fixed to use google_artifact_registry_repository.artifact_registry.repository_id instead.

scott-nag and others added 10 commits July 20, 2026 13:56
…line-fixes

Fix OFE baseline dependencies and job accounting
…m/develop

Sync official develop into NAG develop
…m/develop

Sync official develop into NAG develop
…ugh example

Adds community/modules/container/apptainer-runtime and apptainer-app for
staging and running Apptainer/Singularity SIF images from Google Artifact
Registry on Cluster Toolkit-deployed VMs, plus an example blueprint
(apptainer-artifact-registry-openfoam.yaml) demonstrating them together with
an Artifact Registry REMOTE_REPOSITORY pull-through mirror of Docker Hub.

Also fixes community/modules/container/artifact-registry's registry_url
output, which referenced deployment_name instead of the actual generated
repository_id and so pointed at a repository that doesn't exist.
Local tfenv version-pin file, not a convention this repo's tooling
reads (the Makefile checks the terraform version directly) or has
ever tracked elsewhere.
This module has no dependency on Open OnDemand and isn't scoped to a
particular frontend, so drop language that implied otherwise.
…m/develop

Sync official develop into NAG develop
…ptainer-module

Apptainer Module Addition
@scott-nag
scott-nag requested a review from a team as a code owner July 30, 2026 09:45
@github-actions github-actions Bot added the external PR from external contributor label Jul 30, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the Open Front End (OFE) infrastructure and introduces new container management capabilities via Apptainer. It improves job state tracking, updates dependencies, refines template syntax, and adds robust modules for staging and running containerized applications from Google Artifact Registry.

Highlights

  • OFE Job Accounting: Improved job tracking by implementing a fallback to sacct for completed or failed jobs that drop out of squeue.
  • Dependency Update: Added PyYAML to the c2_daemon installed Python packages.
  • Template Improvements: Standardized Jinja template comments to use {% comment %} blocks and added missing network_storage output wiring in filesystem_config.yaml.j2.
  • New Apptainer Modules: Introduced apptainer-runtime and apptainer-app modules to facilitate staging and running containerized applications from Google Artifact Registry.
  • Example Blueprint: Added apptainer-artifact-registry-openfoam.yaml to demonstrate the new container modules.
  • Bug Fix: Corrected the artifact-registry output to reference the repository_id instead of the deployment_name.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces new Apptainer modules (apptainer-app and apptainer-runtime) and an example blueprint for staging OpenFOAM via a Google Artifact Registry pull-through cache. It also updates the front-end daemon to query Slurm job states using sacct when they are no longer in the active queue. The review feedback highlights critical improvements to prevent plan-time crashes in the new Terraform modules when network_storage is empty, a correction to template escaping for shell PID variables ($$$$), and a reminder to register the new example blueprint in examples/README.md per the repository style guide.

Comment on lines +16 to +17
install_root_resolved = var.install_root != null ? var.install_root : var.network_storage[var.network_storage_index].local_mount
install_root_clean = local.install_root_resolved == "/" ? "" : trimsuffix(local.install_root_resolved, "/")

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.

high

To prevent plan-time crashes when var.network_storage is empty and var.install_root is null, use a try() block to safely access the local_mount attribute. This allows the variables to be safely evaluated during plan time without throwing index out of bounds errors.

  install_root_resolved    = var.install_root != null ? var.install_root : try(var.network_storage[var.network_storage_index].local_mount, null)
  install_root_clean       = local.install_root_resolved == null ? "" : (local.install_root_resolved == "/" ? "" : trimsuffix(local.install_root_resolved, "/"))
References
  1. To allow variables to be explicitly set to null without automatically overriding them to default configurations, keep the variable nullable and use try() blocks in the main configuration to safely access its attributes and prevent plan-time crashes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 75de960

Comment on lines +16 to +17
install_root_resolved = var.install_root != null ? var.install_root : var.network_storage[var.network_storage_index].local_mount
install_root_clean = local.install_root_resolved == "/" ? "" : trimsuffix(local.install_root_resolved, "/")

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.

high

To prevent plan-time crashes when var.network_storage is empty and var.install_root is null, use a try() block to safely access the local_mount attribute. This allows the variables to be safely evaluated during plan time without throwing index out of bounds errors.

  install_root_resolved = var.install_root != null ? var.install_root : try(var.network_storage[var.network_storage_index].local_mount, null)
  install_root_clean    = local.install_root_resolved == null ? "" : (local.install_root_resolved == "/" ? "" : trimsuffix(local.install_root_resolved, "/"))
References
  1. To allow variables to be explicitly set to null without automatically overriding them to default configurations, keep the variable nullable and use try() blocks in the main configuration to safely access its attributes and prevent plan-time crashes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 75de960

exit 0
fi

tmp_path="$${output_path}.tmp.$$"

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.

high

In Terraform templates, $$ is the escape sequence for a single $. Because $$ at the end of the line is not followed by {, it will still be unescaped to a single $, resulting in tmp_path="${output_path}.tmp.$" in the generated shell script. To output a literal $$ (the shell PID variable) in the generated script, you must use $$$$.

      tmp_path="$${output_path}.tmp.$$$$"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 75de960

# https://hub.docker.com/r/opencfd/openfoam-default/tags before deploying,
# and update openfoam_image_tag below if it has moved on.

blueprint_name: apptainer-artifact-registry-openfoam

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.

medium

Since a new example blueprint has been added, please ensure it is registered in the main index in examples/README.md to maintain discoverability and adhere to the repository style guide.

References
  1. If new examples (core or community) are added, ensure they are added to the index in examples/README.md. (link)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 75de960

scott-nag and others added 2 commits July 30, 2026 11:07
- Wrap network_storage local_mount access in try() in both
  apptainer-app and apptainer-runtime so plan-time evaluation of the
  install_root fallback branch doesn't throw index-out-of-bounds when
  network_storage is empty.
- Escape the shell tmp-file suffix as $$$$ so the generated staging
  script gets a literal $$ (PID) instead of Terraform collapsing it to
  a single $.
- Register apptainer-artifact-registry-openfoam.yaml in
  examples/README.md.
…ptainer-module

fix: address apptainer module review feedback
@AdarshK15 AdarshK15 self-assigned this Aug 4, 2026
scott-nag and others added 2 commits August 6, 2026 14:30
Two pre-commit hooks fail on the apptainer modules in CI:

- terraform_tflint: apptainer-runtime declared project_id,
  deployment_name and region but never referenced them, tripping
  terraform_unused_declarations. Unlike apptainer-app, which uses all
  three in its generated manifest, the runtime module only emits layout
  and MODULEPATH runners and has no use for them. Remove them; the
  blueprint never set them explicitly, they were only auto-injected
  globals.

- terraform-readme: both READMEs were generated by terraform-docs
  v0.20.0, which emits compact table separators. CI installs
  terraform-docs@latest (v0.24.0), which emits padded ones, matching
  every other README in the repo. Regenerate via
  tools/autodoc/terraform_docs.sh.
…er-fixes

fix: satisfy pre-commit for apptainer modules
@scott-nag

Copy link
Copy Markdown
Collaborator Author

Fixed precommit stuff now

settings:
name_prefix: $(vars.deployment_name)-vm
machine_type: $(vars.vm_machine_type)
service_account_email: $(hpc_service_account.service_account_email)

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.

instead of referencing the module setting, can we add it to the use clause?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Absolutely. I've applied this fix now in 8d6445a

Comment thread examples/README.md
* [hpc-slurm6-tpu.yaml](#hpc-slurm6-tpuyaml--) ![community-badge] ![experimental-badge]
* [hpc-slurm6-tpu-maxtext.yaml](#hpc-slurm6-tpu-maxtextyaml--) ![community-badge] ![experimental-badge]
* [hpc-slurm6-apptainer.yaml](#hpc-slurm6-apptaineryaml--) ![community-badge] ![experimental-badge]
* [apptainer-artifact-registry-openfoam.yaml](#apptainer-artifact-registry-openfoamyaml--) ![community-badge] ![experimental-badge]

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.

Could you add entries for apptainer-app and apptainer-runtime in modules/README.md file as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have done so now. I've also included the artifact-registry section as I noticed this was missing too!

fi

install -d -m 0700 "$${HOME:-/root}/.apptainer"
apptainer registry login --username=oauth2accesstoken --password="$access_token" "oras://$registry_host"

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.

auth_runner logs in using oras://, but the stage_runner pulls using docker://. Since Apptainer isolates credentials by protocol, this mismatch will likely cause 401 Unauthorized errors on private images. Should we update the login to use docker://$registry_host so the credentials match the pull?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated it to use docker:// now

@AdarshK15 AdarshK15 assigned scott-nag and unassigned AdarshK15 Aug 11, 2026
@AdarshK15

Copy link
Copy Markdown
Member

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces new community modules (apptainer-app and apptainer-runtime) and an example blueprint (apptainer-artifact-registry-openfoam.yaml) to support staging and running Apptainer SIF images using Artifact Registry as a pull-through cache. It also updates the frontend daemon (ghpcfe_c2daemon.py) to query sacct for final job state information when a job leaves the queue, and refactors several Jinja2 templates. Feedback on the daemon changes suggests handling multiple sacct entries for requeued jobs by returning the latest run's state, and ignoring the UNLIMITED placeholder in Slurm time parsing to prevent spurious warning logs.

Comment on lines +270 to +297
for line in proc.stdout.splitlines():
if not line.strip():
continue
fields = line.split("|")
if len(fields) < 4:
continue
sacct_jobid, state, start_time, end_time = fields[:4]
if sacct_jobid != str(jobid):
continue

result = {}
normalized_state = _normalize_slurm_state(state)
if normalized_state:
result["job_state"] = [normalized_state]

start_epoch = _parse_slurm_time(start_time)
if start_epoch is not None:
result["start_time"] = {"number": start_epoch}

end_epoch = _parse_slurm_time(end_time)
if end_epoch is not None:
result["end_time"] = {"number": end_epoch}

if normalized_state:
logger.info(
"sacct returned job %s with state %s", jobid, normalized_state
)
return result or None

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.

medium

When a Slurm job is requeued or resubmitted, sacct can return multiple entries for the same JobID. Returning on the first match will return the older run's state and times instead of the final completed run. Iterating through all lines and keeping the latest matching one ensures the most recent run's state and times are returned.

        latest_result = None
        for line in proc.stdout.splitlines():
            if not line.strip():
                continue
            fields = line.split("|")
            if len(fields) < 4:
                continue
            sacct_jobid, state, start_time, end_time = fields[:4]
            if sacct_jobid != str(jobid):
                continue

            result = {}
            normalized_state = _normalize_slurm_state(state)
            if normalized_state:
                result["job_state"] = [normalized_state]

            start_epoch = _parse_slurm_time(start_time)
            if start_epoch is not None:
                result["start_time"] = {"number": start_epoch}

            end_epoch = _parse_slurm_time(end_time)
            if end_epoch is not None:
                result["end_time"] = {"number": end_epoch}

            if normalized_state:
                latest_result = result

        if latest_result:
            logger.info(
                "sacct returned job %s with state %s", jobid, latest_result["job_state"][0]
            )
            return latest_result

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This has been addressed in 8d6445a

Note: I used greatest-Start rather than Gemini's last-row suggestion (although this is used as a fallback).

Comment on lines +234 to +236
def _parse_slurm_time(value):
if not value or value in ["Unknown", "None", "N/A"]:
return None

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.

medium

Slurm can return UNLIMITED as a placeholder for end times of running or non-expiring jobs. Adding UNLIMITED to the ignored values list prevents unnecessary warning logs.

Suggested change
def _parse_slurm_time(value):
if not value or value in ["Unknown", "None", "N/A"]:
return None
def _parse_slurm_time(value):
if not value or value in ["Unknown", "None", "N/A", "UNLIMITED"]:
return None

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 8d6445a

- log in to Artifact Registry with docker://, matching the pull scheme
- wire the service account through the vm use clause
- list the container modules in modules/README.md
- take the latest sacct row for requeued jobs
- treat Slurm's UNLIMITED as an absent time
@scott-nag

Copy link
Copy Markdown
Collaborator Author

All suggestions added / addressed now.

@AdarshK15 AdarshK15 left a comment

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.

LGTM, @rahimkhan19 please take a look.


locals {
install_root_resolved = var.install_root != null ? var.install_root : try(var.network_storage[var.network_storage_index].local_mount, "")
install_root_clean = local.install_root_resolved == "/" ? "" : trimsuffix(local.install_root_resolved, "/")

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.

Nit: is ternary condition required here? trimsuffix("/", "/") will anyway evaluate to "".

@AdarshK15 AdarshK15 assigned rahimkhan19 and unassigned AdarshK15 Aug 17, 2026
@AdarshK15 AdarshK15 added the release-new-modules Added to release notes under the "New Modules" heading. label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from external contributor release-new-modules Added to release notes under the "New Modules" heading.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants