├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs
├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── VERSION
├── _config.yml
├── _data
│ ├── navigation.yml
│ ├── quizzes
│ │ └── example-quiz.yml
│ └── toc.yml
├── _docs
│ └── getting-started.md
├── _includes
│ ├── alert.html
│ ├── doc.html
│ ├── editable.html
│ ├── feedback.html
│ ├── footer.html
│ ├── google-analytics.html
│ ├── head.html
│ ├── header.html
│ ├── logo.svg
│ ├── navigation.html
│ ├── permalinks.html
│ ├── quiz.html
│ ├── quiz
│ │ └── multiple-choice.html
│ ├── scripts.html
│ ├── scrolltop.html
│ ├── sidebar.html
│ ├── tags.html
│ └── toc.html
├── _layouts
│ ├── default.html
│ ├── page.html
│ └── post.html
├── _posts
│ └── 2019-11-03-hello-world.md
├── assets
│ ├── css
│ │ ├── main.css
│ │ └── palette.css
│ ├── favicons
│ │ ├── android-icon-144x144.png
│ │ ├── android-icon-192x192.png
│ │ ├── android-icon-36x36.png
│ │ ├── android-icon-48x48.png
│ │ ├── android-icon-72x72.png
│ │ ├── android-icon-96x96.png
│ │ ├── apple-icon-114x114.png
│ │ ├── apple-icon-120x120.png
│ │ ├── apple-icon-144x144.png
│ │ ├── apple-icon-152x152.png
│ │ ├── apple-icon-180x180.png
│ │ ├── apple-icon-57x57.png
│ │ ├── apple-icon-60x60.png
│ │ ├── apple-icon-72x72.png
│ │ ├── apple-icon-76x76.png
│ │ ├── apple-icon-precomposed.png
│ │ ├── apple-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── manifest.json
│ │ ├── ms-icon-144x144.png
│ │ ├── ms-icon-150x150.png
│ │ ├── ms-icon-310x310.png
│ │ └── ms-icon-70x70.png
│ ├── img
│ │ ├── docker-clear.png
│ │ ├── docker.png
│ │ ├── docsy-jekyll-preview.png
│ │ ├── docsy-jekyll.png
│ │ ├── favicon.png
│ │ └── logo.png
│ ├── js
│ │ ├── lunr.min.js
│ │ ├── main.js
│ │ └── search.js
│ └── webfonts
│ │ ├── fa-brands-400.eot
│ │ ├── fa-brands-400.svg
│ │ ├── fa-brands-400.ttf
│ │ ├── fa-brands-400.woff
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-regular-400.eot
│ │ ├── fa-regular-400.svg
│ │ ├── fa-regular-400.ttf
│ │ ├── fa-regular-400.woff
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-solid-900.eot
│ │ ├── fa-solid-900.svg
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-solid-900.woff
│ │ └── fa-solid-900.woff2
└── pages
│ ├── about.md
│ ├── archive.md
│ ├── docs.md
│ ├── feed.xml
│ ├── index.md
│ ├── news.md
│ ├── search.html
│ └── sitemap.xml
├── opencontainers
├── __init__.py
├── digest
│ ├── __init__.py
│ ├── algorithm.py
│ ├── digest.py
│ ├── digester.py
│ ├── exceptions.py
│ └── verifiers.py
├── distribution
│ ├── __init__.py
│ ├── reggie
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── config.py
│ │ ├── defaults.py
│ │ ├── request.py
│ │ └── response.py
│ ├── specs.py
│ └── v1
│ │ ├── __init__.py
│ │ ├── error.py
│ │ ├── repository.py
│ │ └── tags.py
├── image
│ ├── __init__.py
│ ├── specs.py
│ └── v1
│ │ ├── __init__.py
│ │ ├── annotations.py
│ │ ├── config.py
│ │ ├── descriptor.py
│ │ ├── index.py
│ │ ├── layout.py
│ │ ├── manifest.py
│ │ └── mediatype.py
├── logger.py
├── struct.py
├── tests
│ ├── __init__.py
│ ├── mock_server.py
│ ├── test_algorithm.py
│ ├── test_config.py
│ ├── test_descriptor.py
│ ├── test_digest.py
│ ├── test_distribution.py
│ ├── test_imageindex.py
│ ├── test_imagelayout.py
│ ├── test_manifest.py
│ ├── test_struct.py
│ └── test_verifiers.py
└── version.py
├── setup.cfg
└── setup.py
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team leader @vsoch. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributor's Agreement
2 |
3 | This code is licensed under the MPL 2.0 [LICENSE](LICENSE).
4 |
5 | # Contributing
6 |
7 | When contributing to OpenContainers Python, it is important to properly communicate the
8 | gist of the contribution. If it is a simple code or editorial fix, simply
9 | explaining this within the GitHub Pull Request (PR) will suffice. But if this
10 | is a larger fix or enhancement, it should be first discussed with the project
11 | leader or developers.
12 |
13 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md) that you should follow in
14 | all your interactions with the project members and users.
15 |
16 | ## Pull Request Process
17 |
18 | 1. Send PRs to the master branch for merge as a pypi release.
19 | 2. Follow the existing code style precedent to the best of your ability (format with black)
20 | 3. Test your PR locally, and provide the steps necessary to test for the reviewers.
21 | 4. The project's default copyright and header have been included in any new source files.
22 | 5. All (major) changes must be documented in [docs](../docs).
23 | 6. If necessary, update the README.md and CHANGELOG.md in the root of the repository.
24 | 7. The pull request will be reviewed by others, and the final merge must be
25 | done by the project lead, @vsoch (or approved by her).
26 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: oci-python-ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches_ignore: []
9 |
10 | jobs:
11 | formatting:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v1
15 |
16 | - name: Setup black linter
17 | run: conda create --quiet --name black
18 |
19 | - name: Lint python code
20 | run: |
21 | export PATH="/usr/share/miniconda/bin:$PATH"
22 | source activate black
23 | pip install black
24 | black --check opencontainers
25 |
26 | testing:
27 | runs-on: ubuntu-latest
28 | needs: formatting
29 | steps:
30 | - uses: actions/checkout@v1
31 |
32 | - name: Setup OpenContainers Python environment
33 | run: |
34 | conda create --quiet --name ocipython
35 | conda install pytest
36 |
37 | - name: Run tests
38 | env:
39 | CI: true
40 | run: |
41 |
42 | # activate conda env
43 | export PATH="/usr/share/miniconda/bin:$PATH"
44 | source activate ocipython
45 |
46 | # run tests
47 | pytest opencontainers/tests/test*.py -v -x
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.egg-info*
3 | .eggs
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | This is a manually generated log to track changes to the repository for each release.
4 | Each section should include general headers such as **Implemented enhancements**
5 | and **Merged pull requests**. All closed issued and bug fixes should be
6 | represented by the pull requests that fixed them.
7 | Critical items to know are:
8 |
9 | - renamed commands
10 | - deprecated / removed commands
11 | - changed defaults
12 | - backward incompatible changes
13 | - migration guidance
14 | - changed behaviour
15 |
16 | Versions here coincide with releases on pypi.
17 |
18 | ## [master](https://github.com/vsoch/oci-python)
19 | - do not set basic auth if no username/password provided (0.0.14)
20 | - allow for update of a structure attribute, if applicable (0.0.13)
21 | - fix to bug with parsing www-Authenticate (0.0.12)
22 | - adding distribution spec (0.0.11)
23 | - adding image-spec and digests (0.0.1)
24 | - skeleton of package while core under development (0.0.0)
25 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md LICENSE
2 | recursive-include opencontainers *
3 | graft opencontainers
4 | prune .env
5 | prune doc*
6 | prune .doc*
7 | recursive-exclude * __pycache__
8 | recursive-exclude * docs
9 | recursive-exclude * .docs
10 | recursive-exclude * *.pyc
11 | recursive-exclude * *.pyo
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Open Containers Python
2 |
3 | [](https://pypi.org/project/opencontainers/)
4 | [](https://github.com/vsoch/oci-python/actions?query=branch%3Amaster+workflow%3Aoci-python-ci)
5 |
6 | A simple Python implementation of Open Containers specifications. The code
7 | is intentionally structured to mirror the go implementations for usability.
8 | This include:
9 |
10 | - [opencontainers/image-spec](https://github.com/opencontainers/image-spec/tree/master/specs-go) maps to [opencontainers/image](opencontainers/image)
11 | - [opencontainers/go-digest](https://github.com/opencontainers/go-digest) maps to [opencontainers/digest](opencontainers/digest)
12 | - [opencontainers/distribution-spec](https://github.com/opencontainers/distribution-spec) maps to [opencontainers/distribution](opencontainers/distribution), which also includes a Python version of the [Reggie client](https://github.com/bloodorangeio/reggie) to interact with an OCI registry.
13 |
14 | See the documentation at [vsoch.github.io/oci-python](https://vsoch.github.io/oci-python).
15 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | _site
3 | Gemfile.lock
4 |
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This is a manually generated log to track changes to the repository for each release.
4 | Each section should include general headers such as **Implemented enhancements**
5 | and **Merged pull requests**. All closed issued and bug fixes should be
6 | represented by the pull requests that fixed them.
7 | Critical items to know are:
8 |
9 | - renamed commands
10 | - deprecated / removed commands
11 | - changed defaults
12 | - backward incompatible changes
13 | - migration guidance
14 | - changed behaviour
15 |
16 | ## [master](https://github.com/vsoch/docsy-jekyll/tree/master)
17 | - adding site.url to config, making links in readme absolute (0.0.1)
18 | - start of theme (0.0.0)
19 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | ruby RUBY_VERSION
3 |
4 | # Hello! This is where you manage which Jekyll version is used to run.
5 | # When you want to use a different version, change it below, save the
6 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
7 | #
8 | # bundle exec jekyll serve
9 | #
10 | # This will help ensure the proper Jekyll version is running.
11 | # Happy Jekylling!
12 | # gem "jekyll", "3.2.1"
13 |
14 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
15 | # gem "minima"
16 |
17 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
18 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
19 | gem "github-pages", group: :jekyll_plugins
20 |
21 | # If you have any plugins, put them here!
22 | # group :jekyll_plugins do
23 | # gem "jekyll-github-metadata", "~> 1.0"
24 | # end
25 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Docsy Jekyll Theme
2 |
3 | [](https://circleci.com/gh/vsoch/docsy-jekyll/tree/master)
4 |
5 |
6 |
7 |
8 | 
9 |
10 | This is a [starter template](https://vsoch.github.com/docsy-jekyll/) for a Docsy jekyll theme, based
11 | on the Beautiful [Docsy](https://github.com/google/docsy) that renders with Hugo. This version is intended for
12 | native deployment on GitHub pages. The original [Apache License](https://github.com/vsoch/docsy-jekyll/blob/master/LICENSE) is included.
13 |
14 | ## Changes
15 |
16 | The site is intended for purely documentation, so while the front page banner
17 | is useful for business or similar, this author (@vsoch) preferred to have
18 | the main site page go directly to the Documentation view. Posts
19 | are still provided via a feed.
20 |
21 | ## Usage
22 |
23 | ### 1. Get the code
24 |
25 | You can clone the repository right to where you want to host the docs:
26 |
27 | ```bash
28 | git clone https://github.com/vsoch/docsy-jekyll.git docs
29 | cd docs
30 | ```
31 |
32 | ### 2. Customize
33 |
34 | To edit configuration values, customize the [_config.yml](https://github.com/vsoch/docsy-jekyll/blob/master/_config.yml).
35 | To add pages, write them into the [pages](https://github.com/vsoch/docsy-jekyll/blob/master/pages) folder.
36 | You define urls based on the `permalink` attribute in your pages,
37 | and then add them to the navigation by adding to the content of [_data/toc.myl](https://github.com/vsoch/docsy-jekyll/blob/master/_data/toc.yml).
38 | The top navigation is controlled by [_data/navigation.yml](https://github.com/vsoch/docsy-jekyll/blob/master/_data/navigation.yml)
39 |
40 | ### 3. Options
41 |
42 | Most of the configuration values in the [_config.yml](https://github.com/vsoch/docsy-jekyll/blob/master/_config.yml) are self explanatory,
43 | and for more details, see the [getting started page](https://vsoch.github.io/docsy-jekyll/docs/getting-started)
44 | rendered on the site.
45 |
46 | ### 4. Serve
47 |
48 | Depending on how you installed jekyll:
49 |
50 | ```bash
51 | jekyll serve
52 | # or
53 | bundle exec jekyll serve
54 | ```
55 |
--------------------------------------------------------------------------------
/docs/Rakefile:
--------------------------------------------------------------------------------
1 | require 'html-proofer'
2 |
3 | # See here for ideas: https://github.com/keegoid/keegoid.github.io/blob/master/Rakefile
4 | def build_site
5 | sh "bundle exec jekyll build"
6 | end
7 |
8 | def doctor_site
9 | sh 'bundle exec jekyll doctor'
10 | end
11 |
12 | def html_proofer
13 | options = {
14 | :url_ignore => [/localhost/],
15 | :empty_alt_ignore => true,
16 | :allow_hash_href => true, # don't break on
17 | :assume_extension => false, # (true) for extensionless paths
18 | :http_status_ignore => [
19 | 999, # LinkedIn throttling errors
20 | 403, # Google scholar errors thrown from Travis (links here will be public anyway)
21 | ],
22 | :typhoeus => {
23 | :connecttimeout => 20,
24 | :timeout => 60,
25 | # avoid strange SSL errors: https://github.com/gjtorikian/html-proofer/issues/376
26 | :ssl_verifypeer => false,
27 | :ssl_verifyhost => 0
28 | }
29 | }
30 | HTMLProofer.check_directory("./_site", options).run
31 | end
32 |
33 | task :test do
34 | build_site
35 | doctor_site
36 | html_proofer
37 | end
38 |
39 | task :test_local do
40 | # Already built so don't build again
41 | doctor_site
42 | html_proofer
43 | end
44 |
45 | task :default => :test
46 |
--------------------------------------------------------------------------------
/docs/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.1
2 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing these this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'jekyll serve'. If you change this file, please restart the server process.
10 |
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 |
17 | title: OpenContainers Python
18 | email: vsochat@stnaford.edu
19 | author: "@vsoch"
20 | description: > # this means to ignore newlines until "baseurl:"
21 | A simple Python implementation of Open Containers specifications. The code
22 | is intentionally structured to mirror the go implementations for usability.
23 |
24 | # DO NOT CHANGE THE LINE OF THIS FILE without editing .circleci/circle_urls.sh
25 | baseurl: "/oci-python" # the subpath of your site, e.g. /blog
26 |
27 | # This is mostly for testing
28 | url: "https://vsoch.github.io" # the base hostname & protocol for your site
29 |
30 | # Social (First three Required)
31 | repo: "https://github.com/vsoch/oci-python"
32 | github_user: "vsoch"
33 | github_repo: "oci-python"
34 |
35 | # Optional
36 | twitter: vsoch
37 | linkedin: vsochat
38 | dockerhub: vanessa
39 |
40 | # Should there be feedback buttons at the bottom of pages?
41 | feedback: true
42 |
43 | # Link to a privacy policy in footer, uncomment and define if wanted
44 | # privacy: https://domain.com/privacy
45 |
46 | # google-analytics: UA-XXXXXXXXXX
47 | # Image and (square) dimension for logo (don't start with /)
48 | # If commented, will use material hat theme
49 | # logo: "assets/img/logo/SRCC-square-red.png"
50 | logo_pixels: 34
51 | color: "#30638e"
52 | # color: "#8c1515" # primary color for header, buttons
53 |
54 | # Build settings
55 | markdown: kramdown
56 |
57 | # If you add tags to pages, you can link them to some external search
58 | # If you want to disable this, comment the URL.
59 | tag_search_endpoint: https://ask.cyberinfrastructure.org/search?q=
60 | tag_color: danger # danger, success, warning, primary, info, secondary
61 |
62 | accentColor: red # purple, green, etc.
63 | themeColor: red # purple, green, blue, orange, purple, grey
64 | fixedNav: 'true' # true or false
65 |
66 | permalink: /:year/:title/
67 | markdown: kramdown
68 | exclude: [_site, CHANGELOG.md, LICENSE, README.md, vendor]
69 |
70 | # Collections
71 | collections:
72 | docs:
73 | output: true
74 | permalink: /:collection/:path
75 |
76 | # Defaults
77 | defaults:
78 | - scope:
79 | path: "_docs"
80 | type: "docs"
81 | values:
82 | layout: page
83 | -
84 | scope:
85 | path: ""
86 | type: "pages"
87 | values:
88 | layout: "page"
89 | -
90 | scope:
91 | path: "posts"
92 | type: "posts"
93 | values:
94 | layout: "post"
95 |
--------------------------------------------------------------------------------
/docs/_data/navigation.yml:
--------------------------------------------------------------------------------
1 | - title: About
2 | url: about
3 | - title: Documentation
4 | url: docs
5 |
--------------------------------------------------------------------------------
/docs/_data/quizzes/example-quiz.yml:
--------------------------------------------------------------------------------
1 | title: This is the Quiz Title
2 | randomized: false
3 | questions:
4 |
5 | - type: "multiple-choice"
6 | question: "What is your favorite color?"
7 | items:
8 | - choice: Red
9 | correct: null
10 | - choice: Blue
11 | correct: null
12 | - choice: Green
13 | correct: null
14 | followup: There is no correct answer to asking your favorite color! All choices would be good.
15 |
16 | - type: "multiple-choice"
17 | question: "True or False, Pittsburgh is West of Philadelphia"
18 | items:
19 | - choice: True
20 | correct: true
21 | - choice: False
22 | correct: false
23 | followup: |
24 | The answer is True! Pittsburgh is 304.9 miles West of Philadelphia, or approximately
25 | a car ride of 4 hours and 52 minutes. Buckle up!
26 |
--------------------------------------------------------------------------------
/docs/_data/toc.yml:
--------------------------------------------------------------------------------
1 | - title: Documentation
2 | url: docs
3 | links:
4 | - title: "Getting Started"
5 | url: "docs/getting-started"
6 | children:
7 | - title: Install
8 | url: "docs/getting-started#getting-started"
9 | - title: "Distribution Specification"
10 | url: "docs/getting-started#distribution-specification"
11 | children:
12 | - title: Reggie Client
13 | url: "docs/getting-started#reggie"
14 | - title: "Image Specification"
15 | url: "docs/getting-started#image-specification"
16 | children:
17 | - title: Manifests
18 | url: "docs/getting-started#image-manifest"
19 | - title: Descriptor
20 | url: "docs/getting-started#descriptor"
21 | - title: Digests
22 | url: "docs/getting-started#digest"
23 | - title: Algorithms
24 | url: "docs/getting-started#algorithms"
25 | - title: "About"
26 | url: "about"
27 | - title: "News"
28 | url: "news"
29 |
--------------------------------------------------------------------------------
/docs/_includes/alert.html:
--------------------------------------------------------------------------------
1 |
2 |
{% if include.title %}{{ include.title }}{% else %}{{ include.type }}{% endif %}
3 | {{ include.content }}
4 |
5 |
--------------------------------------------------------------------------------
/docs/_includes/doc.html:
--------------------------------------------------------------------------------
1 | {% if include.name %}{{ include.name }}{% else %}{{ include.path }}{% endif %}
2 |
--------------------------------------------------------------------------------
/docs/_includes/editable.html:
--------------------------------------------------------------------------------
1 | Edit this page
2 | Create documentation issue
3 | Create project issue
4 |
7 |
--------------------------------------------------------------------------------
/docs/_includes/feedback.html:
--------------------------------------------------------------------------------
1 | {% if site.feedback %}
16 | Feedback
17 | Was this page helpful?
18 | Yes
19 | No
20 |
21 | Glad to hear it! Please tell us how we can improve .
22 |
23 |
24 | Sorry to hear that. Please tell us how we can improve .
25 |
26 | {% endif %}
58 |
59 |
--------------------------------------------------------------------------------
/docs/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% if site.twitter %}
{% endif %}
13 |
14 |
23 |
24 |
© 2019 {{ site.author }} All Rights Reserved
25 | {% if site.privacy %}
Privacy Policy {% endif %}
26 |
About Docsy
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/docs/_includes/google-analytics.html:
--------------------------------------------------------------------------------
1 | {% if site.google-analytics %}
2 | {% endif %}
10 |
--------------------------------------------------------------------------------
/docs/_includes/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/_includes/header.html:
--------------------------------------------------------------------------------
1 |
32 |
33 |
49 |
--------------------------------------------------------------------------------
/docs/_includes/navigation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Skip to content
10 |
61 |
--------------------------------------------------------------------------------
/docs/_includes/permalinks.html:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/docs/_includes/quiz.html:
--------------------------------------------------------------------------------
1 | {% assign quiz = site.data.quizzes[include.file] %}
2 | {% if quiz.randomize == true %}{% assign questions = quiz.questions | sample %}{% else %}{% assign questions = quiz.questions %}{% endif %}{% if quiz.title %}{{ quiz.title }} {% endif %}{% for item in questions %}
3 | {% if item.type == "multiple-choice" %}{% include quiz/multiple-choice.html item=item count=forloop.index %}{% endif %} {% endfor %}
4 |
--------------------------------------------------------------------------------
/docs/_includes/quiz/multiple-choice.html:
--------------------------------------------------------------------------------
1 |
2 |
{% if include.item.question %}{{ include.item.question }}{% else %}Question {{ include.count }}{% endif %}
3 |
{% for choice in include.item.items %}{% if choice.correct == true %}{% endif %}{{ forloop.index }}. {{ choice.choice }}{% if choice.correct == true %} {% endif %} {% endfor %}
4 |
Show Answer
5 | {% if include.item.followup %}{{ include.item.followup }}
{% endif %}
6 |
--------------------------------------------------------------------------------
/docs/_includes/scripts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/_includes/scrolltop.html:
--------------------------------------------------------------------------------
1 |
23 | 🔝
24 |
25 |
43 |
--------------------------------------------------------------------------------
/docs/_includes/sidebar.html:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/docs/_includes/tags.html:
--------------------------------------------------------------------------------
1 | {% if site.tag_search_endpoint %}{% if page.tags %}{% endif %}{% endif %}
3 |
--------------------------------------------------------------------------------
/docs/_includes/toc.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
36 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% include head.html %}
4 | {% include google-analytics.html %}
5 |
6 | {% include header.html %}
7 |
8 |
9 |
10 |
13 |
14 |
15 | {% include editable.html %}
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 | {{ page.title }}
28 |
29 |
30 |
31 |
32 | {{ content }}
33 | {% if section.links %}
34 |
{% for child in section.links %}
35 |
36 |
39 |
{{ child.description }}
40 |
{% endfor %}
41 |
{% endif %}
42 | {% include feedback.html %}
43 |
44 |
45 |
46 |
47 | {% include footer.html %}
48 |
49 | {% include scripts.html %}
50 |
51 |
52 |
--------------------------------------------------------------------------------
/docs/_layouts/page.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 | {{ content }}
5 | {% include toc.html %}
6 | {% include permalinks.html %}
7 |
--------------------------------------------------------------------------------
/docs/_layouts/post.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | ---
4 |
5 | {{ page.title }}
6 | {% if page.badges %}{% for badge in page.badges %}{{ badge.tag }} {% endfor %}{% endif %}
7 | {{ page.date | date: "%B %d, %Y" }}
8 | {{ content }}
9 |
--------------------------------------------------------------------------------
/docs/_posts/2019-11-03-hello-world.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Start of Development"
3 | date: 2019-11-03 18:52:21
4 | categories: jekyll update
5 | badges:
6 | - type: info
7 | tag: development
8 | ---
9 |
10 | Hello World! The OpenContainers Python code base, testing, and documentation
11 | is under development! It's a fun side project, so I don't have a timeline other
12 | than to work on it when it feels fun :)
13 |
--------------------------------------------------------------------------------
/docs/assets/css/palette.css:
--------------------------------------------------------------------------------
1 | .highlight pre {
2 | border: none !important;
3 | }
4 |
5 | .highlight table td { padding: 5px; }
6 | .highlight table pre { margin: 0; }
7 | .highlight .cm {
8 | color: #999988;
9 | font-style: italic;
10 | }
11 | .highlight .cp {
12 | color: #999999;
13 | font-weight: bold;
14 | }
15 | .highlight .c1 {
16 | color: #999988;
17 | font-style: italic;
18 | }
19 | .highlight .cs {
20 | color: #999999;
21 | font-weight: bold;
22 | font-style: italic;
23 | }
24 | .highlight .c, .highlight .ch, .highlight .cd, .highlight .cpf {
25 | color: #999988;
26 | font-style: italic;
27 | }
28 | .highlight .err {
29 | color: #a61717;
30 | background-color: #e3d2d2;
31 | }
32 | .highlight .gd {
33 | color: #000000;
34 | background-color: #ffdddd;
35 | }
36 | .highlight .ge {
37 | color: #000000;
38 | font-style: italic;
39 | }
40 | .highlight .gr {
41 | color: #aa0000;
42 | }
43 | .highlight .gh {
44 | color: #999999;
45 | }
46 | .highlight .gi {
47 | color: #000000;
48 | background-color: #ddffdd;
49 | }
50 | .highlight .go {
51 | color: #888888;
52 | }
53 | .highlight .gp {
54 | color: #555555;
55 | }
56 | .highlight .gs {
57 | font-weight: bold;
58 | }
59 | .highlight .gu {
60 | color: #aaaaaa;
61 | }
62 | .highlight .gt {
63 | color: #aa0000;
64 | }
65 | .highlight .kc {
66 | color: #000000;
67 | font-weight: bold;
68 | }
69 | .highlight .kd {
70 | color: #000000;
71 | font-weight: bold;
72 | }
73 | .highlight .kn {
74 | color: #000000;
75 | font-weight: bold;
76 | }
77 | .highlight .kp {
78 | color: #000000;
79 | font-weight: bold;
80 | }
81 | .highlight .kr {
82 | color: #000000;
83 | font-weight: bold;
84 | }
85 | .highlight .kt {
86 | color: #445588;
87 | font-weight: bold;
88 | }
89 | .highlight .k, .highlight .kv {
90 | color: #000000;
91 | font-weight: bold;
92 | }
93 | .highlight .mf {
94 | color: #009999;
95 | }
96 | .highlight .mh {
97 | color: #009999;
98 | }
99 | .highlight .il {
100 | color: #009999;
101 | }
102 | .highlight .mi {
103 | color: #009999;
104 | }
105 | .highlight .mo {
106 | color: #009999;
107 | }
108 | .highlight .m, .highlight .mb, .highlight .mx {
109 | color: #009999;
110 | }
111 | .highlight .sb {
112 | color: #d14;
113 | }
114 | .highlight .sc {
115 | color: #d14;
116 | }
117 | .highlight .sd {
118 | color: #d14;
119 | }
120 | .highlight .s2 {
121 | color: #d14;
122 | }
123 | .highlight .se {
124 | color: #d14;
125 | }
126 | .highlight .sh {
127 | color: #d14;
128 | }
129 | .highlight .si {
130 | color: #d14;
131 | }
132 | .highlight .sx {
133 | color: #d14;
134 | }
135 | .highlight .sr {
136 | color: #009926;
137 | }
138 | .highlight .s1 {
139 | color: #d14;
140 | }
141 | .highlight .ss {
142 | color: #990073;
143 | }
144 | .highlight .s, .highlight .sa, .highlight .dl {
145 | color: #d14;
146 | }
147 | .highlight .na {
148 | color: #008080;
149 | }
150 | .highlight .bp {
151 | color: #999999;
152 | }
153 | .highlight .nb {
154 | color: #0086B3;
155 | }
156 | .highlight .nc {
157 | color: #445588;
158 | font-weight: bold;
159 | }
160 | .highlight .no {
161 | color: #008080;
162 | }
163 | .highlight .nd {
164 | color: #3c5d5d;
165 | font-weight: bold;
166 | }
167 | .highlight .ni {
168 | color: #800080;
169 | }
170 | .highlight .ne {
171 | color: #990000;
172 | font-weight: bold;
173 | }
174 | .highlight .nf, .highlight .fm {
175 | color: #990000;
176 | font-weight: bold;
177 | }
178 | .highlight .nl {
179 | color: #990000;
180 | font-weight: bold;
181 | }
182 | .highlight .nn {
183 | color: #555555;
184 | }
185 | .highlight .nt {
186 | color: #000080;
187 | }
188 | .highlight .vc {
189 | color: #008080;
190 | }
191 | .highlight .vg {
192 | color: #008080;
193 | }
194 | .highlight .vi {
195 | color: #008080;
196 | }
197 | .highlight .nv, .highlight .vm {
198 | color: #008080;
199 | }
200 | .highlight .ow {
201 | color: #000000;
202 | font-weight: bold;
203 | }
204 | .highlight .o {
205 | color: #000000;
206 | font-weight: bold;
207 | }
208 | .highlight .w {
209 | color: #bbbbbb;
210 | }
211 | .highlight {
212 | background-color: #f8f8f8;
213 | }
214 |
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-144x144.png
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-192x192.png
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-36x36.png
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-48x48.png
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-72x72.png
--------------------------------------------------------------------------------
/docs/assets/favicons/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/android-icon-96x96.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/docs/assets/favicons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/apple-icon.png
--------------------------------------------------------------------------------
/docs/assets/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/docs/assets/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/assets/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/assets/favicons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/favicon-96x96.png
--------------------------------------------------------------------------------
/docs/assets/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/favicon.ico
--------------------------------------------------------------------------------
/docs/assets/favicons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "\/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/docs/assets/favicons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/docs/assets/favicons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/docs/assets/favicons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/docs/assets/favicons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/favicons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/docs/assets/img/docker-clear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/docker-clear.png
--------------------------------------------------------------------------------
/docs/assets/img/docker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/docker.png
--------------------------------------------------------------------------------
/docs/assets/img/docsy-jekyll-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/docsy-jekyll-preview.png
--------------------------------------------------------------------------------
/docs/assets/img/docsy-jekyll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/docsy-jekyll.png
--------------------------------------------------------------------------------
/docs/assets/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/favicon.png
--------------------------------------------------------------------------------
/docs/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/img/logo.png
--------------------------------------------------------------------------------
/docs/assets/js/main.js:
--------------------------------------------------------------------------------
1 | ---
2 | exclude_in_search: true
3 | layout: null
4 | ---
5 | (function($) {
6 | 'use strict';
7 | $(function() {
8 | $('[data-toggle="tooltip"]').tooltip();
9 | $('[data-toggle="popover"]').popover();
10 | $('.popover-dismiss').popover({
11 | trigger: 'focus'
12 | })
13 | });
14 |
15 | function bottomPos(element) {
16 | return element.offset().top + element.outerHeight();
17 | }
18 | $(function() {
19 | var promo = $(".js-td-cover");
20 | if (!promo.length) {
21 | return
22 | }
23 | var promoOffset = bottomPos(promo);
24 | var navbarOffset = $('.js-navbar-scroll').offset().top;
25 | var threshold = Math.ceil($('.js-navbar-scroll').outerHeight());
26 | if ((promoOffset - navbarOffset) < threshold) {
27 | $('.js-navbar-scroll').addClass('navbar-bg-onscroll');
28 | }
29 | $(window).on('scroll', function() {
30 | var navtop = $('.js-navbar-scroll').offset().top - $(window).scrollTop();
31 | var promoOffset = bottomPos($('.js-td-cover'));
32 | var navbarOffset = $('.js-navbar-scroll').offset().top;
33 | if ((promoOffset - navbarOffset) < threshold) {
34 | $('.js-navbar-scroll').addClass('navbar-bg-onscroll');
35 | } else {
36 | $('.js-navbar-scroll').removeClass('navbar-bg-onscroll');
37 | $('.js-navbar-scroll').addClass('navbar-bg-onscroll--fade');
38 | }
39 | });
40 | });
41 | }(jQuery));
42 | (function($) {
43 | 'use strict';
44 | var Search = {
45 | init: function() {
46 | $(document).ready(function() {
47 | $(document).on('keypress', '.td-search-input', function(e) {
48 | if (e.keyCode !== 13) {
49 | return
50 | }
51 | var query = $(this).val();
52 | var searchPage = "{{ site.url }}{{ site.baseurl }}/search/?q=" + query;
53 | document.location = searchPage;
54 | return false;
55 | });
56 | });
57 | },
58 | };
59 | Search.init();
60 | }(jQuery));
61 |
--------------------------------------------------------------------------------
/docs/assets/js/search.js:
--------------------------------------------------------------------------------
1 | ---
2 | layout: null
3 | excluded_in_search: true
4 | ---
5 | (function () {
6 | function getQueryVariable(variable) {
7 | var query = window.location.search.substring(1),
8 | vars = query.split("&");
9 |
10 | for (var i = 0; i < vars.length; i++) {
11 | var pair = vars[i].split("=");
12 |
13 | if (pair[0] === variable) {
14 | return decodeURIComponent(pair[1].replace(/\+/g, '%20')).trim();
15 | }
16 | }
17 | }
18 |
19 | function getPreview(query, content, previewLength) {
20 | previewLength = previewLength || (content.length * 2);
21 |
22 | var parts = query.split(" "),
23 | match = content.toLowerCase().indexOf(query.toLowerCase()),
24 | matchLength = query.length,
25 | preview;
26 |
27 | // Find a relevant location in content
28 | for (var i = 0; i < parts.length; i++) {
29 | if (match >= 0) {
30 | break;
31 | }
32 |
33 | match = content.toLowerCase().indexOf(parts[i].toLowerCase());
34 | matchLength = parts[i].length;
35 | }
36 |
37 | // Create preview
38 | if (match >= 0) {
39 | var start = match - (previewLength / 2),
40 | end = start > 0 ? match + matchLength + (previewLength / 2) : previewLength;
41 |
42 | preview = content.substring(start, end).trim();
43 |
44 | if (start > 0) {
45 | preview = "..." + preview;
46 | }
47 |
48 | if (end < content.length) {
49 | preview = preview + "...";
50 | }
51 |
52 | // Highlight query parts
53 | preview = preview.replace(new RegExp("(" + parts.join("|") + ")", "gi"), "$1 ");
54 | } else {
55 | // Use start of content if no match found
56 | preview = content.substring(0, previewLength).trim() + (content.length > previewLength ? "..." : "");
57 | }
58 |
59 | return preview;
60 | }
61 |
62 | function displaySearchResults(results, query) {
63 | var searchResultsEl = document.getElementById("search-results"),
64 | searchProcessEl = document.getElementById("search-process");
65 |
66 | if (results.length) {
67 | var resultsHTML = "";
68 | results.forEach(function (result) {
69 | var item = window.data[result.ref],
70 | contentPreview = getPreview(query, item.content, 170),
71 | titlePreview = getPreview(query, item.title);
72 |
73 | resultsHTML += "" + contentPreview + "
";
74 | });
75 |
76 | searchResultsEl.innerHTML = resultsHTML;
77 | searchProcessEl.innerText = "Showing";
78 | } else {
79 | searchResultsEl.style.display = "none";
80 | searchProcessEl.innerText = "No";
81 | }
82 | }
83 |
84 | window.index = lunr(function () {
85 | this.field("id");
86 | this.field("title", {boost: 10});
87 | this.field("categories");
88 | this.field("url");
89 | this.field("content");
90 | });
91 |
92 | var query = decodeURIComponent((getQueryVariable("q") || "").replace(/\+/g, "%20")),
93 | searchQueryContainerEl = document.getElementById("search-query-container"),
94 | searchQueryEl = document.getElementById("search-query");
95 |
96 | searchQueryEl.innerText = query;
97 | if (query != ""){
98 | searchQueryContainerEl.style.display = "inline";
99 | }
100 |
101 | for (var key in window.data) {
102 | window.index.add(window.data[key]);
103 | }
104 |
105 | displaySearchResults(window.index.search(query), query); // Hand the results off to be displayed
106 | })();
107 |
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/docs/assets/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/docs/assets/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/docs/pages/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: About
3 | permalink: /about/
4 | ---
5 |
6 | # About
7 |
8 | This is a basic Python implementation of the [opencontainers](https://github.com/opencontainers) specifications.
9 | The intention is for you to be able to load or create different kinds of
10 | manifests and objects using Python.
11 |
12 | ## Purpose
13 |
14 | Most container technologies are implemented in GoLang, and while this
15 | is great for server applications (concurrency! compiled binaries!) it may
16 | not be ideal for everyone's use case. While these "other" use cases are a small
17 | set, they tend to fall more heavily on academic users, and so I think it's
18 | still important to provide tooling for that audience.
19 |
20 | > If you build it, they will come!
21 |
22 | I also truly believe that providing base libraries to generate complex
23 | configurations like these can empower users to build many things - so
24 | many more than I could anticipate in advance!
25 |
26 |
27 | ## Support
28 |
29 | If you need help, please don't hesitate to [open an issue](https://www.github.com/{{ site.github_repo }}/{{ site.github_user }}).
30 |
31 |
--------------------------------------------------------------------------------
/docs/pages/archive.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Articles
4 | permalink: /archive/
5 | ---
6 | # News Archive
7 |
8 | {% for post in site.posts %}{% capture this_year %}{{ post.date | date: "%Y" }}{% endcapture %}{% capture next_year %}{{ post.previous.date | date: "%Y" }}{% endcapture %}
9 |
10 | {% if forloop.first %}{{this_year}}
11 | {% endif %}
12 |
13 | {{ post.date | date: "%b %-d, %Y" }}: {{ post.title }}
14 | {% if forloop.last %} {% else %}{% if this_year != next_year %}
15 |
16 | {{next_year}}
17 | {% endif %}{% endif %}{% endfor %}
18 |
--------------------------------------------------------------------------------
/docs/pages/docs.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Documentation
4 | permalink: /docs/
5 | ---
6 |
7 | # Documentation
8 |
9 | Welcome to the {{ site.title }} Documentation pages! Here you can quickly jump to a
10 | particular page.
11 |
12 |
13 |
14 | {% for post in site.docs %}
15 |
16 |
17 |
{{ post.description }}
18 |
{% endfor %}
19 |
20 |
--------------------------------------------------------------------------------
/docs/pages/feed.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: null
3 | permalink: /feed.xml
4 | ---
5 |
6 |
7 |
8 | {{ site.title | xml_escape }}
9 | {{ site.description | xml_escape }}
10 | {{ site.url }}{{ site.baseurl }}/
11 |
12 | {{ site.time | date_to_rfc822 }}
13 | {{ site.time | date_to_rfc822 }}
14 | Jekyll v{{ jekyll.version }}
15 | {% for post in site.posts limit:10 %}
16 | -
17 |
{{ post.title | xml_escape }}
18 | {{ post.content | xml_escape }}
19 | {{ post.date | date_to_rfc822 }}
20 | {{ post.url | prepend: site.baseurl | prepend: site.url }}
21 | {{ post.url | prepend: site.baseurl | prepend: site.url }}
22 | {% for tag in post.tags %}
23 | {{ tag | xml_escape }}
24 | {% endfor %}
25 | {% for cat in post.categories %}
26 | {{ cat | xml_escape }}
27 | {% endfor %}
28 |
29 | {% endfor %}
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/pages/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: OpenContainers Python
4 | permalink: /
5 | ---
6 |
7 | # Welcome to OpenContainers Python
8 |
9 | This is a basic Python implementation of the [opencontainers](https://github.com/opencontainers) specifications.
10 | The intention is for you to be able to load or create different kinds of
11 | manifests and objects using Python.
12 |
13 | {% include alert.html type="info" title="What are your use cases?" content="How would you want to use OpenContainers Python? @vsoch is looking for interesting and fun use cases, so please open an issue if you have ideas." %}
14 |
15 | ## Support
16 |
17 | For features, getting started with development, see the {% include doc.html name="Getting Started" path="getting-started" %} page. Would you like to request a feature or contribute?
18 | [Open an issue]({{ site.repo }}/issues)
19 |
--------------------------------------------------------------------------------
/docs/pages/news.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: News
3 | permalink: /news/
4 | ---
5 |
6 | # News
7 |
8 | Subscribe with RSS to keep up with the latest news.
9 | For site changes, see the changelog kept with the code base.
10 |
11 |
12 |
13 | {% for post in site.posts limit:10 %}
14 |
15 |
16 |
{{ post.date | date: "%B %d, %Y" }}
17 | {% if post.badges %}{% for badge in post.badges %}
{{ badge.tag }} {% endfor %}{% endif %}
18 | {{ post.content | split:'' | first }}
19 | {% if post.content contains '' %}
20 |
read more
21 | {% endif %}
22 |
23 |
24 | {% endfor %}
25 |
26 | Want to see more? See the News Archive .
27 |
--------------------------------------------------------------------------------
/docs/pages/search.html:
--------------------------------------------------------------------------------
1 | ---
2 | title: Search
3 | sitemap: false
4 | permalink: /search/
5 | not_editable: true
6 | excluded_in_search: true
7 | ---
8 |
9 |
10 |
11 |
12 | Loading results for " "
13 |
14 |
15 |
16 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs/pages/sitemap.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: null
3 | permalink: /sitemap.xml
4 | ---
5 |
6 |
7 |
8 |
9 | /
10 | {{ "now" | date: "%Y-%m-%d" }}
11 | daily
12 |
13 | {% for section in site.data.toc %}
14 | {{ site.baseurl }}{{ section.url }}/
15 | {{ "now" | date: "%Y-%m-%d" }}
16 | daily
17 |
18 | {% endfor %}
19 |
20 |
--------------------------------------------------------------------------------
/opencontainers/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.version import __version__
8 |
--------------------------------------------------------------------------------
/opencontainers/digest/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from .digest import (
8 | Digest,
9 | DigestRegexp,
10 | DigestRegexpAnchored,
11 | NewDigestFromEncoded,
12 | NewDigest,
13 | FromString,
14 | FromBytes,
15 | Parse,
16 | )
17 |
18 | from .algorithm import Algorithm, SHA256, SHA384, SHA512, Canonical
19 |
20 | from .verifiers import hashVerifier
21 |
--------------------------------------------------------------------------------
/opencontainers/digest/algorithm.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import StrStruct
8 | from opencontainers.logger import bot
9 | from .digester import digester
10 | from .exceptions import (
11 | ErrDigestInvalidFormat,
12 | ErrDigestUnsupported,
13 | ErrDigestInvalidLength,
14 | )
15 |
16 | import hashlib
17 | import re
18 | import io
19 |
20 |
21 | class Algorithm(StrStruct):
22 | """
23 | Algorithm identifies and implementation of a digester by an identifier.
24 |
25 | Note the that this defines both the hash algorithm used and the string
26 | encoding.
27 | """
28 |
29 | def __init__(self, value=None):
30 | self._algorithm = value
31 | super().__init__(value)
32 |
33 | def available(self):
34 | """
35 | Available returns true if the digest type is available for use.
36 |
37 | If this returns false, Digester and Hash will return None.
38 | we are flexible to allow the user to also provide a full digest
39 | """
40 | algorithm = self.value
41 |
42 | # If we have a full digest, name is separated by :
43 | match = re.search("^(?P.+?):(?P.+)", self.value)
44 | if match:
45 | algorithm = match.group("algorithm")
46 |
47 | self._algorithm = algorithm
48 |
49 | return algorithm in algorithms
50 |
51 | def digester(self):
52 | """
53 | Digester returns a new digester for the specified algorithm.
54 |
55 | If the algorithm does not have a digester implementation, nil will be
56 | returned. This can be checked by calling Available before calling
57 | Digester. Note that the GoLang implementation also had a Hash() function
58 | that (seemed to) return the same, and instead I'm going to return the
59 | same hashlib new.
60 | """
61 | return digester(self, self.hash())
62 |
63 | def hash(self):
64 | """
65 | Hash returns a new hash as used by the algorithm.
66 | """
67 | if not self.available():
68 | return None
69 | return hashlib.new(self._algorithm)
70 |
71 | def validate(self, encoded):
72 | """
73 | Validate validates the encoded portion string.
74 |
75 | This means ensuring that the algorithm is available, checking it's length,
76 | and the characters provided.
77 | """
78 | if not self.available():
79 | raise ErrDigestUnsupported()
80 |
81 | # Digests much always be hex-encoded, ensuring that their hex portion will
82 | # always be size*2
83 | hashy = hashlib.new(self._algorithm)
84 | if hashy.digest_size * 2 != len(encoded):
85 | raise ErrDigestInvalidLength()
86 |
87 | regexp = anchoredEncodedRegexps.get(self._algorithm)
88 | if not regexp.search(encoded):
89 | raise ErrDigestInvalidFormat()
90 | return True
91 |
92 | def size(self):
93 | """
94 | Size returns number of bytes returned by the hash.
95 | """
96 | if not self.available():
97 | return 0
98 | hashy = hashlib.new(self._algorithm)
99 |
100 | # Need to ensure that the digest size == bytes and we don't want block_size
101 | return hashy.digest_size
102 |
103 | def set(self, value):
104 | """
105 | Set implemented to allow use of Algorithm as a command line flag.
106 |
107 | This isn't useful, as we could already call load (but this will
108 | mirror GoLang.
109 | """
110 | self = self.load(value)
111 | if not self.available():
112 | raise ErrDigestUnsupported()
113 |
114 | def encode(self, content):
115 | """
116 | Encode encodes the raw bytes of a digest, typically from a hash.
117 |
118 | into the encoded portion of the digest.
119 | """
120 | # Currently, all algorithms use a hex encoding. When we
121 | # add support for back registration, we can modify this accordingly.
122 | # https://github.com/opencontainers/go-digest/blob/master/algorithm.go#L137
123 | if not isinstance(content, bytes):
124 | content = bytes(content, "utf-8")
125 | return content.hex()
126 |
127 | def fromReader(self, ioReader):
128 | """
129 | FromReader returns the digest of the reader using the algorithm.
130 |
131 | the input must be type bytes, usually from io.BytesIO.read().
132 | This function likely isn't needed, but is provided to mirror
133 | the GoLang implementation.
134 | """
135 | if not isinstance(ioReader, io.BytesIO):
136 | bot.exit("input must be io.BytesIO")
137 | return self.fromBytes(ioReader.read())
138 |
139 | def fromBytes(self, content):
140 | """FromBytes digests the input and returns a Digest."""
141 | digester = self.digester()
142 | digester.hash.update(content)
143 | return digester.digest()
144 |
145 | def fromString(self, content):
146 | """
147 | FromString digests the string input and returns a Digest.
148 |
149 | TODO not sure what this is intended for.
150 | """
151 | if not isinstance(content, str):
152 | bot.exit("input must be string")
153 | content = bytes(content, "utf-8")
154 | return self.fromBytes(content)
155 |
156 |
157 | # supported digest types only to match GoLang
158 |
159 | SHA256 = Algorithm("sha256") # sha256 with hex encoding (lower case only)
160 | SHA384 = Algorithm("sha384") # sha384 with hex encoding (lower case only)
161 | SHA512 = Algorithm("sha512") # sha512 with hex encoding (lower case only)
162 |
163 | # Canonical is the primary digest algorithm used with the distribution
164 | # project. Other digests may be used but this one is the primary storage digest
165 | Canonical = SHA256
166 |
167 | # algorithms maps values to hash.Hash implementations. Other algorithms
168 | # may be available but they cannot be calculated by the digest package.
169 | # this mirrors GoLang (there are more available in Python)
170 |
171 | algorithms = {"sha256": SHA256, "sha384": SHA384, "sha512": SHA512}
172 |
173 | # anchoredEncodedRegexps contains anchored regular expressions for hex-encoded
174 | # digests. Note that /A-F/ disallowed.
175 |
176 | anchoredEncodedRegexps = {
177 | SHA256: re.compile("^[a-f0-9]{64}$"),
178 | SHA384: re.compile("^[a-f0-9]{96}$"),
179 | SHA512: re.compile("^[a-f0-9]{128}$"),
180 | }
181 |
--------------------------------------------------------------------------------
/opencontainers/digest/digest.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import StrStruct
8 | from opencontainers.logger import bot
9 | from .algorithm import Algorithm
10 | from .exceptions import ErrDigestInvalidFormat
11 | import re
12 |
13 |
14 | class Digest(StrStruct):
15 | """
16 | A Digest
17 |
18 | Digest allows simple protection of hex formatted digest strings, prefixed
19 | by their algorithm. Strings of type Digest have some guarantee of being in
20 | the correct format and it provides quick access to the components of a
21 | digest string.
22 |
23 | The following is an example of the contents of Digest types:
24 | sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
25 | This allows to abstract the digest behind this type and work only in those
26 | terms.
27 | """
28 |
29 | def __init__(self, value=None):
30 | super().__init__(value)
31 |
32 | def validate(self):
33 | """Validate checks that the contents of self (the digest) is valid"""
34 | if not self:
35 | bot.exit("Empty digest")
36 |
37 | regexp = "^[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$"
38 |
39 | # Must match for a digest
40 | if not re.search(regexp, self):
41 | raise ErrDigestInvalidFormat()
42 |
43 | algorithm, encoded = (self).split(":")
44 |
45 | # Remove the extra component, if there
46 | match = re.search("[+._-]", algorithm)
47 | if match:
48 | algorithm = algorithm[: match.start()]
49 | algorithm = Algorithm(algorithm)
50 |
51 | # Also checks if algorithm.available()
52 | return algorithm.validate(encoded)
53 |
54 | def sepIndex(self):
55 | """
56 | Return the index of the : separator.
57 |
58 | return the index of the : separator or the index
59 | that separtes the extra content provided in the algorithm name.
60 | """
61 | try:
62 | algorithm, encoded = (self).split(":")
63 | except:
64 | bot.exit("empty digest or algorithm")
65 |
66 | # Empty algorithm or encoded portion
67 | if not algorithm or not encoded:
68 | bot.exit("empty digest or algorithm")
69 |
70 | match = re.search("[+._-]", algorithm)
71 | if match:
72 | return match.start()
73 | return self.index(":", 1)
74 |
75 | def startEncodedIndex(self):
76 | """
77 | Return the start of the encoded portion
78 |
79 | in the case of having an extra component, return the start of the
80 | encoded portion
81 | """
82 | match = re.search(":", self, 1)
83 | return match.start() + 1
84 |
85 | @property
86 | def algorithm(self):
87 | """
88 | Algorithm returns the algorithm portion of the digest.
89 | """
90 | return Algorithm(self[: self.sepIndex()])
91 |
92 | def encoded(self):
93 | """
94 | Encoded returns the encoded portion of the digest.
95 | """
96 | return self[self.startEncodedIndex() :]
97 |
98 | def verifier(self):
99 | """
100 | Get a verifier
101 |
102 | Verifier returns a writer object that can be used to verify a stream of
103 | content against the digest. If the digest is invalid, the method will panic.
104 | """
105 | from .verifiers import hashVerifier
106 |
107 | hashObj = self.algorithm.hash()
108 | if not hashObj:
109 | bot.exit("Algorithm is not available")
110 | return hashVerifier(hashObj, digest=self)
111 |
112 |
113 | # DigestRegexp matches valid digest types.
114 | DigestRegexp = re.compile("[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+")
115 |
116 | # DigestRegexpAnchored matches valid digest types, anchored to the start and end of the match.
117 | DigestRegexpAnchored = re.compile("^%s$" % DigestRegexp)
118 |
119 |
120 | def NewDigestFromEncoded(algorithm, encoded):
121 | """
122 | NewDigestFromEncoded returns a Digest from alg and the encoded digest.
123 | """
124 | return Digest("%s:%s" % (algorithm, encoded))
125 |
126 |
127 | def NewDigestFromBytes(algorithm, content):
128 | """
129 | NewDigestFromBytes returns a new digest from the byte contents of p.
130 |
131 | Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...)
132 | functions. This is also useful for rebuilding digests from binary
133 | serializations.
134 | """
135 | return NewDigestFromEncoded(algorithm, algorithm.encode(content))
136 |
137 |
138 | def NewDigest(algorithm, hashObj):
139 | """
140 | NewDigest returns a Digest from alg and a hash object
141 | """
142 | return NewDigestFromBytes(algorithm, hashObj.digest())
143 |
144 |
145 | def FromBytes(p):
146 | """
147 | FromBytes digests the input and returns a Digest.
148 | """
149 | from .algorithm import Canonical
150 |
151 | return Canonical.fromBytes(p)
152 |
153 |
154 | def FromString(p):
155 | """
156 | FromString digests the input and returns a Digest.
157 | """
158 | return Canonical.fromString(p)
159 |
160 |
161 | def Parse(string):
162 | """
163 | Parse parses s and returns the validated digest object.
164 |
165 | An error will be returned if the format is invalid.
166 | """
167 | d = Digest(string)
168 | d.validate()
169 | return d
170 |
--------------------------------------------------------------------------------
/opencontainers/digest/digester.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from hashlib import new
9 |
10 |
11 | class Digester(Struct):
12 | """
13 | Digester calculates the digest of written data.
14 |
15 | Writes should go directly to the return value of Hash, while calling Digest
16 | will return the current value of the digest.
17 | """
18 |
19 | def __init__(self):
20 |
21 | self.Hash = digester.digest
22 | self.Digest = digester.digest
23 | super().__init__()
24 |
25 |
26 | class digester(Struct):
27 | """Digester provides a simple digester definition that embeds a hasher."""
28 |
29 | def __init__(self, alg=None, hashObj=None):
30 |
31 | super().__init__()
32 | self.alg = alg
33 | self.hash = hashObj
34 |
35 | def digest(self):
36 | from .digest import NewDigest
37 |
38 | return NewDigest(self.alg, self.hash)
39 |
--------------------------------------------------------------------------------
/opencontainers/digest/exceptions.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | class ErrDigestInvalidFormat(Exception):
9 | """
10 | ErrDigestInvalidFormat returned when digest format invalid.
11 | """
12 |
13 | def __init__(self):
14 | super().__init__("invalid checksum digest format")
15 |
16 |
17 | class ErrDigestInvalidLength(Exception):
18 | """
19 | ErrDigestInvalidLength returned when digest has invalid length.
20 | """
21 |
22 | def __init__(self):
23 | super().__init__("invalid checksum digest length")
24 |
25 |
26 | class ErrDigestUnsupported(Exception):
27 | """
28 | Returned when the digest algorithm is unsupported.
29 | """
30 |
31 | def __init__(self):
32 | super().__init__("unsupported digest algorithm")
33 |
--------------------------------------------------------------------------------
/opencontainers/digest/verifiers.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from hashlib import new
9 | from .digest import Digest, NewDigest
10 |
11 |
12 | class hashVerifier(Struct):
13 | def __init__(self, hashObj=None, digest=None):
14 |
15 | super().__init__()
16 |
17 | self.hash = hashObj
18 | self.digest = digest
19 |
20 | def write(self, content):
21 | """
22 | Add bytes of content to the hash object
23 | """
24 | if not isinstance(content, bytes):
25 | content = bytes(content, "utf-8")
26 | self.hash.update(content)
27 | self.digest = NewDigest(self.digest.algorithm, self.hash)
28 | self.digest.validate()
29 |
30 | def verified(self):
31 | """
32 | Calculate the hex digest against the digest
33 | """
34 | return self.digest == NewDigest(self.digest.algorithm, self.hash)
35 |
36 |
37 | # The GoLang implementation has another Verifier class, not used here
38 |
--------------------------------------------------------------------------------
/opencontainers/distribution/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/opencontainers/distribution/__init__.py
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/README.md:
--------------------------------------------------------------------------------
1 | # Reggie Python
2 |
3 | Reggie Python is a Python version of [Reggie](https://github.com/bloodorangeio/reggie)
4 | to make it easy to interact with an OCI registry.
5 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/__init__.py:
--------------------------------------------------------------------------------
1 | from .response import Response
2 | from .client import (
3 | NewClient,
4 | ClientConfig,
5 | WithUsernamePassword,
6 | WithAuthScope,
7 | WithDefaultName,
8 | WithDebug,
9 | WithUserAgent,
10 | )
11 | from .request import (
12 | WithName,
13 | WithReference,
14 | WithDigest,
15 | WithSessionID,
16 | WithRetryCallback,
17 | )
18 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/client.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Copyright (C) 2020-2022 Vanessa Sochat.
4 |
5 | This Source Code Form is subject to the terms of the
6 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | """
10 |
11 | from .defaults import DEFAULT_USER_AGENT, URL_REGEX
12 | from .request import RequestConfig, RequestClient
13 | from .config import BaseConfig
14 | from copy import deepcopy
15 |
16 | import sys
17 | import re
18 | import requests
19 | import urllib.parse
20 |
21 |
22 | class ClientConfig(BaseConfig):
23 | """
24 | A Client config holds attributes for a Reggie Client.
25 |
26 | Configuration setting
27 | functions are validation at creation time, and further validation is done
28 | with ClientConfig.validate().
29 | """
30 |
31 | valid_functions = [
32 | "WithUsernamePassword",
33 | "WithUserAgent",
34 | "WithDebug",
35 | "WithDefaultName",
36 | "WithAuthScope",
37 | ]
38 |
39 | def __init__(self, address, opts=None):
40 | """
41 | Instantiate a config. An address is required.
42 | """
43 | self.Address = address
44 | self.AuthScope = None
45 | self.Username = None
46 | self.Password = None
47 | self.Debug = False
48 | self.DefaultName = None
49 | self.UserAgent = DEFAULT_USER_AGENT
50 | self.required = [self.Address, self.UserAgent]
51 | super().__init__()
52 |
53 | def _validate(self):
54 | """
55 | Custom validation on top of BaseConfig validation.
56 | """
57 | # Validation 2: Address starts with http
58 | if not re.search(URL_REGEX, self.Address):
59 | raise ValueError("%s does not appear to be a http address." % self.Address)
60 |
61 |
62 | # Attribute setting functions for ClientConfig
63 |
64 |
65 | def WithUsernamePassword(username, password):
66 | """
67 | WithUsernamePassword sets registry username and password configuration settings.
68 | """
69 |
70 | def WithUsernamePassword(config):
71 | config.Username = username
72 | config.Password = password
73 |
74 | return WithUsernamePassword
75 |
76 |
77 | def WithAuthScope(authScope):
78 | """
79 | WithAuthScope overrides the scope provided by the authorization server.
80 | """
81 |
82 | def WithAuthScope(config):
83 | config.AuthScope = authScope
84 |
85 | return WithAuthScope
86 |
87 |
88 | def WithDefaultName(namespace):
89 | """
90 | WithDefaultName sets the default registry namespace configuration setting.
91 | """
92 |
93 | def WithDefaultName(config):
94 | config.DefaultName = namespace
95 |
96 | return WithDefaultName
97 |
98 |
99 | def WithDebug(debug):
100 | """
101 | WithDebug enables or disables debug mode.
102 | """
103 |
104 | def WithDebug(config):
105 | config.Debug = debug
106 |
107 | return WithDebug
108 |
109 |
110 | def WithUserAgent(userAgent):
111 | """
112 | WithUserAgent overrides the client user agent
113 | """
114 |
115 | def WithUserAgent(config):
116 | config.UserAgent = userAgent
117 |
118 | return WithUserAgent
119 |
120 |
121 | # Client
122 |
123 |
124 | class NewClient:
125 | """
126 | A handle to create and issue requests to an OCI distribution registry
127 |
128 | It is based on the Go version of reggie by BloodOrange.io,
129 | https://github.com/bloodorangeio/reggie/blob/master/client.go
130 | """
131 |
132 | def __init__(self, address, *opts):
133 | """
134 | Create a new client
135 |
136 | Requiring an address, and a Client Config.
137 | Matched to NewClient: builds a new Client from provided options.
138 | """
139 | self.Config = ClientConfig(address)
140 | self.Config.set_options(opts)
141 | self.Config.validate()
142 | self.Client = RequestClient()
143 | self.Debug = self.Config.Debug
144 |
145 | # Set max redirects (we don't set a transport here, not sure if required)
146 | self.Client.max_redirects = 20
147 |
148 | def SetDefaultName(self, namespace):
149 | """
150 | SetDefaultName sets the default registry namespace to use for building a Request.
151 | """
152 | self.Config.DefaultName = namespace
153 |
154 | def NewRequest(self, method, path, *opts):
155 | """
156 | Prepare a request for some method, path (url) and set of options.
157 | """
158 | rc = RequestConfig(opts)
159 | requestClient = self.Client.NewRequest()
160 | requestClient.SetMethod(method)
161 |
162 | # Set default namespace, and fill in string replacements
163 | namespace = rc.Name or self.Config.DefaultName
164 |
165 | # Substitute known path paramaters
166 | replacements = {
167 | "": namespace,
168 | "": rc.Reference,
169 | "": rc.Digest,
170 | "": rc.SessionID,
171 | }
172 | for key, value in replacements.items():
173 | if value:
174 | path = path.replace(key, value, -1)
175 |
176 | # Remove trailing slash and prepare url
177 | url = urllib.parse.urljoin(self.Config.Address, path)
178 | requestClient.SetUrl(url)
179 | requestClient.SetHeader("User-Agent", self.Config.UserAgent)
180 | requestClient.SetRetryCallback(rc.RetryCallback)
181 |
182 | # Return the Client, which has Request and retryCallback
183 | return requestClient
184 |
185 | def Do(self, req):
186 | """
187 | Execut a request.
188 |
189 | Given a request (an instance of the RequestClient, execute the request
190 | and return a response.
191 | """
192 | # a requests.Response with additional retryCallback
193 | response = req.Execute()
194 |
195 | # Unauthorized response
196 | if response.status_code == 401:
197 | response = self.retryRequestWithAuth(req, response)
198 | return response
199 |
200 | def retryRequestWithAuth(self, originalRequest, originalResponse):
201 | """
202 | Retry a request with authentication.
203 |
204 | Given a 401 response (Authentication needed) retrieve the WWW-Authenticate
205 | header and retry with authentication
206 | """
207 | authHeaderRaw = originalResponse.headers.get("Www-Authenticate")
208 | if not authHeaderRaw:
209 | return originalResponse
210 |
211 | # If there is a callback, use it, should raise exception if issue
212 | if originalRequest.retryCallback:
213 | try:
214 | originalRequest.retryCallback(originalRequest)
215 | except Exception as exc:
216 | raise Exception("retry callback returned error: %s" % exc)
217 |
218 | # Prepare request to retry
219 | h = parseAuthHeader(authHeaderRaw)
220 | req = (
221 | self.Client.NewRequest()
222 | .SetQueryParam("service", h.Service)
223 | .SetHeader("Accept", "application/json")
224 | .SetHeader("User-Agent", self.Config.UserAgent)
225 | )
226 |
227 | # Do not set basic auth if no username/password provided
228 | if self.Config.Username and self.Config.Password:
229 | req = req.SetBasicAuth(self.Config.Username, self.Config.Password)
230 |
231 | # Set the scope, first priority to config, then header
232 | if self.Config.AuthScope:
233 | req.SetQueryParam("scope", self.Config.AuthScope)
234 | elif h.Scope:
235 | req.SetQueryParam("scope", h.Scope)
236 |
237 | authResponse = req.Execute("GET", h.Realm)
238 |
239 | # Request the token
240 | info = authResponse.json()
241 | token = info.get("token")
242 | if not token:
243 | token = info.get("access_token")
244 |
245 | # Set the token to the original request and retry
246 | originalRequest.SetAuthToken(token)
247 | return originalRequest.Execute(
248 | method=originalRequest.method, url=originalRequest.url
249 | )
250 |
251 |
252 | def parseAuthHeader(authHeaderRaw):
253 | """
254 | Parse an authentication header into pieces
255 | """
256 | regex = re.compile('([a-zA-z]+)="(.+?)"')
257 | matches = regex.findall(authHeaderRaw)
258 | lookup = dict()
259 | for match in matches:
260 | lookup[match[0]] = match[1]
261 | return authHeader(lookup)
262 |
263 |
264 | class authHeader:
265 | def __init__(self, lookup):
266 | """
267 | Given a dictionary of values, match them to class attributes
268 | """
269 | for key in lookup:
270 | if key in ["realm", "service", "scope"]:
271 | setattr(self, key.capitalize(), lookup[key])
272 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/config.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Copyright (C) 2020-2022 Vanessa Sochat.
4 |
5 | This Source Code Form is subject to the terms of the
6 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | """
10 |
11 | import re
12 | import requests
13 | from .defaults import DEFAULT_USER_AGENT, URL_REGEX
14 |
15 |
16 | class BaseConfig:
17 | """
18 | A Base client configuration.
19 |
20 | A BaseClient config holds attributes for some type of Reggie Client.
21 | Setting functions are validation at creation time, and further validation is done
22 | with BaseClient.validate().
23 | """
24 |
25 | def __init__(self, opts=None):
26 | """
27 | Instantiate a config.
28 |
29 | The subclass is required to call validate(), in case
30 | additional parameters or manipulation needs to be done.
31 | """
32 | # Opts must be a list of known functions
33 | if opts:
34 | self.set_options(opts)
35 |
36 | # List of valid function names, set by subclass
37 | self.valid_functions = getattr(self, "valid_functions", [])
38 |
39 | # Required attributes
40 | self.required = getattr(self, "required", [])
41 |
42 | def set_options(self, opts):
43 | """
44 | Validate and set a list of options.
45 |
46 | We loop through the list to set
47 | them for the config client. We also perform validation. Any issues
48 | with one of the functions raises an error.
49 | """
50 | if not isinstance(opts, (list, tuple)):
51 | raise ValueError(
52 | "Options should be provided as a list or tuple of functions."
53 | )
54 |
55 | for func in opts:
56 | self.validate_function(func)
57 | func(self)
58 |
59 | def validate_function(self, func):
60 | """
61 | Ensure that a function is in fact a function.
62 |
63 | And that is it one of the known ones to set an attribute.
64 | """
65 | if not hasattr(func, "__name__"):
66 | raise ValueError(
67 | "%s does not have a __name__ attribute, is it a function?" % func
68 | )
69 | if not callable(func):
70 | raise ValueError("%s is not a callable function." % func.__name__)
71 | if func.__name__ not in self.valid_functions:
72 | raise ValueError(
73 | "%s is not a valid config setting function." % func.__name__
74 | )
75 |
76 | def validate(self):
77 | """
78 | Validate config settings, done after initialization.
79 | """
80 | # Validation 1: required fields
81 | for setting in self.required:
82 | if not setting:
83 | raise ValueError("%s is required, and is not defined." % setting)
84 |
85 | # Call any custom validation routines for the Client
86 | if hasattr(self, "_validate"):
87 | self._validate()
88 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/defaults.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Copyright (C) 2020-2022 Vanessa Sochat.
4 |
5 | This Source Code Form is subject to the terms of the
6 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | """
10 |
11 | from opencontainers.version import __version__
12 |
13 | DEFAULT_USER_AGENT = "reggie-python/%s (https://github.com/vsoch/oci-python)" % (
14 | __version__
15 | )
16 | URL_REGEX = (
17 | r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
18 | )
19 | VALID_METHODS = ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
20 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/request.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Copyright (C) 2020-2022 Vanessa Sochat.
4 |
5 | This Source Code Form is subject to the terms of the
6 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | """
10 |
11 | from .defaults import DEFAULT_USER_AGENT, URL_REGEX, VALID_METHODS
12 | from .config import BaseConfig
13 | from requests.cookies import cookiejar_from_dict
14 | from requests.adapters import HTTPAdapter
15 | from requests.hooks import default_hooks
16 | from collections import OrderedDict
17 |
18 | import base64
19 | import json
20 | import re
21 | import requests
22 |
23 |
24 | class RequestConfig(BaseConfig):
25 | """
26 | A Request Configuration
27 |
28 | A RequestConfig is akin to a ClientConfig to hold options, but for a
29 | particular request.
30 | """
31 |
32 | valid_functions = [
33 | "WithName",
34 | "WithReference",
35 | "WithDigest",
36 | "WithSessionID",
37 | "WithRetryCallback",
38 | ]
39 |
40 | def __init__(self, opts):
41 | """
42 | Instantiate a request config
43 | """
44 | self.Name = None
45 | self.Reference = None
46 | self.Digest = None
47 | self.SessionID = None
48 | self.RetryCallback = None
49 | self.required = [self.Name]
50 | super().__init__(opts or [])
51 |
52 |
53 | def WithName(name):
54 | """
55 | WithName sets the namespace per a single request.
56 | """
57 |
58 | def WithName(config):
59 | config.Name = name
60 |
61 | return WithName
62 |
63 |
64 | def WithReference(ref):
65 | """
66 | WithReference sets the reference per a single request.
67 | """
68 |
69 | def WithReference(config):
70 | config.Reference = ref
71 |
72 | return WithReference
73 |
74 |
75 | def WithDigest(digest):
76 | """
77 | WithDigest sets the digest per a single request.
78 | """
79 |
80 | def WithDigest(config):
81 | config.Digest = digest
82 |
83 | return WithDigest
84 |
85 |
86 | def WithSessionID(session_id):
87 | """
88 | WithSessionID sets the session ID per a single request.
89 | """
90 |
91 | def WithSessionID(config):
92 | config.SessionID = session_id
93 |
94 | return WithSessionID
95 |
96 |
97 | def WithRetryCallback(retryCallback):
98 | """
99 | Set a retry callback on a request.
100 |
101 | WithRetryCallback specifies a callback that will be invoked before a request
102 | is retried.
103 | """
104 |
105 | def WithRetryCallback(config):
106 | config.RetryCallback = retryCallback
107 |
108 | return WithRetryCallback
109 |
110 |
111 | class RequestClient(requests.Session):
112 | """
113 | A Request Client.
114 |
115 | A RequestClient includes a request, and adds some courtesy functions
116 | (wrappers around the self.request object to manipulate settings and
117 | return the same object to allow for chaining. This is implemented to
118 | match the Reggie Go implementation.
119 | """
120 |
121 | def __init__(self):
122 | """
123 | Create a new request.
124 |
125 | Start with an empty request ready to go. We replicate the parent
126 | class but don't set headers as it is provided as a property.
127 | """
128 |
129 | def __init__(self):
130 | self.auth = None
131 | self.proxies = {}
132 | self.hooks = default_hooks()
133 | self.stream = False
134 | self.verify = True
135 | self.cert = None
136 | self.max_redirects = 30
137 | self.trust_env = True
138 | self.cookies = cookiejar_from_dict({})
139 | self.adapters = OrderedDict()
140 | self.mount("https://", HTTPAdapter())
141 | self.mount("http://", HTTPAdapter())
142 | self.retryCallback = None
143 | self.Request = None
144 |
145 | def __str__(self):
146 | return "[%s] %s" % (self.Request.method, self.Request.url)
147 |
148 | @property
149 | def url(self):
150 | return self.Request.url
151 |
152 | @property
153 | def method(self):
154 | return self.Request.method
155 |
156 | @property
157 | def headers(self):
158 | return self.Request.headers
159 |
160 | @property
161 | def body(self):
162 | return (
163 | self.Request.data.decode("utf-8")
164 | if self.Request.data and isinstance(self.Request.data, bytes)
165 | else self.Request.data
166 | )
167 |
168 | @property
169 | def params(self):
170 | return self.Request.params
171 |
172 | def clearParams(self):
173 | self.Request.params = {}
174 |
175 | @classmethod
176 | def NewRequest(cls):
177 | """
178 | Set a new Request object to replace original, still return client
179 | """
180 | newclient = RequestClient()
181 | newclient.Request = requests.Request()
182 | return newclient
183 |
184 | def SetMethod(self, method):
185 | """
186 | SetMethod sets the method for the request
187 | """
188 | assert method in VALID_METHODS
189 | self.Request.method = method
190 | return self
191 |
192 | def SetUrl(self, url):
193 | """
194 | SetMethod sets the method for the request
195 | """
196 | assert re.search(URL_REGEX, url)
197 | self.Request.url = url
198 | return self
199 |
200 | def SetBody(self, body):
201 | """
202 | SetBody wraps the resty SetBody and returns the request, allowing method chaining
203 | """
204 | if isinstance(body, dict):
205 | body = json.dumps(body)
206 | if isinstance(body, str):
207 | body = body.encode("utf-8")
208 | self.Request.data = body
209 | return self
210 |
211 | def SetHeader(self, header, content):
212 | """
213 | SetHeader wraps the resty SetHeader and returns the request, allowing method chaining
214 | """
215 | self.Request.headers[header] = content
216 | return self
217 |
218 | def SetQueryParam(self, param, content):
219 | """
220 | SetQueryParam wraps the resty SetQueryParam and returns the request, allowing method chaining
221 | """
222 | self.Request.params[param] = content
223 | return self
224 |
225 | def SetRetryCallback(self, callback):
226 | """
227 | Helper function to add retry callback as a hook
228 | """
229 | self.hooks["response"].append(callback)
230 | self.retryCallback = callback
231 | return self
232 |
233 | def SetAuthToken(self, token):
234 | """
235 | A wrapper to adding basic authentication to the Request
236 | """
237 | return self.SetHeader("Authorization", "Bearer %s" % token)
238 |
239 | def SetBasicAuth(self, username, password):
240 | """
241 | A wrapper to adding basic authentication to the Request
242 | """
243 | auth_str = "%s:%s" % (username, password)
244 | auth_header = base64.b64encode(auth_str.encode("utf-8"))
245 | return self.SetHeader("Authorization", "Basic %s" % auth_header.decode("utf-8"))
246 |
247 | def Execute(self, method=None, url=None):
248 | """
249 | Execute validates a Request and executes it.
250 |
251 | Optionally, a different url or method can be provided if not set yet.
252 | Typically this is controlled by the Client that uses SetMethod
253 | and SetUrl.
254 | """
255 | self.Request.method = method or self.Request.method
256 | self.Request.url = url or self.Request.url
257 | validateRequest(self.Request)
258 |
259 | # prepare and send the request, add callback
260 | p = self.Request.prepare()
261 | response = self.send(p)
262 | response.retryCallback = self.retryCallback
263 | return response
264 |
265 |
266 | def validateRequest(req):
267 | """
268 | Ensure that we have no unfilled template strings
269 | """
270 | regex = re.compile("||||//{2,}")
271 | if not req.url:
272 | raise ValueError("A url is required to prepare a request.")
273 |
274 | if not req.method:
275 | raise ValueError("A method is required to prepare a request")
276 |
277 | if regex.search(req.url):
278 | raise ValueError("request is invalid")
279 |
--------------------------------------------------------------------------------
/opencontainers/distribution/reggie/response.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Copyright (C) 2020-2022 Vanessa Sochat.
4 |
5 | This Source Code Form is subject to the terms of the
6 | Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | """
10 |
11 | from requests.models import Response
12 |
13 |
14 | def GetRelativeLocation(self):
15 | """
16 | GetRelativeLocation returns the path component of the URL contained
17 | """
18 | loc = self.headers.get("Location", "")
19 | if loc and loc.startswith("http"):
20 | loc = "/%s" % "/".join(loc.split("/")[3:])
21 | return loc
22 |
23 |
24 | def GetAbsoluteLocation(self):
25 | """
26 | Get the absolute url
27 |
28 | GetAbsoluteLocation returns the full URL, including protocol and host,
29 | of the location contained in the `Location` header of the response.
30 | """
31 | return self.headers.get("Location")
32 |
33 |
34 | def IsUnauthorized(self):
35 | """
36 | Determine if a status code indicates the request was not authorized.
37 |
38 | IsUnauthorized returns whether or not the response is a 401
39 | """
40 | return self.status_code == 401
41 |
42 |
43 | def Errors(self):
44 | """
45 | Parse a response into OCI-compliant errors.
46 |
47 | Errors attempts to parse a response as OCI-compliant errors array.
48 | If there are no errors, return an empty list.
49 | """
50 | try:
51 | errorResponse = self.json()
52 | except:
53 | return
54 |
55 | return errorResponse.get("errors", [])
56 |
57 |
58 | setattr(Response, "GetRelativeLocation", GetRelativeLocation)
59 | setattr(Response, "GetAbsoluteLocation", GetAbsoluteLocation)
60 | setattr(Response, "IsUnauthorized", IsUnauthorized)
61 | setattr(Response, "Errors", Errors)
62 |
--------------------------------------------------------------------------------
/opencontainers/distribution/specs.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import IntStruct
8 |
9 | # VersionMajor is for an API incompatible changes
10 | VersionMajor = 0
11 |
12 | # VersionMinor is for functionality in a backwards-compatible manner
13 | VersionMinor = 1
14 |
15 | # VersionPatch is for backwards-compatible bug fixes
16 | VersionPatch = 0
17 |
18 | # VersionDev indicates development branch. Releases will be empty string.
19 | VersionDev = "-dev"
20 |
21 | # Version is the specification version that the package types support.
22 | Version = "%d.%d.%d%s" % (VersionMajor, VersionMinor, VersionPatch, VersionDev)
23 |
--------------------------------------------------------------------------------
/opencontainers/distribution/v1/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from .repository import RepositoryList
8 |
--------------------------------------------------------------------------------
/opencontainers/distribution/v1/error.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | from opencontainers.struct import Struct
9 | from opencontainers.logger import bot
10 |
11 | # ErrRegistry is the string returned by and ErrorResponse error.
12 | ErrRegistry = "distribution: registry returned error"
13 |
14 |
15 | class ErrorInfo(Struct):
16 | """ErrorInfo describes a server error returned from a registry."""
17 |
18 | def __init__(self, code, message, detail):
19 | super().__init__()
20 | self.newAttr(name="Code", attType=str, jsonName="code", required=True)
21 | self.newAttr(name="Message", attType=str, jsonName="message", required=True)
22 | self.newAttr(name="Detail", attType=str, jsonName="detail", required=True)
23 |
24 | self.add("Code", code)
25 | self.add("Message", message)
26 | self.add("Detail", detail)
27 |
28 |
29 | class ErrorResponse(Struct):
30 | """ErrorResponse is returned by a registry on an invalid request."""
31 |
32 | def __init__(self, errors=None):
33 | super().__init__()
34 | self.newAttr(
35 | name="Errors", attType=[ErrorInfo], jsonName="errors", required=True
36 | )
37 | self.add("Errors", errors or [])
38 |
39 | def Error(self):
40 | """Error implements the Error interface."""
41 | return ErrRegistry
42 |
43 | def Detail(self):
44 | """Detail returns an ErrorInfo"""
45 | return self.attrs.get("Errors").value
46 |
47 |
48 | class ErrRegistry(Struct):
49 | """ErrorResponse is returned by a registry on an invalid request."""
50 |
51 | def __init__(self, errors=None):
52 | super().__init__()
53 | self.newAttr(
54 | name="Errors", attType=[ErrorInfo], jsonName="errors", required=True
55 | )
56 | self.add("Errors", errors or [])
57 |
--------------------------------------------------------------------------------
/opencontainers/distribution/v1/repository.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | from opencontainers.struct import Struct
9 | from opencontainers.logger import bot
10 |
11 |
12 | class RepositoryList(Struct):
13 | """RepositoryList returns a catalog of repositories maintained on the registry."""
14 |
15 | def __init__(self, repositories=None):
16 | super().__init__()
17 | self.newAttr(
18 | name="Repositories", attType=[str], jsonName="repositories", required=True
19 | )
20 | self.add("Repositories", repositories or [])
21 |
--------------------------------------------------------------------------------
/opencontainers/distribution/v1/tags.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2020 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | from opencontainers.struct import Struct
9 | from opencontainers.logger import bot
10 |
11 |
12 | class TagList(Struct):
13 | """TagList is a list of tags for a given repository."""
14 |
15 | def __init__(self, name, tags=None):
16 | super().__init__()
17 | self.newAttr(name="Name", attType=str, jsonName="name", required=True)
18 | self.newAttr(name="Name", attType=[str], jsonName="tags", required=True)
19 | self.add("Name", name)
20 | self.add("Tags", tags or [])
21 |
--------------------------------------------------------------------------------
/opencontainers/image/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from .specs import (
8 | VersionMajor,
9 | VersionMinor,
10 | VersionPatch,
11 | VersionDev,
12 | Version,
13 | Versioned,
14 | )
15 |
--------------------------------------------------------------------------------
/opencontainers/image/specs.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import IntStruct
8 |
9 | # VersionMajor is for an API incompatible changes
10 | VersionMajor = 1
11 |
12 | # VersionMinor is for functionality in a backwards-compatible manner
13 | VersionMinor = 0
14 |
15 | # VersionPatch is for backwards-compatible bug fixes
16 | VersionPatch = 1
17 |
18 | # VersionDev indicates development branch. Releases will be empty string.
19 | VersionDev = "-dev"
20 |
21 | # Version is the specification version that the package types support.
22 | Version = "%d.%d.%d%s" % (VersionMajor, VersionMinor, VersionPatch, VersionDev)
23 |
24 | # Versioned provides a struct with the manifest schemaVersion and mediaType.
25 | # Incoming content with unknown schema version can be decoded against this
26 | # struct to check the version.
27 |
28 |
29 | class Versioned(IntStruct):
30 | def __init__(self, schemaVersion=None):
31 | super().__init__(schemaVersion or VersionMajor)
32 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from .annotations import (
8 | AnnotationCreated,
9 | AnnotationAuthors,
10 | AnnotationURL,
11 | AnnotationDocumentation,
12 | AnnotationSource,
13 | AnnotationVersion,
14 | AnnotationRevision,
15 | AnnotationVendor,
16 | AnnotationLicenses,
17 | AnnotationRefName,
18 | AnnotationTitle,
19 | AnnotationDescription,
20 | )
21 |
22 | from .config import ImageConfig, RootFS, Image
23 |
24 | from .descriptor import Descriptor, Platform
25 |
26 | from .index import Index
27 | from .layout import ImageLayoutFile, ImageLayoutVersion, ImageLayout
28 |
29 | from .manifest import Manifest
30 |
31 | from .mediatype import (
32 | MediaTypeDescriptor,
33 | MediaTypeLayoutHeader,
34 | MediaTypeImageManifest,
35 | MediaTypeImageIndex,
36 | MediaTypeImageLayer,
37 | MediaTypeImageLayerGzip,
38 | MediaTypeImageLayerZstd,
39 | MediaTypeImageLayerNonDistributable,
40 | MediaTypeImageLayerNonDistributableGzip,
41 | MediaTypeImageLayerNonDistributableZstd,
42 | MediaTypeImageConfig,
43 | )
44 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/annotations.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | # AnnotationCreated is the annotation key for the date and time on which the image was built (date-time string as defined by RFC 3339).
9 | AnnotationCreated = "org.opencontainers.image.created"
10 |
11 | # AnnotationAuthors is the annotation key for the contact details of the people or organization responsible for the image (freeform string).
12 | AnnotationAuthors = "org.opencontainers.image.authors"
13 |
14 | # AnnotationURL is the annotation key for the URL to find more information on the image.
15 | AnnotationURL = "org.opencontainers.image.url"
16 |
17 | # AnnotationDocumentation is the annotation key for the URL to get documentation on the image.
18 | AnnotationDocumentation = "org.opencontainers.image.documentation"
19 |
20 | # AnnotationSource is the annotation key for the URL to get source code for building the image.
21 | AnnotationSource = "org.opencontainers.image.source"
22 |
23 | # AnnotationVersion is the annotation key for the version of the packaged software.
24 | # The version MAY match a label or tag in the source code repository.
25 | # The version MAY be Semantic versioning-compatible.
26 | AnnotationVersion = "org.opencontainers.image.version"
27 |
28 | # AnnotationRevision is the annotation key for the source control revision identifier for the packaged software.
29 | AnnotationRevision = "org.opencontainers.image.revision"
30 |
31 | # AnnotationVendor is the annotation key for the name of the distributing entity, organization or individual.
32 | AnnotationVendor = "org.opencontainers.image.vendor"
33 |
34 | # AnnotationLicenses is the annotation key for the license(s) under which contained software is distributed as an SPDX License Expression.
35 | AnnotationLicenses = "org.opencontainers.image.licenses"
36 |
37 | # AnnotationRefName is the annotation key for the name of the reference for a target.
38 | # SHOULD only be considered valid when on descriptors on `index.json` within image layout.
39 | AnnotationRefName = "org.opencontainers.image.ref.name"
40 |
41 | # AnnotationTitle is the annotation key for the human-readable title of the image.
42 | AnnotationTitle = "org.opencontainers.image.title"
43 |
44 | # AnnotationDescription is the annotation key for the human-readable description of the software packaged in the image.
45 | AnnotationDescription = "org.opencontainers.image.description"
46 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/config.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from opencontainers.digest import Digest
9 |
10 | from datetime import datetime
11 |
12 |
13 | class ImageConfig(Struct):
14 | """
15 | An ImageConfig structure.
16 |
17 | ImageConfig defines the execution parameters which should be used as a
18 | base when running a container using an image.
19 | """
20 |
21 | def __init__(
22 | self,
23 | user=None,
24 | ports=None,
25 | env=None,
26 | entrypoint=None,
27 | cmd=None,
28 | volumes=None,
29 | workingDir=None,
30 | labels=None,
31 | stopSignal=None,
32 | ):
33 |
34 | super().__init__()
35 |
36 | # User defines the username or UID which the process in the container should run as.
37 | self.newAttr(name="User", attType=str)
38 |
39 | # ExposedPorts a set of ports to expose from a container running this image.
40 | self.newAttr(name="ExposedPorts", attType=dict)
41 |
42 | # Env is a list of environment variables to be used in a container.
43 | self.newAttr(
44 | name="Env", attType=[str], regexp="^(?P.+?)=(?P.+)"
45 | )
46 |
47 | # Entrypoint defines a list of arguments to use as the command to execute when the container starts.
48 | self.newAttr(name="Entrypoint", attType=list)
49 |
50 | # Cmd defines the default arguments to the entrypoint of the container.
51 | self.newAttr(name="Cmd", attType=list)
52 |
53 | # Volumes is a set of directories describing where the process is likely write data specific to a container instance.
54 | self.newAttr(name="Volumes", attType=dict)
55 |
56 | # WorkingDir sets the current working directory of the entrypoint process in the container.
57 | self.newAttr(name="WorkingDir", attType=str)
58 |
59 | # Labels contains arbitrary metadata for the container.
60 | self.newAttr(name="Labels", attType=dict)
61 |
62 | # StopSignal contains the system call signal that will be sent to the container to exit.
63 | self.newAttr(name="StopSignal", attType=str)
64 |
65 | self.add("User", user)
66 | self.add("ExposedPorts", ports)
67 | self.add("Env", env)
68 | self.add("Entrypoint", entrypoint)
69 | self.add("Cmd", cmd)
70 | self.add("Volumes", volumes)
71 | self.add("WorkingDir", workingDir)
72 | self.add("Labels", labels)
73 | self.add("StopSignal", stopSignal)
74 |
75 |
76 | class RootFS(Struct):
77 | """
78 | RootFS describes a layer content addresses
79 | """
80 |
81 | def __init__(self, rootfs_type=None, diff_ids=None):
82 | super().__init__()
83 |
84 | # Type is the type of the rootfs, different from GoLang since type can't be used
85 | self.newAttr(name="RootFSType", attType=str, omitempty=False, jsonName="type")
86 |
87 | # DiffIDs is an array of layer content hashes (DiffIDs), in order from bottom-most to top-most.
88 | self.newAttr(
89 | name="DiffIDs", attType=[Digest], omitempty=False, jsonName="diff_ids"
90 | )
91 |
92 | self.add("RootFSType", rootfs_type)
93 | self.add("DiffIDs", diff_ids)
94 |
95 |
96 | class History(Struct):
97 | """
98 | History describes the history of a layer.
99 | """
100 |
101 | def __init__(
102 | self, created=None, created_by=None, author=None, comment=None, empty_layer=None
103 | ):
104 |
105 | super().__init__()
106 |
107 | # Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6.
108 | self.newAttr("Created", attType=datetime, jsonName="created")
109 |
110 | # CreatedBy is the command which created the layer.
111 | self.newAttr("CreatedBy", attType=str, jsonName="created_by")
112 |
113 | # Author is the author of the build point.
114 | self.newAttr("Author", attType=str, jsonName="author")
115 |
116 | # Comment is a custom message set when creating the layer.
117 | self.newAttr("Comment", attType=str, jsonName="comment")
118 |
119 | # EmptyLayer is used to mark if the history item created a filesystem diff.
120 | self.newAttr("EmptyLayer", attType=bool, jsonName="empty_layer")
121 |
122 | self.add("Created", created)
123 | self.add("CreatedBy", created_by)
124 | self.add("Author", author)
125 | self.add("Comment", comment)
126 | self.add("EmptyLayer", empty_layer)
127 |
128 |
129 | class Image(Struct):
130 | """
131 | An Image Structure
132 |
133 | Image is the JSON structure which describes some basic information about
134 | the image. This provides the `application/vnd.oci.image.config.v1+json`
135 | mediatype when marshalled to JSON.
136 | """
137 |
138 | def __init__(
139 | self,
140 | created=None,
141 | author=None,
142 | arch=None,
143 | imageOS=None,
144 | imageConfig=None,
145 | rootfs=None,
146 | hist=None,
147 | ):
148 |
149 | super().__init__()
150 |
151 | # Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6.
152 | self.newAttr("Created", attType=datetime, jsonName="created")
153 |
154 | # Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image.
155 | self.newAttr("Author", attType=str, jsonName="author")
156 |
157 | # Architecture is the CPU architecture which the binaries in this image are built to run on.
158 | self.newAttr(
159 | name="Architecture", attType=str, jsonName="architecture", required=True
160 | )
161 |
162 | # OS is the name of the operating system which the image is built to run on.
163 | self.newAttr("OS", attType=str, jsonName="os", required=True)
164 |
165 | # Config defines the execution parameters which should be used as a base when running a container using the image.
166 | self.newAttr("Config", attType=ImageConfig, jsonName="config")
167 |
168 | # RootFS references the layer content addresses used by the image.
169 | self.newAttr("RootFS", attType=RootFS, jsonName="rootfs", required=True)
170 |
171 | # History describes the history of each layer.
172 | self.newAttr("History", attType=[History], jsonName="history")
173 |
174 | self.add("Created", created)
175 | self.add("Author", author)
176 | self.add("Architecture", arch)
177 | self.add("OS", imageOS)
178 | self.add("Config", imageConfig)
179 | self.add("RootFS", rootfs)
180 | self.add("History", hist)
181 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/descriptor.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from opencontainers.digest import Digest
9 |
10 |
11 | class Descriptor(Struct):
12 | """
13 | Descriptor describes the disposition of targeted content.
14 |
15 | This structure provides `application/vnd.oci.descriptor.v1+json`
16 | mediatype when marshalled to JSON.
17 | """
18 |
19 | def __init__(
20 | self,
21 | digest=None,
22 | size=None,
23 | mediatype=None,
24 | urls=None,
25 | annotations=None,
26 | platform=None,
27 | ):
28 | super().__init__()
29 |
30 | # MediaType is the media type of the object this schema refers to.
31 | regexp = "^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"
32 | self.newAttr(
33 | name="MediaType",
34 | attType=str,
35 | jsonName="mediaType",
36 | regexp=regexp,
37 | required=True,
38 | )
39 |
40 | # Digest is the digest of the targeted content.
41 | self.newAttr(name="Digest", attType=Digest, jsonName="digest", required=True)
42 |
43 | # Size specifies the size in bytes of the blob.
44 | self.newAttr(name="Size", attType=int, jsonName="size", required=True)
45 |
46 | # URLs specifies a list of URLs from which this object MAY be downloaded
47 | regexp = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\), ]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
48 | self.newAttr(name="URLs", attType=[str], jsonName="urls", regexp=regexp)
49 |
50 | # Annotations contains arbitrary metadata relating to the targeted content.
51 | self.newAttr(name="Annotations", attType=dict, jsonName="annotations")
52 |
53 | # Platform describes the platform which the image in the manifest runs on.
54 | # This should only be used when referring to a manifest.
55 | self.newAttr(name="Platform", attType=Platform, jsonName="platform")
56 |
57 | self.add("Digest", digest)
58 | self.add("Size", size)
59 | self.add("MediaType", mediatype)
60 | self.add("URLs", urls)
61 | self.add("Annotations", annotations)
62 | self.add("Platform", platform)
63 |
64 |
65 | class Platform(Struct):
66 | """
67 | Platform describes the platform which the image in the manifest runs on.
68 | """
69 |
70 | def __init__(
71 | self,
72 | arch=None,
73 | platform_os=None,
74 | os_version=None,
75 | os_features=None,
76 | variant=None,
77 | ):
78 |
79 | super().__init__()
80 |
81 | # Architecture field specifies the CPU architecture, for example
82 | # `amd64` or `ppc64`.
83 | self.newAttr(
84 | name="Architecture", attType=str, jsonName="architecture", required=True
85 | )
86 |
87 | # OS specifies the operating system, for example `linux` or `windows`.
88 | self.newAttr(name="OS", attType=str, jsonName="os", required=True)
89 |
90 | # OSVersion is an optional field specifying the operating system
91 | # version, for example on Windows `10.0.14393.1066`.
92 | self.newAttr(name="OSVersion", attType=str, jsonName="os.version")
93 |
94 | # OSFeatures is an optional field specifying an array of strings,
95 | # each listing a required OS feature (for example on Windows `win32k`).
96 | self.newAttr(name="OSFeatures", attType=[str], jsonName="os.features")
97 |
98 | # Variant is an optional field specifying a variant of the CPU, for
99 | # example `v7` to specify ARMv7 when architecture is `arm`.
100 | self.newAttr(name="Variant", attType=str, jsonName="variant")
101 |
102 | self.add("Architecture", arch)
103 | self.add("OS", platform_os)
104 | self.add("OSVersion", os_version)
105 | self.add("OSFeatures", os_features)
106 | self.add("Variant", variant)
107 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/index.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from opencontainers.image.specs import Versioned
9 | from opencontainers.logger import bot
10 | from .mediatype import MediaTypeImageIndex, MediaTypeImageManifest
11 | from .descriptor import Descriptor
12 | import re
13 |
14 |
15 | class Index(Struct):
16 | """
17 | Index references manifests for various platforms.
18 |
19 | This structure provides `application/vnd.oci.image.index.v1+json`
20 | mediatype when marshalled to JSON.
21 | """
22 |
23 | def __init__(self, manifests=None, schemaVersion=None, annotations=None):
24 | super().__init__()
25 |
26 | self.newAttr(name="schemaVersion", attType=Versioned, required=True)
27 |
28 | # Manifests references platform specific manifests.
29 | self.newAttr(
30 | name="Manifests", attType=[Descriptor], jsonName="manifests", required=True
31 | )
32 |
33 | # Annotations contains arbitrary metadata for the image index.
34 | self.newAttr(name="Annotations", attType=dict, jsonName="annotations")
35 |
36 | self.add("Manifests", manifests)
37 | self.add("Annotations", annotations)
38 | self.add("schemaVersion", schemaVersion)
39 |
40 | def _validate(self):
41 | """
42 | Validation functions for an index.
43 |
44 | custom validation function to ensure that Manifests mediaTypes
45 | are valid.
46 | """
47 | valid_types = [MediaTypeImageManifest, MediaTypeImageIndex]
48 |
49 | manifests = self.attrs.get("Manifests").value
50 | if manifests:
51 | for manifest in manifests:
52 | mediaType = manifest.attrs.get("MediaType")
53 | if mediaType.value not in valid_types:
54 |
55 | # Case 1: it's a custom media type (allowed) but give warning
56 | if mediaType.validate_regexp(mediaType.value):
57 | bot.warning(
58 | "%s is valid, but not registered." % mediaType.value
59 | )
60 |
61 | # Case 2: not valid and doesn't match regular expression
62 | else:
63 | bot.error("%s is not valid for index manifest." % mediaType)
64 | return False
65 |
66 | return True
67 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/layout.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 |
9 | # ImageLayoutFile is the file name of oci image layout file
10 | ImageLayoutFile = "oci-layout"
11 |
12 | # ImageLayoutVersion is the version of ImageLayout
13 | ImageLayoutVersion = "1.0.0"
14 |
15 |
16 | class ImageLayout(Struct):
17 | """
18 | An ImageLayout structure.
19 |
20 | ImageLayout is the structure in the "oci-layout" file, found in the root
21 | of an OCI Image-layout directory.
22 | """
23 |
24 | def __init__(self, version=None):
25 | super().__init__()
26 |
27 | # This is for semver, but without the v
28 | regexp = r"^(?P\d+)\.(?P\d+)\.(?P\d+)~?(?P[a-z]\w+[\d+])?$"
29 | self.newAttr(
30 | name="Version",
31 | attType=str,
32 | jsonName="imageLayoutVersion",
33 | required=True,
34 | regexp=regexp,
35 | )
36 | self.add("Version", version or ImageLayoutVersion)
37 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/manifest.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from opencontainers.struct import Struct
8 | from opencontainers.image.specs import Versioned
9 | from opencontainers.logger import bot
10 | from .descriptor import Descriptor
11 | from .mediatype import (
12 | MediaTypeImageConfig,
13 | MediaTypeImageLayer,
14 | MediaTypeImageLayerGzip,
15 | MediaTypeImageLayerZstd,
16 | MediaTypeImageLayerNonDistributable,
17 | MediaTypeImageLayerNonDistributableGzip,
18 | MediaTypeImageLayerNonDistributableZstd,
19 | )
20 |
21 |
22 | class Manifest(Struct):
23 | """
24 | A Manifest Structure
25 |
26 | Manifest provides `application/vnd.oci.image.manifest.v1+json`
27 | mediatype structure when marshalled to JSON.
28 | """
29 |
30 | def __init__(
31 | self, manifestConfig=None, layers=None, schemaVersion=None, annotations=None
32 | ):
33 | super().__init__()
34 |
35 | self.newAttr(name="schemaVersion", attType=Versioned, required=True)
36 |
37 | # Config references a configuration object for a container, by digest.
38 | # The referenced configuration object is a JSON blob that the runtime uses to set up the container.
39 | self.newAttr(
40 | name="Config", attType=Descriptor, jsonName="config", required=True
41 | )
42 |
43 | # Layers is an indexed list of layers referenced by the manifest.
44 | self.newAttr(
45 | name="Layers", attType=[Descriptor], jsonName="layers", required=True
46 | )
47 |
48 | # Annotations contains arbitrary metadata for the image manifest.
49 | self.newAttr(name="Annotations", attType=dict, jsonName="annotations")
50 |
51 | self.add("Config", manifestConfig)
52 | self.add("Layers", layers)
53 | self.add("Annotations", annotations)
54 | self.add("schemaVersion", schemaVersion)
55 |
56 | def _validate(self):
57 | """
58 | Custom validation functions for an image Manifest.
59 |
60 | custom validation function to ensure that Config and Layers mediaTypes
61 | are valid. By the time we get here, we know there is a Config object,
62 | and there can be one or more layers.
63 | """
64 | if not self._validateLayerMediaTypes() or not self._validateConfigMediaType():
65 | return False
66 | return True
67 |
68 | def _validateConfigMediaType(self):
69 | """validate the config media type."""
70 | # The media type of the config must be for the config
71 | manifestConfig = self.attrs.get("Config").value
72 |
73 | # Missing config is not valid
74 | if not manifestConfig:
75 | return False
76 |
77 | mediaType = manifestConfig.attrs.get("MediaType").value
78 | if not mediaType:
79 | return False
80 |
81 | if mediaType != MediaTypeImageConfig:
82 | bot.error(
83 | "config mediaType %s is invalid, should be %s"
84 | % (mediaType, MediaTypeImageConfig)
85 | )
86 | return False
87 | return True
88 |
89 | def _validateLayerMediaTypes(self):
90 | """
91 | Validate the Layer Media Types
92 | """
93 | # These are valid mediaTypes for layers
94 | layerMediaTypes = [
95 | MediaTypeImageLayer,
96 | MediaTypeImageLayerGzip,
97 | MediaTypeImageLayerZstd,
98 | MediaTypeImageLayerNonDistributable,
99 | MediaTypeImageLayerNonDistributableGzip,
100 | MediaTypeImageLayerNonDistributableZstd,
101 | ]
102 |
103 | # No layers, not valid
104 | layers = self.attrs.get("Layers").value
105 | if layers == None:
106 | return False
107 |
108 | # Check against valid mediaType Layers
109 | for layer in layers:
110 | mediaType = layer.attrs.get("MediaType").value
111 | if mediaType not in layerMediaTypes:
112 | bot.error("layer mediaType %s is invalid" % mediaType)
113 | return False
114 |
115 | return True
116 |
--------------------------------------------------------------------------------
/opencontainers/image/v1/mediatype.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 |
8 | # MediaTypeDescriptor specifies the media type for a content descriptor.
9 | MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json"
10 |
11 | # MediaTypeLayoutHeader specifies the media type for the oci-layout.
12 | MediaTypeLayoutHeader = "application/vnd.oci.layout.header.v1+json"
13 |
14 | # MediaTypeImageManifest specifies the media type for an image manifest.
15 | MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json"
16 |
17 | # MediaTypeImageIndex specifies the media type for an image index.
18 | MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json"
19 |
20 | # MediaTypeImageLayer is the media type used for layers referenced by the manifest.
21 | MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar"
22 |
23 | # MediaTypeImageLayerGzip is the media type used for gzipped layers
24 | # referenced by the manifest.
25 | MediaTypeImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip"
26 |
27 | # MediaTypeImageLayerZstd is the media type used for zstd compressed
28 | # layers referenced by the manifest.
29 | MediaTypeImageLayerZstd = "application/vnd.oci.image.layer.v1.tar+zstd"
30 |
31 | # MediaTypeImageLayerNonDistributable is the media type for layers referenced by
32 | # the manifest but with distribution restrictions.
33 | MediaTypeImageLayerNonDistributable = (
34 | "application/vnd.oci.image.layer.nondistributable.v1.tar"
35 | )
36 |
37 | # MediaTypeImageLayerNonDistributableGzip is the media type for
38 | # gzipped layers referenced by the manifest but with distribution
39 | # restrictions.
40 | MediaTypeImageLayerNonDistributableGzip = (
41 | "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
42 | )
43 |
44 | # MediaTypeImageLayerNonDistributableZstd is the media type for zstd#
45 | # compressed layers referenced by the manifest but with distribution
46 | # restrictions.
47 | MediaTypeImageLayerNonDistributableZstd = (
48 | "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"
49 | )
50 |
51 | # MediaTypeImageConfig specifies the media type for the image configuration.
52 | MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json"
53 |
--------------------------------------------------------------------------------
/opencontainers/logger.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | import logging as _logging
8 | import platform
9 | import sys
10 | import os
11 | import threading
12 | import inspect
13 |
14 |
15 | class ColorizingStreamHandler(_logging.StreamHandler):
16 |
17 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
18 | RESET_SEQ = "\033[0m"
19 | COLOR_SEQ = "\033[%dm"
20 | BOLD_SEQ = "\033[1m"
21 |
22 | colors = {
23 | "WARNING": YELLOW,
24 | "INFO": GREEN,
25 | "DEBUG": BLUE,
26 | "CRITICAL": RED,
27 | "ERROR": RED,
28 | }
29 |
30 | def __init__(self, nocolor=False, stream=sys.stderr, use_threads=False):
31 | super().__init__(stream=stream)
32 | self._output_lock = threading.Lock()
33 | self.nocolor = nocolor or not self.can_color_tty()
34 |
35 | def can_color_tty(self):
36 | if "TERM" in os.environ and os.environ["TERM"] == "dumb":
37 | return False
38 | return self.is_tty and not platform.system() == "Windows"
39 |
40 | @property
41 | def is_tty(self):
42 | isatty = getattr(self.stream, "isatty", None)
43 | return isatty and isatty()
44 |
45 | def emit(self, record):
46 | with self._output_lock:
47 | try:
48 | self.format(record) # add the message to the record
49 | self.stream.write(self.decorate(record))
50 | self.stream.write(getattr(self, "terminator", "\n"))
51 | self.flush()
52 | except BrokenPipeError as e:
53 | raise e
54 | except (KeyboardInterrupt, SystemExit):
55 | # ignore any exceptions in these cases as any relevant messages have been printed before
56 | pass
57 | except Exception:
58 | self.handleError(record)
59 |
60 | def decorate(self, record):
61 | message = record.message
62 | message = [message]
63 | if not self.nocolor and record.levelname in self.colors:
64 | message.insert(0, self.COLOR_SEQ % (30 + self.colors[record.levelname]))
65 | message.append(self.RESET_SEQ)
66 | return "".join(message)
67 |
68 |
69 | class Logger:
70 | def __init__(self):
71 | self.logger = _logging.getLogger(__name__)
72 | self.log_handler = [self.text_handler]
73 | self.stream_handler = None
74 | self.printshellcmds = False
75 | self.quiet = False
76 | self.logfile = None
77 | self.last_msg_was_job_info = False
78 | self.logfile_handler = None
79 |
80 | def cleanup(self):
81 | if self.logfile_handler is not None:
82 | self.logger.removeHandler(self.logfile_handler)
83 | self.logfile_handler.close()
84 | self.log_handler = [self.text_handler]
85 |
86 | def handler(self, msg):
87 | for handler in self.log_handler:
88 | handler(msg)
89 |
90 | def set_stream_handler(self, stream_handler):
91 | if self.stream_handler is not None:
92 | self.logger.removeHandler(self.stream_handler)
93 | self.stream_handler = stream_handler
94 | self.logger.addHandler(stream_handler)
95 |
96 | def set_level(self, level):
97 | self.logger.setLevel(level)
98 |
99 | def location(self, msg):
100 | callerframerecord = inspect.stack()[1]
101 | frame = callerframerecord[0]
102 | info = inspect.getframeinfo(frame)
103 | self.debug(
104 | "{}: {info.filename}, {info.function}, {info.lineno}".format(msg, info=info)
105 | )
106 |
107 | def info(self, msg):
108 | self.handler(dict(level="info", msg=msg))
109 |
110 | def warning(self, msg):
111 | self.handler(dict(level="warning", msg=msg))
112 |
113 | def debug(self, msg):
114 | self.handler(dict(level="debug", msg=msg))
115 |
116 | def error(self, msg):
117 | self.handler(dict(level="error", msg=msg))
118 |
119 | def exit(self, msg, return_code=1):
120 | self.handler(dict(level="error", msg=msg))
121 | sys.exit(return_code)
122 |
123 | def progress(self, done=None, total=None):
124 | self.handler(dict(level="progress", done=done, total=total))
125 |
126 | def shellcmd(self, msg):
127 | if msg is not None:
128 | msg = dict(level="shellcmd", msg=msg)
129 | self.handler(msg)
130 |
131 | def text_handler(self, msg):
132 | """The default snakemake log handler.
133 | Prints the output to the console.
134 | Args:
135 | msg (dict): the log message dictionary
136 | """
137 | level = msg["level"]
138 | if level == "info" and not self.quiet:
139 | self.logger.info(msg["msg"])
140 | if level == "warning":
141 | self.logger.warning(msg["msg"])
142 | elif level == "error":
143 | self.logger.error(msg["msg"])
144 | elif level == "debug":
145 | self.logger.debug(msg["msg"])
146 | elif level == "progress" and not self.quiet:
147 | done = msg["done"]
148 | total = msg["total"]
149 | p = done / total
150 | percent_fmt = ("{:.2%}" if p < 0.01 else "{:.0%}").format(p)
151 | self.logger.info(
152 | "{} of {} steps ({}) done".format(done, total, percent_fmt)
153 | )
154 | elif level == "shellcmd":
155 | if self.printshellcmds:
156 | self.logger.warning(msg["msg"])
157 |
158 |
159 | bot = Logger()
160 |
161 |
162 | def setup_logger(
163 | quiet=False,
164 | printshellcmds=False,
165 | nocolor=False,
166 | stdout=False,
167 | debug=False,
168 | verbose=False,
169 | use_threads=False,
170 | wms_monitor=None,
171 | ):
172 | # console output only if no custom logger was specified
173 | stream_handler = ColorizingStreamHandler(
174 | nocolor=nocolor,
175 | stream=sys.stdout if stdout else sys.stderr,
176 | use_threads=use_threads,
177 | )
178 | level = _logging.INFO
179 | if verbose:
180 | level = _logging.VERBOSE
181 | elif debug:
182 | level = _logging.DEBUG
183 |
184 | logger.set_stream_handler(stream_handler)
185 | logger.set_level(level)
186 | logger.quiet = quiet
187 | logger.printshellcmds = printshellcmds
188 |
--------------------------------------------------------------------------------
/opencontainers/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsoch/oci-python/ceb4fcc090851717a3069d78e85ceb1e86c2740c/opencontainers/tests/__init__.py
--------------------------------------------------------------------------------
/opencontainers/tests/mock_server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from http.server import BaseHTTPRequestHandler, HTTPServer
10 | import json
11 | import re
12 | import socket
13 | import base64
14 | from threading import Thread
15 |
16 | import requests
17 | import json
18 |
19 |
20 | class MockRegistryRequestHandler(BaseHTTPRequestHandler):
21 | """The mock server can handle authentication"""
22 |
23 | AUTH_PATTERN = re.compile(r"/auth")
24 |
25 | def do_GET(self):
26 | print("GET %s" % self.path)
27 |
28 | # Authentication request
29 | if re.search(self.AUTH_PATTERN, self.path):
30 |
31 | # We expect these credentials
32 | expectedAuthHeader = "Basic " + base64.b64encode(
33 | b"testuser:testpass"
34 | ).decode("utf-8")
35 | foundAuthHeader = self.headers.get("Authorization")
36 |
37 | if foundAuthHeader != expectedAuthHeader:
38 | self.send_response(requests.codes.unauthorized)
39 | else:
40 | self.send_response(requests.codes.ok)
41 | self.end_headers()
42 | if self.authUseAccessToken:
43 | self.wfile.write(
44 | json.dumps({"access_token": "abc123"}).encode("utf-8")
45 | )
46 | else:
47 | self.wfile.write(json.dumps({"token": "abc123"}).encode("utf-8"))
48 |
49 | # Add response headers.
50 | # self.send_header('Content-Type', 'application/json; charset=utf-8')
51 |
52 | # Add response content.
53 | # response_content = json.dumps([])
54 | # self.wfile.write(response_content.encode('utf-8'))
55 | return
56 |
57 | # Registry request to return Location header
58 | elif re.search("withlocation", self.path):
59 | print("/tags/list withlocation endpoint was hit.")
60 | self.send_response(requests.codes.ok)
61 |
62 | # This is an artificially generated (successful) case that includes errors and Location to parse
63 | self.send_header(
64 | "Location",
65 | "http://abc123location.io/v2/blobs/uploads/e361aeb8-3181-11ea-850d-2e728ce88125",
66 | )
67 | self.end_headers()
68 | return
69 |
70 | # Registry request that has errors
71 | elif re.search("witherrors", self.path):
72 | print("/tags/list with errors endpoint was hit.")
73 | self.send_response(requests.codes.ok)
74 | error_response = {
75 | "errors": [
76 | {
77 | "code": "BLOB_UNKNOWN",
78 | "message": "blob unknown to registry",
79 | "detail": "lol",
80 | }
81 | ]
82 | }
83 | self.end_headers()
84 | self.wfile.write(json.dumps(error_response).encode("utf-8"))
85 | return
86 |
87 | # Registry request that doesn't require auth
88 | elif re.search("/tags/list", self.path):
89 | print("/tags/list endpoint was hit.")
90 | self.send_response(requests.codes.ok)
91 | self.end_headers()
92 | return
93 |
94 | def do_PUT(self):
95 | print("PUT %s" % self.path)
96 | header = self.headers.get("Authorization")
97 | if header == "Bearer abc123":
98 | self.send_response(requests.codes.ok)
99 | self.send_header(
100 | "Location",
101 | "http://abc123location.io/v2/blobs/uploads/e361aeb8-3181-11ea-850d-2e728ce88125",
102 | )
103 | self.end_headers()
104 | else:
105 | self.send_response(requests.codes.unauthorized)
106 | wwwHeader = (
107 | 'Bearer realm="http://localhost:%s/auth",service="testservice",scope="testscope"'
108 | % self.port
109 | )
110 | self.send_header("www-authenticate", wwwHeader)
111 | self.end_headers()
112 | return
113 |
114 |
115 | def get_free_port():
116 | s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
117 | s.bind(("localhost", 0))
118 | address, port = s.getsockname()
119 | s.close()
120 | return port
121 |
122 |
123 | def start_mock_server(port, authUseAccessToken=True):
124 | MockRegistryRequestHandler.port = port
125 | MockRegistryRequestHandler.authUseAccessToken = authUseAccessToken
126 | mock_server = HTTPServer(("localhost", port), MockRegistryRequestHandler)
127 | mock_server.authUseAccessToken = authUseAccessToken
128 | mock_server_thread = Thread(target=mock_server.serve_forever)
129 | mock_server_thread.setDaemon(True)
130 | mock_server_thread.start()
131 | return mock_server, mock_server_thread
132 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_algorithm.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.digest import Digest, FromBytes
10 | from opencontainers.digest.algorithm import Algorithm, algorithms
11 |
12 | from opencontainers.digest.exceptions import (
13 | ErrDigestInvalidLength,
14 | ErrDigestInvalidFormat,
15 | ErrDigestUnsupported,
16 | )
17 | import os
18 | import io
19 | import string
20 | import random
21 | import pytest
22 |
23 |
24 | def test_algorithms(tmp_path):
25 | """test creation of an opencontainers Algorithm"""
26 | # Generate random bytes
27 | asciitext = "".join([random.choice(string.ascii_letters) for n in range(20)])
28 | p = bytes(asciitext, "utf-8")
29 |
30 | for name, alg in algorithms.items():
31 | h = alg.hash()
32 | h.update(p)
33 | expected = Digest("%s:%s" % (alg, h.hexdigest()))
34 |
35 | # Calculate from reader (not necessary for Python, but mirroring golang)
36 | newReader = io.BytesIO(p)
37 | readerDgst = alg.fromReader(newReader)
38 |
39 | assert alg.fromBytes(p) == readerDgst == alg.fromString(asciitext)
40 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.image.v1 import Image
10 | import os
11 | import pytest
12 |
13 |
14 | config_invalid_os = {
15 | "architecture": "amd64",
16 | "os": 123,
17 | "rootfs": {
18 | "diff_ids": [
19 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
20 | ],
21 | "type": "layers",
22 | },
23 | }
24 |
25 | config_invalid_user = {
26 | "created": "2015-10-31T22:22:56.015925234Z",
27 | "author": "Alyssa P. Hacker ",
28 | "architecture": "amd64",
29 | "os": "linux",
30 | "config": {"User": 1234},
31 | "rootfs": {
32 | "diff_ids": [
33 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
34 | ],
35 | "type": "layers",
36 | },
37 | }
38 |
39 | config_invalid_history = {
40 | "history": "should be an array",
41 | "architecture": "amd64",
42 | "os": "linux",
43 | "rootfs": {
44 | "diff_ids": [
45 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
46 | ],
47 | "type": "layers",
48 | },
49 | }
50 |
51 | config_invalid_envint = {
52 | "architecture": "amd64",
53 | "os": "linux",
54 | "config": {"Env": [7353]},
55 | "rootfs": {
56 | "diff_ids": [
57 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
58 | ],
59 | "type": "layers",
60 | },
61 | }
62 |
63 |
64 | config_invalid_volumes = {
65 | "architecture": "amd64",
66 | "os": "linux",
67 | "config": {"Volumes": ["/var/job-result-data", "/var/log/my-app-logs"]},
68 | "rootfs": {
69 | "diff_ids": [
70 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
71 | ],
72 | "type": "layers",
73 | },
74 | }
75 |
76 | config_valid_with_optional = {
77 | "created": "2015-10-31T22:22:56.015925234Z",
78 | "author": "Alyssa P. Hacker ",
79 | "architecture": "amd64",
80 | "os": "linux",
81 | "config": {
82 | "User": "1:1",
83 | "ExposedPorts": {"8080/tcp": {}},
84 | "Env": [
85 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
86 | "FOO=docker_is_a_really",
87 | "BAR=great_tool_you_know",
88 | ],
89 | "Entrypoint": ["/bin/sh"],
90 | "Cmd": ["--foreground", "--config", "/etc/my-app.d/default.cfg"],
91 | "Volumes": {"/var/job-result-data": {}, "/var/log/my-app-logs": {}},
92 | "StopSignal": "SIGKILL",
93 | "WorkingDir": "/home/alice",
94 | "Labels": {
95 | "com.example.project.git.url": "https://example.com/project.git",
96 | "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b",
97 | },
98 | },
99 | "rootfs": {
100 | "diff_ids": [
101 | "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827",
102 | "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d",
103 | ],
104 | "type": "layers",
105 | },
106 | "history": [
107 | {
108 | "created": "2015-10-31T22:22:54.690851953Z",
109 | "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /",
110 | },
111 | {
112 | "created": "2015-10-31T22:22:55.613815829Z",
113 | "created_by": '/bin/sh -c #(nop) CMD ["sh"]',
114 | "empty_layer": True,
115 | },
116 | ],
117 | }
118 |
119 | config_valid_required = {
120 | "architecture": "amd64",
121 | "os": "linux",
122 | "rootfs": {
123 | "diff_ids": [
124 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
125 | ],
126 | "type": "layers",
127 | },
128 | }
129 |
130 |
131 | config_invalid_env = {
132 | "architecture": "amd64",
133 | "os": "linux",
134 | "config": {"Env": ["foo"]},
135 | "rootfs": {
136 | "diff_ids": [
137 | "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
138 | ],
139 | "type": "layers",
140 | },
141 | }
142 |
143 |
144 | def test_example_config(tmp_path):
145 | """test creation of an opencontainers Image"""
146 | image = Image()
147 |
148 | # OS is int, and is invalid
149 | with pytest.raises(SystemExit):
150 | image.load(config_invalid_os)
151 |
152 | # User should be string
153 | with pytest.raises(SystemExit):
154 | image.load(config_invalid_user)
155 |
156 | # History should be list
157 | with pytest.raises(SystemExit):
158 | image.load(config_invalid_history)
159 |
160 | # Env is numeric, must be list of strings
161 | with pytest.raises(SystemExit):
162 | image.load(config_invalid_envint)
163 |
164 | # volumes cannot be list
165 | with pytest.raises(SystemExit):
166 | image.load(config_invalid_volumes)
167 |
168 | # invalid environment
169 | with pytest.raises(SystemExit):
170 | image.load(config_invalid_env)
171 |
172 | # valid config with optional fields
173 | image.load(config_valid_with_optional)
174 | assert image.validate()
175 |
176 | # minimum valid required
177 | image.load(config_valid_required)
178 | assert image.validate()
179 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_descriptor.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.image.v1 import Descriptor
10 | from opencontainers.digest.exceptions import (
11 | ErrDigestInvalidFormat,
12 | ErrDigestInvalidLength,
13 | ErrDigestUnsupported,
14 | )
15 | import os
16 | import pytest
17 |
18 |
19 | valid_descriptor = {
20 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
21 | "size": 7682,
22 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
23 | }
24 |
25 | mediatype_missing = {
26 | "size": 7682,
27 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
28 | }
29 |
30 | mediatype_nosubtype = {
31 | "mediaType": "application",
32 | "size": 7682,
33 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
34 | }
35 |
36 | mediatype_invalidtype = {
37 | "mediaType": ".foo/bar",
38 | "size": 7682,
39 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
40 | }
41 |
42 | mediatype_invalidsubtype = {
43 | "mediaType": "foo/.bar",
44 | "size": 7682,
45 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
46 | }
47 |
48 | expected_success = {
49 | "mediaType": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567",
50 | "size": 7682,
51 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
52 | }
53 |
54 | type_toolong = {
55 | "mediaType": "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678/bar",
56 | "size": 7682,
57 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
58 | }
59 |
60 | subtype_toolong = {
61 | "mediaType": "foo/12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678",
62 | "size": 7682,
63 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
64 | }
65 |
66 | size_missing = {
67 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
68 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
69 | }
70 |
71 | size_string = {
72 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
73 | "size": "7682",
74 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
75 | }
76 |
77 | digest_missing = {
78 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
79 | "size": 7682,
80 | }
81 |
82 |
83 | no_algorithm = {
84 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
85 | "size": 7682,
86 | "digest": ":5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
87 | }
88 |
89 | no_hash = {
90 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
91 | "size": 7682,
92 | "digest": "sha256",
93 | }
94 |
95 | invalid_algchars = {
96 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
97 | "size": 7682,
98 | "digest": "SHA256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
99 | }
100 |
101 | uppercase_digest = {
102 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
103 | "size": 7682,
104 | "digest": "sha256:5B0BCABD1ED22E9FB1310CF6C2DEC7CDEF19F0AD69EFA1F392E94A4333501270",
105 | }
106 |
107 | valid_urls = {
108 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
109 | "size": 7682,
110 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
111 | "urls": ["https://example.com/foo"],
112 | }
113 |
114 | invalid_urls = {
115 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
116 | "size": 7682,
117 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
118 | "urls": ["value"],
119 | }
120 |
121 | valids = [
122 | {
123 | "mediaType": "application/vnd.oci.image.config.v1+json",
124 | "size": 1470,
125 | "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
126 | },
127 | {
128 | "mediaType": "application/vnd.oci.image.config.v1+json",
129 | "size": 1470,
130 | "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
131 | },
132 | {
133 | "mediaType": "application/vnd.oci.image.config.v1+json",
134 | "size": 1470,
135 | "digest": "sha256+foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
136 | },
137 | {
138 | "mediaType": "application/vnd.oci.image.config.v1+json",
139 | "size": 1470,
140 | "digest": "sha256.foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
141 | # multihash example removed, not supported is invalid
142 | },
143 | ]
144 |
145 | repeated_seps_invalid = {
146 | "mediaType": "application/vnd.oci.image.config.v1+json",
147 | "size": 1470,
148 | "digest": "sha256+foo+-b:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
149 | }
150 |
151 | invalid_digest_length = {
152 | "digest": "sha256+b64u:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564",
153 | "size": 1000000,
154 | "mediaType": "application/vnd.oci.image.config.v1+json",
155 | }
156 |
157 | digest_unknown = {
158 | "digest": "sha256+b64u.unknownlength:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564=",
159 | "size": 1000000,
160 | "mediaType": "application/vnd.oci.image.config.v1+json",
161 | }
162 |
163 |
164 | def test_descriptor(tmp_path):
165 | """test creation of opencontiners Descriptor"""
166 | desc = Descriptor()
167 |
168 | # expected pass: valid descriptor
169 | desc.load(valid_descriptor)
170 |
171 | # expected failure: mediaType missing
172 | with pytest.raises(SystemExit):
173 | desc.load(mediatype_missing)
174 |
175 | # expected failure: mediaType does not match pattern (no subtype)
176 | with pytest.raises(SystemExit):
177 | desc.load(mediatype_nosubtype)
178 |
179 | # expected failure: mediaType does not match pattern (invalid first type character)
180 | with pytest.raises(SystemExit):
181 | desc.load(mediatype_invalidtype)
182 |
183 | # expected failure: mediaType does not match pattern (invalid first subtype character)
184 | with pytest.raises(SystemExit):
185 | desc.load(mediatype_invalidsubtype)
186 |
187 | # expected success: mediaType has type and subtype as long as possible
188 | desc.load(expected_success)
189 |
190 | # expected failure: mediaType does not match pattern (type too long)
191 | with pytest.raises(SystemExit):
192 | desc.load(type_toolong)
193 |
194 | # expected failure: mediaType does not match pattern (subtype too long)
195 | with pytest.raises(SystemExit):
196 | desc.load(subtype_toolong)
197 |
198 | # expected failure: size missing
199 | with pytest.raises(SystemExit):
200 | desc.load(size_missing)
201 |
202 | # expected failure: size is a string, expected integer
203 | with pytest.raises(SystemExit):
204 | desc.load(size_string)
205 |
206 | # expected failure: digest missing
207 | with pytest.raises(SystemExit):
208 | desc.load(digest_missing)
209 |
210 | # expected failure: digest does not match pattern (no algorithm)
211 | with pytest.raises(ErrDigestInvalidFormat):
212 | desc.load(no_algorithm)
213 |
214 | # expected failure: digest does not match pattern (no hash)
215 | with pytest.raises(ErrDigestInvalidFormat):
216 | desc.load(no_hash)
217 |
218 | # expected failure: digest does not match pattern (invalid aglorithm characters)
219 | with pytest.raises(ErrDigestInvalidFormat):
220 | desc.load(invalid_algchars)
221 |
222 | # expected failure: digest does not match pattern (characters needs to be lower for sha256)
223 | with pytest.raises(ErrDigestInvalidFormat):
224 | desc.load(uppercase_digest)
225 |
226 | # expected success: valid URL entry
227 | desc.load(valid_urls)
228 |
229 | # expected failure: urls does not match format (invalide url characters)
230 | with pytest.raises(SystemExit):
231 | desc.load(invalid_urls)
232 |
233 | # these are all valid
234 | for valid in valids:
235 | desc.load(valid)
236 |
237 | # fail: repeated separators in algorithm
238 | with pytest.raises(ErrDigestInvalidFormat):
239 | desc.load(repeated_seps_invalid)
240 |
241 | # invalid digest length (also chars)
242 | with pytest.raises(ErrDigestInvalidLength):
243 | desc.load(invalid_digest_length)
244 |
245 | # test for those who cannot use modulo arithmetic to recover padding.
246 | with pytest.raises(ErrDigestInvalidLength):
247 | desc.load(digest_unknown)
248 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_digest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.digest import Parse, NewDigestFromEncoded
10 |
11 | from opencontainers.digest.exceptions import (
12 | ErrDigestInvalidLength,
13 | ErrDigestInvalidFormat,
14 | ErrDigestUnsupported,
15 | )
16 | import os
17 | import pytest
18 |
19 |
20 | digests = [
21 | {
22 | "input": "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
23 | "algorithm": "sha256",
24 | "encoded": "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b",
25 | },
26 | {
27 | "input": "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
28 | "algorithm": "sha384",
29 | "encoded": "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
30 | },
31 | {
32 | # empty hex
33 | "input": "sha256:",
34 | "err": ErrDigestInvalidFormat,
35 | },
36 | {
37 | # empty hex
38 | "input": ":",
39 | "err": ErrDigestInvalidFormat,
40 | },
41 | {
42 | # just hex
43 | "input": "d41d8cd98f00b204e9800998ecf8427e",
44 | "err": ErrDigestInvalidFormat,
45 | },
46 | {
47 | # not hex
48 | "input": "sha256:d41d8cd98f00b204e9800m98ecf8427e",
49 | "err": ErrDigestInvalidLength,
50 | },
51 | {
52 | # too short
53 | "input": "sha256:abcdef0123456789",
54 | "err": ErrDigestInvalidLength,
55 | },
56 | {
57 | # too short (from different algorithm)
58 | "input": "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
59 | "err": ErrDigestInvalidLength,
60 | },
61 | {"input": "foo:d41d8cd98f00b204e9800998ecf8427e", "err": ErrDigestUnsupported},
62 | {
63 | # repeated separators
64 | "input": "sha384__foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
65 | "err": ErrDigestInvalidFormat,
66 | },
67 | {
68 | # ensure that we parse, but we don't have support for the algorithm
69 | "input": "sha384.foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
70 | "algorithm": "sha384.foo+bar",
71 | "encoded": "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
72 | "err": ErrDigestUnsupported,
73 | },
74 | {
75 | "input": "sha384_foo+bar:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
76 | "algorithm": "sha384_foo+bar",
77 | "encoded": "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d",
78 | "err": ErrDigestUnsupported,
79 | },
80 | {
81 | "input": "sha256:E58FCF7418D4390DEC8E8FB69D88C06EC07039D651FEDD3AA72AF9972E7D046B",
82 | "err": ErrDigestInvalidFormat,
83 | },
84 | ]
85 |
86 | digest_unsupported = {
87 | "input": "sha256+b64:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564",
88 | "algorithm": "sha256+b64",
89 | "encoded": "LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564",
90 | "err": ErrDigestInvalidLength, # also unsupported
91 | }
92 |
93 |
94 | def test_digests(tmp_path):
95 | """test creation of an opencontainers Digest"""
96 | with pytest.raises(digest_unsupported["err"]):
97 | d = Parse(digest_unsupported["input"])
98 |
99 | for digest in digests:
100 |
101 | # Case 1: we expect an error (algorithm not provided)
102 | if "err" in digest and "algorithm" not in digest:
103 | with pytest.raises(digest["err"]):
104 | Parse(digest["input"])
105 |
106 | else:
107 |
108 | d = Parse(digest["input"])
109 |
110 | # These are cases we can parse, but don't have support for algorithm
111 | if "err" in digest:
112 | assert d.algorithm != digest["algorithm"]
113 | else:
114 | assert d.algorithm == digest["algorithm"]
115 | assert d.encoded() == digest["encoded"]
116 |
117 | # Try creating new digest from encoded
118 | if "encoded" in digest and "algorithm" in digest:
119 | newFromEncoded = NewDigestFromEncoded(
120 | digest["algorithm"], digest["encoded"]
121 | )
122 | assert newFromEncoded == d
123 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_distribution.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from .mock_server import get_free_port, start_mock_server
10 | from opencontainers.distribution.reggie import *
11 | import os
12 | import re
13 | import pytest
14 |
15 |
16 | # Use the same port across tests
17 | port = get_free_port()
18 | mock_server = None
19 | mock_server_thread = None
20 |
21 |
22 | def setup_module(module):
23 | """setup any state specific to the execution of the given module."""
24 | global mock_server
25 | global mock_server_thread
26 | mock_server, mock_server_thread = start_mock_server(port)
27 |
28 |
29 | def teardown_module(module):
30 | """teardown any state that was previously setup with a setup_module
31 | method.
32 | """
33 | mock_server.server_close()
34 |
35 |
36 | def test_distribution_mock_server(tmp_path):
37 | """test creation and communication with a mock server"""
38 |
39 | mock_url = "http://localhost:{port}".format(port=port)
40 |
41 | print("Testing creation of generic client")
42 | client = NewClient(
43 | mock_url,
44 | WithUsernamePassword("testuser", "testpass"),
45 | WithDefaultName("testname"),
46 | WithUserAgent("reggie-tests"),
47 | )
48 | assert not client.Config.Debug
49 |
50 | print("Testing setting debug option")
51 | clientDebug = NewClient(mock_url, WithDebug(True))
52 | assert clientDebug.Config.Debug
53 |
54 | print("Testing providing auth scope")
55 | testScope = (
56 | 'realm="https://silly.com/v2/auth",service="testservice",scope="pull,push"'
57 | )
58 | client3 = NewClient(mock_url, WithAuthScope(testScope))
59 | assert client3.Config.AuthScope == testScope
60 |
61 | print("Testing that default name is replaced in template.")
62 | req = client.NewRequest("GET", "/v2//tags/list")
63 |
64 | # The name should be replaced in the template
65 | if "/v2//tags/list" in req.url or "testname" not in req.url:
66 | sys.exit("NewRequest does not add default namespace to URL")
67 |
68 | print("Checking user agent")
69 | uaHeader = req.headers.get("User-Agent")
70 | if uaHeader != "reggie-tests":
71 | sys.exit(
72 | 'Expected User-Agent header to be "reggie-tests" but instead got "%s"'
73 | % uaHeader
74 | )
75 |
76 | print("Testing doing the request %s" % req)
77 | response = client.Do(req)
78 | if response.status_code != 200:
79 | sys.exit("Expected response code 200 but was %d", response.status_code)
80 |
81 | print("Test default name reset")
82 | client.SetDefaultName("othername")
83 | req = client.NewRequest("GET", "/v2//tags/list")
84 | if "othername" not in req.url:
85 | sys.exit("NewRequest does not add runtime namespace to URL")
86 |
87 | print("Test custom name on request")
88 | req = client.NewRequest("GET", "/v2//tags/list", WithName("customname"))
89 | if "/v2/customname/tags/list" not in req.url:
90 | sys.exit("NewRequest does not add runtime namespace to URL")
91 |
92 | print("test Location header on request")
93 | req = client.NewRequest("GET", "/v2//tags/list", WithName("withlocation"))
94 | response = client.Do(req)
95 | relativeLocation = response.GetRelativeLocation()
96 | if re.search("(http://|https://)", relativeLocation):
97 | sys.exit("Relative Location contains host")
98 | if relativeLocation == "":
99 | sys.exit("Location header not present")
100 |
101 | print("Testing absolute location")
102 | absoluteLocation = response.GetAbsoluteLocation()
103 | if not re.search("(http://|https://)", absoluteLocation):
104 | sys.exit("Absolute location missing http prefix")
105 | if absoluteLocation == "":
106 | sys.exit("Location header not present.")
107 |
108 | print("Test error function on response")
109 | req = client.NewRequest("GET", "/v2//tags/list", WithName("witherrors"))
110 | response = client.Do(req)
111 | errorList = response.Errors()
112 | if not errorList:
113 | sys.exit("Error list has length 0.")
114 |
115 | e1 = errorList[0]
116 | if e1["code"] == "":
117 | sys.exit("Code not returned in response body.")
118 |
119 | if e1["message"] == "":
120 | sys.exit("Message not returned in response body.")
121 |
122 | if e1["detail"] == "":
123 | sys.exit("Detail not returned in response body.")
124 |
125 | print("Test reference on request")
126 | req = client.NewRequest(
127 | "HEAD", "/v2//manifests/", WithReference("silly")
128 | )
129 | if not req.url.endswith("silly"):
130 | sys.exit("NewRequest does not add runtime reference to URL.")
131 |
132 | print("Test digest on request")
133 | digest = "6f4e69a5ff18d92e7315e3ee31c62165ebf25bfa05cad05c0d09d8f412dae401"
134 | req = client.NewRequest("GET", "/v2//blobs/", WithDigest(digest))
135 | if not req.url.endswith(digest):
136 | sys.exit("NewRequest does not add runtime digest to URL")
137 |
138 | print("Test session id on request")
139 | session_id = "f0ca5d12-5557-4747-9c21-3d916f2fc885"
140 | req = client.NewRequest(
141 | "GET", "/v2//blobs/uploads/", WithSessionID(session_id)
142 | )
143 | if not req.url.endswith(session_id):
144 | sys.exit("NewRequest does not add runtime digest to URL")
145 |
146 | print("invalid request (no ref)")
147 | req = client.NewRequest("HEAD", "/v2//manifests/")
148 |
149 | # We should expect an error
150 | with pytest.raises(ValueError):
151 | response = client.Do(req)
152 |
153 | print("invalid request (no digest)")
154 | req = client.NewRequest("GET", "/v2//blobs/")
155 | with pytest.raises(ValueError):
156 | response = client.Do(req)
157 |
158 | print("invalid request (no session id)")
159 | req = client.NewRequest("GET", "/v2//blobs/uploads/")
160 | with pytest.raises(ValueError):
161 | response = client.Do(req)
162 |
163 | print("bad address on client")
164 | with pytest.raises(ValueError):
165 | badClient = NewClient("xwejknxw://jshnws")
166 |
167 | print("Make sure headers and body match after going through auth")
168 | req = (
169 | client.NewRequest("PUT", "/a/b/c")
170 | .SetHeader("Content-Length", "3")
171 | .SetHeader("Content-Range", "0-2")
172 | .SetHeader("Content-Type", "application/octet-stream")
173 | .SetQueryParam("digest", "xyz")
174 | .SetBody(b"abc")
175 | )
176 | response = client.Do(req)
177 |
178 | print("Checking for expected headers")
179 | assert len(req.headers) == 5
180 | for header in [
181 | "Content-Length",
182 | "Content-Range",
183 | "Content-Type",
184 | "Authorization",
185 | "User-Agent",
186 | ]:
187 | assert header in req.headers
188 |
189 | print("Check that the body did not get lost somewhere")
190 | assert req.body == "abc"
191 |
192 | print("Test that the retry callback is invoked, if configured.")
193 | newBody = "not the original body"
194 |
195 | # Function to take a request and set a new body
196 | def func(r):
197 | r.SetBody(newBody)
198 |
199 | req = client.NewRequest("PUT", "/a/b/c", WithRetryCallback(func))
200 | req.SetBody("original body")
201 | response = client.Do(req)
202 | assert req.body == "not the original body"
203 |
204 | print("Test the case where the retry callback returns an error")
205 |
206 | def errorFunc(r):
207 | raise ValueError("ruhroh")
208 |
209 | req = client.NewRequest("PUT", "/a/b/c", WithRetryCallback(errorFunc))
210 | try:
211 | response = client.Do(req)
212 | raise ValueError(
213 | "Expected error from callback function, but request returned no error"
214 | )
215 | except Exception as exc:
216 | assert "ruhroh" in str(exc)
217 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_imageindex.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.image.v1 import Index
10 | import os
11 | import pytest
12 |
13 |
14 | mediatype_invalid_pattern = {
15 | "schemaVersion": 2,
16 | "manifests": [
17 | {
18 | "mediaType": "invalid",
19 | "size": 7143,
20 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
21 | "platform": {"architecture": "ppc64le", "os": "linux"},
22 | }
23 | ],
24 | }
25 |
26 | manifest_invalid_string = {
27 | "schemaVersion": 2,
28 | "manifests": [
29 | {
30 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
31 | "size": "7682",
32 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
33 | "platform": {"architecture": "amd64", "os": "linux"},
34 | }
35 | ],
36 | }
37 |
38 | digest_missing = {
39 | "schemaVersion": 2,
40 | "manifests": [
41 | {
42 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
43 | "size": 7682,
44 | "platform": {"architecture": "amd64", "os": "linux"},
45 | }
46 | ],
47 | }
48 |
49 |
50 | platform_arch_missing = {
51 | "schemaVersion": 2,
52 | "manifests": [
53 | {
54 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
55 | "size": 7682,
56 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
57 | "platform": {"os": "linux"},
58 | }
59 | ],
60 | }
61 |
62 | invalid_manifest_mediatype = {
63 | "schemaVersion": 2,
64 | "manifests": [
65 | {
66 | "mediaType": "invalid",
67 | "size": 7682,
68 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
69 | "platform": {"architecture": "amd64", "os": "linux"},
70 | }
71 | ],
72 | }
73 |
74 | empty_manifest_mediatype = {
75 | "schemaVersion": 2,
76 | "manifests": [
77 | {
78 | "mediaType": "",
79 | "size": 7682,
80 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
81 | "platform": {"architecture": "amd64", "os": "linux"},
82 | }
83 | ],
84 | }
85 |
86 | index_with_optional = {
87 | "schemaVersion": 2,
88 | "manifests": [
89 | {
90 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
91 | "size": 7143,
92 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
93 | "platform": {"architecture": "ppc64le", "os": "linux"},
94 | },
95 | {
96 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
97 | "size": 7682,
98 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
99 | "platform": {"architecture": "amd64", "os": "linux"},
100 | },
101 | ],
102 | "annotations": {"com.example.key1": "value1", "com.example.key2": "value2"},
103 | }
104 |
105 | index_with_required = {
106 | "schemaVersion": 2,
107 | "manifests": [
108 | {
109 | "mediaType": "application/vnd.oci.image.manifest.v1+json",
110 | "size": 7143,
111 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
112 | }
113 | ],
114 | }
115 |
116 | index_with_custom = {
117 | "schemaVersion": 2,
118 | "manifests": [
119 | {
120 | "mediaType": "application/customized.manifest+json",
121 | "size": 7143,
122 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
123 | "platform": {"architecture": "ppc64le", "os": "linux"},
124 | }
125 | ],
126 | }
127 |
128 |
129 | def test_imageindex(tmp_path):
130 | """test creation of an opencontainers Index"""
131 | index = Index()
132 |
133 | # expected failure: mediaType does not match pattern
134 | with pytest.raises(SystemExit):
135 | index.load(mediatype_invalid_pattern)
136 |
137 | # expected failure: manifest.size is string, expected integer
138 | with pytest.raises(SystemExit):
139 | index.load(manifest_invalid_string)
140 |
141 | # expected failure: manifest.digest is missing, expected required
142 | with pytest.raises(SystemExit):
143 | index.load(digest_missing)
144 |
145 | # expected failure: in the optional field platform platform.architecture is missing, expected required
146 | with pytest.raises(SystemExit):
147 | index.load(platform_arch_missing)
148 |
149 | # expected failure: invalid referenced manifest media type
150 | with pytest.raises(SystemExit):
151 | index.load(invalid_manifest_mediatype)
152 |
153 | # expected failure: empty referenced manifest media type
154 | with pytest.raises(SystemExit):
155 | index.load(empty_manifest_mediatype)
156 |
157 | # valid image index, with optional fields
158 | index.load(index_with_optional)
159 |
160 | # valid image index, with required fields only
161 | index.load(index_with_required)
162 |
163 | # valid image index, with customized media type of referenced manifest
164 | index.load(index_with_custom)
165 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_imagelayout.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.image.v1 import ImageLayout
10 | import os
11 | import pytest
12 |
13 |
14 | def test_imagelayout(tmp_path):
15 | """test creation of an opencontainers ImageLayout"""
16 | layout = ImageLayout()
17 |
18 | # expected faulure: imageLayoutVersion does not match pattern or type
19 | with pytest.raises(SystemExit):
20 | layout.load({"imageLayoutVersion": 1.0})
21 |
22 | with pytest.raises(SystemExit):
23 | layout.load({"imageLayoutVersion": "1.0"})
24 |
25 | # valid layout
26 | layout.load({"imageLayoutVersion": "1.0.0"})
27 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_manifest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.image.v1 import Manifest
10 | from opencontainers.digest.exceptions import ErrDigestInvalidFormat
11 | import os
12 | import pytest
13 |
14 | invalid_mediatype_pattern = {
15 | "schemaVersion": 2,
16 | "config": {
17 | "mediaType": "invalid",
18 | "size": 1470,
19 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
20 | },
21 | "layers": [
22 | {
23 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
24 | "size": 148,
25 | "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd",
26 | }
27 | ],
28 | }
29 |
30 | invalid_config_size_string = {
31 | "schemaVersion": 2,
32 | "config": {
33 | "mediaType": "application/vnd.oci.image.config.v1+json",
34 | "size": "1470",
35 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
36 | },
37 | "layers": [
38 | {
39 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
40 | "size": 148,
41 | "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd",
42 | }
43 | ],
44 | }
45 |
46 | invalid_layers_size_string = {
47 | "schemaVersion": 2,
48 | "config": {
49 | "mediaType": "application/vnd.oci.image.config.v1+json",
50 | "size": 1470,
51 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
52 | },
53 | "layers": [
54 | {
55 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
56 | "size": "675598",
57 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
58 | }
59 | ],
60 | }
61 |
62 |
63 | valid_with_optional = {
64 | "schemaVersion": 2,
65 | "config": {
66 | "mediaType": "application/vnd.oci.image.config.v1+json",
67 | "size": 1470,
68 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
69 | },
70 | "layers": [
71 | {
72 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
73 | "size": 675598,
74 | "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827",
75 | },
76 | {
77 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
78 | "size": 156,
79 | "digest": "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d",
80 | },
81 | {
82 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
83 | "size": 148,
84 | "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd",
85 | },
86 | ],
87 | "annotations": {"key1": "value1", "key2": "value2"},
88 | }
89 |
90 | valid_with_required = {
91 | "schemaVersion": 2,
92 | "config": {
93 | "mediaType": "application/vnd.oci.image.config.v1+json",
94 | "size": 1470,
95 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
96 | },
97 | "layers": [
98 | {
99 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
100 | "size": 675598,
101 | "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827",
102 | },
103 | {
104 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
105 | "size": 156,
106 | "digest": "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d",
107 | },
108 | {
109 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
110 | "size": 148,
111 | "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd",
112 | },
113 | ],
114 | }
115 |
116 | invalid_empty_layers = {
117 | "schemaVersion": 2,
118 | "config": {
119 | "mediaType": "application/vnd.oci.image.config.v1+json",
120 | "size": 1470,
121 | "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
122 | },
123 | "layers": [],
124 | }
125 |
126 | expected_bounds_pass = {
127 | "schemaVersion": 2,
128 | "config": {
129 | "mediaType": "application/vnd.oci.image.config.v1+json",
130 | "size": 1470,
131 | "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
132 | },
133 | "layers": [
134 | {
135 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
136 | "size": 1470,
137 | "digest": "sha256+foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
138 | },
139 | {
140 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
141 | "size": 1470,
142 | "digest": "sha256.foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
143 | },
144 | # multihash is not registered, but still valid formatting, but here we would consider it invalid
145 | # {
146 | # "mediaType": "application/vnd.oci.image.config.v1+json",
147 | # "size": 1470,
148 | # "digest": "multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8"
149 | # }
150 | ],
151 | }
152 |
153 | expected_bounds_fail = {
154 | "schemaVersion": 2,
155 | "config": {
156 | "mediaType": "application/vnd.oci.image.config.v1+json",
157 | "size": 1470,
158 | "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
159 | },
160 | "layers": [
161 | {
162 | "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
163 | "size": 1470,
164 | "digest": "sha256+foo+-b:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b",
165 | }
166 | ],
167 | }
168 |
169 |
170 | def test_manifests(tmp_path):
171 | """test creation of an opencontainers Manifest"""
172 | manifest = Manifest()
173 |
174 | # expected failure: mediaType does not match pattern
175 | with pytest.raises(SystemExit):
176 | manifest.load(invalid_mediatype_pattern)
177 |
178 | # config size is string, should be int
179 | with pytest.raises(SystemExit):
180 | manifest.load(invalid_config_size_string)
181 |
182 | # layers.size is string, should be integer
183 | with pytest.raises(SystemExit):
184 | manifest.load(invalid_layers_size_string)
185 |
186 | # valid manifest with optional fields
187 | manifest.load(valid_with_optional)
188 |
189 | # valid manifest with only required fields
190 | manifest.load(valid_with_required)
191 |
192 | # expected failure: empty layer, expected at least one
193 | with pytest.raises(SystemExit):
194 | manifest.load(invalid_empty_layers)
195 |
196 | # expected pass: test bounds of algorithm field in digest.
197 | manifest.load(expected_bounds_pass)
198 |
199 | # expected failure: push bounds of algorithm field in digest too far.
200 | with pytest.raises(ErrDigestInvalidFormat):
201 | manifest.load(expected_bounds_fail)
202 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_struct.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.struct import Struct, IntStruct, StrStruct
10 | import os
11 | import pytest
12 |
13 |
14 | class StructTest(Struct):
15 | def __init__(
16 | self, Dict=None, List=None, Int=None, Str=None, Another=None, AnotherList=None
17 | ):
18 | super().__init__()
19 |
20 | self.newAttr(name="Dict", attType=dict)
21 | self.newAttr(name="List", attType=list)
22 | self.newAttr(name="Int", attType=IntStruct)
23 | self.newAttr(name="Str", attType=StrStruct)
24 |
25 | self.newAttr(name="Another", attType=AnotherStruct)
26 | self.newAttr(name="AnotherList", attType=[AnotherStruct])
27 |
28 | self.add("Dict", Dict)
29 | self.add("List", List)
30 | self.add("Int", Int)
31 | self.add("Str", Str)
32 | self.add("Another", Another)
33 | self.add("AnotherList", AnotherList)
34 |
35 |
36 | class AnotherStruct(Struct):
37 | def __init__(self, Attr=None, AttrList=None):
38 | super().__init__()
39 |
40 | self.newAttr("Attr", attType=StrStruct)
41 | self.newAttr("AttrList", attType=[AnotherStruct])
42 |
43 | self.add("Attr", Attr)
44 | self.add("AttrList", AttrList)
45 |
46 |
47 | def test_add(tmp_path):
48 | t = StructTest()
49 |
50 | t.add("Dict", {"a": "b"})
51 | t.add("List", [0, 1, 2])
52 | t.add("Int", 987)
53 | t.add("Str", "abc")
54 |
55 | t.add("List", 3)
56 | assert t.to_dict()["List"] == [0, 1, 2, 3]
57 |
58 | t.add("Dict", {"a": "c", "b": "d"})
59 | assert t.to_dict()["Dict"] == {"a": "b", "b": "d"}
60 |
61 | t.add("Int", 13)
62 | assert t.to_dict()["Int"] == 1000
63 |
64 | t.add("Str", "def")
65 | assert t.to_dict()["Str"] == "abcdef"
66 |
67 | # Test support for nested Structs (list of Structs containing list of Structs)
68 | t.add(
69 | "Another",
70 | AnotherStruct(
71 | Attr="test",
72 | AttrList=[
73 | {
74 | "Attr": "abc",
75 | "AttrList": [
76 | {
77 | "Attr": "onetwothree",
78 | }
79 | ],
80 | }
81 | ],
82 | ),
83 | )
84 | assert t.to_dict()["Another"] == {
85 | "Attr": "test",
86 | "AttrList": [
87 | {
88 | "Attr": "abc",
89 | "AttrList": [
90 | {
91 | "Attr": "onetwothree",
92 | }
93 | ],
94 | }
95 | ],
96 | }
97 |
98 | # We can add Structs as object
99 | t.add("Another", AnotherStruct("test"))
100 | assert t.to_dict()["Another"] == {"Attr": "test"}
101 |
102 | # Or as Dict
103 | t.add("Another", {"Attr": "test"})
104 | assert t.to_dict()["Another"] == {"Attr": "test"}
105 |
106 | t.add("AnotherList", AnotherStruct("test"))
107 | assert t.to_dict()["AnotherList"] == [{"Attr": "test"}]
108 |
109 | t.add("AnotherList", {"Attr": "123"})
110 | t.add("AnotherList", [AnotherStruct("value"), {"Attr": "456"}])
111 | assert {"Attr": "test"} in t.to_dict()["AnotherList"]
112 | assert {"Attr": "123"} in t.to_dict()["AnotherList"]
113 | assert {"Attr": "value"} in t.to_dict()["AnotherList"]
114 | assert {"Attr": "456"} in t.to_dict()["AnotherList"]
115 |
116 | assert (
117 | StructTest(
118 | Dict={"a": "b", "b": "d"},
119 | List=[0, 1, 2, 3],
120 | Int=1000,
121 | Str="abcdef",
122 | Another={"Attr": "test"},
123 | AnotherList=[
124 | {"Attr": "test"},
125 | {"Attr": "123"},
126 | {"Attr": "value"},
127 | {"Attr": "456"},
128 | ],
129 | ).to_dict()
130 | == t.to_dict()
131 | )
132 |
--------------------------------------------------------------------------------
/opencontainers/tests/test_verifiers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Copyright (C) 2019-2022 Vanessa Sochat.
4 |
5 | # This Source Code Form is subject to the terms of the
6 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
7 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 |
9 | from opencontainers.digest import Digest, FromBytes
10 |
11 | import string
12 | import io
13 | import random
14 | import pytest
15 |
16 |
17 | def test_digest_verifier(tmp_path):
18 | """test creation of an opencontainers verifiers"""
19 | asciitext = "".join([random.choice(string.ascii_letters) for n in range(20)])
20 | p = bytes(asciitext, "utf-8")
21 | digest = FromBytes(p)
22 | verifier = digest.verifier()
23 | verifier.write(p)
24 | verifier.verified()
25 |
26 |
27 | def test_digest_verifier_unsupported(tmp_path):
28 | """TestVerifierUnsupportedDigest ensures that unsupported digest validation is
29 | flowing through verifier creation.
30 | """
31 | # expected failure: empty digest
32 | digest = Digest("")
33 | with pytest.raises(SystemExit):
34 | digest.verifier()
35 |
36 | # expected failure, empty algorithm
37 | digest = Digest(":")
38 | with pytest.raises(SystemExit):
39 | digest.verifier()
40 |
41 | # expected failure, unsupported algorithm
42 | digest = Digest("bean:0123456789abcdef")
43 | with pytest.raises(SystemExit):
44 | verifier = digest.verifier()
45 |
46 | # expected failure
47 | digest = Digest("sha256-garbage:pure")
48 | verifier = digest.verifier()
49 | assert not verifier.verified()
50 |
--------------------------------------------------------------------------------
/opencontainers/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | __version__ = "0.0.14"
8 | AUTHOR = "Vanessa Sochat"
9 | AUTHOR_EMAIL = "vsoch@users.noreply.github.com"
10 | NAME = "opencontainers"
11 | PACKAGE_URL = "http://github.com/vsoch/oci-python"
12 | KEYWORDS = "open containers, oci"
13 | DESCRIPTION = "Python module for oci specifications"
14 | LICENSE = "LICENSE"
15 |
16 | INSTALL_REQUIRES = ()
17 |
18 | REGGIE_REQUIRES = (("requests", {"min_version": None}),)
19 | TESTS_REQUIRES = (("pytest", {"min_version": "4.6.2"}),)
20 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description_file = README.md
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2019-2022 Vanessa Sochat.
2 |
3 | # This Source Code Form is subject to the terms of the
4 | # Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
5 | # with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | from setuptools import setup, find_packages
8 | import os
9 |
10 | ################################################################################
11 | # HELPER FUNCTIONS #############################################################
12 | ################################################################################
13 |
14 |
15 | def get_lookup():
16 | """get version by way of the version file"""
17 | lookup = dict()
18 | version_file = os.path.join("opencontainers", "version.py")
19 | with open(version_file) as filey:
20 | exec(filey.read(), lookup)
21 | return lookup
22 |
23 |
24 | def get_requirements(lookup=None, key="INSTALL_REQUIRES"):
25 | """get_requirements reads in requirements and versions from
26 | the lookup obtained with get_lookup"""
27 |
28 | if lookup is None:
29 | lookup = get_lookup()
30 |
31 | install_requires = []
32 | for module in lookup[key]:
33 | module_name = module[0]
34 | module_meta = module[1]
35 | if "exact_version" in module_meta:
36 | dependency = "%s==%s" % (module_name, module_meta["exact_version"])
37 | elif "min_version" in module_meta:
38 | if module_meta["min_version"] is None:
39 | dependency = module_name
40 | else:
41 | dependency = "%s>=%s" % (module_name, module_meta["min_version"])
42 | install_requires.append(dependency)
43 | return install_requires
44 |
45 |
46 | # Make sure everything is relative to setup.py
47 | install_path = os.path.dirname(os.path.abspath(__file__))
48 | os.chdir(install_path)
49 |
50 | # Get version information from the lookup
51 | lookup = get_lookup()
52 | VERSION = lookup["__version__"]
53 | NAME = lookup["NAME"]
54 | AUTHOR = lookup["AUTHOR"]
55 | AUTHOR_EMAIL = lookup["AUTHOR_EMAIL"]
56 | PACKAGE_URL = lookup["PACKAGE_URL"]
57 | KEYWORDS = lookup["KEYWORDS"]
58 | DESCRIPTION = lookup["DESCRIPTION"]
59 | LICENSE = lookup["LICENSE"]
60 | with open("README.md") as readme:
61 | LONG_DESCRIPTION = readme.read()
62 |
63 | ################################################################################
64 | # MAIN #########################################################################
65 | ################################################################################
66 |
67 |
68 | if __name__ == "__main__":
69 |
70 | INSTALL_REQUIRES = get_requirements(lookup)
71 | REGGIE_REQUIRES = get_requirements(lookup, "REGGIE_REQUIRES")
72 | TESTS_REQUIRES = get_requirements(lookup, "TESTS_REQUIRES")
73 |
74 | setup(
75 | name=NAME,
76 | version=VERSION,
77 | author=AUTHOR,
78 | author_email=AUTHOR_EMAIL,
79 | maintainer=AUTHOR,
80 | maintainer_email=AUTHOR_EMAIL,
81 | packages=find_packages(),
82 | include_package_data=True,
83 | zip_safe=False,
84 | url=PACKAGE_URL,
85 | license=LICENSE,
86 | description=DESCRIPTION,
87 | long_description=LONG_DESCRIPTION,
88 | long_description_content_type="text/markdown",
89 | keywords=KEYWORDS,
90 | setup_requires=["pytest-runner"],
91 | tests_require=TESTS_REQUIRES,
92 | install_requires=INSTALL_REQUIRES,
93 | classifiers=[
94 | "Intended Audience :: Science/Research",
95 | "Intended Audience :: Developers",
96 | "Programming Language :: Python",
97 | "Topic :: Software Development",
98 | "Topic :: Scientific/Engineering",
99 | "Operating System :: Unix",
100 | "Programming Language :: Python :: 3",
101 | ],
102 | )
103 |
--------------------------------------------------------------------------------