├── .ci ├── ansible │ ├── Containerfile.j2 │ ├── ansible.cfg │ ├── build_container.yaml │ ├── filter │ │ └── repr.py │ ├── inventory.yaml │ ├── settings.py.j2 │ ├── smash-config.json │ └── start_container.yaml ├── assets │ ├── .gitkeep │ ├── ci_constraints.txt │ ├── httpie │ │ └── config.json │ └── release_requirements.txt └── scripts │ ├── calc_constraints.py │ ├── check_gettext.sh │ ├── check_pulpcore_imports.sh │ ├── check_release.py │ ├── check_requirements.py │ ├── collect_changes.py │ ├── pr_labels.py │ ├── schema.py │ ├── update_github.py │ └── validate_commit_message.py ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── task.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── build.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── create-branch.yml │ ├── docs.yml │ ├── lint.yml │ ├── nightly.yml │ ├── pr_checks.yml │ ├── publish.yml │ ├── release.yml │ ├── scripts │ ├── before_install.sh │ ├── before_script.sh │ ├── build_python_client.sh │ ├── build_ruby_client.sh │ ├── check_commit.sh │ ├── install.sh │ ├── post_before_script.sh │ ├── pre_before_install.sh │ ├── pre_before_script.sh │ ├── publish_client_gem.sh │ ├── publish_client_pypi.sh │ ├── publish_plugin_pypi.sh │ ├── push_branch_and_tag_to_github.sh │ ├── release.sh │ ├── script.sh │ ├── secrets.py │ ├── stage-changelog-for-default-branch.py │ ├── update_backport_labels.py │ └── utils.sh │ ├── test.yml │ ├── update-labels.yml │ └── update_ci.yml ├── .gitignore ├── .gitleaks.toml ├── .pep8speaks.yml ├── CHANGES.md ├── CHANGES ├── .TEMPLATE.md ├── .gitignore ├── 1997.bugfix ├── 1998.bugfix └── 1999.doc ├── COMMITMENT ├── CONTRIBUTING.md ├── COPYRIGHT ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dev_requirements.txt ├── doc_requirements.txt ├── docs ├── admin │ ├── guides │ │ ├── build-image.md │ │ ├── change-allowed-artifacts.md │ │ ├── customize-access-policy.md │ │ ├── customize-roles.md │ │ ├── import-export.md │ │ ├── migrate-permissions.md │ │ ├── pull-through-caching.md │ │ └── sign-image.md │ └── learn │ │ ├── authentication.md │ │ ├── domain.md │ │ ├── rbac.md │ │ └── tech-preview.md ├── index.md └── user │ ├── guides │ ├── manage-cosign-signature.md │ ├── manage-credentials.md │ ├── manage-flatpak.md │ ├── manage-helm-chart.md │ ├── manage-image.md │ ├── push-image.md │ └── registry-catalog.md │ └── tutorials │ └── sync-and-host.md ├── functest_requirements.txt ├── lint_requirements.txt ├── pulp_container ├── __init__.py ├── app │ ├── __init__.py │ ├── access_policy.py │ ├── authorization.py │ ├── cache.py │ ├── checks.py │ ├── content.py │ ├── downloaders.py │ ├── exceptions.py │ ├── fields.py │ ├── global_access_conditions.py │ ├── json_schemas.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── container-handle-image-data.py │ │ │ └── container-repair-media-type.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_containerrepository.py │ │ ├── 0003_oci_mediatype.py │ │ ├── 0004_upload.py │ │ ├── 0005_contentredirectcontentguard.py │ │ ├── 0006_containerpushrepository.py │ │ ├── 0007_clear_tags_artifacts_refs.py │ │ ├── 0008_include_exclude_tags.py │ │ ├── 0009_container_namespace.py │ │ ├── 0010_remove_uploadchunk.py │ │ ├── 0011_add_container_repository_permissions.py │ │ ├── 0012_add_container_namespace_permissions.py │ │ ├── 0013_add_pull_push_permissions.py │ │ ├── 0014_containerdistribution_private.py │ │ ├── 0015_manage_tags_push_repo.py │ │ ├── 0016_add_delete_versions_permission.py │ │ ├── 0017_add_granular_perms.py │ │ ├── 0018_containerdistribution_description.py │ │ ├── 0019_DATA_distribution_model_swap.py │ │ ├── 0020_update_push_repo_perms.py │ │ ├── 0021_data_move_redirect_content_guard_to_core.py │ │ ├── 0022_delete_contentredirectcontentguard.py │ │ ├── 0023_manifestsignature.py │ │ ├── 0024_containerremote_sigstore.py │ │ ├── 0025_signature_stored_in_textfield.py │ │ ├── 0026_manifest_signing_service.py │ │ ├── 0027_data_translate_perms_to_roles.py │ │ ├── 0028_add_role_manage_permissions.py │ │ ├── 0029_remove_blob_media_type.py │ │ ├── 0030_enforce_tagged_manifest_reference.py │ │ ├── 0031_replace_charf_with_textf.py │ │ ├── 0032_upload_artifact.py │ │ ├── 0033_raise_warning_for_repair.py │ │ ├── 0034_translate_signed_schema.py │ │ ├── 0035_alter_blob_content_ptr_and_more.py │ │ ├── 0036_containerpushrepository_pending_blobs_manifests.py │ │ ├── 0037_create_pull_through_cache_models.py │ │ ├── 0038_add_manifest_metadata_fields.py │ │ ├── 0039_manifest_data.py │ │ ├── 0040_add_remote_repo_filter.py │ │ ├── 0041_add_pull_through_pull_permissions.py │ │ ├── 0042_add_manifest_nature_field.py │ │ ├── 0043_add_os_arch_image_size_manifest_fields.py │ │ ├── 0044_add_domain.py │ │ ├── 0045_alter_manifest_compressed_image_size.py │ │ └── __init__.py │ ├── modelresource.py │ ├── models.py │ ├── redirects.py │ ├── registry.py │ ├── registry_api.py │ ├── replica.py │ ├── serializers.py │ ├── settings.py │ ├── tasks │ │ ├── __init__.py │ │ ├── builder.py │ │ ├── download_image_data.py │ │ ├── recursive_add.py │ │ ├── recursive_remove.py │ │ ├── sign.py │ │ ├── sync_stages.py │ │ ├── synchronize.py │ │ ├── tag.py │ │ └── untag.py │ ├── token_verification.py │ ├── urls.py │ ├── utils.py │ ├── viewsets.py │ └── webserver_snippets │ │ ├── __init__.py │ │ ├── apache.conf │ │ └── nginx.conf ├── constants.py └── tests │ ├── __init__.py │ ├── functional │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── test_build_images.py │ │ ├── test_content_cache.py │ │ ├── test_crud_distributions.py │ │ ├── test_crud_remotes.py │ │ ├── test_domains.py │ │ ├── test_flatpak.py │ │ ├── test_pull_content.py │ │ ├── test_pull_through_cache.py │ │ ├── test_pulpimportexport.py │ │ ├── test_push_content.py │ │ ├── test_push_signatures.py │ │ ├── test_rbac_push_repositories.py │ │ ├── test_rbac_remotes.py │ │ ├── test_rbac_repo_content.py │ │ ├── test_rbac_repo_versions.py │ │ ├── test_rbac_sync_repositories.py │ │ ├── test_recursive_add.py │ │ ├── test_recursive_remove.py │ │ ├── test_remote_filter_pull_through.py │ │ ├── test_repositories_list.py │ │ ├── test_sign_manifests.py │ │ ├── test_sync.py │ │ ├── test_sync_signatures.py │ │ ├── test_tagging_images.py │ │ └── test_token_authentication.py │ ├── conftest.py │ ├── constants.py │ └── utils.py │ └── unit │ ├── __init__.py │ ├── test_json_schemas.py │ ├── test_models.py │ └── test_serializers.py ├── pyproject.toml ├── releasing.md ├── template_config.yml ├── test_requirements.txt └── unittest_requirements.txt /.ci/ansible/Containerfile.j2: -------------------------------------------------------------------------------- 1 | FROM {{ ci_base | default(pulp_default_container) }} 2 | 3 | # Add source directories to container 4 | {% for item in plugins %} 5 | ADD ./{{ item.name }} ./{{ item.name }} 6 | {% endfor %} 7 | 8 | # Install python packages 9 | # S3 botocore needs to be patched to handle responses from minio during 0-byte uploads 10 | # Hacking botocore (https://github.com/boto/botocore/pull/1990) 11 | 12 | # This MUST be the ONLY call to pip install in inside the container. 13 | RUN pip3 install --upgrade pip setuptools wheel && \ 14 | rm -rf /root/.cache/pip && \ 15 | pip3 install 16 | {%- if s3_test | default(false) -%} 17 | {{ " " }}git+https://github.com/gerrod3/botocore.git@fix-100-continue 18 | {%- endif -%} 19 | {%- for item in plugins -%} 20 | {{ " " }}{{ item.source }} 21 | {%- if item.upperbounds | default(false) -%} 22 | {{ " " }}-c ./{{ item.name }}/upperbounds_constraints.txt 23 | {%- endif -%} 24 | {%- if item.lowerbounds | default(false) -%} 25 | {{ " " }}-c ./{{ item.name }}/lowerbounds_constraints.txt 26 | {%- endif -%} 27 | {%- if item.ci_requirements | default(false) -%} 28 | {{ " " }}-r ./{{ item.name }}/ci_requirements.txt 29 | {%- endif -%} 30 | {%- endfor %} 31 | {{ " " }}-c ./{{ plugins[0].name }}/.ci/assets/ci_constraints.txt && \ 32 | rm -rf /root/.cache/pip 33 | 34 | {% if pulp_env is defined and pulp_env %} 35 | {% for key, value in pulp_env.items() %} 36 | ENV {{ key | upper }}={{ value }} 37 | {% endfor %} 38 | {% endif %} 39 | 40 | {% if pulp_scenario_env is defined and pulp_scenario_env %} 41 | {% for key, value in pulp_scenario_env.items() %} 42 | ENV {{ key | upper }}={{ value }} 43 | {% endfor %} 44 | {% endif %} 45 | 46 | USER pulp:pulp 47 | RUN PULP_STATIC_ROOT=/var/lib/operator/static/ PULP_CONTENT_ORIGIN=localhost \ 48 | /usr/local/bin/pulpcore-manager collectstatic --clear --noinput --link 49 | USER root:root 50 | 51 | {% for item in plugins %} 52 | RUN export plugin_path="$(pip3 show {{ item.name }} | sed -n -e 's/Location: //p')/{{ item.name }}" && \ 53 | ln $plugin_path/app/webserver_snippets/nginx.conf /etc/nginx/pulp/{{ item.name }}.conf || true 54 | {% endfor %} 55 | 56 | ENTRYPOINT ["/init"] 57 | -------------------------------------------------------------------------------- /.ci/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = inventory.yaml 3 | filter_plugins = filter 4 | retry_files_enabled = False 5 | transport = local 6 | nocows = 1 7 | stdout_callback = yaml 8 | -------------------------------------------------------------------------------- /.ci/ansible/build_container.yaml: -------------------------------------------------------------------------------- 1 | # Ansible playbook to create the pulp service containers image 2 | --- 3 | - hosts: localhost 4 | gather_facts: false 5 | vars_files: 6 | - vars/main.yaml 7 | tasks: 8 | - name: "Generate Containerfile from template" 9 | template: 10 | src: Containerfile.j2 11 | dest: Containerfile 12 | 13 | - name: "Build pulp image" 14 | # We build from the ../.. (parent dir of pulpcore git repo) Docker build 15 | # "context" so that repos like pulp-smash are accessible to Docker 16 | # build. So that PR branches can be used via relative paths. 17 | # 18 | # We default to using the docker build / podman buildah cache, for 19 | # 1-off-builds and CI purposes (which has no cache across CI runs.) 20 | # Run build.yaml with -e cache=false if your builds are using outdated 21 | # layers. 22 | command: "docker build --network host --no-cache={{ not cache | default(true) | bool }} -t {{ image.name }}:{{ image.tag }} -f {{ playbook_dir }}/Containerfile ../../.." 23 | 24 | - name: "Clean image cache" 25 | docker_prune: 26 | images : true 27 | ... 28 | -------------------------------------------------------------------------------- /.ci/ansible/filter/repr.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | from packaging.version import parse as parse_version 3 | 4 | __metaclass__ = type 5 | 6 | 7 | ANSIBLE_METADATA = { 8 | "metadata_version": "1.1", 9 | "status": ["preview"], 10 | "supported_by": "community", 11 | } 12 | 13 | 14 | def _repr_filter(value): 15 | return repr(value) 16 | 17 | 18 | def _canonical_semver_filter(value): 19 | return str(parse_version(value)) 20 | 21 | 22 | # ---- Ansible filters ---- 23 | class FilterModule(object): 24 | """Repr filter.""" 25 | 26 | def filters(self): 27 | """Filter associations.""" 28 | return { 29 | "repr": _repr_filter, 30 | "canonical_semver": _canonical_semver_filter, 31 | } 32 | -------------------------------------------------------------------------------- /.ci/ansible/inventory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | containers: 5 | hosts: 6 | pulp: 7 | pulp-fixtures: 8 | minio: 9 | ci-sftp: 10 | vars: 11 | ansible_connection: docker 12 | ... 13 | -------------------------------------------------------------------------------- /.ci/ansible/settings.py.j2: -------------------------------------------------------------------------------- 1 | CONTENT_ORIGIN = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}" 2 | ANSIBLE_API_HOSTNAME = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}" 3 | ANSIBLE_CONTENT_HOSTNAME = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}/pulp/content" 4 | PRIVATE_KEY_PATH = "/etc/pulp/certs/token_private_key.pem" 5 | PUBLIC_KEY_PATH = "/etc/pulp/certs/token_public_key.pem" 6 | TOKEN_SERVER = "{{ pulp_scheme }}://pulp:{{ 443 if pulp_scheme == 'https' else 80 }}/token/" 7 | TOKEN_SIGNATURE_ALGORITHM = "ES256" 8 | CACHE_ENABLED = True 9 | REDIS_HOST = "localhost" 10 | REDIS_PORT = 6379 11 | ANALYTICS = False 12 | 13 | {% if api_root is defined %} 14 | API_ROOT = {{ api_root | repr }} 15 | {% endif %} 16 | 17 | {% if pulp_settings %} 18 | {% for key, value in pulp_settings.items() %} 19 | {{ key | upper }} = {{ value | repr }} 20 | {% endfor %} 21 | {% endif %} 22 | 23 | {% if pulp_scenario_settings is defined and pulp_scenario_settings %} 24 | {% for key, value in pulp_scenario_settings.items() %} 25 | {{ key | upper }} = {{ value | repr }} 26 | {% endfor %} 27 | {% endif %} 28 | 29 | {% if s3_test | default(false) %} 30 | MEDIA_ROOT: "" 31 | S3_USE_SIGV4 = True 32 | {% if test_storages_compat_layer is defined and test_storages_compat_layer %} 33 | STORAGES = { 34 | "default": { 35 | "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", 36 | "OPTIONS": { 37 | "access_key": "{{ minio_access_key }}", 38 | "secret_key": "{{ minio_secret_key }}", 39 | "region_name": "eu-central-1", 40 | "addressing_style": "path", 41 | "signature_version": "s3v4", 42 | "bucket_name": "pulp3", 43 | "endpoint_url": "http://minio:9000", 44 | "default_acl": "@none None", 45 | }, 46 | }, 47 | "staticfiles": { 48 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 49 | }, 50 | } 51 | {% else %} 52 | DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 53 | AWS_ACCESS_KEY_ID = "{{ minio_access_key }}" 54 | AWS_SECRET_ACCESS_KEY = "{{ minio_secret_key }}" 55 | AWS_S3_REGION_NAME = "eu-central-1" 56 | AWS_S3_ADDRESSING_STYLE = "path" 57 | AWS_S3_SIGNATURE_VERSION = "s3v4" 58 | AWS_STORAGE_BUCKET_NAME = "pulp3" 59 | AWS_S3_ENDPOINT_URL = "http://minio:9000" 60 | AWS_DEFAULT_ACL = "@none None" 61 | {% endif %} 62 | {% endif %} 63 | 64 | {% if azure_test | default(false) %} 65 | DEFAULT_FILE_STORAGE = "storages.backends.azure_storage.AzureStorage" 66 | MEDIA_ROOT = "" 67 | AZURE_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" 68 | AZURE_ACCOUNT_NAME = "devstoreaccount1" 69 | AZURE_CONTAINER = "pulp-test" 70 | AZURE_LOCATION = "pulp3" 71 | AZURE_OVERWRITE_FILES = True 72 | AZURE_URL_EXPIRATION_SECS = 120 73 | AZURE_CONNECTION_STRING = 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://ci-azurite:10000/devstoreaccount1;' 74 | {% endif %} 75 | 76 | {% if gcp_test | default(false) %} 77 | DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" 78 | MEDIA_ROOT = "" 79 | GS_BUCKET_NAME = "gcppulp" 80 | GS_CUSTOM_ENDPOINT = "http://ci-gcp:4443" 81 | {% endif %} 82 | -------------------------------------------------------------------------------- /.ci/ansible/smash-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "pulp": { 3 | "auth": [ 4 | "admin", 5 | "password" 6 | ], 7 | "selinux enabled": false, 8 | "version": "3", 9 | "aiohttp_fixtures_origin": "127.0.0.1" 10 | }, 11 | "hosts": [ 12 | { 13 | "hostname": "pulp", 14 | "roles": { 15 | "api": { 16 | "port": 443, 17 | "scheme": "https", 18 | "service": "nginx" 19 | }, 20 | "content": { 21 | "port": 443, 22 | "scheme": "https", 23 | "service": "pulp_content_app" 24 | }, 25 | "pulp resource manager": {}, 26 | "pulp workers": {}, 27 | "redis": {}, 28 | "shell": { 29 | "transport": "local" 30 | } 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.ci/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/.ci/assets/.gitkeep -------------------------------------------------------------------------------- /.ci/assets/ci_constraints.txt: -------------------------------------------------------------------------------- 1 | # Pulpcore versions without the openapi command do no longer work in the CI 2 | pulpcore>=3.21.30,!=3.23.*,!=3.24.*,!=3.25.*,!=3.26.*,!=3.27.*,!=3.29.*,!=3.30.*,!=3.31.*,!=3.32.*,!=3.33.*,!=3.34.*,!=3.35.*,!=3.36.*,!=3.37.*,!=3.38.*,!=3.40.*,!=3.41.*,!=3.42.*,!=3.43.*,!=3.44.*,!=3.45.*,!=3.46.*,!=3.47.*,!=3.48.*,!=3.50.*,!=3.51.*,!=3.52.*,!=3.53.*,!=3.54.* 3 | 4 | 5 | tablib!=3.6.0 6 | # 3.6.0: This release introduced a regression removing the "html" optional dependency. 7 | 8 | 9 | multidict!=6.3.0 10 | # This release failed the lower bounds test for some case sensitivity in CIMultiDict. 11 | -------------------------------------------------------------------------------- /.ci/assets/httpie/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_options": [ 3 | "--ignore-stdin", 4 | "--pretty=format", 5 | "--traceback" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.ci/assets/release_requirements.txt: -------------------------------------------------------------------------------- 1 | bump-my-version 2 | gitpython 3 | towncrier 4 | -------------------------------------------------------------------------------- /.ci/scripts/check_gettext.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | # make sure this script runs at the repo root 11 | cd "$(dirname "$(realpath -e "$0")")"/../.. 12 | 13 | set -uv 14 | 15 | MATCHES=$(grep -n -r --include \*.py "_(f") 16 | 17 | if [ $? -ne 1 ]; then 18 | printf "\nERROR: Detected mix of f-strings and gettext:\n" 19 | echo "$MATCHES" 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /.ci/scripts/check_pulpcore_imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | # make sure this script runs at the repo root 11 | cd "$(dirname "$(realpath -e "$0")")"/../.. 12 | 13 | set -uv 14 | 15 | # check for imports not from pulpcore.plugin. exclude tests 16 | MATCHES=$(grep -n -r --include \*.py "from pulpcore.*import" . | grep -v "tests\|plugin") 17 | 18 | if [ $? -ne 1 ]; then 19 | printf "\nERROR: Detected bad imports from pulpcore:\n" 20 | echo "$MATCHES" 21 | printf "\nPlugins should import from pulpcore.plugin." 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /.ci/scripts/check_requirements.py: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | import tomllib 9 | import warnings 10 | from packaging.requirements import Requirement 11 | 12 | 13 | CHECK_MATRIX = [ 14 | ("pyproject.toml", True, True, True), 15 | ("requirements.txt", True, True, True), 16 | ("dev_requirements.txt", False, True, False), 17 | ("ci_requirements.txt", False, True, True), 18 | ("doc_requirements.txt", False, True, False), 19 | ("lint_requirements.txt", False, True, True), 20 | ("unittest_requirements.txt", False, True, True), 21 | ("functest_requirements.txt", False, True, True), 22 | ("clitest_requirements.txt", False, True, True), 23 | ] 24 | 25 | 26 | def iterate_file(filename): 27 | if filename == "pyproject.toml": 28 | with open(filename, "rb") as fd: 29 | pyproject_toml = tomllib.load(fd) 30 | if "project" in pyproject_toml: 31 | for nr, line in enumerate(pyproject_toml["project"]["dependencies"]): 32 | yield nr, line 33 | else: 34 | with open(filename, "r") as fd: 35 | for nr, line in enumerate(fd.readlines()): 36 | line = line.strip() 37 | if not line or line.startswith("#"): 38 | continue 39 | if "#" in line: 40 | line = line.split("#", maxsplit=1)[0] 41 | yield nr, line.strip() 42 | 43 | 44 | def main(): 45 | errors = [] 46 | 47 | for filename, check_upperbound, check_prereleases, check_r in CHECK_MATRIX: 48 | try: 49 | for nr, line in iterate_file(filename): 50 | try: 51 | req = Requirement(line) 52 | except ValueError: 53 | if line.startswith("git+"): 54 | # The single exception... 55 | if "pulp-smash" not in line: 56 | errors.append(f"{filename}:{nr}: Invalid source requirement: {line}") 57 | elif line.startswith("-r "): 58 | if check_r: 59 | errors.append(f"{filename}:{nr}: Invalid deferred requirement: {line}") 60 | else: 61 | errors.append(f"{filename}:{nr}: Unreadable requirement {line}") 62 | else: 63 | if check_prereleases and req.specifier.prereleases: 64 | # Do not even think about begging for more exceptions! 65 | if ( 66 | not req.name.startswith("opentelemetry") 67 | and req.name != "pulp-container-client" 68 | ): 69 | errors.append(f"{filename}:{nr}: Prerelease versions found in {line}.") 70 | ops = [spec.operator for spec in req.specifier] 71 | if "~=" in ops: 72 | warnings.warn(f"{filename}:{nr}: Please avoid using ~= on {req.name}!") 73 | elif "<" not in ops and "<=" not in ops and "==" not in ops: 74 | if check_upperbound: 75 | errors.append(f"{filename}:{nr}: Upper bound missing in {line}.") 76 | except FileNotFoundError: 77 | # skip this test for plugins that don't use this requirements.txt 78 | pass 79 | 80 | if errors: 81 | print("Dependency issues found:") 82 | print("\n".join(errors)) 83 | exit(1) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /.ci/scripts/pr_labels.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | # This script is running with elevated privileges from the main branch against pull requests. 4 | 5 | import re 6 | import sys 7 | import tomllib 8 | from pathlib import Path 9 | 10 | from git import Repo 11 | 12 | 13 | def main(): 14 | assert len(sys.argv) == 3 15 | 16 | with open("pyproject.toml", "rb") as fp: 17 | PYPROJECT_TOML = tomllib.load(fp) 18 | BLOCKING_REGEX = re.compile(r"DRAFT|WIP|NO\s*MERGE|DO\s*NOT\s*MERGE|EXPERIMENT") 19 | ISSUE_REGEX = re.compile(r"(?:fixes|closes)[\s:]+#(\d+)") 20 | CHERRY_PICK_REGEX = re.compile(r"^\s*\(cherry picked from commit [0-9a-f]*\)\s*$") 21 | try: 22 | CHANGELOG_EXTS = { 23 | f".{item['directory']}" for item in PYPROJECT_TOML["tool"]["towncrier"]["type"] 24 | } 25 | except KeyError: 26 | CHANGELOG_EXTS = {".feature", ".bugfix", ".doc", ".removal", ".misc"} 27 | 28 | repo = Repo(".") 29 | 30 | base_commit = repo.commit(sys.argv[1]) 31 | head_commit = repo.commit(sys.argv[2]) 32 | 33 | pr_commits = list(repo.iter_commits(f"{base_commit}..{head_commit}")) 34 | 35 | labels = { 36 | "multi-commit": len(pr_commits) > 1, 37 | "cherry-pick": False, 38 | "no-issue": False, 39 | "no-changelog": False, 40 | "wip": False, 41 | } 42 | for commit in pr_commits: 43 | labels["wip"] |= BLOCKING_REGEX.search(commit.summary) is not None 44 | no_issue = ISSUE_REGEX.search(commit.message, re.IGNORECASE) is None 45 | labels["no-issue"] |= no_issue 46 | cherry_pick = CHERRY_PICK_REGEX.search(commit.message) is not None 47 | labels["cherry-pick"] |= cherry_pick 48 | changelog_snippets = [ 49 | k 50 | for k in commit.stats.files 51 | if k.startswith("CHANGES/") and Path(k).suffix in CHANGELOG_EXTS 52 | ] 53 | labels["no-changelog"] |= not changelog_snippets 54 | 55 | print("ADD_LABELS=" + ",".join((k for k, v in labels.items() if v))) 56 | print("REMOVE_LABELS=" + ",".join((k for k, v in labels.items() if not v))) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /.ci/scripts/schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Customizing OpenAPI validation. 3 | 4 | OpenAPI requires paths to start with slashes: 5 | https://spec.openapis.org/oas/v3.0.3#patterned-fields 6 | 7 | But some pulp paths start with curly brackets e.g. {artifact_href} 8 | This script modifies drf-spectacular schema validation to accept slashes and curly brackets. 9 | """ 10 | 11 | import json 12 | from drf_spectacular.validation import JSON_SCHEMA_SPEC_PATH 13 | 14 | with open(JSON_SCHEMA_SPEC_PATH) as fh: 15 | openapi3_schema_spec = json.load(fh) 16 | 17 | properties = openapi3_schema_spec["definitions"]["Paths"]["patternProperties"] 18 | # Making OpenAPI validation to accept paths starting with / and { 19 | if "^\\/|{" not in properties: 20 | properties["^\\/|{"] = properties["^\\/"] 21 | del properties["^\\/"] 22 | 23 | with open(JSON_SCHEMA_SPEC_PATH, "w") as fh: 24 | json.dump(openapi3_schema_spec, fh) 25 | -------------------------------------------------------------------------------- /.ci/scripts/update_github.py: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | import os 9 | from github import Github 10 | 11 | g = Github(os.environ.get("GITHUB_TOKEN")) 12 | repo = g.get_repo("pulp/pulp_container") 13 | 14 | GH_ISSUES = os.environ.get("GH_ISSUES") 15 | 16 | for issue in GH_ISSUES.split(","): 17 | issue = repo.get_issue(int(issue)) 18 | if issue.state != "closed": 19 | print(f"Closing issue: {issue.number}") 20 | issue.edit(state="closed") 21 | -------------------------------------------------------------------------------- /.ci/scripts/validate_commit_message.py: -------------------------------------------------------------------------------- 1 | # This file is managed by the plugin template. 2 | # Do not edit. 3 | 4 | import os 5 | import re 6 | import subprocess 7 | import sys 8 | import tomllib 9 | import yaml 10 | from pathlib import Path 11 | 12 | from github import Github 13 | 14 | 15 | def check_status(issue, repo, cherry_pick): 16 | gi = repo.get_issue(int(issue)) 17 | if gi.pull_request: 18 | sys.exit(f"Error: issue #{issue} is a pull request.") 19 | if gi.closed_at and not cherry_pick: 20 | print("Make sure to use 'git cherry-pick -x' when backporting a change.") 21 | print( 22 | "If a backport of a change requires a significant amount of rewriting, " 23 | "consider creating a new issue." 24 | ) 25 | sys.exit(f"Error: issue #{issue} is closed.") 26 | 27 | 28 | def check_changelog(issue, CHANGELOG_EXTS): 29 | matches = list(Path("CHANGES").rglob(f"{issue}.*")) 30 | 31 | if len(matches) < 1: 32 | sys.exit(f"Could not find changelog entry in CHANGES/ for {issue}.") 33 | for match in matches: 34 | if match.suffix not in CHANGELOG_EXTS: 35 | sys.exit(f"Invalid extension for changelog entry '{match}'.") 36 | 37 | 38 | def main() -> None: 39 | TEMPLATE_CONFIG = yaml.safe_load(Path("template_config.yml").read_text()) 40 | GITHUB_ORG = TEMPLATE_CONFIG["github_org"] 41 | PLUGIN_NAME = TEMPLATE_CONFIG["plugin_name"] 42 | 43 | with Path("pyproject.toml").open("rb") as _fp: 44 | PYPROJECT_TOML = tomllib.load(_fp) 45 | KEYWORDS = ["fixes", "closes"] 46 | BLOCKING_REGEX = [ 47 | r"^DRAFT", 48 | r"^WIP", 49 | r"^NOMERGE", 50 | r"^DO\s*NOT\s*MERGE", 51 | r"^EXPERIMENT", 52 | r"^FIXUP", 53 | r"^fixup!", # This is created by 'git commit --fixup' 54 | r"Apply suggestions from code review", # This usually comes from GitHub 55 | ] 56 | try: 57 | CHANGELOG_EXTS = [ 58 | f".{item['directory']}" for item in PYPROJECT_TOML["tool"]["towncrier"]["type"] 59 | ] 60 | except KeyError: 61 | CHANGELOG_EXTS = [".feature", ".bugfix", ".doc", ".removal", ".misc"] 62 | NOISSUE_MARKER = "[noissue]" 63 | 64 | sha = sys.argv[1] 65 | message = subprocess.check_output(["git", "log", "--format=%B", "-n 1", sha]).decode("utf-8") 66 | 67 | if NOISSUE_MARKER in message: 68 | sys.exit(f"Do not add '{NOISSUE_MARKER}' in the commit message.") 69 | 70 | blocking_matches = [m for m in (re.match(pattern, message) for pattern in BLOCKING_REGEX) if m] 71 | if blocking_matches: 72 | print("Found these phrases in the commit message:") 73 | for m in blocking_matches: 74 | print(" - " + m.group(0)) 75 | sys.exit("This PR is not ready for consumption.") 76 | 77 | g = Github(os.environ.get("GITHUB_TOKEN")) 78 | repo = g.get_repo(f"{GITHUB_ORG}/{PLUGIN_NAME}") 79 | 80 | print("Checking commit message for {sha}.".format(sha=sha[0:7])) 81 | 82 | # validate the issue attached to the commit 83 | issue_regex = r"(?:{keywords})[\s:]+#(\d+)".format(keywords="|".join(KEYWORDS)) 84 | issues = re.findall(issue_regex, message, re.IGNORECASE) 85 | cherry_pick_regex = r"^\s*\(cherry picked from commit [0-9a-f]*\)\s*$" 86 | cherry_pick = re.search(cherry_pick_regex, message, re.MULTILINE) 87 | 88 | if issues: 89 | for issue in issues: 90 | check_status(issue, repo, cherry_pick) 91 | check_changelog(issue, CHANGELOG_EXTS) 92 | 93 | print("Commit message for {sha} passed.".format(sha=sha[0:7])) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | [flake8] 8 | exclude = ./docs/*,*/migrations/* 9 | per-file-ignores = */__init__.py: F401 10 | 11 | ignore = E203,W503,Q000,Q003,D100,D104,D106,D200,D205,D400,D401,D402,F824 12 | max-line-length = 100 13 | 14 | # Flake8 builtin codes 15 | # -------------------- 16 | # E203: no whitespace around ':'. disabled until https://github.com/PyCQA/pycodestyle/issues/373 is fixed 17 | # W503: This enforces operators before line breaks which is not pep8 or black compatible. 18 | # F824: 'nonlocal' is unused: name is never assigned in scope 19 | 20 | # Flake8-quotes extension codes 21 | # ----------------------------- 22 | # Q000: double or single quotes only, default is double (don't want to enforce this) 23 | # Q003: Change outer quotes to avoid escaping inner quotes 24 | 25 | # Flake8-docstring extension codes 26 | # -------------------------------- 27 | # D100: missing docstring in public module 28 | # D104: missing docstring in public package 29 | # D106: missing docstring in public nested class (complains about "class Meta:" and documenting those is silly) 30 | # D200: one-line docstring should fit on one line with quotes 31 | # D205: 1 blank line required between summary line and description 32 | # D400: First line should end with a period 33 | # D401: first line should be imperative (nitpicky) 34 | # D402: first line should not be the function’s “signature” (false positives) 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Issue, Triage-Needed 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | Please provide the versions of the pulpcore and pulp_container packages in use, and how they are installed. If you are using Pulp via Katello, please provide the Katello version. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. Please provide links to any previous discussions via Discourse or Bugzilla. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Feature, Triage-Needed 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🗒️ Task 3 | about: Documentation, CI/CD, refactors, investigations 4 | title: '' 5 | labels: Task, Triage-Needed 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | # Configuration for probot-stale - https://github.com/probot/stale 8 | 9 | # Number of days of inactivity before an Issue or Pull Request becomes stale 10 | daysUntilStale: 90 11 | 12 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 13 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 14 | daysUntilClose: 30 15 | 16 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 17 | onlyLabels: [] 18 | 19 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 20 | exemptLabels: 21 | - security 22 | - planned 23 | 24 | # Set to true to ignore issues in a project (defaults to false) 25 | exemptProjects: false 26 | 27 | # Set to true to ignore issues in a milestone (defaults to false) 28 | exemptMilestones: false 29 | 30 | # Set to true to ignore issues with an assignee (defaults to false) 31 | exemptAssignees: false 32 | 33 | # Label to use when marking as stale 34 | staleLabel: stale 35 | 36 | # Limit the number of actions per hour, from 1-30. Default is 30 37 | limitPerRun: 30 38 | # Limit to only `issues` or `pulls` 39 | only: pulls 40 | 41 | pulls: 42 | markComment: |- 43 | This pull request has been marked 'stale' due to lack of recent activity. If there is no further activity, the PR will be closed in another 30 days. Thank you for your contribution! 44 | 45 | unmarkComment: >- 46 | This pull request is no longer marked for closure. 47 | 48 | closeComment: >- 49 | This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details. 50 | 51 | issues: 52 | markComment: |- 53 | This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! 54 | 55 | unmarkComment: >- 56 | This issue is no longer marked for closure. 57 | 58 | closeComment: >- 59 | This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: "Container CI" 10 | on: {pull_request: {branches: ['*']}} 11 | 12 | concurrency: 13 | group: ${{ github.ref_name }}-${{ github.workflow }} 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | working-directory: "pulp_container" 19 | 20 | jobs: 21 | check-commits: 22 | runs-on: "ubuntu-latest" 23 | steps: 24 | - uses: "actions/checkout@v4" 25 | with: 26 | fetch-depth: 0 27 | path: "pulp_container" 28 | - uses: "actions/setup-python@v5" 29 | with: 30 | python-version: "3.11" 31 | - name: "Install python dependencies" 32 | run: | 33 | echo ::group::PYDEPS 34 | pip install requests pygithub pyyaml 35 | echo ::endgroup:: 36 | - name: "Check commit message" 37 | if: github.event_name == 'pull_request' 38 | env: 39 | PY_COLORS: "1" 40 | ANSIBLE_FORCE_COLOR: "1" 41 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 42 | GITHUB_CONTEXT: "${{ github.event.pull_request.commits_url }}" 43 | run: | 44 | .github/workflows/scripts/check_commit.sh 45 | 46 | docs: 47 | uses: "./.github/workflows/docs.yml" 48 | 49 | lint: 50 | uses: "./.github/workflows/lint.yml" 51 | 52 | build: 53 | needs: "lint" 54 | uses: "./.github/workflows/build.yml" 55 | 56 | test: 57 | needs: "build" 58 | uses: "./.github/workflows/test.yml" 59 | with: 60 | matrix_env: | 61 | [{"TEST": "pulp"}, {"TEST": "azure"}, {"TEST": "s3"}, {"TEST": "lowerbounds"}] 62 | 63 | deprecations: 64 | runs-on: "ubuntu-latest" 65 | if: github.base_ref == 'main' 66 | needs: "test" 67 | steps: 68 | - name: "Create working directory" 69 | run: | 70 | mkdir -p "pulp_container" 71 | working-directory: "." 72 | - name: "Download Deprecations" 73 | uses: actions/download-artifact@v4 74 | with: 75 | pattern: "deprecations-*" 76 | path: "pulp_container" 77 | merge-multiple: true 78 | - name: "Print deprecations" 79 | run: | 80 | cat deprecations-*.txt | sort -u 81 | ! cat deprecations-*.txt | grep '[^[:space:]]' 82 | 83 | ready-to-ship: 84 | # This is a dummy dependent task to have a single entry for the branch protection rules. 85 | runs-on: "ubuntu-latest" 86 | needs: 87 | - "check-commits" 88 | - "lint" 89 | - "test" 90 | - "docs" 91 | if: "always()" 92 | steps: 93 | - name: "Collect needed jobs results" 94 | working-directory: "." 95 | run: | 96 | echo '${{toJson(needs)}}' | jq -r 'to_entries[]|select(.value.result!="success")|.key + ": " + .value.result' 97 | echo '${{toJson(needs)}}' | jq -e 'to_entries|map(select(.value.result!="success"))|length == 0' 98 | echo "CI says: Looks good!" 99 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | name: "Container CodeQL" 8 | 9 | on: 10 | workflow_dispatch: 11 | schedule: 12 | - cron: '37 1 * * 6' 13 | 14 | concurrency: 15 | group: ${{ github.ref_name }}-${{ github.workflow }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | language: [ 'python' ] 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | -------------------------------------------------------------------------------- /.github/workflows/create-branch.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: Create New Release Branch 10 | on: 11 | workflow_dispatch: 12 | 13 | env: 14 | RELEASE_WORKFLOW: true 15 | 16 | jobs: 17 | create-branch: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | fail-fast: false 22 | 23 | steps: 24 | - uses: "actions/checkout@v4" 25 | with: 26 | fetch-depth: 0 27 | path: "pulp_container" 28 | 29 | - uses: "actions/checkout@v4" 30 | with: 31 | fetch-depth: 1 32 | repository: "pulp/plugin_template" 33 | path: "plugin_template" 34 | 35 | - uses: "actions/setup-python@v5" 36 | with: 37 | python-version: "3.11" 38 | 39 | - name: "Install python dependencies" 40 | run: | 41 | echo ::group::PYDEPS 42 | pip install bump-my-version packaging -r plugin_template/requirements.txt 43 | echo ::endgroup:: 44 | 45 | - name: "Setting secrets" 46 | working-directory: "pulp_container" 47 | run: | 48 | python3 .github/workflows/scripts/secrets.py "$SECRETS_CONTEXT" 49 | env: 50 | SECRETS_CONTEXT: "${{ toJson(secrets) }}" 51 | 52 | - name: Determine new branch name 53 | working-directory: pulp_container 54 | run: | 55 | # Just to be sure... 56 | git checkout main 57 | NEW_BRANCH="$(bump-my-version show new_version --increment release | sed -Ene 's/^([[:digit:]]+\.[[:digit:]]+)\.[[:digit:]]+$/\1/p')" 58 | if [ -z "$NEW_BRANCH" ] 59 | then 60 | echo Could not determine the new branch name. 61 | exit 1 62 | fi 63 | echo "NEW_BRANCH=${NEW_BRANCH}" >> "$GITHUB_ENV" 64 | 65 | - name: Create release branch 66 | working-directory: pulp_container 67 | run: | 68 | git branch "${NEW_BRANCH}" 69 | 70 | - name: Bump version on main branch 71 | working-directory: pulp_container 72 | run: | 73 | bump-my-version bump --no-commit minor 74 | 75 | - name: Remove entries from CHANGES directory 76 | working-directory: pulp_container 77 | run: | 78 | find CHANGES -type f -regex ".*\.\(bugfix\|doc\|feature\|misc\|deprecation\|removal\)" -exec git rm {} + 79 | 80 | - name: Update CI branches in template_config 81 | working-directory: plugin_template 82 | run: | 83 | python3 ./plugin-template pulp_container --github --latest-release-branch "${NEW_BRANCH}" 84 | git add -A 85 | 86 | - name: Make a PR with version bump and without CHANGES/* 87 | uses: peter-evans/create-pull-request@v6 88 | with: 89 | path: pulp_container 90 | token: ${{ secrets.RELEASE_TOKEN }} 91 | committer: pulpbot 92 | author: pulpbot 93 | branch: minor-version-bump 94 | base: main 95 | title: Bump minor version 96 | commit-message: | 97 | Bump minor version 98 | delete-branch: true 99 | 100 | - name: Push release branch 101 | working-directory: pulp_container 102 | run: | 103 | git push origin "${NEW_BRANCH}" 104 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: "Docs" 10 | on: 11 | workflow_call: 12 | 13 | jobs: 14 | test: 15 | if: "endsWith(github.base_ref, 'main')" 16 | runs-on: "ubuntu-latest" 17 | defaults: 18 | run: 19 | working-directory: "pulp_container" 20 | steps: 21 | - uses: "actions/checkout@v4" 22 | with: 23 | fetch-depth: 1 24 | path: "pulp_container" 25 | - uses: "actions/checkout@v4" 26 | with: 27 | fetch-depth: 0 28 | repository: "pulp/pulp-docs" 29 | path: "pulp-docs" 30 | ref: "rewrite-as-mkdocs-plugin" 31 | - uses: "actions/setup-python@v5" 32 | with: 33 | python-version: "3.12" 34 | - name: "Install python dependencies" 35 | run: | 36 | echo ::group::PYDEPS 37 | pip install ../pulp-docs towncrier 38 | echo ::endgroup:: 39 | - name: "Build changelog" 40 | run: | 41 | towncrier build --yes --version 4.0.0.ci 42 | - name: "Build docs" 43 | working-directory: "pulp-docs" 44 | run: | 45 | pulp-docs fetch --dest .. 46 | pulp-docs build 47 | 48 | no-test: 49 | if: "!endsWith(github.base_ref, 'main')" 50 | runs-on: "ubuntu-latest" 51 | steps: 52 | - run: | 53 | echo "Skip docs testing on non-default branches." 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: "Lint" 10 | on: 11 | workflow_call: 12 | 13 | defaults: 14 | run: 15 | working-directory: "pulp_container" 16 | 17 | jobs: 18 | lint: 19 | runs-on: "ubuntu-latest" 20 | 21 | steps: 22 | - uses: "actions/checkout@v4" 23 | with: 24 | fetch-depth: 1 25 | path: "pulp_container" 26 | 27 | - uses: "actions/setup-python@v5" 28 | with: 29 | python-version: "3.11" 30 | 31 | - name: "Install python dependencies" 32 | run: | 33 | echo ::group::PYDEPS 34 | pip install -r lint_requirements.txt 35 | echo ::endgroup:: 36 | 37 | - name: "Lint workflow files" 38 | run: | 39 | yamllint -s -d '{extends: relaxed, rules: {line-length: disable}}' .github/workflows 40 | 41 | - name: "Verify bump version config" 42 | run: | 43 | bump-my-version bump --dry-run release 44 | bump-my-version show-bump 45 | 46 | # run black separately from flake8 to get a diff 47 | - name: "Run black" 48 | run: | 49 | black --version 50 | black --check --diff . 51 | 52 | # Lint code. 53 | - name: "Run flake8" 54 | run: | 55 | flake8 56 | 57 | - name: "Run extra lint checks" 58 | run: | 59 | [ ! -x .ci/scripts/extra_linting.sh ] || .ci/scripts/extra_linting.sh 60 | 61 | - name: "Check for any files unintentionally left out of MANIFEST.in" 62 | run: | 63 | check-manifest 64 | 65 | - name: "Verify requirements files" 66 | run: | 67 | python .ci/scripts/check_requirements.py 68 | 69 | - name: "Check for pulpcore imports outside of pulpcore.plugin" 70 | run: | 71 | sh .ci/scripts/check_pulpcore_imports.sh 72 | 73 | - name: "Check for common gettext problems" 74 | run: | 75 | sh .ci/scripts/check_gettext.sh 76 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: "Container Nightly CI" 10 | on: 11 | schedule: 12 | # * is a special character in YAML so you have to quote this string 13 | # runs at 3:00 UTC daily 14 | - cron: '00 3 * * *' 15 | workflow_dispatch: 16 | 17 | defaults: 18 | run: 19 | working-directory: "pulp_container" 20 | 21 | concurrency: 22 | group: "${{ github.ref_name }}-${{ github.workflow }}" 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | build: 27 | uses: "./.github/workflows/build.yml" 28 | 29 | test: 30 | needs: "build" 31 | uses: "./.github/workflows/test.yml" 32 | with: 33 | matrix_env: | 34 | [{"TEST": "pulp"}, {"TEST": "azure"}, {"TEST": "s3"}, {"TEST": "lowerbounds"}] 35 | 36 | changelog: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: "actions/checkout@v4" 40 | with: 41 | fetch-depth: 0 42 | path: "pulp_container" 43 | 44 | - uses: "actions/setup-python@v5" 45 | with: 46 | python-version: "3.11" 47 | 48 | - name: "Install python dependencies" 49 | run: | 50 | echo ::group::PYDEPS 51 | pip install gitpython packaging toml 52 | echo ::endgroup:: 53 | 54 | - name: "Configure Git with pulpbot name and email" 55 | run: | 56 | git config --global user.name 'pulpbot' 57 | git config --global user.email 'pulp-infra@redhat.com' 58 | 59 | - name: Collect changes from all branches 60 | run: python .ci/scripts/collect_changes.py 61 | 62 | - name: Create Pull Request 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.RELEASE_TOKEN }} 66 | title: "Update Changelog" 67 | body: "" 68 | branch: "changelog/update" 69 | delete-branch: true 70 | path: "pulp_container" 71 | ... 72 | -------------------------------------------------------------------------------- /.github/workflows/pr_checks.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: "Container PR static checks" 10 | on: 11 | pull_request_target: 12 | types: 13 | - "opened" 14 | - "synchronize" 15 | - "reopened" 16 | branches: 17 | - "main" 18 | - "[0-9]+.[0-9]+" 19 | 20 | # This workflow runs with elevated permissions. 21 | # Do not even think about running a single bit of code from the PR. 22 | # Static analysis should be fine however. 23 | 24 | concurrency: 25 | group: "${{ github.event.pull_request.number }}-${{ github.workflow }}" 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | apply_labels: 30 | runs-on: "ubuntu-latest" 31 | name: "Label PR" 32 | permissions: 33 | pull-requests: "write" 34 | steps: 35 | - uses: "actions/checkout@v4" 36 | with: 37 | fetch-depth: 0 38 | - uses: "actions/setup-python@v5" 39 | with: 40 | python-version: "3.11" 41 | - name: "Determine PR labels" 42 | run: | 43 | pip install GitPython==3.1.42 44 | git fetch origin ${{ github.event.pull_request.head.sha }} 45 | python .ci/scripts/pr_labels.py "origin/${{ github.base_ref }}" "${{ github.event.pull_request.head.sha }}" >> "$GITHUB_ENV" 46 | - uses: "actions/github-script@v7" 47 | name: "Apply PR Labels" 48 | with: 49 | script: | 50 | const { ADD_LABELS, REMOVE_LABELS } = process.env; 51 | 52 | if (REMOVE_LABELS.length) { 53 | for await (const labelName of REMOVE_LABELS.split(",")) { 54 | try { 55 | await github.rest.issues.removeLabel({ 56 | issue_number: context.issue.number, 57 | owner: context.repo.owner, 58 | repo: context.repo.repo, 59 | name: labelName, 60 | }); 61 | } catch(err) { 62 | } 63 | } 64 | } 65 | if (ADD_LABELS.length) { 66 | await github.rest.issues.addLabels({ 67 | issue_number: context.issue.number, 68 | owner: context.repo.owner, 69 | repo: context.repo.repo, 70 | labels: ADD_LABELS.split(","), 71 | }); 72 | } 73 | ... 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | --- 9 | name: Container Release Pipeline 10 | on: 11 | workflow_dispatch: 12 | 13 | defaults: 14 | run: 15 | working-directory: "pulp_container" 16 | 17 | jobs: 18 | build-artifacts: 19 | runs-on: "ubuntu-latest" 20 | 21 | strategy: 22 | fail-fast: false 23 | 24 | steps: 25 | - uses: "actions/checkout@v4" 26 | with: 27 | fetch-depth: 0 28 | path: "pulp_container" 29 | token: ${{ secrets.RELEASE_TOKEN }} 30 | 31 | - uses: "actions/setup-python@v5" 32 | with: 33 | python-version: "3.11" 34 | 35 | - name: "Install python dependencies" 36 | run: | 37 | echo ::group::PYDEPS 38 | pip install bump-my-version towncrier 39 | echo ::endgroup:: 40 | 41 | - name: "Configure Git with pulpbot name and email" 42 | run: | 43 | git config --global user.name 'pulpbot' 44 | git config --global user.email 'pulp-infra@redhat.com' 45 | 46 | - name: "Setting secrets" 47 | run: | 48 | python3 .github/workflows/scripts/secrets.py "$SECRETS_CONTEXT" 49 | env: 50 | SECRETS_CONTEXT: "${{ toJson(secrets) }}" 51 | 52 | - name: "Tag the release" 53 | run: | 54 | .github/workflows/scripts/release.sh 55 | shell: "bash" 56 | env: 57 | PY_COLORS: "1" 58 | ANSIBLE_FORCE_COLOR: "1" 59 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 60 | GITHUB_CONTEXT: "${{ github.event.pull_request.commits_url }}" 61 | -------------------------------------------------------------------------------- /.github/workflows/scripts/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | # make sure this script runs at the repo root 11 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 12 | 13 | set -mveuo pipefail 14 | 15 | if [ "${GITHUB_REF##refs/heads/}" = "${GITHUB_REF}" ] 16 | then 17 | BRANCH_BUILD=0 18 | else 19 | BRANCH_BUILD=1 20 | BRANCH="${GITHUB_REF##refs/heads/}" 21 | fi 22 | if [ "${GITHUB_REF##refs/tags/}" = "${GITHUB_REF}" ] 23 | then 24 | TAG_BUILD=0 25 | else 26 | TAG_BUILD=1 27 | BRANCH="${GITHUB_REF##refs/tags/}" 28 | fi 29 | 30 | COMMIT_MSG=$(git log --format=%B --no-merges -1) 31 | export COMMIT_MSG 32 | 33 | COMPONENT_VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") 34 | 35 | mkdir .ci/ansible/vars || true 36 | echo "---" > .ci/ansible/vars/main.yaml 37 | echo "legacy_component_name: pulp_container" >> .ci/ansible/vars/main.yaml 38 | echo "component_name: container" >> .ci/ansible/vars/main.yaml 39 | echo "component_version: '${COMPONENT_VERSION}'" >> .ci/ansible/vars/main.yaml 40 | 41 | export PRE_BEFORE_INSTALL=$PWD/.github/workflows/scripts/pre_before_install.sh 42 | export POST_BEFORE_INSTALL=$PWD/.github/workflows/scripts/post_before_install.sh 43 | 44 | if [ -f $PRE_BEFORE_INSTALL ]; then 45 | source $PRE_BEFORE_INSTALL 46 | fi 47 | 48 | if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "${BRANCH_BUILD}" = "1" -a "${BRANCH}" != "main" ] 49 | then 50 | echo $COMMIT_MSG | sed -n -e 's/.*CI Base Image:\s*\([-_/[:alnum:]]*:[-_[:alnum:]]*\).*/ci_base: "\1"/p' >> .ci/ansible/vars/main.yaml 51 | fi 52 | 53 | for i in {1..3} 54 | do 55 | ansible-galaxy collection install "amazon.aws:8.1.0" && s=0 && break || s=$? && sleep 3 56 | done 57 | if [[ $s -gt 0 ]] 58 | then 59 | echo "Failed to install amazon.aws" 60 | exit $s 61 | fi 62 | 63 | if [[ "$TEST" = "pulp" ]]; then 64 | python3 .ci/scripts/calc_constraints.py -u pyproject.toml > upperbounds_constraints.txt 65 | fi 66 | if [[ "$TEST" = "lowerbounds" ]]; then 67 | python3 .ci/scripts/calc_constraints.py pyproject.toml > lowerbounds_constraints.txt 68 | fi 69 | 70 | if [ -f $POST_BEFORE_INSTALL ]; then 71 | source $POST_BEFORE_INSTALL 72 | fi 73 | -------------------------------------------------------------------------------- /.github/workflows/scripts/before_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | # make sure this script runs at the repo root 11 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 12 | 13 | set -euv 14 | 15 | source .github/workflows/scripts/utils.sh 16 | 17 | export PRE_BEFORE_SCRIPT=$PWD/.github/workflows/scripts/pre_before_script.sh 18 | export POST_BEFORE_SCRIPT=$PWD/.github/workflows/scripts/post_before_script.sh 19 | 20 | if [[ -f $PRE_BEFORE_SCRIPT ]]; then 21 | source $PRE_BEFORE_SCRIPT 22 | fi 23 | 24 | # Developers should be able to reproduce the containers with this config 25 | echo "CI vars:" 26 | tail -v -n +1 .ci/ansible/vars/main.yaml 27 | 28 | # Developers often want to know the final pulp config 29 | echo "PULP CONFIG:" 30 | tail -v -n +1 .ci/ansible/settings/settings.* ~/.config/pulp_smash/settings.json 31 | 32 | echo "Containerfile:" 33 | tail -v -n +1 .ci/ansible/Containerfile 34 | 35 | echo "Constraints Files:" 36 | # The need not even exist. 37 | tail -v -n +1 ../*/*constraints.txt || true 38 | 39 | # Needed for some functional tests 40 | cmd_prefix bash -c "echo '%wheel ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/nopasswd" 41 | cmd_prefix bash -c "usermod -a -G wheel pulp" 42 | 43 | if [[ "${REDIS_DISABLED:-false}" == true ]]; then 44 | cmd_prefix bash -c "s6-rc -d change redis" 45 | echo "The Redis service was disabled for $TEST" 46 | fi 47 | 48 | if [[ -f $POST_BEFORE_SCRIPT ]]; then 49 | source $POST_BEFORE_SCRIPT 50 | fi 51 | 52 | # Lots of plugins try to use this path, and throw warnings if they cannot access it. 53 | cmd_prefix mkdir /.pytest_cache 54 | cmd_prefix chown pulp:pulp /.pytest_cache 55 | -------------------------------------------------------------------------------- /.github/workflows/scripts/build_python_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script expects all -api.json files to exist in the plugins root directory. 4 | # It produces a -python-client.tar and -python-client-docs.tar file in the plugins root directory. 5 | 6 | # WARNING: DO NOT EDIT! 7 | # 8 | # This file was generated by plugin_template, and is managed by it. Please use 9 | # './plugin-template --github pulp_container' to update this file. 10 | # 11 | # For more info visit https://github.com/pulp/plugin_template 12 | 13 | set -mveuo pipefail 14 | 15 | # make sure this script runs at the repo root 16 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 17 | 18 | pushd ../pulp-openapi-generator 19 | rm -rf "pulp_container-client" 20 | 21 | ./gen-client.sh "../pulp_container/container-api.json" "container" python "pulp_container" 22 | 23 | pushd pulp_container-client 24 | python setup.py sdist bdist_wheel --python-tag py3 25 | 26 | twine check "dist/pulp_container_client-"*"-py3-none-any.whl" 27 | twine check "dist/pulp_container-client-"*".tar.gz" 28 | 29 | tar cvf "../../pulp_container/container-python-client.tar" ./dist 30 | 31 | find ./docs/* -exec sed -i 's/Back to README/Back to HOME/g' {} \; 32 | find ./docs/* -exec sed -i 's/README//g' {} \; 33 | cp README.md docs/index.md 34 | sed -i 's/docs\///g' docs/index.md 35 | find ./docs/* -exec sed -i 's/\.md//g' {} \; 36 | 37 | cat >> mkdocs.yml << DOCSYAML 38 | --- 39 | site_name: PulpContainer Client 40 | site_description: Container bindings 41 | site_author: Pulp Team 42 | site_url: https://docs.pulpproject.org/pulp_container_client/ 43 | repo_name: pulp/pulp_container 44 | repo_url: https://github.com/pulp/pulp_container 45 | theme: readthedocs 46 | DOCSYAML 47 | 48 | # Building the bindings docs 49 | mkdocs build 50 | 51 | # Pack the built site. 52 | tar cvf ../../pulp_container/container-python-client-docs.tar ./site 53 | popd 54 | popd 55 | -------------------------------------------------------------------------------- /.github/workflows/scripts/build_ruby_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script expects all -api.json files to exist in the plugins root directory. 4 | # It produces a -ruby-client.tar file in the plugins root directory. 5 | 6 | # WARNING: DO NOT EDIT! 7 | # 8 | # This file was generated by plugin_template, and is managed by it. Please use 9 | # './plugin-template --github pulp_container' to update this file. 10 | # 11 | # For more info visit https://github.com/pulp/plugin_template 12 | 13 | set -mveuo pipefail 14 | 15 | # make sure this script runs at the repo root 16 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 17 | 18 | pushd ../pulp-openapi-generator 19 | rm -rf "pulp_container-client" 20 | 21 | ./gen-client.sh "../pulp_container/container-api.json" "container" ruby "pulp_container" 22 | 23 | pushd pulp_container-client 24 | gem build pulp_container_client 25 | tar cvf "../../pulp_container/container-ruby-client.tar" "./pulp_container_client-"*".gem" 26 | popd 27 | popd 28 | -------------------------------------------------------------------------------- /.github/workflows/scripts/check_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | # make sure this script runs at the repo root 11 | cd "$(dirname "$(realpath -e "$0")")/../../.." 12 | 13 | set -euv 14 | 15 | for SHA in $(curl -H "Authorization: token $GITHUB_TOKEN" "$GITHUB_CONTEXT" | jq -r '.[].sha') 16 | do 17 | python3 .ci/scripts/validate_commit_message.py "$SHA" 18 | done 19 | -------------------------------------------------------------------------------- /.github/workflows/scripts/post_before_script.sh: -------------------------------------------------------------------------------- 1 | SCENARIOS=("pulp" "performance" "azure" "gcp" "s3" "generate-bindings" "lowerbounds") 2 | if [[ " ${SCENARIOS[*]} " =~ " ${TEST} " ]]; then 3 | # Needed by pulp_container/tests/functional/api/test_flatpak.py: 4 | cmd_prefix dnf install -yq dbus-daemon flatpak 5 | fi 6 | 7 | # This allows flatpak to trust Pulp, but currently it breaks the trust for bindings 8 | # TODO: Figure out another command to fix this 9 | # add the copied certificates from install.sh to the container's trusted certificates list 10 | # if [[ "$TEST" = "azure" ]]; then 11 | # cmd_prefix trust anchor /etc/pki/tls/cert.pem 12 | # else 13 | # cmd_prefix trust anchor /etc/pulp/certs/pulp_webserver.crt 14 | # fi 15 | -------------------------------------------------------------------------------- /.github/workflows/scripts/pre_before_install.sh: -------------------------------------------------------------------------------- 1 | # make sure this script runs at the repo root 2 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 3 | 4 | set -mveuo pipefail 5 | 6 | if [ -f "/etc/docker/daemon.json" ] 7 | then 8 | echo "INFO: 9 | Updating docker configuration 10 | " 11 | 12 | echo "$(cat /etc/docker/daemon.json | jq -s '.[0] + { 13 | "insecure-registries" : ["pulp.example.com", "pulp"] 14 | }')" | sudo tee /etc/docker/daemon.json 15 | sudo service docker restart || true 16 | fi 17 | 18 | if [ -f "/etc/containers/registries.conf" ] 19 | then 20 | echo "INFO: 21 | Updating registries configuration 22 | " 23 | echo "[registries.insecure] 24 | registries = ['pulp.example.com', 'pulp'] 25 | " | sudo tee -a /etc/containers/registries.conf 26 | fi 27 | 28 | # Configure the GHA host for buildah/skopeo running within the pulp container. 29 | # Default UID & GID range is 165536-231071, which is 64K long. 30 | # But nested buildah/skopeo always needs more than needs 64K. 31 | # The Pulp image is configured for 64K + 10000 . 32 | sudo sed -i "s\runner:165536:65536\runner:165536:75536\g" /etc/subuid /etc/subgid 33 | podman system migrate 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/scripts/pre_before_script.sh: -------------------------------------------------------------------------------- 1 | # make sure this script runs at the repo root 2 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 3 | 4 | set -mveuo pipefail 5 | 6 | # add pulp.example.com ot the /etc/hosts 7 | # docker clients cannot identify 'pulp' as host 8 | cat /etc/hosts | grep pulp 9 | PULP_HOSTNAME=$(cat /etc/hosts | sed -En "s/pulp/pulp.example.com/p") 10 | echo $PULP_HOSTNAME | sudo tee -a /etc/hosts 11 | cat /etc/hosts | grep pulp 12 | 13 | echo $PULP_HOSTNAME | docker exec -i pulp bash -c "cat >> /etc/hosts" 14 | 15 | echo "machine pulp.example.com 16 | login admin 17 | password password 18 | " >> ~/.netrc 19 | 20 | sed -i 's/https:\/\/pulp/https:\/\/pulp.example.com/g' $PWD/.github/workflows/scripts/script.sh 21 | sed -i 's/\"hostname\": \"pulp\",/\"hostname\": \"pulp.example.com\",/g' ~/.config/pulp_smash/settings.json 22 | -------------------------------------------------------------------------------- /.github/workflows/scripts/publish_client_gem.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | set -euv 11 | 12 | # make sure this script runs at the repo root 13 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 14 | 15 | VERSION="$1" 16 | 17 | if [[ -z "${VERSION}" ]] 18 | then 19 | echo "No version specified." 20 | exit 1 21 | fi 22 | 23 | mkdir -p ~/.gem 24 | touch ~/.gem/credentials 25 | echo "--- 26 | :rubygems_api_key: ${RUBYGEMS_API_KEY}" > ~/.gem/credentials 27 | sudo chmod 600 ~/.gem/credentials 28 | gem push "pulp_container_client-${VERSION}.gem" 29 | -------------------------------------------------------------------------------- /.github/workflows/scripts/publish_client_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | set -euv 11 | 12 | # make sure this script runs at the repo root 13 | cd "$(dirname "$(realpath -e "$0")")/../../.." 14 | 15 | VERSION="$1" 16 | 17 | if [[ -z "${VERSION}" ]] 18 | then 19 | echo "No version specified." 20 | exit 1 21 | fi 22 | 23 | twine upload -u __token__ -p "${PYPI_API_TOKEN}" \ 24 | "dist/pulp_container_client-${VERSION}-py3-none-any.whl" \ 25 | "dist/pulp_container-client-${VERSION}.tar.gz" \ 26 | ; 27 | -------------------------------------------------------------------------------- /.github/workflows/scripts/publish_plugin_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | set -euv 11 | 12 | # make sure this script runs at the repo root 13 | cd "$(dirname "$(realpath -e "$0")")"/../../.. 14 | 15 | VERSION="$1" 16 | 17 | if [[ -z "${VERSION}" ]] 18 | then 19 | echo "No version specified." 20 | exit 1 21 | fi 22 | 23 | twine upload -u __token__ -p "${PYPI_API_TOKEN}" \ 24 | dist/pulp?container-"${VERSION}"-py3-none-any.whl \ 25 | dist/pulp?container-"${VERSION}".tar.gz \ 26 | ; 27 | -------------------------------------------------------------------------------- /.github/workflows/scripts/push_branch_and_tag_to_github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | set -eu 11 | 12 | BRANCH_NAME="$(echo "$GITHUB_REF" | sed -rn 's/refs\/heads\/(.*)/\1/p')" 13 | 14 | remote_repo="https://pulpbot:${RELEASE_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 15 | 16 | git push "${remote_repo}" "$BRANCH_NAME" "$1" 17 | -------------------------------------------------------------------------------- /.github/workflows/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu -o pipefail 4 | 5 | BRANCH=$(git branch --show-current) 6 | 7 | if ! [[ "${BRANCH}" =~ ^[0-9]+\.[0-9]+$ ]] 8 | then 9 | echo ERROR: This is not a release branch! 10 | exit 1 11 | fi 12 | 13 | # The tail is a necessary workaround to remove the warning from the output. 14 | NEW_VERSION="$(bump-my-version show new_version --increment release | tail -n -1)" 15 | echo "Release ${NEW_VERSION}" 16 | 17 | if ! [[ "${NEW_VERSION}" == "${BRANCH}"* ]] 18 | then 19 | echo ERROR: Version does not match release branch 20 | exit 1 21 | fi 22 | 23 | towncrier build --yes --version "${NEW_VERSION}" 24 | bump-my-version bump release --commit --message "Release {new_version}" --tag --tag-name "{new_version}" --tag-message "Release {new_version}" --allow-dirty 25 | bump-my-version bump patch --commit 26 | 27 | git push origin "${BRANCH}" "${NEW_VERSION}" 28 | -------------------------------------------------------------------------------- /.github/workflows/scripts/secrets.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | secrets = json.loads(sys.argv[1]) 6 | for key, value in secrets.items(): 7 | print(f"Setting {key} ...") 8 | lines = len(value.split("\n")) 9 | if lines > 1: 10 | os.system(f"/bin/bash -c \"echo '{key}<> $GITHUB_ENV\"") 11 | os.system(f"/bin/bash -c \"echo '{value}' >> $GITHUB_ENV\"") 12 | os.system("/bin/bash -c \"echo 'EOF' >> $GITHUB_ENV\"") 13 | else: 14 | os.system(f"/bin/bash -c \"echo '{key}={value}' >> $GITHUB_ENV\"") 15 | -------------------------------------------------------------------------------- /.github/workflows/scripts/stage-changelog-for-default-branch.py: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | import argparse 9 | import os 10 | import textwrap 11 | 12 | from git import Repo 13 | from git.exc import GitCommandError 14 | 15 | 16 | helper = textwrap.dedent( 17 | """\ 18 | Stage the changelog for a release on main branch. 19 | 20 | Example: 21 | $ python .github/workflows/scripts/stage-changelog-for-default-branch.py 3.4.0 22 | 23 | """ 24 | ) 25 | 26 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=helper) 27 | 28 | parser.add_argument( 29 | "release_version", 30 | type=str, 31 | help="The version string for the release.", 32 | ) 33 | 34 | args = parser.parse_args() 35 | 36 | release_version_arg = args.release_version 37 | 38 | release_path = os.path.dirname(os.path.abspath(__file__)) 39 | plugin_path = release_path.split("/.github")[0] 40 | 41 | if not release_version_arg.endswith(".0"): 42 | os._exit(os.system("python .ci/scripts/changelog.py")) 43 | 44 | print(f"\n\nRepo path: {plugin_path}") 45 | repo = Repo(plugin_path) 46 | 47 | changelog_commit = None 48 | # Look for a commit with the requested release version 49 | for commit in repo.iter_commits(): 50 | if f"{release_version_arg} changelog" == commit.message.split("\n")[0]: 51 | changelog_commit = commit 52 | break 53 | if f"Add changelog for {release_version_arg}" == commit.message.split("\n")[0]: 54 | changelog_commit = commit 55 | break 56 | 57 | if not changelog_commit: 58 | raise RuntimeError("Changelog commit for {release_version_arg} was not found.") 59 | 60 | git = repo.git 61 | git.stash() 62 | git.checkout("origin/main") 63 | try: 64 | git.cherry_pick(changelog_commit.hexsha) 65 | except GitCommandError: 66 | git.add("CHANGES/") 67 | # Don't try opening an editor for the commit message 68 | with git.custom_environment(GIT_EDITOR="true"): 69 | git.cherry_pick("--continue") 70 | git.reset("origin/main") 71 | -------------------------------------------------------------------------------- /.github/workflows/scripts/update_backport_labels.py: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | import requests 9 | import yaml 10 | import random 11 | import os 12 | 13 | 14 | def random_color(): 15 | """Generates a random 24-bit number in hex""" 16 | color = random.randrange(0, 2**24) 17 | return format(color, "06x") 18 | 19 | 20 | session = requests.Session() 21 | token = os.getenv("GITHUB_TOKEN") 22 | 23 | headers = { 24 | "Authorization": f"token {token}", 25 | "Accept": "application/vnd.github+json", 26 | "X-GitHub-Api-Version": "2022-11-28", 27 | } 28 | session.headers.update(headers) 29 | 30 | # get all labels from the repository's current state 31 | response = session.get("https://api.github.com/repos/pulp/pulp_container/labels", headers=headers) 32 | assert response.status_code == 200 33 | old_labels = set([x["name"] for x in response.json() if x["name"].startswith("backport-")]) 34 | 35 | # get list of branches from template_config.yml 36 | with open("./template_config.yml", "r") as f: 37 | plugin_template = yaml.safe_load(f) 38 | branches = set(plugin_template["supported_release_branches"]) 39 | latest_release_branch = plugin_template["latest_release_branch"] 40 | if latest_release_branch is not None: 41 | branches.add(latest_release_branch) 42 | new_labels = {"backport-" + x for x in branches} 43 | 44 | # delete old labels that are not in new labels 45 | for label in old_labels.difference(new_labels): 46 | response = session.delete( 47 | f"https://api.github.com/repos/pulp/pulp_container/labels/{label}", headers=headers 48 | ) 49 | assert response.status_code == 204 50 | 51 | # create new labels that are not in old labels 52 | for label in new_labels.difference(old_labels): 53 | color = random_color() 54 | response = session.post( 55 | "https://api.github.com/repos/pulp/pulp_container/labels", 56 | headers=headers, 57 | json={"name": label, "color": color}, 58 | ) 59 | assert response.status_code == 201 60 | -------------------------------------------------------------------------------- /.github/workflows/scripts/utils.sh: -------------------------------------------------------------------------------- 1 | # This file is meant to be sourced by ci-scripts 2 | 3 | # WARNING: DO NOT EDIT! 4 | # 5 | # This file was generated by plugin_template, and is managed by it. Please use 6 | # './plugin-template --github pulp_container' to update this file. 7 | # 8 | # For more info visit https://github.com/pulp/plugin_template 9 | 10 | PULP_CI_CONTAINER=pulp 11 | 12 | # Run a command 13 | cmd_prefix() { 14 | docker exec "$PULP_CI_CONTAINER" "$@" 15 | } 16 | 17 | # Run a command as the limited pulp user 18 | cmd_user_prefix() { 19 | docker exec -u pulp "$PULP_CI_CONTAINER" "$@" 20 | } 21 | 22 | # Run a command, and pass STDIN 23 | cmd_stdin_prefix() { 24 | docker exec -i "$PULP_CI_CONTAINER" "$@" 25 | } 26 | 27 | # Run a command as the lmited pulp user, and pass STDIN 28 | cmd_user_stdin_prefix() { 29 | docker exec -i -u pulp "$PULP_CI_CONTAINER" "$@" 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/update-labels.yml: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | 9 | --- 10 | name: "Container Update Labels" 11 | on: 12 | push: 13 | branches: 14 | - "main" 15 | paths: 16 | - "template_config.yml" 17 | 18 | jobs: 19 | update_backport_labels: 20 | runs-on: "ubuntu-latest" 21 | steps: 22 | - uses: "actions/setup-python@v5" 23 | with: 24 | python-version: "3.11" 25 | - name: "Configure Git with pulpbot name and email" 26 | run: | 27 | git config --global user.name 'pulpbot' 28 | git config --global user.email 'pulp-infra@redhat.com' 29 | - name: "Install python dependencies" 30 | run: | 31 | echo ::group::PYDEPS 32 | pip install requests pyyaml 33 | echo ::endgroup:: 34 | - uses: "actions/checkout@v4" 35 | - name: "Update labels" 36 | run: | 37 | python3 .github/workflows/scripts/update_backport_labels.py 38 | env: 39 | GITHUB_TOKEN: "${{ secrets.RELEASE_TOKEN }}" 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | pip-wheel-metadata/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # Environments 83 | .env 84 | .venv 85 | env/ 86 | venv/ 87 | ENV/ 88 | env.bak/ 89 | venv.bak/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # Editors 105 | .vscode/ 106 | .idea/ 107 | 108 | # A generated API schema 109 | docs/_static/api.json 110 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [allowlist] 2 | description = "Our test install exports a test only MINIO ACCESS KEY" 3 | paths = [ 4 | ".github/workflows/scripts/install.sh", 5 | ".travis/install.sh", 6 | ] 7 | regexes = [ 8 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkdNMkQ6SU9CVDpHQVpEOk1aUlE6RzQyVzpDWkJaOkdWUlQ6R00zRzpNRTJUOlFNSlk6R1JURDpNTUpRIn0.eyJhY2Nlc3MiOlt7InR5cGUiOiIiLCJuYW1lIjoiIiwiYWN0aW9ucyI6W119XSwiYXVkIjoicHVscDMtc291cmNlLWZlZG9yYTMxLmxvY2FsaG9zdC5leGFtcGxlLmNvbSIsImV4cCI6MTU5NDYzNDU0NSwiaWF0IjoxNTk0NjM0MjQ1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjI0ODE3L3Rva2VuLyIsImp0aSI6ImU4ZTUyYzVhLWYxMzAtNGJlMi1iNjFhLTUwNzVhMjhkMTA0YSIsIm5iZiI6MTU5NDYzNDI0NSwic3ViIjoiIn0.ySDUHooaURbsyKLkHoXqA1JJPwlcDtpz_u6GgcqA8fmFGmSWJFlAGYtA2GLXDzPioH-bh1JkMJdBDs61c5JnFw", 9 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkhBM1Q6SVlSUjpHUTNUOklPTEM6TVE0RzpFT0xDOkdGUVQ6QVpURTpHQlNXOkNaUlY6TUlZVzpLTkpWIn0.eyJhY2Nlc3MiOlt7InR5cGUiOiIiLCJuYW1lIjoiIiwiYWN0aW9ucyI6W119XSwiYXVkIjoibG9jYWxob3N0OjI0ODE2IiwiZXhwIjoxNTcxMDcxOTUzLCJpYXQiOjE1NzEwNzE2NTMsImlzcyI6ImxvY2FsaG9zdDoyNDgxNi90b2tlbiIsImp0aSI6IjRmYTliYTYwLTY0ZTUtNDA3MC1hMzMyLWZmZTRlMTk2YzVjNyIsIm5iZiI6MTU3MTA3MTY1Mywic3ViIjoiIn0.pirj8yhbjYnldxmZ-jIZ72VJrzxkAnwLXLu1ND9QAL-kl3gZrvPbp98w2xdhEoQ_7WEka4veb6uU5ZzmD87X1Q", 10 | ] 11 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | pycodestyle: 2 | max-line-length: 100 # Default is 79 in PEP8 3 | ignore: # Errors and warnings to ignore 4 | - E401 # multiple imports on one line 5 | exclude: 6 | - "./docs/*" 7 | - "*/build/*" 8 | - "*/migrations/*" 9 | -------------------------------------------------------------------------------- /CHANGES/.TEMPLATE.md: -------------------------------------------------------------------------------- 1 | {# TOWNCRIER TEMPLATE #} 2 | {% for section, _ in sections.items() %} 3 | {%- set section_slug = "-" + section|replace(" ", "-")|replace("_", "-")|lower %} 4 | {%- if section %} 5 | 6 | ### {{section}} {: #{{versiondata.version}}{{section_slug}} } 7 | {% else %} 8 | {%- set section_slug = "" %} 9 | {% endif %} 10 | {% if sections[section] %} 11 | {% for category, val in definitions.items() if category in sections[section]%} 12 | 13 | #### {{ definitions[category]['name'] }} {: #{{versiondata.version}}{{section_slug}}-{{category}} } 14 | 15 | {% if definitions[category]['showcontent'] %} 16 | {% for text, values in sections[section][category].items() %} 17 | - {{ text }} 18 | {% if values %} 19 | {{ values|join(',\n ') }} 20 | {% endif %} 21 | {% endfor %} 22 | {% else %} 23 | - {{ sections[section][category]['']|join(', ') }} 24 | {% endif %} 25 | {% if sections[section][category]|length == 0 %} 26 | 27 | No significant changes. 28 | {% else %} 29 | {% endif %} 30 | {% endfor %} 31 | {% else %} 32 | 33 | No significant changes. 34 | {% endif %} 35 | {% endfor %} 36 | 37 | --- 38 | 39 | 40 | -------------------------------------------------------------------------------- /CHANGES/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /CHANGES/1997.bugfix: -------------------------------------------------------------------------------- 1 | Fixed the Registry API returning 'charset utf-8' in the Content-Type header. 2 | -------------------------------------------------------------------------------- /CHANGES/1998.bugfix: -------------------------------------------------------------------------------- 1 | Fixed a 500 error when using a file storage domain at a custom MEDIA_ROOT. 2 | -------------------------------------------------------------------------------- /CHANGES/1999.doc: -------------------------------------------------------------------------------- 1 | Replaced mention of staff for superuser in docs. 2 | -------------------------------------------------------------------------------- /COMMITMENT: -------------------------------------------------------------------------------- 1 | GPL Cooperation Commitment, version 1.0 2 | 3 | Before filing or continuing to prosecute any legal proceeding or claim 4 | (other than a Defensive Action) arising from termination of a Covered 5 | License, we commit to extend to the person or entity ('you') accused 6 | of violating the Covered License the following provisions regarding 7 | cure and reinstatement, taken from GPL version 3. As used here, the 8 | term 'this License' refers to the specific Covered License being 9 | enforced. 10 | 11 | However, if you cease all violation of this License, then your 12 | license from a particular copyright holder is reinstated (a) 13 | provisionally, unless and until the copyright holder explicitly 14 | and finally terminates your license, and (b) permanently, if the 15 | copyright holder fails to notify you of the violation by some 16 | reasonable means prior to 60 days after the cessation. 17 | 18 | Moreover, your license from a particular copyright holder is 19 | reinstated permanently if the copyright holder notifies you of the 20 | violation by some reasonable means, this is the first time you 21 | have received notice of violation of this License (for any work) 22 | from that copyright holder, and you cure the violation prior to 30 23 | days after your receipt of the notice. 24 | 25 | We intend this Commitment to be irrevocable, and binding and 26 | enforceable against us and assignees of or successors to our 27 | copyrights. 28 | 29 | Definitions 30 | 31 | 'Covered License' means the GNU General Public License, version 2 32 | (GPLv2), the GNU Lesser General Public License, version 2.1 33 | (LGPLv2.1), or the GNU Library General Public License, version 2 34 | (LGPLv2), all as published by the Free Software Foundation. 35 | 36 | 'Defensive Action' means a legal proceeding or claim that We bring 37 | against you in response to a prior proceeding or claim initiated by 38 | you or your affiliate. 39 | 40 | 'We' means each contributor to this repository as of the date of 41 | inclusion of this file, including subsidiaries of a corporate 42 | contributor. 43 | 44 | This work is available under a [Creative Commons Attribution-ShareAlike 4.0 International license] 45 | (https://creativecommons.org/licenses/by-sa/4.0/). 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | We have provided detailed documentation for ways in which you can 4 | contribute to Pulp here: 5 | https://pulpproject.org/dev/ 6 | 7 | This documentation includes: 8 | 9 | * Suggestions of how to contribute 10 | * How we track bugs 11 | * Ways to get in touch with other contributors who can advise you 12 | * A contribution checklist 13 | * A developer guide 14 | 15 | Join us! We look forward to hearing from you. 16 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright © 2019 Pulp Project developers. 2 | 3 | This software is licensed to you under the GNU General Public 4 | License as published by the Free Software Foundation; either version 5 | 2 of the License (GPLv2) or (at your option) any later version. 6 | There is NO WARRANTY for this software, express or implied, 7 | including the implied warranties of MERCHANTABILITY, 8 | NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should 9 | have received a copy of GPLv2 along with this software; if not, see 10 | http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include pulp_container/app/webserver_snippets/* 3 | include pyproject.toml 4 | include CHANGES.md 5 | include COMMITMENT 6 | include COPYRIGHT 7 | include functest_requirements.txt 8 | include test_requirements.txt 9 | include unittest_requirements.txt 10 | exclude CONTRIBUTING.md 11 | exclude releasing.md 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``pulp_container`` Plugin 2 | ========================= 3 | 4 | .. figure:: https://github.com/pulp/pulp_container/actions/workflows/nightly.yml/badge.svg?branch=main 5 | :alt: Container Nightly CI/CD 6 | 7 | This is the ``pulp_container`` Plugin for `Pulp Project 8 | 3.0+ `__. This plugin provides Pulp with support for container 9 | images and OCI artifacts. 10 | 11 | For more information, please see the `documentation 12 | `_ or the `Pulp project page 13 | `_. 14 | 15 | How to File an Issue 16 | -------------------- 17 | 18 | `New pulp_container issue `_. 19 | 20 | .. warning:: 21 | Is this security related? If so, please follow the `Security Disclosures `_ procedure. 22 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | coverage 3 | flake8 4 | flake8-black 5 | flake8-docstrings 6 | flake8-tuple 7 | flake8-quotes 8 | pydocstyle 9 | requests 10 | -------------------------------------------------------------------------------- /doc_requirements.txt: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | towncrier 8 | pulp-docs @ git+https://github.com/pulp/pulp-docs@main 9 | -------------------------------------------------------------------------------- /docs/admin/guides/build-image.md: -------------------------------------------------------------------------------- 1 | # Build Images 2 | 3 | !!! warning 4 | 5 | All container build APIs are in tech preview. Backwards compatibility when upgrading is not 6 | guaranteed. 7 | 8 | Users can add new images to a container repository by uploading a Containerfile. The syntax for 9 | Containerfile is the same as for a Dockerfile. 10 | 11 | To pass arbitrary files or artifacts to the image building context, the `build_context` property (a reference to a file repository) can be provided in the request payload. 12 | These files can be referenced in Containerfile by passing their `relative-path`: 13 | ``` 14 | ADD/COPY 15 | ``` 16 | 17 | It is possible to define the Containerfile in two ways: 18 | 19 | * from a [local file](site:pulp_container/docs/admin/guides/build-image#build-from-a-containerfile-uploaded-during-build-request) and pass it during build request 20 | * from an [existing file](site:pulp_container/docs/admin/guides/build-image#upload-the-containerfile-as-a-file-content) in the `build_context` 21 | 22 | ## Create a Container Repository 23 | 24 | ```bash 25 | CONTAINER_REPO=$(pulp container repository create --name building | jq -r '.pulp_href') 26 | ``` 27 | 28 | ## Create a File Repository and populate it 29 | 30 | ```bash 31 | FILE_REPO=$(pulp file repository create --name bar --autopublish | jq -r '.pulp_href') 32 | 33 | echo 'Hello world!' > example.txt 34 | 35 | pulp file content upload --relative-path foo/bar/example.txt \ 36 | --file ./example.txt --repository bar 37 | ``` 38 | 39 | ## Create a Containerfile 40 | 41 | ```bash 42 | echo 'FROM centos:7 43 | 44 | # Copy a file using COPY statement. Use the path specified in the '--relative-path' parameter. 45 | COPY foo/bar/example.txt /inside-image.txt 46 | 47 | # Print the content of the file when the container starts 48 | CMD ["cat", "/inside-image.txt"]' >> Containerfile 49 | ``` 50 | 51 | 52 | ## Build from a Containerfile uploaded during build request 53 | 54 | ```bash 55 | TASK_HREF=$(http --form POST ${BASE_ADDR}${CONTAINER_REPO}'build_image/' "containerfile@./Containerfile" \ 56 | build_context=${FILE_REPO}versions/1/ | jq -r '.task') 57 | ``` 58 | 59 | ## Upload the Containerfile to a File Repository and use it to build 60 | 61 | ### Upload the Containerfile as a File Content 62 | 63 | ```bash 64 | pulp file content upload --relative-path MyContainerfile --file ./Containerfile --repository bar 65 | ``` 66 | 67 | ### Build an OCI image from a Containerfile present in build_context 68 | 69 | ```bash 70 | TASK_HREF=$(http --form POST ${BASE_ADDR}${CONTAINER_REPO}'build_image/' containerfile_name=MyContainerfile \ 71 | build_context=${FILE_REPO}versions/2/ | jq -r '.task') 72 | ``` 73 | 74 | 75 | !!! warning 76 | 77 | File repositories synced with the on-demand policy will not automatically download the missing artifacts. 78 | Trying to build an image using a file that has not yet been downloaded will fail. 79 | -------------------------------------------------------------------------------- /docs/admin/guides/change-allowed-artifacts.md: -------------------------------------------------------------------------------- 1 | # OCI Artifacts Support 2 | 3 | Pulp is not only a container registry, it also supports OCI artifacts by leveraging the config 4 | property on the image manifest. 5 | Here are some examples of compliant OCI artifacts supported by `pulp_container` plugin: 6 | 7 | * [OCI images](site:pulp_container/docs/user/guides/manage-image) 8 | * [Helm](site:pulp_container/docs/user/guides/manage-helm-chart) 9 | * [Flatpak images](site:pulp_container/docs/user/guides/manage-flatpak) 10 | * [Cosign, SBOMs, attestations](site:pulp_container/docs/user/guides/manage-cosign-signature) 11 | * Source containers 12 | * Singularity 13 | * Conftest policies 14 | * WASM 15 | -------------------------------------------------------------------------------- /docs/admin/guides/customize-access-policy.md: -------------------------------------------------------------------------------- 1 | # Customize Access Policies 2 | 3 | The plugin is shipped with default access policies that can be modified to achieve different RBAC 4 | behaviour. Administrators can update creation hooks accordingly: 5 | 6 | ```bash 7 | pulp access-policy update --viewset-name "repositories/container/container" --creation-hooks '[{"function": "add_roles_for_object_creator", "parameters": {"roles": "container.containerrepository_syncer"}}]' 8 | ``` 9 | 10 | !!! note 11 | 12 | Access polices can be reset to their defaults using the `pulp access-policy reset` command. 13 | 14 | !!! note 15 | 16 | Customizing the access policy will cause any future changes to the default policies, like 17 | statement changes and bug fixes, to be ignored unless reset to the default policy. 18 | 19 | Visit [Role-based Access Control](site:pulp_container/docs/admin/learn/rbac) to learn more about 20 | access policies. 21 | -------------------------------------------------------------------------------- /docs/admin/guides/customize-roles.md: -------------------------------------------------------------------------------- 1 | # Customize Roles 2 | 3 | In Pulp, administrators are allowed to create or update roles. To create a role with permissions 4 | required only for syncing content, one can do the following: 5 | 6 | ```bash 7 | pulp role create --name "container.containerrepository_syncer" \ 8 | --permission "container.view_containerrepository" \ 9 | --permission "container.view_containerremote" \ 10 | --permission "container.change_containerrepository" \ 11 | --permission "container.modify_content_containerrepository" \ 12 | --permission "container.sync_containerrepository" 13 | 14 | pulp user role-assignment add --username "alice" --role "container.containerrepository_syncer" --object "" 15 | ``` 16 | 17 | Visit [Role-based Access Control](site:pulp_container/docs/admin/learn/rbac) to learn more about roles. 18 | -------------------------------------------------------------------------------- /docs/admin/guides/import-export.md: -------------------------------------------------------------------------------- 1 | # Export and Import Images 2 | 3 | When maintaining an **air-gapped** environment, one can benefit from using the import/export 4 | machinery. A common workflow usually resembles the following steps: 5 | 6 | 1. **An administrator exports Pulp's content on a system with the internet connectivity.** The 7 | system runs a Pulp instance that syncs content from remote repositories. 8 | 2. **The exported content (tarball) is moved to another (air-gapped) system.** The transfer can 9 | be made through the intranet or via an external hard drive. 10 | 3. **The administrator imports the exported content by initiating an import task.** The 11 | procedure takes care of importing the content to another Pulp instance running in the air-gapped 12 | environment. 13 | 14 | ## Exporting a Repository 15 | 16 | To export a repository, run the following set of commands: 17 | 18 | ```bash 19 | podman pull ghcr.io/pulp/test-fixture-1:manifest_a 20 | 21 | # push a tagged image to the registry 22 | podman login ${REGISTRY_ADDR} -u admin -p password --tls-verify=false 23 | podman tag ghcr.io/pulp/test-fixture-1:manifest_a \ 24 | ${REGISTRY_ADDR}/test/fixture:manifest_a 25 | podman push ${REGISTRY_ADDR}/test/fixture:manifest_a --tls-verify=false 26 | 27 | # a repository of the push type is automatically created 28 | REPOSITORY_HREF=$(pulp container repository -t push show \ 29 | --name "test/fixture" | jq -r ".pulp_href") 30 | 31 | # export the repository to the directory '/tmp/exports/test-fixture' 32 | EXPORTER_HREF=$(http ${BASE_ADDR}/pulp/api/v3/exporters/core/pulp/ \ 33 | name=both repositories:="[\"${REPOSITORY_HREF}\"]" \ 34 | path=/tmp/exports/test-fixture | jq -r ".pulp_href") 35 | ``` 36 | 37 | If the exported content is no longer needed to be managed on the system, delete it: 38 | 39 | ```bash 40 | pulp container distribution destroy --name "test/fixture" 41 | pulp orphan cleanup --protection-time 0 42 | ``` 43 | 44 | ## Importing the Repository 45 | 46 | Import the exported content by running the next commands and monitor the task: 47 | 48 | ```bash 49 | http ${BASE_ADDR}/pulp/api/v3/repositories/container/container/ \ 50 | name="test/fixture" | jq -r ".pulp_href" 51 | 52 | # import the exported repository stored in '/tmp/exports/test-fixture' 53 | IMPORTER_HREF=$(http ${BASE_ADDR}/pulp/api/v3/importers/core/pulp/ \ 54 | name="test/fixture" | jq -r ".pulp_href") 55 | EXPORTED_REPO_PATH=$(find "/tmp/exports/test-fixture" -type f -name \ 56 | "*.tar.gz" | head -n 1) 57 | GROUP_HREF=$(http ${BASE_ADDR}${IMPORTER_HREF}imports/ \ 58 | path=${EXPORTED_REPO_PATH} | jq -r ".task_group") 59 | ``` 60 | 61 | !!! note 62 | 63 | Pass `create_repositories=True` to the `http POST ${BASE_ADDR}${IMPORTER_HREF}imports/` 64 | request to tell Pulp to create missing repositories during the import procedure on the fly. 65 | Otherwise, the repositories need to be created ahead of the import. 66 | 67 | 68 | !!! warning 69 | 70 | Repositories of the push type are automatically converted to sync repositories at import time. 71 | -------------------------------------------------------------------------------- /docs/admin/guides/migrate-permissions.md: -------------------------------------------------------------------------------- 1 | # Migrate from Permissions to Roles 2 | 3 | As of release 2.11.0, the plugin started to support roles instead of separate groups and 4 | permissions. Default permission classes provided by Pulp are **automatically** migrated when 5 | upgrading from older releases. But, custom permissions created before release 2.11.0 require 6 | additional **post-upgrade steps** to preserve the initial behaviour. 7 | 8 | Usually, administrators define permissions for two types of operations: 9 | 10 | 1. **pull** - Pulling content from all or a number of specific repositories 11 | 2. **push** - Pushing content to all or concrete repositories 12 | 13 | During the upgrade, the custom permissions need to be manually revised and assigned. To do so, one 14 | can proceed as follows: 15 | 16 | 1. Make all repositories private: 17 | 18 | ```bash 19 | for name in $(pulp container distribution list | jq -re '.[].name') 20 | do 21 | pulp container distribution update --name $name --private 22 | done 23 | ``` 24 | 25 | 2. Start assigning Pulp-provided/adjusted roles to a particular user. For instance, use the role 26 | `container.containerdistribution_consumer` to enable user `alice` to consume content from 27 | distributions `dist1`, `dist2`, `dist3`: 28 | 29 | ```bash 30 | for distribution in "dist1" "dist2" "dist3" 31 | do 32 | DISTRIBUTION_HREF=$(pulp container distribution show --name $distribution | jq -r ".pulp_href") 33 | pulp user role-assignment add --username "alice" --role "container.containerdistribution_consumer" --object $DISTRIBUTION_HREF 34 | done 35 | ``` 36 | 37 | Similarly, execute an adjusted script for other repository objects that were asserted under 38 | the permissions' scope. 39 | 40 | !!! note 41 | 42 | As of release 2.13.0, administrators should use the `pulpcore-manager dump-permissions` 43 | command to list deprecated permissions not yet translated into roles. 44 | -------------------------------------------------------------------------------- /docs/admin/guides/pull-through-caching.md: -------------------------------------------------------------------------------- 1 | # Configure Pull-Through Caching 2 | 3 | !!! warning 4 | 5 | This feature is provided as a tech preview and could change in backwards incompatible 6 | ways in the future. 7 | 8 | The Pull-Through Caching feature offers an alternative way to host content by leveraging a **remote 9 | registry** as the source of truth. This eliminates the need for in-advance repository 10 | synchronization because Pulp acts as a **caching proxy** and stores images, after they have been 11 | pulled by an end client, in a local repository. 12 | 13 | ## Configuring the caching: 14 | 15 | ``` 16 | # initialize a pull-through remote (the concept of upstream-name is not applicable here) 17 | REMOTE_HREF=$(http ${BASE_ADDR}/pulp/api/v3/remotes/container/pull-through/ name=docker-cache url=https://registry-1.docker.io | jq -r ".pulp_href") 18 | 19 | # create a pull-through distribution linked to the initialized remote 20 | http ${BASE_ADDR}/pulp/api/v3/distributions/container/pull-through/ remote=${REMOTE_HREF} name=docker-cache base_path=docker-cache 21 | ``` 22 | 23 | Pulling content: 24 | 25 | ``` 26 | podman pull localhost:24817/docker-cache/library/busybox 27 | ``` 28 | 29 | In the example above, the image "busybox" is pulled from *DockerHub* through the "docker-cache" 30 | distribution, acting as a transparent caching layer. 31 | 32 | By incorporating the Pull-Through Caching feature into standard workflows, users **do not need** to 33 | pre-configure a new repository and sync it to facilitate the retrieval of the actual content. This 34 | speeds up the whole process of shipping containers from its early management stages to distribution. 35 | Similarly to on-demand syncing, the feature also **reduces external network dependencies**, and 36 | ensures a more reliable container deployment system in production environments. 37 | 38 | !!! note 39 | 40 | During the pull-through operation, Pulp creates a local repository that maintains a single 41 | version for pulled images. For instance, when pulling an image like "debian:10," a local 42 | repository named "debian" with the tag "10" is created. Subsequent pulls, such as "debian:11," 43 | generate a new repository version that incorporates both the "10" and "11" tags, automatically 44 | removing the previous version. Repositories and their content remain manageable through standard 45 | Pulp API endpoints. The repositories are read-only and public by default. 46 | 47 | 48 | ### Filtering the repositories 49 | 50 | It is possible to use the includes/excludes fields to set a list of upstream repositories that Pulp 51 | will be able to pull from. 52 | 53 | ``` 54 | # define a pull-through remote with the includes/excludes fields 55 | REMOTE_HREF=$(http ${BASE_ADDR}/pulp/api/v3/remotes/container/pull-through/ name=docker-cache url=https://registry-1.docker.io includes=["*pulp*"] excludes=["*molecule_debian*"] | jq -r ".pulp_href") 56 | 57 | # create a pull-through distribution linked to the initialized remote 58 | http ${BASE_ADDR}/pulp/api/v3/distributions/container/pull-through/ remote=${REMOTE_HREF} name=docker-cache base_path=docker-cache 59 | ``` 60 | 61 | Pulling allowed content: 62 | 63 | ``` 64 | podman pull localhost:24817/docker-cache/pulp/test-fixture-1:manifest_a 65 | ``` 66 | 67 | Pulling from a repository that includes *molecule_debian* in its name will fail because it is filtered by the *excludes* definition: 68 | ``` 69 | podman pull localhost:24817/docker-cache/pulp/molecule_debian11 70 | Error response from daemon: repository localhost:24817/docker-cache/pulp/molecule_debian11 not found: name unknown: Repository not found. 71 | ``` 72 | 73 | Since only repositories with *pulp* in their names are included (`includes=["*pulp*"]`), the following image pull will also fail: 74 | 75 | ``` 76 | podman pull localhost:24817/docker-cache/library/hello-world 77 | Error response from daemon: repository localhost:24817/docker-cache/library/hello-world not found: name unknown: Repository not found. 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/admin/learn/domain.md: -------------------------------------------------------------------------------- 1 | # Domain support 2 | 3 | Enabling domain support in pulp_container works a bit differently than other plugins due to the 4 | nature of the Registry API. Each domain is scoped as a unique registry by the domain name. This 5 | means that all images will include the domain name as a prefix to its repository path. 6 | 7 | ## Examples 8 | 9 | ### Sync and Pull 10 | 11 | Here's an example of syncing and hosting an image (`pulp/pulp`) in the domain `foo`. 12 | 13 | ```bash 14 | pulp --domain foo container repository create --name pulp 15 | pulp --domain foo container remote create --name quay-pulp --url https://quay.io --upstream-name pulp/pulp 16 | pulp --domain foo container repository sync --name pulp --remote quay-pulp 17 | pulp --domain foo distribution create --name pulp --repository pulp --base-path pulp/pulp 18 | 19 | # 'foo' is added to the repository path 20 | docker pull localhost:24817/foo/pulp/pulp:latest 21 | ``` 22 | 23 | ### Push 24 | 25 | Here's an example of pushing an image (`pulp/pulp`) to the domain `foo`. 26 | 27 | ```bash 28 | docker tag pulp/pulp localhost:24817/foo/pulp/pulp:latest 29 | # This will create a 'pulp/pulp' repository in the domain 'foo' 30 | docker push localhost:24817/foo/pulp/pulp:latest 31 | ``` 32 | 33 | ### RBAC 34 | 35 | With domain support enabled, roles can now be assigned at the domain level. 36 | 37 | ```bash 38 | pulp --domain foo container distribution create --name "bar" --base-path "bar" --private 39 | pulp user create --username "alice" 40 | # This will allow alice to pull all images from the 'foo' domain 41 | pulp user role-assignment add --username "alice" --role "container.containerdistribution_consumer" --domain "foo" 42 | 43 | docker login localhost:24817 -u alice 44 | docker pull localhost:24817/foo/bar:latest 45 | ``` 46 | 47 | !!! note 48 | 49 | Objects are still prohibited from being used across domains even if you have permissions for both. 50 | 51 | !!! note 52 | 53 | The Flatpak endpoints will only work within the default domain, even with domains enabled. 54 | 55 | -------------------------------------------------------------------------------- /docs/admin/learn/tech-preview.md: -------------------------------------------------------------------------------- 1 | # Tech Previews 2 | 3 | The following features are currently being released as part of tech preview: 4 | 5 | - [Building an OCI image](site:pulp_container/docs/admin/guides/build-image) from a Containerfile. 6 | - Support for [hosting Flatpak content](site:pulp_container/docs/user/tutorials/04-manage-flatpak) in OCI format. 7 | - [Pull-through caching](site:pulp_container/docs/admin/guides/pull-through-caching) (i.e., proxy cache) for upstream registries. 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Pulp Container 2 | 3 | The Pulp Container plugin extends Pulp so that you can host your container registry and distribute containers in an on-premises environment. 4 | Pulp is a Container and Artifact Registry that implements [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec/) 5 | and [OCI Image Format Specification](https://github.com/opencontainers/image-spec). 6 | 7 | On top of the standard registry capabilites, Pulp Registry provides additional features. For example, you can synchronize from any Container Registry that is HTTP API V2-compatible. 8 | Depending on your needs, you can perform whole or partial syncs from these remote repositories, blend content from different sources, and distribute them throughout your organization using Pulp. 9 | You can also build OCI-compatible images with Pulp Container and push them to a repository in Pulp so you can distribute private containers. 10 | 11 | For information about why you might think about hosting your own container registry, see [5 reasons to host your container registry with Pulp](https://opensource.com/article/21/5/container-management-pulp/). At the time of this article's publication, there was no native way to perform import and exports to disconnected or air-gapped environments. This has since been introduced and is available. 12 | 13 | If you'd like to watch a recent talk about Pulp Container and see it in action, check out [Registry Native Delivery of Software Content](https://video.fosdem.org/2021/D.infra/registrynativedeliverysoftwarecontentpulp3.mp4). 14 | 15 | ## Features 16 | 17 | - Synchronize container image repositories hosted on Docker-hub, Google Container Registry, 18 | Quay.io, etc., in mirror or additive mode 19 | - Automatically create Versioned Repositories so every operation is a restorable snapshot 20 | - Download content on-demand when requested by clients to reduce disk space 21 | - Perform docker/podman pull from a container distribution served by Pulp 22 | - Perform docker/podman push to the Pulp Registry 23 | - Curate container images by filtering what is mirrored from an external repository 24 | - Curate container images by creating repository versions with a specific set of images 25 | - Build an OCI format image from a Containerfile and make it available from the Pulp Registry 26 | - Host content either locally or on S3 27 | - De-duplicate all saved content 28 | - Support disconnected and air-gapped environments with the Pulp Import/Export facility for container repositories 29 | - Host Flatpak content in OCI format 30 | - Host Helm Charts 31 | - Host OCI artifacts 32 | - Sign images and host Cosign signatures, SBOMS and attestations 33 | - Sign images and host Atomic signature type via extensions API 34 | - Host content via pull-through caching distributions 35 | - Discover and replicate distributions to serve the same content as the upstream Pulp 36 | -------------------------------------------------------------------------------- /docs/user/guides/manage-credentials.md: -------------------------------------------------------------------------------- 1 | # Manage Credentials 2 | 3 | Registry's credentials may be stored in a separate file. At the moment, Pulp does not provide 4 | support for reading from this file. Therefore, a user who wants to synchronize content from 5 | a registry, which requires the authentication, he or she has to manually extract data from this 6 | file and pass it directly to Pulp. 7 | 8 | !!! note 9 | 10 | A file which contains registry's credentials is also called a pull secret. These terms are 11 | considered interchangeable. 12 | 13 | 14 | When using `podman`, the default path for such a file is 15 | `${XDG_RUNTIME_DIR}/containers/auth.json`. The file can have the following content: 16 | 17 | ``` 18 | cat ${XDG_RUNTIME_DIR}/containers/auth.json 19 | { 20 | "auths": { 21 | "registry.hub.docker.com": { 22 | "auth": "YWRtaW46cGFzc3dvcmQ=" 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | The content of the file is usually updated by running `podman login ${REGISTRY}` and providing a 29 | valid username and password for the registry `${REGISTRY}`. 30 | 31 | !!! note 32 | 33 | In some cases, a pull secret is handled by a registry's maintainer and it is not stored locally 34 | by default. If so, it is necessary to download it 35 | (e.g. from ). 36 | 37 | 38 | Suppose a user wants to retrieve credentials from the file shown above in order to sync the content. 39 | First, the user retrieves the field `auth`: 40 | 41 | ``` 42 | export AUTH=$(cat ${XDG_RUNTIME_DIR}/containers/auth.json \ 43 | | jq -r '.auths["registry.hub.docker.com"].auth') 44 | ``` 45 | 46 | Then, he or she fetches the username and password by running: 47 | 48 | ``` 49 | read USERNAME PASSWORD <<< $(echo $AUTH | base64 -d | awk -F':' '{print $1, $2}') 50 | ``` 51 | 52 | And finally, the user creates a new Pulp remote, for example, by executing: 53 | 54 | ``` 55 | http POST http://localhost:24817/pulp/api/v3/remotes/container/container/ \ 56 | name='foo/bar' upstream_name='foo/bar' url='https://registry.hub.docker.com' \ 57 | policy='immediate' username=$USERNAME password=$PASSWORD 58 | ``` 59 | 60 | The remote is used by the sync machinery afterwards. Refer to [Mirror and Host Content](site:pulp_container/docs/user/tutorials/01-sync-and-host) if you missed the syncing part. 61 | -------------------------------------------------------------------------------- /docs/user/guides/manage-flatpak.md: -------------------------------------------------------------------------------- 1 | # Manage Flatpak Images 2 | 3 | Pulp can host Flatpak application and runtime images that are distributed in OCI format. To make 4 | such content discoverable, it can provide `/index/dynamic` and `/index/static` endpoints as 5 | specified by [the Flatpak registry index protocol](https://github.com/flatpak/flatpak-oci-specs/blob/main/registry-index.md). This is not enabled 6 | by default. To enable it, define `FLATPAK_INDEX=True` in the settings file. 7 | 8 | Clients like the `flatpak` command-line tool or the GNOME Software application will typically 9 | query the `/index/static` endpoint, which is intended to be called repeatedly with identical query 10 | parameters, and whose responses are meant to be cached. The `/index/dynamic` endpoint serves 11 | exactly the same content, but is intended for one-off requests that should not be cached. These 12 | endpoints can be accessed without authentication. They only provide information about public 13 | repositories. 14 | 15 | The two endpoints support a number of query parameters (`architecture`, `tag`, `label`, etc.), 16 | see the protocol specification for details. Two notes: 17 | 18 | - Every request must include a `label:org.flatpak.ref:exists=1` query parameter. This acts as a 19 | marker to only report Flatpak content, and to exclude other container content that may also be 20 | provided by the Pulp instance. 21 | - This implementation does not support annotations. Including any `annotation` query parameters 22 | will result in a 400 failure response. Use `label` query parameters instead. (Existing clients 23 | like the `flatpak` command-line tool never issue requests including any `annotation` query 24 | parameters.) 25 | 26 | ## Install a Flatpak image from Pulp 27 | 28 | This section assumes that you have created at least one public distribution in your Pulp instance 29 | that serves a repository containing Flatpak content. To do this, see the general {doc}`host` 30 | documentation. 31 | 32 | You can for example use the `flatpak` [command-line tool](https://docs.flatpak.org/en/latest/using-flatpak.html#the-flatpak-command) to set up a Flatpak 33 | remote (named `pulp` here) that references your Pulp instance: 34 | 35 | ```bash 36 | flatpak remote-add pulp oci+"$BASE_ADDR" 37 | ``` 38 | 39 | Then, use 40 | 41 | ```bash 42 | flatpak remote-ls pulp 43 | ``` 44 | 45 | to retrieve a list of all Flatpak applications and runtimes that your Pulp instance serves. (This 46 | queries the `/index/static` endpoint, as explained above.) Finally, if your Pulp instance serves 47 | e.g. the `org.gnome.gedit` application, use 48 | 49 | ```bash 50 | flatpak install pulp org.gnome.gedit 51 | ``` 52 | 53 | to install it and run it with 54 | 55 | ```bash 56 | flatpak run org.gnome.gedit 57 | ``` 58 | 59 | !!! warning 60 | 61 | This functionality is shipped as part of the tech preview offering. Role-based access control 62 | and listing of Flatpak images from within the API may be a subject to change. 63 | -------------------------------------------------------------------------------- /docs/user/guides/manage-helm-chart.md: -------------------------------------------------------------------------------- 1 | # Manage Helm Charts 2 | 3 | Helm charts are packages for Kubernetes applications, bundling YAML files defining resources like 4 | deployments and services. To package them as container images, developers use tools like Helm to 5 | embed the application code and dependencies into a containerized environment. Container registries 6 | provide platforms for storing and sharing these containerized Helm charts, simplifying deployment 7 | across Kubernetes clusters. 8 | 9 | ## Push and Host 10 | 11 | Use the following **example** to download and push an etherpad chart from the Red Hat community repository. 12 | 13 | Add a chart repository: 14 | 15 | ``` 16 | helm repo add redhat-cop https://redhat-cop.github.io/helm-charts 17 | ``` 18 | 19 | Update the information of available charts locally from the chart repository: 20 | 21 | ``` 22 | helm repo update 23 | ``` 24 | 25 | Download a chart from a repository: 26 | 27 | ``` 28 | helm pull redhat-cop/etherpad --version=0.0.4 --untar 29 | ``` 30 | 31 | Package the chart into a chart archive: 32 | 33 | ``` 34 | helm package ./etherpad 35 | ``` 36 | 37 | Log in to your Pulp container registry using helm registry login: 38 | 39 | ``` 40 | helm registry login pulp3-source-fedora36.puffy.example.com 41 | ``` 42 | 43 | Push the chart to your Pulp Container registry using the helm push command: 44 | 45 | === "Script" 46 | 47 | ``` 48 | helm push etherpad-0.0.4.tgz oci://pulp3-source-fedora36.puffy.example.com 49 | ``` 50 | 51 | === "Output" 52 | 53 | ``` 54 | Pushed: pulp3-source-fedora36.puffy.example.com/etherpad:0.0.4 55 | Digest: sha256:a6667ff2a0e2bd7aa4813db9ac854b5124ff1c458d170b70c2d2375325f2451b 56 | ``` 57 | 58 | Ensure that the push worked by deleting the local copy, and then pulling the chart from the repository: 59 | 60 | === "Script" 61 | 62 | ``` 63 | rm -rf etherpad-0.0.4.tgz 64 | 65 | helm pull oci://pulp3-source-fedora36.puffy.example.com/etherpad --version 0.0.4 66 | ``` 67 | 68 | === "Script" 69 | 70 | ``` 71 | Pulled: pulp3-source-fedora36.puffy.example.com/etherpad:0.0.4 72 | Digest: sha256:4f627399685880daf30cf77b6026dc129034d68c7676c7e07020b70cf7130902 73 | ``` 74 | 75 | The chart can then be installed using the helm install command: 76 | 77 | ``` 78 | helm install etherpad-0.0.4.tgz 79 | ``` 80 | 81 | Alternatively, charts can be installed directly from the registry without needing to download locally. 82 | Use the helm install command and reference the registry location: 83 | 84 | ``` 85 | helm install oci://pulp3-source-fedora36.puffy.example.com/helm/etherpad --version=0.0.4 86 | ``` 87 | 88 | ## Mirror 89 | 90 | Being an OCI compliant registry, Pulp Container registry can natively mirror helm charts 91 | that are stored as an OCI image: 92 | 93 | ``` 94 | { 95 | "schemaVersion": 2, 96 | "config": { 97 | "mediaType": "application/vnd.cncf.helm.config.v1+json", 98 | "digest": "sha256:8ec7c0f2f6860037c19b54c3cfbab48d9b4b21b485a93d87b64690fdb68c2111", 99 | "size": 117 100 | }, 101 | "layers": [ 102 | { 103 | "mediaType": "application/vnd.cncf.helm.chart.content.v1.tar+gzip", 104 | "digest": "sha256:1b251d38cfe948dfc0a5745b7af5ca574ecb61e52aed10b19039db39af6e1617", 105 | "size": 2487 106 | }, 107 | { 108 | "mediaType": "application/vnd.cncf.helm.chart.provenance.v1.prov", 109 | "digest": "sha256:3e207b409db364b595ba862cdc12be96dcdad8e36c59a03b7b3b61c946a5741a", 110 | "size": 643 111 | } 112 | ] 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/user/guides/push-image.md: -------------------------------------------------------------------------------- 1 | # Push Images 2 | 3 | Users can push images (manifests and manifest lists) to repositories hosted by the Container 4 | Registry. It is possible to push images that container foreign (non-distributable) layers. Only the 5 | users who are logged in to the registry are allowed to perform push operation. Find below a complete 6 | example of pushing a tagged image. 7 | 8 | !!! note 9 | 10 | Having disabled the token authentication, only users with superuser privileges (i.e., 11 | administrators) are allowed to push content to the registry. 12 | 13 | The registry supports cross repository blob mounting. When uploading blobs that already exist in 14 | the registry as a part of a different repository, the content is not being uploaded but rather 15 | referenced from another repository to reduce network traffic. 16 | 17 | ``` 18 | podman tag d21d863f69b5 localhost:24817/test/this:mytag1.8 19 | podman login -u user -p password localhost:24817 20 | podman push d21d863f69b5 localhost:24817/test/this:mytag1.8 21 | ``` 22 | 23 | ``` 24 | http GET $BASE_ADDR/v2/test/this/tags/list 25 | ``` 26 | 27 | ``` 28 | HTTP/1.1 200 OK 29 | Allow: GET, HEAD, OPTIONS 30 | Connection: close 31 | Content-Length: 40 32 | Content-Type: application/json 33 | Date: Wed, 03 Jun 2020 18:25:46 GMT 34 | Docker-Distribution-API-Version: registry/2.0 35 | Server: gunicorn/20.0.4 36 | Vary: Accept 37 | X-Frame-Options: SAMEORIGIN 38 | 39 | { 40 | "name": "test/this", 41 | "tags": [ 42 | "mytag1.8" 43 | ] 44 | } 45 | ``` 46 | 47 | !!! note 48 | 49 | Content is pushed to a push repository type. A push repository does not support mirroring of the 50 | remote content via the Pulp API. Trying to push content with the same name as an existing 51 | "regular" repository will fail. 52 | 53 | !!! note 54 | 55 | Rollback to the previous repository versions is not possible with a push repository. Its latest version will always be served. 56 | 57 | !!! warning 58 | 59 | Image that has been pulled from a registry and then subsequently pushed to another registy can lead to the blobs digest change. 60 | Most image layers on registries are compressed. Pull operation decompresses them to get an uncompressed stream, and extracts it 61 | to create the local filesystem. Push creates an uncompressed tarball from the local filesystem and recompresses it during upload. 62 | The recompression is not at all guaranteed to be reproducible, it is client implementation dependent — push with different 63 | compression implementation than the original author used is more likely to result in a different blob digest. 64 | -------------------------------------------------------------------------------- /docs/user/guides/registry-catalog.md: -------------------------------------------------------------------------------- 1 | # Registry catalog 2 | 3 | A registry may contain several repositories which hold collections of multiple images. Each 4 | repository is identified by its unique name. The list of names of all distributed repositories 5 | is made available through the ``_catalog`` endpoint. 6 | 7 | For instance, let's assume that a new distribution of a repository with the name ``library/zoo`` 8 | was recently created. Its name is now possible to fetch from the list of names of distributed 9 | repositories. 10 | 11 | ```bash 12 | $ http https://puffy.example.com/v2/_catalog 13 | HTTP/1.1 200 OK 14 | Access-Control-Expose-Headers: Correlation-ID 15 | Allow: GET, HEAD, OPTIONS 16 | Connection: keep-alive 17 | Content-Length: 63 18 | Content-Type: application/json 19 | Correlation-ID: 1de33d4807a244f1b00c10df3fdc7a1b 20 | Cross-Origin-Opener-Policy: same-origin 21 | Date: Fri, 17 May 2024 10:20:25 GMT 22 | Docker-Distribution-Api-Version: registry/2.0 23 | Referrer-Policy: same-origin 24 | Server: nginx 25 | Strict-Transport-Security: max-age=15768000 26 | Vary: Accept 27 | X-Content-Type-Options: nosniff 28 | X-Frame-Options: DENY 29 | X-Registry-Supports-Signatures: 1 30 | 31 | { 32 | "repositories": [ 33 | "library/zoo", 34 | "alice/azure", 35 | ] 36 | } 37 | ``` 38 | !!! note 39 | For the sake of simplicity of this example, there is missing required user token authentication 40 | to this endpoint. Users will see only those repositories in the registry catalog that they have access to. 41 | Visit [Token authentication section](site:pulp_container/docs/admin/learn/authentication.md) to learn more. 42 | -------------------------------------------------------------------------------- /functest_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest<8 2 | python-gnupg 3 | pytest-xdist 4 | pytest-timeout 5 | pytest-custom_exit_code 6 | trustme~=1.2.1 -------------------------------------------------------------------------------- /lint_requirements.txt: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT EDIT! 2 | # 3 | # This file was generated by plugin_template, and is managed by it. Please use 4 | # './plugin-template --github pulp_container' to update this file. 5 | # 6 | # For more info visit https://github.com/pulp/plugin_template 7 | 8 | black==24.3.0 9 | bump-my-version 10 | check-manifest 11 | flake8 12 | flake8-black 13 | packaging 14 | yamllint 15 | -------------------------------------------------------------------------------- /pulp_container/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "pulp_container.app.PulpContainerPluginAppConfig" 2 | -------------------------------------------------------------------------------- /pulp_container/app/__init__.py: -------------------------------------------------------------------------------- 1 | from pulpcore.plugin import PulpPluginAppConfig 2 | 3 | 4 | class PulpContainerPluginAppConfig(PulpPluginAppConfig): 5 | """Entry point for the container plugin.""" 6 | 7 | name = "pulp_container.app" 8 | label = "container" 9 | version = "2.26.0.dev" 10 | python_package_name = "pulp-container" 11 | domain_compatible = True 12 | 13 | def ready(self): 14 | super().ready() 15 | from . import checks 16 | -------------------------------------------------------------------------------- /pulp_container/app/access_policy.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from rest_access_policy import AccessPolicy 4 | 5 | from pulpcore.plugin.models import AccessPolicy as AccessPolicyModel 6 | 7 | from pulp_container.app import models 8 | 9 | _logger = getLogger(__name__) 10 | 11 | 12 | class RegistryAccessPolicy(AccessPolicy): 13 | """ 14 | An AccessPolicy that loads statements from the ContainerDistribution, ContainerNamespace, 15 | and ContainerPushRepository viewsets. 16 | """ 17 | 18 | def get_policy_statements(self, request, view): 19 | """ 20 | Return the policy statements for the container distribution and namespace viewsets. 21 | 22 | Args: 23 | request (rest_framework.request.Request): The request being checked for authorization. 24 | view (subclass rest_framework.viewsets.GenericViewSet): The view name being requested. 25 | 26 | Returns: 27 | The access policy statements in drf-access-policy policy structure. 28 | 29 | """ 30 | if isinstance(view.get_object(), models.ContainerDistribution): 31 | access_policy_obj = AccessPolicyModel.objects.get( 32 | viewset_name="distributions/container/container" 33 | ) 34 | elif isinstance(view.get_object(), models.ContainerPullThroughDistribution): 35 | access_policy_obj = AccessPolicyModel.objects.get( 36 | viewset_name="distributions/container/pull-through" 37 | ) 38 | else: 39 | access_policy_obj = AccessPolicyModel.objects.get( 40 | viewset_name="pulp_container/namespaces" 41 | ) 42 | return access_policy_obj.statements 43 | -------------------------------------------------------------------------------- /pulp_container/app/checks.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.checks import Error as CheckError, register 3 | 4 | 5 | @register(deploy=True) 6 | def container_settings_check(app_configs, **kwargs): 7 | errors = [] 8 | 9 | # Other checks only apply if token auth is enabled 10 | if str(getattr(settings, "TOKEN_AUTH_DISABLED", False)).lower() == "true": 11 | return errors 12 | 13 | if getattr(settings, "TOKEN_SERVER", None) is None: 14 | errors.append( 15 | CheckError( 16 | "TOKEN_SERVER is a required setting that has to be configured when token" 17 | " authentification is enabled", 18 | id="pulp_container.E001", 19 | ), 20 | ) 21 | if getattr(settings, "TOKEN_SIGNATURE_ALGORITHM", None) is None: 22 | errors.append( 23 | CheckError( 24 | "TOKEN_SIGNATURE_ALGORITHM is a required setting that has to be configured when" 25 | " token authentification is enabled", 26 | id="pulp_container.E001", 27 | ) 28 | ) 29 | if getattr(settings, "PUBLIC_KEY_PATH", None) is None: 30 | errors.append( 31 | CheckError( 32 | "PUBLIC_KEY_PATH is a required setting that has to be configured when token" 33 | " authentification is enabled", 34 | id="pulp_container.E001", 35 | ) 36 | ) 37 | if getattr(settings, "PRIVATE_KEY_PATH", None) is None: 38 | errors.append( 39 | CheckError( 40 | "PRIVATE_KEY_PATH is a required setting that has to be configured when token" 41 | " authentification is enabled", 42 | id="pulp_container.E001", 43 | ) 44 | ) 45 | 46 | return errors 47 | -------------------------------------------------------------------------------- /pulp_container/app/content.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | from django.conf import settings 3 | 4 | from pulpcore.plugin.content import app 5 | from pulp_container.app.registry import Registry 6 | 7 | registry = Registry() 8 | 9 | PREFIX = "/pulp/container/{pulp_domain}/" if settings.DOMAIN_ENABLED else "/pulp/container/" 10 | 11 | app.add_routes( 12 | [ 13 | web.get( 14 | PREFIX + r"{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}", 15 | registry.get_by_digest, 16 | ) 17 | ] 18 | ) 19 | app.add_routes([web.get(PREFIX + r"{path:.+}/manifests/{tag_name}", registry.get_tag)]) 20 | -------------------------------------------------------------------------------- /pulp_container/app/fields.py: -------------------------------------------------------------------------------- 1 | from drf_spectacular.utils import extend_schema_field 2 | from drf_spectacular.types import OpenApiTypes 3 | from rest_framework import serializers 4 | 5 | 6 | @extend_schema_field(OpenApiTypes.OBJECT) 7 | class JSONObjectField(serializers.JSONField): 8 | """A drf JSONField override to force openapi schema to use 'object' type. 9 | 10 | Not strictly correct, but we relied on that for a long time. 11 | See: https://github.com/tfranzel/drf-spectacular/issues/1095 12 | """ 13 | -------------------------------------------------------------------------------- /pulp_container/app/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/app/management/__init__.py -------------------------------------------------------------------------------- /pulp_container/app/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/app/management/commands/__init__.py -------------------------------------------------------------------------------- /pulp_container/app/management/commands/container-repair-media-type.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from gettext import gettext as _ 4 | 5 | from django.conf import settings 6 | from django.core.management import BaseCommand 7 | 8 | from pulpcore.plugin.cache import SyncContentCache 9 | 10 | from pulp_container.app.models import Manifest, ContainerDistribution 11 | from pulp_container.app.utils import determine_media_type_from_json 12 | 13 | from pulp_container.constants import MEDIA_TYPE 14 | 15 | 16 | class Command(BaseCommand): 17 | """ 18 | A django management command to repair the media types of manifests. 19 | 20 | Older versions of pulp_container could sometimes assign an invalid media type to a manifest. 21 | If the media type could not be extracted from the Content-Type header, the sync pipeline 22 | assumed that the media type is "application/vnd.docker.distribution.manifest.v1+json". The 23 | repair command iterates over synced manifests and updates their media types based on the 24 | internals of the associated manifest.json files if needed. 25 | 26 | This command also deletes the cache for all distributions across the plugin. 27 | """ 28 | 29 | help = _(__doc__) 30 | 31 | def handle(self, *args, **options): 32 | """Run the management command.""" 33 | manifests_schema_v1 = Manifest.objects.filter( 34 | media_type=MEDIA_TYPE.MANIFEST_V1 35 | ).prefetch_related("_artifacts") 36 | 37 | manifests_to_update = [] 38 | for manifest in manifests_schema_v1: 39 | artifact_file = manifest._artifacts.first().file 40 | json_data = json.load(artifact_file) 41 | artifact_file.close() 42 | 43 | media_type = determine_media_type_from_json(json_data) 44 | if media_type != MEDIA_TYPE.MANIFEST_V1: 45 | manifest.media_type = media_type 46 | manifests_to_update.append(manifest) 47 | 48 | if manifests_to_update: 49 | Manifest.objects.bulk_update(manifests_to_update, ["media_type"], batch_size=100) 50 | 51 | manifests_schema_v1_signed = Manifest.objects.filter( 52 | media_type=MEDIA_TYPE.MANIFEST_V1_SIGNED 53 | ) 54 | manifests_schema_v1_signed.update(media_type=MEDIA_TYPE.MANIFEST_V1) 55 | self.stdout.write( 56 | self.style.SUCCESS( 57 | "Successfully repaired %d manifests." 58 | % (len(manifests_to_update) + len(manifests_schema_v1_signed)) 59 | ) 60 | ) 61 | 62 | if settings.CACHE_ENABLED: 63 | base_paths = ContainerDistribution.objects.values_list("base_path", flat=True) 64 | if base_paths: 65 | SyncContentCache().delete(base_key=base_paths) 66 | 67 | self.stdout.write(self.style.SUCCESS("Successfully flushed the cache.")) 68 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0002_containerrepository.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ContainerRepository', 16 | fields=[ 17 | ('repository_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_containerrepository', serialize=False, to='core.repository')), 18 | ], 19 | options={ 20 | 'default_related_name': '%(app_label)s_%(model_name)s', 21 | }, 22 | bases=('core.repository',), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0003_oci_mediatype.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-11-29 22:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0002_containerrepository'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='blob', 15 | name='media_type', 16 | field=models.CharField(choices=[('application/vnd.docker.container.image.v1+json', 'application/vnd.docker.container.image.v1+json'), ('application/vnd.docker.image.rootfs.diff.tar.gzip', 'application/vnd.docker.image.rootfs.diff.tar.gzip'), ('application/vnd.docker.image.rootfs.foreign.diff.tar.gzip', 'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip'), ('application/vnd.oci.image.config.v1+json', 'application/vnd.oci.image.config.v1+json'), ('application/vnd.oci.image.layer.v1.tar+gzip', 'application/vnd.oci.image.layer.v1.tar+gzip'), ('application/vnd.oci.image.layer.nondistributable.v1.tar+gzip', 'application/vnd.oci.image.layer.nondistributable.v1.tar+gzip')], max_length=80), 17 | ), 18 | migrations.AlterField( 19 | model_name='manifest', 20 | name='media_type', 21 | field=models.CharField(choices=[('application/vnd.docker.distribution.manifest.v1+json', 'application/vnd.docker.distribution.manifest.v1+json'), ('application/vnd.docker.distribution.manifest.v2+json', 'application/vnd.docker.distribution.manifest.v2+json'), ('application/vnd.docker.distribution.manifest.list.v2+json', 'application/vnd.docker.distribution.manifest.list.v2+json'), ('application/vnd.oci.image.manifest.v1+json', 'application/vnd.oci.image.manifest.v1+json'), ('application/vnd.oci.image.index.v1+json', 'application/vnd.oci.image.index.v1+json')], max_length=60), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0004_upload.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | import django.core.files.storage 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import pulp_container.app.models 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('container', '0003_oci_mediatype'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Upload', 19 | fields=[ 20 | ('pulp_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 21 | ('pulp_created', models.DateTimeField(auto_now_add=True)), 22 | ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), 23 | ('offset', models.BigIntegerField(default=0)), 24 | ('file', models.FileField(max_length=255, null=True, storage=django.core.files.storage.FileSystemStorage(location='/var/lib/pulp/upload/'), upload_to=pulp_container.app.models.generate_filename)), 25 | ('size', models.IntegerField(null=True)), 26 | ('md5', models.CharField(max_length=32, null=True)), 27 | ('sha1', models.CharField(max_length=40, null=True)), 28 | ('sha224', models.CharField(max_length=56, null=True)), 29 | ('sha256', models.CharField(max_length=64, null=True)), 30 | ('sha384', models.CharField(max_length=96, null=True)), 31 | ('sha512', models.CharField(max_length=128, null=True)), 32 | ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='core.repository')), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0005_contentredirectcontentguard.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | import os 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | def _gen_secret(): 10 | return os.urandom(32) 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ('container', '0004_upload'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='ContentRedirectContentGuard', 22 | fields=[ 23 | ('contentguard_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_contentredirectcontentguard', serialize=False, to='core.contentguard')), 24 | ('shared_secret', models.BinaryField(default=_gen_secret, max_length=32)), 25 | ], 26 | options={ 27 | 'default_related_name': '%(app_label)s_%(model_name)s', 28 | }, 29 | bases=('core.contentguard',), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0006_containerpushrepository.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0005_contentredirectcontentguard'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ContainerPushRepository', 16 | fields=[ 17 | ('repository_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_containerpushrepository', serialize=False, to='core.repository')), 18 | ], 19 | options={ 20 | 'default_related_name': '%(app_label)s_%(model_name)s', 21 | }, 22 | bases=('core.repository',), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0007_clear_tags_artifacts_refs.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.14 on 2020-07-17 12:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | def remove_tag_artifacts_up(apps, schema_editor): 7 | Tag = apps.get_model('container', 'Tag') 8 | for tag in Tag.objects.all(): 9 | tag._artifacts.clear() 10 | 11 | 12 | def remove_tag_artifacts_down(apps, schema_editor): 13 | Tag = apps.get_model('container', 'Tag') 14 | for tag in Tag.objects.all(): 15 | if tag.tagged_manifest: 16 | tag._artifacts.add(tag.tagged_manifest._artifacts.get()) 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [ 22 | ('container', '0006_containerpushrepository'), 23 | ] 24 | 25 | operations = [ 26 | migrations.RunPython(remove_tag_artifacts_up, remove_tag_artifacts_down, elidable=True) 27 | ] 28 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0008_include_exclude_tags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.14 on 2020-08-10 06:39 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0007_clear_tags_artifacts_refs'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameField( 15 | model_name='containerremote', 16 | old_name='whitelist_tags', 17 | new_name='include_tags', 18 | ), 19 | migrations.AddField( 20 | model_name='containerremote', 21 | name='exclude_tags', 22 | field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255, null=True), null=True, size=None), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0009_container_namespace.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-04 12:37 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import django_lifecycle.mixins 6 | import uuid 7 | 8 | 9 | def initialize_namespaces(apps, schema_editor): 10 | """ 11 | Look for all ContainerDistributions, and assign a namespace matching their base_path. 12 | """ 13 | ContainerNamespace = apps.get_model('container', 'ContainerNamespace') 14 | ContainerDistribution = apps.get_model('container', 'ContainerDistribution') 15 | for distribution in ContainerDistribution.objects.all(): 16 | namespace_name = distribution.base_path.split('/')[0] 17 | distribution.namespace = ContainerNamespace.objects.get_or_create(name=namespace_name)[0] 18 | distribution.save() 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [ 24 | ('container', '0008_include_exclude_tags'), 25 | ] 26 | 27 | operations = [ 28 | migrations.CreateModel( 29 | name='ContainerNamespace', 30 | fields=[ 31 | ('pulp_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 32 | ('pulp_created', models.DateTimeField(auto_now_add=True)), 33 | ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), 34 | ('name', models.CharField(db_index=True, max_length=255)), 35 | ], 36 | options={ 37 | 'unique_together': {('name',)}, 38 | }, 39 | bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), 40 | ), 41 | migrations.AddField( 42 | model_name='containerdistribution', 43 | name='namespace', 44 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='container_distributions', to='container.ContainerNamespace'), 45 | ), 46 | # Reverting that step is simply removing the new relation and table. 47 | migrations.RunPython( 48 | initialize_namespaces, reverse_code=migrations.RunPython.noop, elidable=True 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0010_remove_uploadchunk.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0009_container_namespace'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='upload', 16 | name='file', 17 | ), 18 | migrations.RemoveField( 19 | model_name='upload', 20 | name='md5', 21 | ), 22 | migrations.RemoveField( 23 | model_name='upload', 24 | name='offset', 25 | ), 26 | migrations.RemoveField( 27 | model_name='upload', 28 | name='pulp_created', 29 | ), 30 | migrations.RemoveField( 31 | model_name='upload', 32 | name='pulp_id', 33 | ), 34 | migrations.RemoveField( 35 | model_name='upload', 36 | name='pulp_last_updated', 37 | ), 38 | migrations.RemoveField( 39 | model_name='upload', 40 | name='sha1', 41 | ), 42 | migrations.RemoveField( 43 | model_name='upload', 44 | name='sha224', 45 | ), 46 | migrations.RemoveField( 47 | model_name='upload', 48 | name='sha256', 49 | ), 50 | migrations.RemoveField( 51 | model_name='upload', 52 | name='sha384', 53 | ), 54 | migrations.RemoveField( 55 | model_name='upload', 56 | name='sha512', 57 | ), 58 | migrations.RemoveField( 59 | model_name='upload', 60 | name='size', 61 | ), 62 | migrations.AddField( 63 | model_name='upload', 64 | name='upload_ptr', 65 | field=models.OneToOneField(auto_created=True, default=None, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.upload'), 66 | preserve_default=False, 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0011_add_container_repository_permissions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-14 12:11 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0010_remove_uploadchunk'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containerrepository', 15 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('sync_containerrepository', 'Can start a sync task'), ('modify_content_containerrepository', 'Can modify content in a repository'), ('build_image_containerrepository', 'Can use the image builder in a repository')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0012_add_container_namespace_permissions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-22 16:43 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0011_add_container_repository_permissions'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containernamespace', 15 | options={'permissions': [('manage_namespace_distributions', 'Can manage distributions in a namespace')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0013_add_pull_push_permissions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-20 17:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0012_add_container_namespace_permissions'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containerdistribution', 15 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('pull_containerdistribution', 'Can pull from a registry repo'), ('push_containerdistribution', 'Can push into the registry repo')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0014_containerdistribution_private.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-18 10:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0013_add_pull_push_permissions'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='containerdistribution', 15 | name='private', 16 | field=models.BooleanField(default=False, help_text='Restrict pull access to explicitly authorized users. Defaults to unrestricted pull access.'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0015_manage_tags_push_repo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-28 19:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0014_containerdistribution_private'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containerpushrepository', 15 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('modify_content_containerpushrepository', 'Can modify content in a push repository')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0016_add_delete_versions_permission.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-28 19:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0015_manage_tags_push_repo'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containerrepository', 15 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('sync_containerrepository', 'Can start a sync task'), ('modify_content_containerrepository', 'Can modify content in a repository'), ('build_image_containerrepository', 'Can use the image builder in a repository'), ('delete_containerrepository_versions', 'Can delete repository versions')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0017_add_granular_perms.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-02-04 02:18 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0016_add_delete_versions_permission'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containernamespace', 15 | options={'permissions': [('namespace_add_containerdistribution', 'Add any distribution to a namespace'), ('namespace_delete_containerdistribution', 'Delete any distribution from a namespace'), ('namespace_view_containerdistribution', 'View any distribution in a namespace'), ('namespace_pull_containerdistribution', 'Pull from any distribution in a namespace'), ('namespace_push_containerdistribution', 'Push to any distribution in a namespace'), ('namespace_change_containerdistribution', 'Change any distribution in a namespace'), ('namespace_view_containerpushrepository', 'View any push repository in a namespace'), ('namespace_modify_content_containerpushrepository', 'Modify content in any push repository in a namespace')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0018_containerdistribution_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-02-04 14:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0017_add_granular_perms'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='containerdistribution', 15 | name='description', 16 | field=models.TextField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0021_data_move_redirect_content_guard_to_core.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | from django.db import migrations, transaction 4 | 5 | 6 | def move_content_guards_up(apps, schema_editor, up=True): 7 | SourceContentRedirectContentGuard = apps.get_model("container", "ContentRedirectContentGuard") 8 | DestContentRedirectContentGuard = apps.get_model("core", "ContentRedirectContentGuard") 9 | 10 | if up: 11 | dest_pulp_type = "core.content_redirect" 12 | else: 13 | dest_pulp_type = "container.content_redirect" 14 | SourceContentRedirectContentGuard, DestContentRedirectContentGuard = DestContentRedirectContentGuard, SourceContentRedirectContentGuard 15 | 16 | for content_guard in SourceContentRedirectContentGuard.objects.all(): 17 | with transaction.atomic(): 18 | new_content_guard = DestContentRedirectContentGuard( 19 | pulp_id=content_guard.pulp_id, 20 | pulp_created=content_guard.pulp_created, 21 | pulp_last_updated=content_guard.pulp_last_updated, 22 | pulp_type=dest_pulp_type, 23 | name=content_guard.name, 24 | description=content_guard.description, 25 | shared_secret=content_guard.shared_secret, 26 | ) 27 | distributions = list(content_guard.distribution_set.all()) 28 | content_guard.delete() 29 | new_content_guard.save() 30 | new_content_guard.distribution_set.set(distributions) 31 | 32 | 33 | def move_content_guards_down(apps, schema_editor): 34 | move_content_guards_up(apps, schema_editor, up=False) 35 | 36 | 37 | class Migration(migrations.Migration): 38 | 39 | dependencies = [ 40 | ('container', '0020_update_push_repo_perms'), 41 | ] 42 | 43 | operations = [ 44 | migrations.RunPython(code=move_content_guards_up, reverse_code=move_content_guards_down, elidable=True), 45 | ] 46 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0022_delete_contentredirectcontentguard.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-01-14 20:23 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0021_data_move_redirect_content_guard_to_core'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='ContentRedirectContentGuard', 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0023_manifestsignature.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-29 13:44 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0022_delete_contentredirectcontentguard'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ManifestSignature', 16 | fields=[ 17 | ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_manifestsignature', serialize=False, to='core.content')), 18 | ('name', models.CharField(db_index=True, max_length=255)), 19 | ('digest', models.CharField(max_length=255)), 20 | ('type', models.CharField(choices=[('atomic', 'atomic')], max_length=255)), 21 | ('key_id', models.CharField(db_index=True, max_length=255)), 22 | ('timestamp', models.PositiveIntegerField()), 23 | ('creator', models.CharField(blank=True, max_length=255)), 24 | ('data', models.BinaryField()), 25 | ('signed_manifest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signed_manifests', to='container.manifest')), 26 | ], 27 | options={ 28 | 'default_related_name': '%(app_label)s_%(model_name)s', 29 | 'unique_together': {('digest',)}, 30 | }, 31 | bases=('core.content',), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0024_containerremote_sigstore.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-29 21:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0023_manifestsignature'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='containerremote', 15 | name='sigstore', 16 | field=models.TextField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0025_signature_stored_in_textfield.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.11 on 2022-01-10 11:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0024_containerremote_sigstore'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='manifestsignature', 15 | name='data', 16 | field=models.TextField(), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0026_manifest_signing_service.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-26 15:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0025_signature_stored_in_textfield'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ManifestSigningService', 16 | fields=[ 17 | ('signingservice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.signingservice')), 18 | ], 19 | options={ 20 | 'abstract': False, 21 | }, 22 | bases=('core.signingservice',), 23 | ), 24 | migrations.AddField( 25 | model_name='containerpushrepository', 26 | name='manifest_signing_service', 27 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='container_containerpushrepository', to='container.manifestsigningservice'), 28 | ), 29 | migrations.AddField( 30 | model_name='containerrepository', 31 | name='manifest_signing_service', 32 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='container_containerrepository', to='container.manifestsigningservice'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0028_add_role_manage_permissions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.11 on 2022-02-03 11:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0027_data_translate_perms_to_roles'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containerdistribution', 15 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('pull_containerdistribution', 'Can pull from a registry repo'), ('push_containerdistribution', 'Can push into the registry repo'), ('manage_roles_containerdistribution', 'Can manage role assignments on container distribution')]}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='containernamespace', 19 | options={'permissions': [('namespace_add_containerdistribution', 'Add any distribution to a namespace'), ('namespace_delete_containerdistribution', 'Delete any distribution from a namespace'), ('namespace_view_containerdistribution', 'View any distribution in a namespace'), ('namespace_pull_containerdistribution', 'Pull from any distribution in a namespace'), ('namespace_push_containerdistribution', 'Push to any distribution in a namespace'), ('namespace_change_containerdistribution', 'Change any distribution in a namespace'), ('namespace_view_containerpushrepository', 'View any push repository in a namespace'), ('namespace_modify_content_containerpushrepository', 'Modify content in any push repository in a namespace'), ('namespace_change_containerpushrepository', 'Update any existing push repository in a namespace'), ('manage_roles_containernamespace', 'Can manage role assignments on container namespace')]}, 20 | ), 21 | migrations.AlterModelOptions( 22 | name='containerpushrepository', 23 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('modify_content_containerpushrepository', 'Can modify content in a push repository'), ('manage_roles_containerpushrepository', 'Can manage role assignments on container pushrepository')]}, 24 | ), 25 | migrations.AlterModelOptions( 26 | name='containerremote', 27 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_containerremote', 'Can manage role assignments on container remote')]}, 28 | ), 29 | migrations.AlterModelOptions( 30 | name='containerrepository', 31 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('sync_containerrepository', 'Can start a sync task'), ('modify_content_containerrepository', 'Can modify content in a repository'), ('build_image_containerrepository', 'Can use the image builder in a repository'), ('delete_containerrepository_versions', 'Can delete repository versions'), ('manage_roles_containerrepository', 'Can manage role assignments on container repository')]}, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0029_remove_blob_media_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-02-22 15:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0028_add_role_manage_permissions'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='blob', 15 | name='media_type', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0030_enforce_tagged_manifest_reference.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-05-16 16:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | def remove_tags_without_manifests(apps, schema_editor): 8 | Tag = apps.get_model("container", "Tag") 9 | Tag.objects.filter(tagged_manifest=None).delete() 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('container', '0029_remove_blob_media_type'), 16 | ] 17 | 18 | operations = [ 19 | migrations.RunPython(code=remove_tags_without_manifests), 20 | migrations.AlterField( 21 | model_name='tag', 22 | name='tagged_manifest', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_manifests', to='container.manifest'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0032_upload_artifact.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-06-23 11:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0091_systemid'), 11 | ('container', '0031_replace_charf_with_textf'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='upload', 17 | name='artifact', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploads', to='core.artifact'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0033_raise_warning_for_repair.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from django.db import migrations 4 | 5 | from pulp_container.constants import MEDIA_TYPE 6 | 7 | 8 | def print_warning_for_repair(apps, schema_editor): 9 | Manifest = apps.get_model("container", "Manifest") 10 | if Manifest.objects.filter(media_type=MEDIA_TYPE.MANIFEST_V1).exists(): 11 | warnings.warn( 12 | "Manifests with potentially invalid media types were detected. Please, run the " 13 | "'pulpcore-manager container-repair-media-type' command to repair the media types " 14 | "of the manifests that could be incorrectly parsed during the sync procedure." 15 | ) 16 | 17 | 18 | class Migration(migrations.Migration): 19 | 20 | dependencies = [('container', '0032_upload_artifact')] 21 | 22 | operations = [ 23 | migrations.RunPython( 24 | code=print_warning_for_repair, 25 | reverse_code=migrations.RunPython.noop, 26 | elidable=True, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0034_translate_signed_schema.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | from pulp_container.constants import MEDIA_TYPE 4 | 5 | 6 | def update_schema_media_type(apps, schema_editor): 7 | Manifest = apps.get_model("container", "Manifest") 8 | Manifest.objects.filter(media_type=MEDIA_TYPE.MANIFEST_V1_SIGNED).update( 9 | media_type=MEDIA_TYPE.MANIFEST_V1 10 | ) 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [("container", "0033_raise_warning_for_repair")] 16 | 17 | operations = [ 18 | migrations.RunPython( 19 | code=update_schema_media_type, 20 | reverse_code=migrations.RunPython.noop, 21 | elidable=True, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0036_containerpushrepository_pending_blobs_manifests.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.1 on 2023-05-17 17:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("container", "0035_alter_blob_content_ptr_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="containerpushrepository", 14 | name="pending_blobs", 15 | field=models.ManyToManyField(to="container.blob"), 16 | ), 17 | migrations.AddField( 18 | model_name="containerpushrepository", 19 | name="pending_manifests", 20 | field=models.ManyToManyField(to="container.manifest"), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0037_create_pull_through_cache_models.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2023-12-12 21:15 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import pulpcore.app.models.access_policy 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0116_alter_remoteartifact_md5_alter_remoteartifact_sha1_and_more'), 12 | ('container', '0036_containerpushrepository_pending_blobs_manifests'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ContainerPullThroughRemote', 18 | fields=[ 19 | ('remote_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.remote')), 20 | ], 21 | options={ 22 | 'permissions': [('manage_roles_containerpullthroughremote', 'Can manage role assignments on pull-through container remote')], 23 | 'default_related_name': '%(app_label)s_%(model_name)s', 24 | }, 25 | bases=('core.remote', pulpcore.app.models.access_policy.AutoAddObjPermsMixin), 26 | ), 27 | migrations.AddField( 28 | model_name='containerrepository', 29 | name='pending_blobs', 30 | field=models.ManyToManyField(to='container.blob'), 31 | ), 32 | migrations.AddField( 33 | model_name='containerrepository', 34 | name='pending_manifests', 35 | field=models.ManyToManyField(to='container.manifest'), 36 | ), 37 | migrations.CreateModel( 38 | name='ContainerPullThroughDistribution', 39 | fields=[ 40 | ('distribution_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.distribution')), 41 | ('private', models.BooleanField(default=False, help_text='Restrict pull access to explicitly authorized users. Related distributions inherit this value. Defaults to unrestricted pull access.')), 42 | ('description', models.TextField(null=True)), 43 | ('namespace', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='container_pull_through_distributions', to='container.containernamespace')), 44 | ], 45 | options={ 46 | 'permissions': [('manage_roles_containerpullthroughdistribution', 'Can manage role assignments on pull-through cache distribution')], 47 | 'default_related_name': '%(app_label)s_%(model_name)s', 48 | }, 49 | bases=('core.distribution', pulpcore.app.models.access_policy.AutoAddObjPermsMixin), 50 | ), 51 | migrations.AddField( 52 | model_name='containerdistribution', 53 | name='pull_through_distribution', 54 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='distributions', to='container.containerpullthroughdistribution'), 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0038_add_manifest_metadata_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-02-29 16:04 2 | import warnings 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | def print_warning_for_initializing_image_nature(apps, schema_editor): 8 | warnings.warn( 9 | "Run 'pulpcore-manager container-handle-image-data' to initialize and expose metadata " 10 | "(i.e., annotations and labels) for all manifests." 11 | ) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ('container', '0037_create_pull_through_cache_models'), 18 | ] 19 | 20 | operations = [ 21 | migrations.AddField( 22 | model_name='manifest', 23 | name='annotations', 24 | field=models.JSONField(default=dict), 25 | ), 26 | migrations.AddField( 27 | model_name='manifest', 28 | name='is_bootable', 29 | field=models.BooleanField(default=False), 30 | ), 31 | migrations.AddField( 32 | model_name='manifest', 33 | name='is_flatpak', 34 | field=models.BooleanField(default=False), 35 | ), 36 | migrations.AddField( 37 | model_name='manifest', 38 | name='labels', 39 | field=models.JSONField(default=dict), 40 | ), 41 | migrations.RunPython( 42 | code=print_warning_for_initializing_image_nature, 43 | reverse_code=migrations.RunPython.noop, 44 | elidable=True, 45 | ) 46 | ] 47 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0039_manifest_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-05 11:22 2 | import warnings 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | def print_warning_for_initializing_manifest_data(apps, schema_editor): 8 | warnings.warn( 9 | "Run 'pulpcore-manager container-handle-image-data' to move the manifests' " 10 | "data from artifacts to the new 'data' database field." 11 | ) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ("container", "0038_add_manifest_metadata_fields"), 18 | ] 19 | 20 | operations = [ 21 | migrations.AddField( 22 | model_name="manifest", 23 | name="data", 24 | field=models.TextField(null=True), 25 | ), 26 | migrations.RunPython( 27 | print_warning_for_initializing_manifest_data, 28 | reverse_code=migrations.RunPython.noop, 29 | elidable=True, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0040_add_remote_repo_filter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.13 on 2024-06-28 10:34 2 | 3 | import django.contrib.postgres.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('container', '0039_manifest_data'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='containerpullthroughremote', 16 | name='excludes', 17 | field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(null=True), null=True, size=None), 18 | ), 19 | migrations.AddField( 20 | model_name='containerpullthroughremote', 21 | name='includes', 22 | field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(null=True), null=True, size=None), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0041_add_pull_through_pull_permissions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-07 21:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0040_add_remote_repo_filter'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='containernamespace', 15 | options={'permissions': [('namespace_add_containerdistribution', 'Add any distribution to a namespace'), ('namespace_delete_containerdistribution', 'Delete any distribution from a namespace'), ('namespace_view_containerdistribution', 'View any distribution in a namespace'), ('namespace_pull_containerdistribution', 'Pull from any distribution in a namespace'), ('namespace_push_containerdistribution', 'Push to any distribution in a namespace'), ('namespace_change_containerdistribution', 'Change any distribution in a namespace'), ('namespace_view_containerpushrepository', 'View any push repository in a namespace'), ('namespace_modify_content_containerpushrepository', 'Modify content in any push repository in a namespace'), ('namespace_modify_content_containerrepository', 'Modify content in any repository in a namespace'), ('namespace_change_containerpushrepository', 'Update any existing push repository in a namespace'), ('manage_roles_containernamespace', 'Can manage role assignments on container namespace')]}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='containerpullthroughdistribution', 19 | options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_containerpullthroughdistribution', 'Can manage role assignments on pull-through cache distribution'), ('pull_new_containerdistribution', 'Can pull new content via the pull-through cache distribution')]}, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0042_add_manifest_nature_field.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-10-21 19:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('container', '0041_add_pull_through_pull_permissions'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='manifest', 15 | name='type', 16 | field=models.CharField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0043_add_os_arch_image_size_manifest_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-10-30 11:09 2 | import warnings 3 | 4 | from django.db import migrations, models 5 | 6 | def print_warning_for_updating_manifest_fields(apps, schema_editor): 7 | warnings.warn( 8 | "Run 'pulpcore-manager container-handle-image-data' to update the manifests' " 9 | "os, architecture, and compressed_image_size fields." 10 | ) 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('container', '0042_add_manifest_nature_field'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name='manifest', 21 | name='architecture', 22 | field=models.TextField(null=True), 23 | ), 24 | migrations.AddField( 25 | model_name='manifest', 26 | name='compressed_image_size', 27 | field=models.IntegerField(null=True), 28 | ), 29 | migrations.AddField( 30 | model_name='manifest', 31 | name='os', 32 | field=models.TextField(null=True), 33 | ), 34 | migrations.AlterField( 35 | model_name='manifestlistmanifest', 36 | name='architecture', 37 | field=models.TextField(blank=True, default=''), 38 | ), 39 | migrations.AlterField( 40 | model_name='manifestlistmanifest', 41 | name='os', 42 | field=models.TextField(blank=True, default=''), 43 | ), 44 | migrations.RunPython( 45 | print_warning_for_updating_manifest_fields, 46 | reverse_code=migrations.RunPython.noop, 47 | elidable=True, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0044_add_domain.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-21 20:59 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import pulpcore.app.util 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0125_openpgpdistribution_openpgpkeyring_openpgppublickey_and_more'), 12 | ('container', '0043_add_os_arch_image_size_manifest_fields'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterUniqueTogether( 17 | name='blob', 18 | unique_together=set(), 19 | ), 20 | migrations.AlterUniqueTogether( 21 | name='containernamespace', 22 | unique_together=set(), 23 | ), 24 | migrations.AlterUniqueTogether( 25 | name='manifest', 26 | unique_together=set(), 27 | ), 28 | migrations.AlterUniqueTogether( 29 | name='manifestsignature', 30 | unique_together=set(), 31 | ), 32 | migrations.AlterUniqueTogether( 33 | name='tag', 34 | unique_together=set(), 35 | ), 36 | migrations.AddField( 37 | model_name='blob', 38 | name='_pulp_domain', 39 | field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), 40 | ), 41 | migrations.AddField( 42 | model_name='containernamespace', 43 | name='pulp_domain', 44 | field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), 45 | ), 46 | migrations.AddField( 47 | model_name='manifest', 48 | name='_pulp_domain', 49 | field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), 50 | ), 51 | migrations.AddField( 52 | model_name='manifestsignature', 53 | name='_pulp_domain', 54 | field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), 55 | ), 56 | migrations.AddField( 57 | model_name='tag', 58 | name='_pulp_domain', 59 | field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), 60 | ), 61 | migrations.AlterUniqueTogether( 62 | name='blob', 63 | unique_together={('digest', '_pulp_domain')}, 64 | ), 65 | migrations.AlterUniqueTogether( 66 | name='containernamespace', 67 | unique_together={('name', 'pulp_domain')}, 68 | ), 69 | migrations.AlterUniqueTogether( 70 | name='manifest', 71 | unique_together={('digest', '_pulp_domain')}, 72 | ), 73 | migrations.AlterUniqueTogether( 74 | name='manifestsignature', 75 | unique_together={('digest', '_pulp_domain')}, 76 | ), 77 | migrations.AlterUniqueTogether( 78 | name='tag', 79 | unique_together={('name', 'tagged_manifest', '_pulp_domain')}, 80 | ), 81 | ] 82 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/0045_alter_manifest_compressed_image_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2025-03-06 22:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("container", "0044_add_domain"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="manifest", 15 | name="compressed_image_size", 16 | field=models.BigIntegerField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /pulp_container/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/app/migrations/__init__.py -------------------------------------------------------------------------------- /pulp_container/app/replica.py: -------------------------------------------------------------------------------- 1 | from pulpcore.plugin.replica import Replicator 2 | from pulpcore.plugin.util import get_url 3 | 4 | from pulp_glue.container.context import ( 5 | PulpContainerDistributionContext, 6 | PulpContainerRepositoryContext, 7 | ) 8 | 9 | from pulp_container.app.models import ContainerDistribution, ContainerRemote, ContainerRepository 10 | from pulp_container.app.tasks import synchronize as container_synchronize 11 | 12 | 13 | class ContainerReplicator(Replicator): 14 | distribution_ctx_cls = PulpContainerDistributionContext 15 | repository_ctx_cls = PulpContainerRepositoryContext 16 | remote_model_cls = ContainerRemote 17 | repository_model_cls = ContainerRepository 18 | distribution_model_cls = ContainerDistribution 19 | distribution_serializer_name = "ContainerDistributionSerializer" 20 | repository_serializer_name = "ContainerRepositorySerializer" 21 | remote_serializer_name = "ContainerRemoteSerializer" 22 | app_label = "container" 23 | sync_task = container_synchronize 24 | 25 | def sync_params(self, repository, remote): 26 | """Returns a dictionary where key is a parameter for the sync task.""" 27 | return dict( 28 | remote_pk=str(remote.pk), 29 | repository_pk=str(repository.pk), 30 | signed_only=False, 31 | mirror=True, 32 | ) 33 | 34 | def url(self, upstream_distribution): 35 | return self.pulp_ctx._api_kwargs["base_url"] 36 | 37 | def remote_extra_fields(self, upstream_distribution): 38 | upstream_name = upstream_distribution["registry_path"].split("/", 1)[1] 39 | return {"upstream_name": upstream_name} 40 | 41 | def distribution_data(self, repository, upstream_distribution): 42 | """ 43 | Return the fields that need to be updated/cleared on distributions for idempotence. 44 | """ 45 | return { 46 | "repository": get_url(repository), 47 | "base_path": upstream_distribution["base_path"], 48 | "private": upstream_distribution["private"], 49 | "description": upstream_distribution["description"], 50 | } 51 | 52 | 53 | REPLICATION_ORDER = [ContainerReplicator] 54 | -------------------------------------------------------------------------------- /pulp_container/app/settings.py: -------------------------------------------------------------------------------- 1 | DRF_ACCESS_POLICY = { 2 | "dynaconf_merge_unique": True, 3 | "reusable_conditions": ["pulp_container.app.global_access_conditions"], 4 | } 5 | 6 | TOKEN_AUTH_DISABLED = False 7 | FLATPAK_INDEX = False 8 | 9 | # The number of allowed threads to sign manifests in parallel 10 | MAX_PARALLEL_SIGNING_TASKS = 10 11 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .download_image_data import download_image_data # noqa 2 | from .builder import build_image_from_containerfile, build_image # noqa 3 | from .recursive_add import recursive_add_content # noqa 4 | from .recursive_remove import recursive_remove_content # noqa 5 | from .sign import sign # noqa 6 | from .synchronize import synchronize # noqa 7 | from .tag import tag_image # noqa 8 | from .untag import untag_image # noqa 9 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/download_image_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from pulpcore.plugin.stages import DeclarativeContent 5 | 6 | from pulp_container.app.models import ContainerRemote, ContainerRepository, Tag 7 | from pulp_container.app.utils import determine_media_type_from_json 8 | from pulp_container.constants import MEDIA_TYPE 9 | 10 | from .synchronize import ContainerDeclarativeVersion 11 | from .sync_stages import ContainerFirstStage 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def download_image_data(repository_pk, remote_pk, raw_text_manifest_data, tag_name): 17 | repository = ContainerRepository.objects.get(pk=repository_pk) 18 | remote = ContainerRemote.objects.get(pk=remote_pk) 19 | log.info("Pulling cache: repository={r} remote={p}".format(r=repository.name, p=remote.name)) 20 | first_stage = ContainerPullThroughFirstStage(remote, raw_text_manifest_data, tag_name) 21 | dv = ContainerDeclarativeVersion(first_stage, repository) 22 | return dv.create() 23 | 24 | 25 | class ContainerPullThroughFirstStage(ContainerFirstStage): 26 | """The stage that prepares the pipeline for downloading a single tag and its related data.""" 27 | 28 | def __init__(self, remote, raw_text_manifest_data, tag_name): 29 | """Initialize the stage with the artifact defined in content-app.""" 30 | super().__init__(remote, signed_only=False) 31 | self.tag_name = tag_name 32 | self.raw_text_manifest_data = raw_text_manifest_data 33 | 34 | async def run(self): 35 | """Run the stage and create declarative content for one tag, its manifest, and blobs. 36 | 37 | This method is a tinified method based on ``ContainerFirstStage.run`` with syncing just 38 | a single tag. 39 | """ 40 | tag_dc = DeclarativeContent(Tag(name=self.tag_name)) 41 | self.tag_dcs.append(tag_dc) 42 | 43 | content_data = json.loads(self.raw_text_manifest_data) 44 | 45 | media_type = determine_media_type_from_json(content_data) 46 | if media_type in (MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI): 47 | list_dc = self.create_manifest_list( 48 | content_data, self.raw_text_manifest_data, media_type 49 | ) 50 | for manifest_data in content_data.get("manifests"): 51 | listed_manifest = await self.create_listed_manifest(manifest_data) 52 | list_dc.extra_data["listed_manifests"].append(listed_manifest) 53 | else: 54 | tag_dc.extra_data["tagged_manifest_dc"] = list_dc 55 | for listed_manifest in list_dc.extra_data["listed_manifests"]: 56 | await self.handle_blobs( 57 | listed_manifest["manifest_dc"], listed_manifest["content_data"] 58 | ) 59 | self.manifest_dcs.append(listed_manifest["manifest_dc"]) 60 | self.manifest_list_dcs.append(list_dc) 61 | else: 62 | # Simple tagged manifest 63 | man_dc = self.create_manifest(content_data, self.raw_text_manifest_data, media_type) 64 | tag_dc.extra_data["tagged_manifest_dc"] = man_dc 65 | await self.handle_blobs(man_dc, content_data) 66 | self.manifest_dcs.append(man_dc) 67 | 68 | await self.resolve_flush() 69 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/recursive_add.py: -------------------------------------------------------------------------------- 1 | from pulp_container.app.models import ( 2 | Blob, 3 | ContainerRepository, 4 | Manifest, 5 | MEDIA_TYPE, 6 | Tag, 7 | ) 8 | 9 | 10 | def recursive_add_content(repository_pk, content_units): 11 | """ 12 | Create a new repository version by recursively adding content. 13 | 14 | For each unit that is specified, we also need to add related content. For example, if a 15 | manifest-list is specified, we need to add all referenced manifests, and all blobs referenced 16 | by those manifests. 17 | 18 | Args: 19 | repository_pk (int): The primary key for a Repository for which a new Repository Version 20 | should be created. 21 | content_units (list): List of PKs for :class:`~pulpcore.app.models.Content` that 22 | should be added to the previous Repository Version for this Repository. 23 | 24 | """ 25 | repository = ContainerRepository.objects.get(pk=repository_pk) 26 | 27 | tags_to_add = Tag.objects.filter(pk__in=content_units) 28 | 29 | manifest_lists_to_add = Manifest.objects.filter( 30 | pk__in=content_units, media_type__in=[MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI] 31 | ) | Manifest.objects.filter( 32 | pk__in=tags_to_add.values_list("tagged_manifest", flat=True), 33 | media_type__in=[MEDIA_TYPE.MANIFEST_LIST, MEDIA_TYPE.INDEX_OCI], 34 | ) 35 | 36 | manifests_to_add = ( 37 | Manifest.objects.filter( 38 | pk__in=content_units, 39 | media_type__in=[ 40 | MEDIA_TYPE.MANIFEST_V1, 41 | MEDIA_TYPE.MANIFEST_V1_SIGNED, 42 | MEDIA_TYPE.MANIFEST_V2, 43 | MEDIA_TYPE.MANIFEST_OCI, 44 | ], 45 | ) 46 | | Manifest.objects.filter( 47 | pk__in=manifest_lists_to_add.values_list("listed_manifests", flat=True) 48 | ) 49 | | Manifest.objects.filter( 50 | pk__in=tags_to_add.values_list("tagged_manifest", flat=True), 51 | media_type__in=[ 52 | MEDIA_TYPE.MANIFEST_V1, 53 | MEDIA_TYPE.MANIFEST_V1_SIGNED, 54 | MEDIA_TYPE.MANIFEST_V2, 55 | MEDIA_TYPE.MANIFEST_OCI, 56 | ], 57 | ) 58 | ) 59 | 60 | blobs_to_add = ( 61 | Blob.objects.filter(pk__in=content_units) 62 | | Blob.objects.filter(pk__in=manifests_to_add.values_list("blobs", flat=True)) 63 | | Blob.objects.filter(pk__in=manifests_to_add.values_list("config_blob", flat=True)) 64 | ) 65 | 66 | latest_version = repository.latest_version() 67 | if latest_version: 68 | tags_in_repo = latest_version.content.filter(pulp_type=Tag.get_pulp_type()) 69 | tags_to_replace = Tag.objects.filter( 70 | pk__in=tags_in_repo, name__in=tags_to_add.values_list("name", flat=True) 71 | ) 72 | else: 73 | tags_to_replace = [] 74 | 75 | with repository.new_version() as new_version: 76 | new_version.remove_content(tags_to_replace) 77 | new_version.add_content(tags_to_add) 78 | new_version.add_content(manifest_lists_to_add) 79 | new_version.add_content(manifests_to_add) 80 | new_version.add_content(blobs_to_add) 81 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/synchronize.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pulpcore.plugin.stages import ( 4 | ArtifactDownloader, 5 | ArtifactSaver, 6 | DeclarativeVersion, 7 | RemoteArtifactSaver, 8 | ResolveContentFutures, 9 | QueryExistingArtifacts, 10 | QueryExistingContents, 11 | ) 12 | 13 | from .sync_stages import ContainerFirstStage, ContainerContentSaver 14 | from pulp_container.app.models import ContainerRemote, ContainerRepository 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | def synchronize(remote_pk, repository_pk, mirror, signed_only): 21 | """ 22 | Sync content from the remote repository. 23 | 24 | Create a new version of the repository that is synchronized with the remote. 25 | 26 | Args: 27 | remote_pk (str): The remote PK. 28 | repository_pk (str): The repository PK. 29 | mirror (boolean): A boolean indicating enabled or disabled mirror mode. 30 | signed_only (boolean): A boolean indicating whether to sync only signed content or all. 31 | 32 | Raises: 33 | ValueError: If the remote does not specify a URL to sync 34 | 35 | """ 36 | remote = ContainerRemote.objects.get(pk=remote_pk) 37 | repository = ContainerRepository.objects.get(pk=repository_pk) 38 | log.info("Synchronizing: repository={r} remote={p}".format(r=repository.name, p=remote.name)) 39 | first_stage = ContainerFirstStage(remote, signed_only) 40 | dv = ContainerDeclarativeVersion(first_stage, repository, mirror) 41 | return dv.create() 42 | 43 | 44 | class ContainerDeclarativeVersion(DeclarativeVersion): 45 | """ 46 | Subclassed Declarative version creates a custom pipeline for Container sync. 47 | """ 48 | 49 | def pipeline_stages(self, new_version): 50 | """ 51 | Build a list of stages feeding into the ContentUnitAssociation stage. 52 | 53 | This defines the "architecture" of the entire sync. 54 | 55 | Args: 56 | new_version (:class:`~pulpcore.plugin.models.RepositoryVersion`): The 57 | new repository version that is going to be built. 58 | 59 | Returns: 60 | list: List of :class:`~pulpcore.plugin.stages.Stage` instances 61 | 62 | """ 63 | pipeline = [ 64 | self.first_stage, 65 | QueryExistingArtifacts(), 66 | ArtifactDownloader(), 67 | ArtifactSaver(), 68 | QueryExistingContents(), 69 | ContainerContentSaver(), 70 | RemoteArtifactSaver(), 71 | ResolveContentFutures(), 72 | ] 73 | 74 | return pipeline 75 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/tag.py: -------------------------------------------------------------------------------- 1 | from pulpcore.plugin.models import CreatedResource, Repository 2 | from pulpcore.plugin.util import get_domain 3 | from pulp_container.app.models import Manifest, Tag 4 | 5 | 6 | def tag_image(manifest_pk, tag, repository_pk): 7 | """ 8 | Create a new repository version out of the passed tag name and the manifest. 9 | 10 | If the tag name is already associated with an existing manifest with the same digest, 11 | no new content is created. Note that a same tag name cannot be used for two different 12 | manifests. Due to this fact, an old Tag object is going to be removed from 13 | a new repository version when a manifest contains a digest which is not equal to the 14 | digest passed with POST request. 15 | """ 16 | manifest = Manifest.objects.get(pk=manifest_pk) 17 | 18 | repository = Repository.objects.get(pk=repository_pk).cast() 19 | latest_version = repository.latest_version() 20 | 21 | tags_to_remove = Tag.objects.filter(pk__in=latest_version.content.all(), name=tag).exclude( 22 | tagged_manifest=manifest 23 | ) 24 | 25 | manifest_tag, created = Tag.objects.get_or_create( 26 | name=tag, tagged_manifest=manifest, _pulp_domain=get_domain() 27 | ) 28 | 29 | if created: 30 | resource = CreatedResource(content_object=manifest_tag) 31 | resource.save() 32 | else: 33 | manifest_tag.touch() 34 | 35 | tags_to_add = Tag.objects.filter(pk=manifest_tag.pk).exclude( 36 | pk__in=latest_version.content.all() 37 | ) 38 | 39 | with repository.new_version() as repository_version: 40 | repository_version.remove_content(tags_to_remove) 41 | repository_version.add_content(tags_to_add) 42 | -------------------------------------------------------------------------------- /pulp_container/app/tasks/untag.py: -------------------------------------------------------------------------------- 1 | from pulpcore.plugin.models import Repository 2 | from pulp_container.app.models import Tag 3 | 4 | 5 | def untag_image(tag, repository_pk): 6 | """ 7 | Create a new repository version without a specified manifest's tag name. 8 | """ 9 | repository = Repository.objects.get(pk=repository_pk).cast() 10 | latest_version = repository.latest_version() 11 | 12 | tags_in_latest_repository = latest_version.content.filter(pulp_type=Tag.get_pulp_type()) 13 | 14 | tags_to_remove = Tag.objects.filter(pk__in=tags_in_latest_repository, name=tag) 15 | 16 | with repository.new_version() as repository_version: 17 | repository_version.remove_content(tags_to_remove) 18 | -------------------------------------------------------------------------------- /pulp_container/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import include, path 3 | from rest_framework.routers import Route, SimpleRouter 4 | from pulp_container.app.registry_api import ( 5 | BearerTokenView, 6 | Blobs, 7 | BlobUploads, 8 | CatalogView, 9 | FlatpakIndexDynamicView, 10 | FlatpakIndexStaticView, 11 | Manifests, 12 | Signatures, 13 | TagsListView, 14 | VersionView, 15 | ) 16 | 17 | if settings.DOMAIN_ENABLED: 18 | re_path = "(?P[-a-zA-Z0-9_]+)/(?P.+)" 19 | da_path = "/" 20 | else: 21 | re_path = "(?P.+)" 22 | da_path = "" 23 | 24 | router = SimpleRouter(trailing_slash=False) 25 | 26 | head_route = Route( 27 | url=r"^{prefix}/{lookup}{trailing_slash}$", 28 | mapping={"head": "head"}, 29 | name="{basename}-detail", 30 | detail=True, 31 | initkwargs={"suffix": "Instance"}, 32 | ) 33 | 34 | router.routes.append(head_route) 35 | router.register(rf"v2/{re_path}/blobs/uploads\/?", BlobUploads, basename="docker-upload") 36 | router.register(rf"v2/{re_path}/blobs", Blobs, basename="blobs") 37 | router.register(rf"v2/{re_path}/manifests", Manifests, basename="manifests") 38 | router.register(rf"extensions/v2/{re_path}/signatures", Signatures, basename="signatures") 39 | 40 | urlpatterns = [ 41 | path("token/", BearerTokenView.as_view()), 42 | path("v2/", VersionView.as_view()), 43 | path("v2/_catalog", CatalogView.as_view()), 44 | path(f"v2/{da_path}/tags/list", TagsListView.as_view()), 45 | path("", include(router.urls)), 46 | ] 47 | # print(router.urls) 48 | if settings.FLATPAK_INDEX: 49 | urlpatterns.extend( 50 | [ 51 | path("index/dynamic", FlatpakIndexDynamicView.as_view()), 52 | path("index/static", FlatpakIndexStaticView.as_view()), 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /pulp_container/app/webserver_snippets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/app/webserver_snippets/__init__.py -------------------------------------------------------------------------------- /pulp_container/app/webserver_snippets/apache.conf: -------------------------------------------------------------------------------- 1 | ProxyPass /v2 ${pulp-api}/v2 2 | ProxyPassReverse /v2 ${pulp-api}/v2 3 | 4 | ProxyPass /extensions/v2 ${pulp-api}/extensions/v2 5 | ProxyPassReverse /extensions/v2 ${pulp-api}/extensions/v2 6 | 7 | ProxyPass /pulp/container ${pulp-content}/pulp/container 8 | ProxyPassReverse /pulp/container ${pulp-content}/pulp/container 9 | 10 | ProxyPass /token ${pulp-api}/token 11 | ProxyPassReverse /token ${pulp-api}/token 12 | -------------------------------------------------------------------------------- /pulp_container/app/webserver_snippets/nginx.conf: -------------------------------------------------------------------------------- 1 | location /v2/ { 2 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 3 | proxy_set_header X-Forwarded-Proto $scheme; 4 | proxy_set_header Host $http_host; 5 | # we don't want nginx trying to do something clever with 6 | # redirects, we set the Host: header above already. 7 | proxy_redirect off; 8 | proxy_pass http://pulp-api; 9 | client_max_body_size 0; 10 | } 11 | 12 | location /extensions/v2/ { 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_set_header X-Forwarded-Proto $scheme; 15 | proxy_set_header Host $http_host; 16 | # we don't want nginx trying to do something clever with 17 | # redirects, we set the Host: header above already. 18 | proxy_redirect off; 19 | proxy_pass http://pulp-api; 20 | } 21 | 22 | location /pulp/container/ { 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header X-Forwarded-Proto $scheme; 25 | proxy_set_header Host $http_host; 26 | # we don't want nginx trying to do something clever with 27 | # redirects, we set the Host: header above already. 28 | proxy_redirect off; 29 | proxy_pass http://pulp-content; 30 | } 31 | 32 | location /token/ { 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header X-Forwarded-Proto $scheme; 35 | proxy_set_header Host $http_host; 36 | # we don't want nginx trying to do something clever with 37 | # redirects, we set the Host: header above already. 38 | proxy_redirect off; 39 | proxy_pass http://pulp-api; 40 | } 41 | -------------------------------------------------------------------------------- /pulp_container/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/tests/__init__.py -------------------------------------------------------------------------------- /pulp_container/tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for container plugin.""" 2 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests that communicate with container plugin via the v3 API.""" 2 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/test_crud_distributions.py: -------------------------------------------------------------------------------- 1 | """Tests that CRUD distributions.""" 2 | 3 | import pytest 4 | import uuid 5 | 6 | 7 | @pytest.mark.parallel 8 | def test_crud_distributions( 9 | container_bindings, container_distribution_factory, add_to_cleanup, monitor_task 10 | ): 11 | """Test CRUD distributions.""" 12 | # Create a distribution. 13 | name = str(uuid.uuid4()) 14 | base_path = name.replace("-", "/") 15 | distribution = container_distribution_factory(name=name, base_path=base_path) 16 | assert base_path == distribution.base_path 17 | assert name == distribution.name 18 | 19 | # assert that namespace was created and it matches first component of base_path 20 | assert ( 21 | container_bindings.PulpContainerNamespacesApi.read(distribution.namespace).name 22 | == base_path.split("/")[0] 23 | ) 24 | add_to_cleanup(container_bindings.PulpContainerNamespacesApi, distribution.namespace) 25 | 26 | # Create a second distribution with the same name. 27 | with pytest.raises(container_bindings.ApiException): 28 | container_distribution_factory(name=name) 29 | 30 | # Update the distribution. 31 | new_base_path = str(uuid.uuid4()).replace("-", "/") 32 | response = container_bindings.DistributionsContainerApi.partial_update( 33 | distribution.pulp_href, {"base_path": new_base_path} 34 | ) 35 | monitor_task(response.task) 36 | distribution = container_bindings.DistributionsContainerApi.read(distribution.pulp_href) 37 | assert new_base_path == distribution.base_path 38 | 39 | assert ( 40 | container_bindings.PulpContainerNamespacesApi.read(distribution.namespace).name 41 | == new_base_path.split("/")[0] 42 | ) 43 | add_to_cleanup(container_bindings.PulpContainerNamespacesApi, distribution.namespace) 44 | 45 | # Delete the distribution. 46 | delete_response = container_bindings.DistributionsContainerApi.delete(distribution.pulp_href) 47 | monitor_task(delete_response.task) 48 | with pytest.raises(container_bindings.ApiException): 49 | container_bindings.DistributionsContainerApi.read(distribution.pulp_href) 50 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/test_crud_remotes.py: -------------------------------------------------------------------------------- 1 | """Tests that CRUD container remotes.""" 2 | 3 | from random import choice 4 | import pytest 5 | import uuid 6 | 7 | from pulp_container.tests.functional.conftest import gen_container_remote 8 | 9 | 10 | ON_DEMAND_DOWNLOAD_POLICIES = ("on_demand", "streamed") 11 | 12 | 13 | def test_crud_remote(container_bindings, monitor_task, add_to_cleanup): 14 | # Create a remote. 15 | body = _gen_verbose_remote() 16 | remote = container_bindings.RemotesContainerApi.create(body) 17 | add_to_cleanup(container_bindings.RemotesContainerApi, remote.pulp_href) 18 | for key in ("username", "password"): 19 | del body[key] 20 | for key, val in body.items(): 21 | assert getattr(remote, key) == val, key 22 | 23 | # Try to create a second remote with an identical name. 24 | body = gen_container_remote() 25 | body["name"] = remote.name 26 | with pytest.raises(container_bindings.ApiException): 27 | container_bindings.RemotesContainerApi.create(body) 28 | 29 | # Read a remote by its href. 30 | read_remote = container_bindings.RemotesContainerApi.read(remote.pulp_href) 31 | assert read_remote.pulp_href == remote.pulp_href 32 | 33 | # Read a remote by its name. 34 | page = container_bindings.RemotesContainerApi.list(name=remote.name) 35 | assert len(page.results) == 1 36 | assert page.results[0].pulp_href == remote.pulp_href 37 | 38 | # Update a remote using HTTP PATCH. 39 | body = _gen_verbose_remote() 40 | response = container_bindings.RemotesContainerApi.partial_update(remote.pulp_href, body) 41 | monitor_task(response.task) 42 | for key in ("username", "password"): 43 | del body[key] 44 | remote = container_bindings.RemotesContainerApi.read(remote.pulp_href) 45 | for key, val in body.items(): 46 | assert getattr(remote, key) == val, key 47 | 48 | # Update a remote using HTTP PUT. 49 | body = _gen_verbose_remote() 50 | response = container_bindings.RemotesContainerApi.update(remote.pulp_href, body) 51 | monitor_task(response.task) 52 | for key in ("username", "password"): 53 | del body[key] 54 | remote = container_bindings.RemotesContainerApi.read(remote.pulp_href) 55 | for key, val in body.items(): 56 | assert getattr(remote, key) == val, key 57 | 58 | # Delete a remote. 59 | response = container_bindings.RemotesContainerApi.delete(remote.pulp_href) 60 | monitor_task(response.task) 61 | with pytest.raises(container_bindings.ApiException): 62 | container_bindings.RemotesContainerApi.read(remote.pulp_href) 63 | 64 | 65 | def _gen_verbose_remote(): 66 | """Return a semi-random dict for use in defining a remote. 67 | 68 | For most tests, it's desirable to create remotes with as few attributes 69 | as possible, so that the tests can specifically target and attempt to break 70 | specific features. This module specifically targets remotes, so it makes 71 | sense to provide as many attributes as possible. 72 | 73 | Note that 'username' and 'password' are write-only attributes. 74 | """ 75 | attrs = gen_container_remote() 76 | attrs.update( 77 | { 78 | "password": str(uuid.uuid4()), 79 | "username": str(uuid.uuid4()), 80 | "policy": choice(ON_DEMAND_DOWNLOAD_POLICIES), 81 | } 82 | ) 83 | return attrs 84 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/test_push_signatures.py: -------------------------------------------------------------------------------- 1 | """Tests that verify that an image signature can be pushed to Pulp.""" 2 | 3 | import base64 4 | import json 5 | import pytest 6 | 7 | from pulp_container.tests.functional.constants import REGISTRY_V2_REPO_PULP 8 | from pulp_container.constants import SIGNATURE_TYPE 9 | 10 | 11 | @pytest.fixture 12 | def distribution( 13 | registry_client, 14 | local_registry, 15 | container_distribution_api, 16 | signing_gpg_metadata, 17 | add_to_cleanup, 18 | full_path, 19 | ): 20 | """Return a distribution created after pushing a signed content to the Pulp Registry.""" 21 | if registry_client.name != "podman": 22 | pytest.skip("This test requires podman to sign pulled content", allow_module_level=True) 23 | 24 | image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a" 25 | registry_client.pull(image_path) 26 | 27 | gpg, fingerprint, keyid = signing_gpg_metadata 28 | 29 | with registry_client.set_env(GNUPGHOME=str(gpg.gnupghome)): 30 | local_registry.tag_and_push(image_path, full_path("test-1:manifest_a"), "--sign-by", keyid) 31 | 32 | # push the same image for the second time with a different signature (timestamp) 33 | local_registry.tag_and_push(image_path, full_path("test-1:manifest_a"), "--sign-by", keyid) 34 | 35 | distribution = container_distribution_api.list(name="test-1").results[0] 36 | add_to_cleanup(container_distribution_api, distribution.pulp_href) 37 | 38 | return distribution 39 | 40 | 41 | def test_assert_signed_image( 42 | local_registry, 43 | container_push_repository_api, 44 | container_manifest_api, 45 | container_signature_api, 46 | signing_gpg_metadata, 47 | distribution, 48 | full_path, 49 | ): 50 | """Test whether an admin user can fetch a signature from the Pulp Registry.""" 51 | gpg, fingerprint, keyid = signing_gpg_metadata 52 | 53 | repository = container_push_repository_api.read(distribution.repository) 54 | manifest = container_manifest_api.list( 55 | repository_version=repository.latest_version_href 56 | ).results[0] 57 | 58 | signature = container_signature_api.list( 59 | repository_version=repository.latest_version_href 60 | ).results[0] 61 | 62 | assert manifest.digest in signature.name 63 | assert signature.signed_manifest == manifest.pulp_href 64 | assert signature.key_id == keyid 65 | 66 | path = f"/extensions/v2/{full_path(distribution)}/signatures/{manifest.digest}" 67 | response, _ = local_registry.get_response("GET", path) 68 | 69 | signatures = response.json()["signatures"] 70 | 71 | assert len(signatures) == 2 72 | 73 | timestamps = [] 74 | for s in signatures: 75 | raw_s = base64.b64decode(s["content"]) 76 | decrypted = gpg.decrypt(raw_s) 77 | 78 | assert decrypted.key_id == keyid 79 | assert decrypted.status == "signature valid" 80 | 81 | json_s = json.loads(decrypted.data) 82 | 83 | image_path = json_s["critical"]["identity"]["docker-reference"] 84 | assert image_path == f"{local_registry.name}/{full_path(distribution)}:manifest_a" 85 | 86 | s_type = json_s["critical"]["type"] 87 | assert s_type == SIGNATURE_TYPE.ATOMIC_FULL 88 | 89 | referenced_manifest = json_s["critical"]["image"]["docker-manifest-digest"] 90 | assert referenced_manifest == manifest.digest 91 | 92 | timestamps.append(json_s["optional"]["timestamp"]) 93 | 94 | assert timestamps[0] != timestamps[1] 95 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/test_remote_filter_pull_through.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import subprocess 3 | 4 | from pulp_container.tests.functional.constants import ( 5 | REGISTRY_V2, 6 | PULP_HELLO_WORLD_REPO, 7 | PULP_FIXTURE_1, 8 | ) 9 | 10 | 11 | @pytest.fixture 12 | def pull_and_verify( 13 | capfd, 14 | local_registry, 15 | registry_client, 16 | full_path, 17 | ): 18 | def _pull_and_verify(images, pull_through_distribution, includes, excludes, expected): 19 | distr = pull_through_distribution(includes, excludes) 20 | for image_path in images: 21 | remote_image_path = f"{REGISTRY_V2}/{image_path}" 22 | local_image_path = full_path(f"{distr.base_path}/{image_path}") 23 | 24 | if image_path not in expected: 25 | with pytest.raises(subprocess.CalledProcessError): 26 | local_registry.pull(local_image_path) 27 | assert "Repository not found" in capfd.readouterr().err 28 | else: 29 | local_registry.pull(local_image_path) 30 | local_image = local_registry.inspect(local_image_path) 31 | registry_client.pull(remote_image_path) 32 | remote_image = registry_client.inspect(remote_image_path) 33 | assert local_image[0]["Id"] == remote_image[0]["Id"] 34 | 35 | return _pull_and_verify 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "images, includes, excludes, expected", 40 | [ 41 | ( 42 | [f"{PULP_FIXTURE_1}:manifest_a", f"{PULP_FIXTURE_1}:manifest_b"], 43 | None, 44 | [], 45 | [f"{PULP_FIXTURE_1}:manifest_a", f"{PULP_FIXTURE_1}:manifest_b"], 46 | ), 47 | ([f"{PULP_FIXTURE_1}:manifest_a", f"{PULP_FIXTURE_1}:manifest_b"], [], ["pulp*"], []), 48 | ( 49 | [f"{PULP_FIXTURE_1}:manifest_a", f"{PULP_FIXTURE_1}:manifest_b"], 50 | [], 51 | ["pulp/test-fixture-1"], 52 | [], 53 | ), 54 | ( 55 | [ 56 | f"{PULP_FIXTURE_1}:manifest_a", 57 | f"{PULP_FIXTURE_1}:manifest_b", 58 | f"{PULP_HELLO_WORLD_REPO}:linux", 59 | ], 60 | ["*hello*"], 61 | ["*fixture*"], 62 | [f"{PULP_HELLO_WORLD_REPO}:linux"], 63 | ), 64 | ( 65 | ["custom_namespace/custom_repo:latest"], 66 | ["*pulp*"], 67 | None, 68 | [], 69 | ), 70 | ], 71 | ) 72 | def test_includes_excludes_filter( 73 | images, 74 | includes, 75 | excludes, 76 | expected, 77 | pull_through_distribution, 78 | pull_and_verify, 79 | delete_orphans_pre, 80 | ): 81 | pull_and_verify(images, pull_through_distribution, includes, excludes, expected) 82 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/api/test_sign_manifests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pulp_container.constants import SIGNATURE_TYPE 4 | from pulp_container.tests.functional.constants import REGISTRY_V2_REPO_PULP 5 | 6 | MANIFEST_TAG = "manifest_a" 7 | 8 | 9 | @pytest.fixture 10 | def distribution( 11 | registry_client, local_registry, container_distribution_api, full_path, add_to_cleanup 12 | ): 13 | """The fixture for a distribution that references a repository of the push type.""" 14 | image_path = f"{REGISTRY_V2_REPO_PULP}:{MANIFEST_TAG}" 15 | registry_client.pull(image_path) 16 | local_registry.tag_and_push(image_path, full_path(f"test-1:{MANIFEST_TAG}")) 17 | 18 | distribution = container_distribution_api.list(name="test-1").results[0] 19 | add_to_cleanup(container_distribution_api, distribution.pulp_href) 20 | 21 | return distribution 22 | 23 | 24 | def test_sign_manifest( 25 | signing_gpg_metadata, 26 | distribution, 27 | container_signing_service, 28 | container_push_repository_api, 29 | container_signature_api, 30 | container_tag_api, 31 | container_manifest_api, 32 | monitor_task, 33 | ): 34 | """Test whether a user can sign a manifest by leveraging a signing service.""" 35 | _, _, keyid = signing_gpg_metadata 36 | sign_data = {"manifest_signing_service": container_signing_service.pulp_href} 37 | 38 | response = container_push_repository_api.sign(distribution.repository, sign_data) 39 | created_resources = monitor_task(response.task).created_resources 40 | 41 | tags = container_tag_api.list(repository_version=created_resources[0]) 42 | assert tags.count == 1 43 | 44 | tag = tags.results[0] 45 | assert tag.name == MANIFEST_TAG 46 | 47 | signatures = container_signature_api.list() 48 | assert signatures.count == 1 49 | 50 | signature = signatures.results[0] 51 | assert signature.key_id == keyid 52 | assert signature.type == SIGNATURE_TYPE.ATOMIC_SHORT 53 | 54 | manifest = container_manifest_api.read(tag.tagged_manifest) 55 | assert signature.signed_manifest == manifest.pulp_href 56 | assert signature.name.startswith(manifest.digest) 57 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/constants.py: -------------------------------------------------------------------------------- 1 | CONTAINER_CONTENT_NAME = "container.blob" 2 | 3 | # GH Packages registry 4 | REGISTRY_V2 = "ghcr.io" 5 | REGISTRY_V2_FEED_URL = "https://ghcr.io" 6 | 7 | # a repository having the size of 1.84kB 8 | PULP_HELLO_WORLD_REPO = "pulp/hello-world" 9 | PULP_HELLO_WORLD_LINUX_AMD64_DIGEST = ( 10 | "sha256:239de6dd745ed1a211123322865b4c342c706e7c1e01644a1bbefe8f8846c5ff" 11 | ) 12 | 13 | # a repository containing 4 manifest lists and 5 manifests 14 | PULP_FIXTURE_1 = "pulp/test-fixture-1" 15 | PULP_FIXTURE_1_MANIFEST_A_DIGEST = ( 16 | "sha256:d8fbbbf3fec1857c32c110292a9decf9744f9f97d7247019ae4776c241395221" 17 | ) 18 | 19 | # a dummy repository containing two manifests (index and image) with an arbitrary bootc label 20 | PULP_LABELED_FIXTURE = "pulp/bootc-labeled" 21 | 22 | # an alternative tag for the PULP_HELLO_WORLD image referencing a manifest list 23 | PULP_HELLO_WORLD_LINUX_TAG = ":linux" 24 | 25 | REGISTRY_V2_REPO_PULP = f"{REGISTRY_V2}/{PULP_FIXTURE_1}" 26 | REGISTRY_V2_REPO_HELLO_WORLD = f"{REGISTRY_V2}/{PULP_HELLO_WORLD_REPO}" 27 | -------------------------------------------------------------------------------- /pulp_container/tests/functional/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities for tests for the container plugin.""" 2 | 3 | import pytest 4 | import requests 5 | 6 | from django.conf import settings 7 | from requests.auth import AuthBase 8 | 9 | from pulp_container.tests.functional.constants import ( 10 | PULP_HELLO_WORLD_REPO, 11 | REGISTRY_V2_FEED_URL, 12 | ) 13 | 14 | 15 | def get_blobsums_from_remote_registry(upstream_name=PULP_HELLO_WORLD_REPO): 16 | """Get remote blobsum list from a remote registry.""" 17 | token_server_response = requests.get( 18 | f"{REGISTRY_V2_FEED_URL}/token?service=ghcr.io&scope=repository:{upstream_name}:pull" 19 | ) 20 | token_server_response.raise_for_status() 21 | token = token_server_response.json()["token"] 22 | 23 | s = requests.Session() 24 | s.headers.update({"Authorization": "Bearer " + token}) 25 | 26 | # the tag 'latest' references a manifest list 27 | manifest_url = f"{REGISTRY_V2_FEED_URL}/v2/{upstream_name}/manifests/latest" 28 | response = s.get(manifest_url) 29 | response.raise_for_status() 30 | 31 | checksums = [] 32 | for manifest in response.json()["manifests"]: 33 | manifest_url = f"{REGISTRY_V2_FEED_URL}/v2/{upstream_name}/manifests/{manifest['digest']}" 34 | response = s.get(manifest_url) 35 | response.raise_for_status() 36 | 37 | for layer in response.json()["layers"]: 38 | checksums.append(layer["digest"]) 39 | 40 | return checksums 41 | 42 | 43 | class BearerTokenAuth(AuthBase): 44 | """A subclass for building a JWT Authorization header out of a provided token.""" 45 | 46 | def __init__(self, token): 47 | """Store a Bearer token that is going to be used in the request object.""" 48 | self.token = token 49 | 50 | def __call__(self, r): 51 | """Attaches a Bearer token authentication to the given request object.""" 52 | r.headers["Authorization"] = "Bearer {}".format(self.token) 53 | return r 54 | 55 | 56 | class AuthenticationHeaderQueries: 57 | """A data class to store header queries located in the Www-Authenticate header.""" 58 | 59 | def __init__(self, authenticate_header): 60 | """ 61 | Extract service and realm from the header. 62 | 63 | The scope is not provided by the token server because we are accessing the endpoint from 64 | the root. 65 | """ 66 | self.scopes = [] 67 | 68 | if not authenticate_header.lower().startswith("bearer "): 69 | raise Exception(f"Authentication header has wrong format.\n{authenticate_header}") 70 | for item in authenticate_header[7:].split(","): 71 | key, value = item.split("=") 72 | if key == "scope": 73 | self.scopes.append(value.strip('"')) 74 | else: 75 | setattr(self, key, value.strip('"')) 76 | 77 | 78 | def get_auth_for_url(registry_endpoint_url, auth=None): 79 | """Return authentication details based on the the status of token authentication.""" 80 | if settings.TOKEN_AUTH_DISABLED: 81 | auth = () 82 | else: 83 | with pytest.raises(requests.HTTPError): 84 | response = requests.get(registry_endpoint_url) 85 | response.raise_for_status() 86 | assert response.status_code == 401 87 | 88 | authenticate_header = response.headers["WWW-Authenticate"] 89 | queries = AuthenticationHeaderQueries(authenticate_header) 90 | content_response = requests.get( 91 | queries.realm, params={"service": queries.service, "scope": queries.scopes}, auth=auth 92 | ) 93 | content_response.raise_for_status() 94 | token = content_response.json()["token"] 95 | auth = BearerTokenAuth(token) 96 | 97 | return auth 98 | -------------------------------------------------------------------------------- /pulp_container/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulp/pulp_container/8b99c2e13fb72f738aa6e601fcce07d5b2f9792b/pulp_container/tests/unit/__init__.py -------------------------------------------------------------------------------- /pulp_container/tests/unit/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class TestNothing(TestCase): 5 | """Test Nothing (placeholder).""" 6 | 7 | def test_nothing_at_all(self): 8 | """Test that the tests are running and that's it.""" 9 | self.assertTrue(True) 10 | -------------------------------------------------------------------------------- /releasing.md: -------------------------------------------------------------------------------- 1 | [//]: # "WARNING: DO NOT EDIT!" 2 | [//]: # "" 3 | [//]: # "This file was generated by plugin_template, and is managed by it. Please use" 4 | [//]: # "'./plugin-template --github pulp_container' to update this file." 5 | [//]: # "" 6 | [//]: # "For more info visit https://github.com/pulp/plugin_template" 7 | # Releasing (For Internal Use) 8 | 9 | This document outlines the steps to perform a release. 10 | 11 | ### Determine if a Release is Required 12 | - Make sure to have GitPython python package installed 13 | - Run the release checker script: 14 | ``` 15 | python3 .ci/scripts/check_release.py 16 | ``` 17 | 18 | ### Release a New Y-version (e.g., 3.23.0) 19 | - If a new minor version (Y) is needed, trigger a [Create New Release Branch](https://github.com/pulp/pulp_container/actions/workflows/create-branch.yml) job from the main branch via the GitHub Actions. 20 | - Look for the "Bump minor version" pull request and merge it. 21 | - Trigger a [Release Pipeline](https://github.com/pulp/pulp_container/actions/workflows/release.yml) job by specifying the new release branch (X.**Y**) via the GitHub Actions. 22 | 23 | ### Release a New Z-version (Patch Release) (e.g., 3.23.1, 3.22.12) 24 | - Trigger a [Release Pipeline](https://github.com/pulp/pulp_container/actions/workflows/release.yml) job by specifying the release branch (X.Y) via the GitHub Actions. 25 | 26 | ## Final Steps 27 | - Ensure the new version appears on PyPI (it should appear after [Publish Release](https://github.com/pulp/pulp_container/actions/workflows/publish.yml) workflow succeeds). 28 | - Verify that the changelog has been updated by looking for the "Update Changelog" pull request (A new PR should be available on the next day). 29 | - [optional] Post a brief announcement about the new release on the [Pulp Discourse](https://discourse.pulpproject.org/). 30 | -------------------------------------------------------------------------------- /template_config.yml: -------------------------------------------------------------------------------- 1 | # This config represents the latest values used when running the plugin-template. Any settings that 2 | # were not present before running plugin-template have been added with their default values. 3 | 4 | # generated with plugin_template 5 | 6 | api_root: /pulp/ 7 | black: true 8 | check_commit_message: true 9 | check_gettext: true 10 | check_manifest: true 11 | check_stray_pulpcore_imports: true 12 | ci_base_image: ghcr.io/pulp/pulp-ci-centos9 13 | ci_env: {} 14 | ci_trigger: '{pull_request: {branches: [''*'']}}' 15 | cli_package: pulp-cli 16 | cli_repo: https://github.com/pulp/pulp-cli.git 17 | core_import_allowed: [] 18 | deploy_client_to_pypi: true 19 | deploy_client_to_rubygems: true 20 | deploy_to_pypi: true 21 | disabled_redis_runners: [] 22 | docker_fixtures: false 23 | flake8: true 24 | flake8_ignore: [] 25 | github_org: pulp 26 | latest_release_branch: '2.25' 27 | lint_requirements: true 28 | os_required_packages: [] 29 | parallel_test_workers: 8 30 | plugin_app_label: container 31 | plugin_default_branch: main 32 | plugin_name: pulp_container 33 | plugins: 34 | - app_label: container 35 | name: pulp_container 36 | post_job_template: null 37 | pre_job_template: null 38 | pulp_env: {} 39 | pulp_env_azure: {} 40 | pulp_env_gcp: {} 41 | pulp_env_s3: {} 42 | pulp_scheme: https 43 | pulp_settings: 44 | allowed_content_checksums: 45 | - sha1 46 | - sha224 47 | - sha256 48 | - sha384 49 | - sha512 50 | allowed_export_paths: 51 | - /tmp 52 | allowed_import_paths: 53 | - /tmp 54 | flatpak_index: true 55 | pulp_settings_azure: 56 | content_origin: null 57 | domain_enabled: true 58 | flatpak_index: true 59 | pulp_settings_gcp: null 60 | pulp_settings_s3: 61 | domain_enabled: true 62 | flatpak_index: false 63 | token_auth_disabled: true 64 | pydocstyle: true 65 | release_email: pulp-infra@redhat.com 66 | release_user: pulpbot 67 | stalebot: true 68 | stalebot_days_until_close: 30 69 | stalebot_days_until_stale: 90 70 | stalebot_limit_to_pulls: true 71 | supported_release_branches: 72 | - '2.14' 73 | - '2.15' 74 | - '2.16' 75 | - '2.19' 76 | - '2.20' 77 | sync_ci: true 78 | test_azure: true 79 | test_cli: true 80 | test_deprecations: true 81 | test_gcp: false 82 | test_lowerbounds: true 83 | test_performance: false 84 | test_reroute: true 85 | test_s3: true 86 | test_storages_compat_layer: true 87 | use_issue_template: true 88 | 89 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | # All test requirements 2 | -r functest_requirements.txt 3 | -r unittest_requirements.txt 4 | -------------------------------------------------------------------------------- /unittest_requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | pytest-django 3 | pytest<8 4 | --------------------------------------------------------------------------------