Blob Blame History Raw
#!/usr/bin/python3
"""Fetch OSTree commits from an repository

Uses ostree to pull specific commits from (remote) repositories
at the provided `url`. Can verify the commit, if one or more
gpg keys are provided via `gpgkeys`.
"""


import json
import os
import sys
import subprocess
import uuid


SCHEMA = """
"additionalProperties": false,
"properties": {
  "commits": {
    "description": "The commits to fetch indexed their checksum",
    "type": "object",
    "additionalProperties": false,
    "patternProperties": {
      "[0-9a-f]{5,64}": {
        "type": "object",
        "additionalProperties": false,
        "required": ["remote"],
        "properties": {
          "remote": {
            "type": "object",
            "additionalProperties": false,
            "required": ["url"],
            "properties": {
              "url": {
                "type": "string",
                "description": "URL of the repository."
              },
              "gpgkeys": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "GPG keys to verify the commits"
                }
              }
            }
          }
        }
      }
    }
  }
}
"""


def ostree(*args, _input=None, **kwargs):
    args = list(args) + [f'--{k}={v}' for k, v in kwargs.items()]
    print("ostree " + " ".join(args), file=sys.stderr)
    subprocess.run(["ostree"] + args,
                   encoding="utf-8",
                   stdout=sys.stderr,
                   input=_input,
                   check=True)


def main(options, checksums, cache, output):
    commits = options["commits"]

    os.makedirs(output, exist_ok=True)
    os.makedirs(cache, exist_ok=True)

    # Prepare the cache and the output repo
    repo_cache = os.path.join(cache, "repo")
    ostree("init", mode="archive", repo=repo_cache)

    # Make sure the cache repository uses locks to protect the metadata during
    # shared access. This is the default since `2018.5`, but lets document this
    # explicitly here.
    ostree("config", "set", "repo.locking", "true", repo=repo_cache)

    repo_out = os.path.join(output, "repo")
    ostree("init", mode="archive", repo=repo_out)

    for commit in checksums:
        remote = commits[commit]["remote"]
        url = remote["url"]
        gpg = remote.get("gpgkeys", [])
        uid = str(uuid.uuid4())

        ostree("remote", "add",
               "--no-gpg-verify",
               uid, url,
               repo=repo_cache)

        # this temporary remote is needed to verify
        # the commit signatures via gpg below
        ostree("remote", "add",
               uid, repo_cache,
               repo=repo_out)

        for key in gpg:
            ostree("remote", "gpg-import", "--stdin", uid,
                   repo=repo_out, _input=key)

        # Transfer the commit: remote → cache
        print(f"pulling {commit}", file=sys.stderr)
        ostree("pull", uid, commit, repo=repo_cache)

        # Transfer the commit: cache → output
        verify_args = []
        if gpg:
            verify_args = ["--gpg-verify", "--remote", uid]

        ostree("pull-local", repo_cache, commit,
               *verify_args,
               repo=repo_out)

        # Remove the temporary remotes again
        ostree("remote", "delete", uid,
               repo=repo_cache)

        ostree("remote", "delete", uid,
               repo=repo_out)

    json.dump({}, sys.stdout)
    return 0


if __name__ == '__main__':
    source_args = json.load(sys.stdin)
    r = main(source_args["options"],
             source_args["checksums"],
             source_args["cache"],
             source_args["output"])
    sys.exit(r)