diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 18:10:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 18:10:12 +0000 |
commit | aaf261d1e385f1b1c1292ead4ccbd631f6b26dc9 (patch) | |
tree | 1063057bfb2f9e946f939f5b18347295988cfb99 | |
parent | Initial commit. (diff) | |
download | golang-github-opencontainers-image-spec-aaf261d1e385f1b1c1292ead4ccbd631f6b26dc9.tar.xz golang-github-opencontainers-image-spec-aaf261d1e385f1b1c1292ead4ccbd631f6b26dc9.zip |
Adding upstream version 1.1.0~rc5.upstream/1.1.0_rc5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
68 files changed, 6621 insertions, 0 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE/maintainer_nomination.md b/.github/PULL_REQUEST_TEMPLATE/maintainer_nomination.md new file mode 100644 index 0000000..82a1b4a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/maintainer_nomination.md @@ -0,0 +1,23 @@ +# Nomination for a New Maintainer + +## Nominating Maintainer + +Name of the existing OCI maintainer with GitHub username + +## New Maintainer + +Name of the new maintainer with GitHub username + +## Justification + +Highlight any work contributed by the new maintainer. Examples of contributions may be: + +- Community involvement in mailing lists and meetings +- Involvement in any OCI working groups +- Contributions to any of the OCI git repositories + +Other considerations may be: + +- Diversity of organizations +- Time involved in the community +- Personal experience working with the new maintainer diff --git a/.github/workflows/docs-and-linting.yml b/.github/workflows/docs-and-linting.yml new file mode 100644 index 0000000..248791c --- /dev/null +++ b/.github/workflows/docs-and-linting.yml @@ -0,0 +1,45 @@ +name: Render and Lint Documentation + +on: + pull_request: + branches_ignore: [] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.18', '1.19', '1.20'] + + name: Documentation and Linting + steps: + + - uses: actions/checkout@v3 + with: + path: go/src/github.com/opencontainers/image-spec + + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go }} + + - name: Render and Lint + env: + GOPATH: /home/runner/work/image-spec/image-spec/go + run: | + export PATH=$GOPATH/bin:$PATH + cd go/src/github.com/opencontainers/image-spec + make install.tools + go get -t -d ./... + ls ../ + make + make .gitvalidation + make lint + make check-license + make test + make docs + + - name: documentation artifacts + uses: actions/upload-artifact@v3 + with: + name: oci-docs + path: go/src/github.com/opencontainers/image-spec/output diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcc5d55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/oci-validate-examples +output +header.html diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a566f84 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,23 @@ +run: + timeout: 10m + +linters: + disable-all: true + enable: + - dupl + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - revive + - unused + - staticcheck + +linters-settings: + gofmt: + simplify: true + dupl: + threshold: 400 @@ -0,0 +1,14 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..6135518 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,18 @@ +# all lists use a `-` +MD004: + style: dash + +# allow tabs in code blocks (for Go) +MD010: + code_blocks: false + +# disable line length, prefer one sentence per line for PRs +MD013: false + +# emphasis with underscore (`_emphasis_`) +MD049: + style: "underscore" + +# bold with asterisk (`**bold**`) +MD050: + style: "asterisk" diff --git a/.tool/check-license b/.tool/check-license new file mode 100755 index 0000000..11baafc --- /dev/null +++ b/.tool/check-license @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +ret=0 + +for file in $(find . -type f -iname '*.go' ! -path './vendor/*'); do + if ! head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)"; then + echo "${file}:missing license header" + ret=1 + fi +done + +exit $ret diff --git a/.tool/genheader.go b/.tool/genheader.go new file mode 100644 index 0000000..6d1e176 --- /dev/null +++ b/.tool/genheader.go @@ -0,0 +1,54 @@ +// Copyright 2017 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "text/template" + + specs "github.com/opencontainers/image-spec/specs-go" +) + +var headerTemplate = template.Must(template.New("gen").Parse(`<title>image-spec {{.Version}}</title> +<base href="https://raw.githubusercontent.com/opencontainers/image-spec/{{.Branch}}/">`)) + +type Obj struct { + Version string + Branch string +} + +func main() { + obj := Obj{ + Version: specs.Version, + Branch: specs.Version, + } + if strings.HasSuffix(specs.Version, "-dev") { + cmd := exec.Command("git", "log", "-1", `--pretty=%H`, "HEAD") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + obj.Branch = strings.Trim(out.String(), " \n\r") + } + headerTemplate.Execute(os.Stdout, obj) +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..26c4127 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ + +* @jonjohnsonjr @jonboulle @stevvooe @sudo-bmitch @sajayantony @tianon @vbatts @cyphar + diff --git a/EMERITUS.md b/EMERITUS.md new file mode 100644 index 0000000..452ff0b --- /dev/null +++ b/EMERITUS.md @@ -0,0 +1,11 @@ +# Emeritus + +We would like to acknowledge previous OCI image spec maintainers and their huge contributions to our collective success: + +- Brandon Philips (@philips) +- Brendan Burns (@brendandburns) +- Jason Bouzane (@jbouzane) +- John Starks (@jstarks) +- Keyang Xie (@xiekeyang) + +We thank these members for their service to the OCI community. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..7fab7b3 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,70 @@ +# Project governance + +The [OCI charter][charter] §5.b.viii tasks an OCI Project's maintainers (listed in the repository's MAINTAINERS file and sometimes referred to as "the TDC", [§5.e][charter]) with: + +> Creating, maintaining and enforcing governance guidelines for the TDC, approved by the maintainers, and which shall be posted visibly for the TDC. + +This section describes generic rules and procedures for fulfilling that mandate. + +## Proposing a motion + +A maintainer SHOULD propose a motion on the <dev@opencontainers.org> mailing list (except [security issues](#security-issues)) with another maintainer as a co-sponsor. + +## Voting + +Voting on a proposed motion SHOULD happen on the <dev@opencontainers.org> mailing list (except [security issues](#security-issues)) with maintainers posting LGTM or REJECT. +Maintainers MAY also explicitly not vote by posting ABSTAIN (which is useful to revert a previous vote). +Maintainers MAY post multiple times (e.g. as they revise their position based on feedback), but only their final post counts in the tally. +A proposed motion is adopted if two-thirds of votes cast, a quorum having voted, are in favor of the release. + +Voting SHOULD remain open for a week to collect feedback from the wider community and allow the maintainers to digest the proposed motion. +Under exceptional conditions (e.g. non-major security fix releases) proposals which reach quorum with unanimous support MAY be adopted earlier. + +A maintainer MAY choose to reply with REJECT. +A maintainer posting a REJECT MUST include a list of concerns or links to written documentation for those concerns (e.g. GitHub issues or mailing-list threads). +The maintainers SHOULD try to resolve the concerns and wait for the rejecting maintainer to change their opinion to LGTM. +However, a motion MAY be adopted with REJECTs, as outlined in the previous paragraphs. + +## Quorum + +A quorum is established when at least two-thirds of maintainers have voted. + +For projects that are not specifications, a [motion to release](#proposing-a-motion) MAY be adopted if the tally is at least three LGTMs and no REJECTs, even if three votes does not meet the usual two-thirds quorum. + +## Security issues + +Motions with sensitive security implications MUST be proposed on the <security@opencontainers.org> mailing list instead of <dev@opencontainers.org>, but should otherwise follow the standard [proposal](#proposing-a-motion) process. +The <security@opencontainers.org> mailing list includes all members of the TOB. +The TOB will contact the project maintainers and provide a channel for discussing and voting on the motion, but voting will otherwise follow the standard [voting](#voting) and [quorum](#quorum) rules. +The TOB and project maintainers will work together to notify affected parties before making an adopted motion public. + +## Amendments + +The [project governance](#project-governance) rules and procedures MAY be amended or replaced using the procedures themselves. +The MAINTAINERS of this project governance document is the total set of MAINTAINERS from all Open Containers projects (runC, runtime-spec, and image-spec). + +## Subject templates + +Maintainers are busy and get lots of email. +To make project proposals recognizable, proposed motions SHOULD use the following subject templates. + +### Proposing a motion template + +> [{project} VOTE]: {motion description} (closes {end of voting window}) + +For example: + +> [image-spec VOTE]: Tag 0647920 as 1.0.0-rc (closes 2016-06-03 20:00 UTC) + +### Tallying results template + +After voting closes, a maintainer SHOULD post a tally to the motion thread with a subject template like: + +> [{project} {status}]: {motion description} (+{LGTMs} -{REJECTs} #{ABSTAINs}) + +Where `{status}` is either `adopted` or `rejected`. +For example: + +> [image-spec adopted]: Tag 0647920 as 1.0.0-rc (+6 -0 #3) + +[charter]: https://github.com/opencontainers/tob/blob/main/CHARTER.md diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..15ad62f --- /dev/null +++ b/HACKING.md @@ -0,0 +1,105 @@ +# Hacking Guide + +## Overview + +This guide contains instructions for building artifacts contained in this repository. + +### Go + +This spec includes several Go packages, and a command line tool considered to be a reference implementation of the OCI image specification. + +Prerequisites: + +- Go - current release only, earlier releases are not supported +- make + +The following make targets are relevant for any work involving the Go packages. + +### Linting + +The included Go source code is being examined for any linting violations not included in the standard Go compiler. +Linting is done using [golangci-lint][golangci-lint]. + +Invocation: + +```shell +make lint +``` + +### Tests + +This target executes all Go based tests. + +Invocation: + +```shell +make test +make validate-examples +``` + +### JSON schema formatting + +This target auto-formats all JSON files in the `schema` directory using the `jq` tool. + +Prerequisites: + +- [jq][jq] >=1.5 + +Invocation: + +```shell +make fmt +``` + +### OCI image specification PDF/HTML documentation files + +This target generates a PDF/HTML version of the OCI image specification. + +Prerequisites: + +- [Docker][docker] + +Invocation: + +```shell +make docs +``` + +### License header check + +This target checks if the source code includes necessary headers. + +Invocation: + +```shell +make check-license +``` + +### Clean build artifacts + +This target cleans all generated/compiled artifacts. + +Invocation: + +```shell +make clean +``` + +### Create PNG images from dot files + +This target generates PNG image files from DOT source files in the `img` directory. + +Prerequisites: + +- [graphviz][graphviz] + +Invocation: + +```shell +make img/media-types.png +``` + +[docker]: https://www.docker.com/ +[golangci-lint]: https://github.com/golangci/golangci-lint +[graphviz]: https://www.graphviz.org/ +[jq]: https://stedolan.github.io/jq/ @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2016 The Linux Foundation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..2f35a92 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,8 @@ +Brandon Mitchell <git@bmitch.net> (@sudo-bmitch) +Jon Johnson <jon.johnson@chainguard.dev> (@jonjohnsonjr) +Jonathan Boulle <jonathanboulle@gmail.com> (@jonboulle) +Sajay Antony <sajaya@microsoft.com> (@sajayantony) +Stephen Day <stevvooe@gmail.com> (@stevvooe) +Tianon Gravi <admwiggin@gmail.com> (@tianon) +Vincent Batts <vbatts@hashbangbash.com> (@vbatts) +Aleksa Sarai <asarai@suse.de> (@cyphar) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..828b2a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,138 @@ +EPOCH_TEST_COMMIT ?= v0.2.0 + +DOCKER ?= $(shell command -v docker 2>/dev/null) +PANDOC ?= $(shell command -v pandoc 2>/dev/null) + +GOPATH:=$(shell go env GOPATH) + +OUTPUT_DIRNAME ?= output +DOC_FILENAME ?= oci-image-spec + +PANDOC_CONTAINER ?= ghcr.io/opencontainers/pandoc:2.9.2.1-8.fc33.x86_64@sha256:5d81ff930a043295a557be8b003ece2a33d14e91b28c50d368413b83372f8d28 +ifeq "$(strip $(PANDOC))" '' + ifneq "$(strip $(DOCKER))" '' + PANDOC = $(DOCKER) run \ + --rm \ + -v $(shell pwd)/:/input/:ro \ + -v $(shell pwd)/$(OUTPUT_DIRNAME)/:/$(OUTPUT_DIRNAME)/ \ + -u $(shell id -u) \ + --workdir /input \ + $(PANDOC_CONTAINER) + PANDOC_SRC := /input/ + PANDOC_DST := / + endif +endif + +# These docs are in an order that determines how they show up in the PDF/HTML docs. +DOC_FILES := \ + spec.md \ + media-types.md \ + descriptor.md \ + image-layout.md \ + manifest.md \ + image-index.md \ + layer.md \ + config.md \ + annotations.md \ + conversion.md \ + considerations.md \ + implementations.md + +FIGURE_FILES := \ + img/media-types.png + +MARKDOWN_LINT_VER?=v0.8.1 + +TOOLS := gitvalidation + +default: check-license lint test + +.PHONY: fmt +fmt: ## format the json with indentation + for i in schema/*.json ; do jq --indent 2 -M . "$${i}" > xx && cat xx > "$${i}" && rm xx ; done + +.PHONY: docs +docs: $(OUTPUT_DIRNAME)/$(DOC_FILENAME).pdf $(OUTPUT_DIRNAME)/$(DOC_FILENAME).html ## generate a PDF/HTML version of the OCI image specification + +ifeq "$(strip $(PANDOC))" '' +$(OUTPUT_DIRNAME)/$(DOC_FILENAME).pdf: $(DOC_FILES) $(FIGURE_FILES) + $(error cannot build $@ without either pandoc or docker) +else +$(OUTPUT_DIRNAME)/$(DOC_FILENAME).pdf: $(DOC_FILES) $(FIGURE_FILES) + @mkdir -p $(OUTPUT_DIRNAME)/ && \ + $(PANDOC) -f gfm -t latex --pdf-engine=xelatex -V geometry:margin=0.5in,bottom=0.8in -V block-headings -o $(PANDOC_DST)$@ $(patsubst %,$(PANDOC_SRC)%,$(DOC_FILES)) + ls -sh $(realpath $@) + +$(OUTPUT_DIRNAME)/$(DOC_FILENAME).html: header.html $(DOC_FILES) $(FIGURE_FILES) + @mkdir -p $(OUTPUT_DIRNAME)/ && \ + cp -ap img/ $(shell pwd)/$(OUTPUT_DIRNAME)/&& \ + $(PANDOC) -f gfm -t html5 -H $(PANDOC_SRC)header.html --standalone -o $(PANDOC_DST)$@ $(patsubst %,$(PANDOC_SRC)%,$(DOC_FILES)) + ls -sh $(realpath $@) +endif + +header.html: .tool/genheader.go specs-go/version.go + go run .tool/genheader.go > $@ + +.PHONY: validate-examples +validate-examples: schema/schema.go ## validate examples in the specification markdown files + go test -run TestValidate ./schema + +.PHONY: check-license +check-license: ## check license headers in source files + @echo "checking license headers" + @./.tool/check-license + +.PHONY: lint + +.PHONY: lint +lint: lint-go lint-md ## Run all linters + +.PHONY: lint-go +lint-go: .install.lint ## lint check of Go files using golangci-lint + @echo "checking Go lint" + @GO111MODULE=on $(GOPATH)/bin/golangci-lint run + +.PHONY: lint-md +lint-md: ## Run linting for markdown + docker run --rm -v "$(PWD):/workdir:ro" docker.io/davidanson/markdownlint-cli2:$(MARKDOWN_LINT_VER) \ + "**/*.md" "#vendor" + +.PHONY: test +test: ## run the unit tests + go test -race -cover $(shell go list ./... | grep -v /vendor/) + +img/%.png: img/%.dot ## generate PNG from dot file + dot -Tpng $^ > $@ + +# When this is running in GitHub, it will only check the commit range +.PHONY: .gitvalidation +.gitvalidation: + @which git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found. Consider 'make install.tools' target" && false) +ifdef GITHUB_SHA + $(GOPATH)/bin/git-validation -q -run DCO,short-subject,dangling-whitespace -range $(GITHUB_SHA)..HEAD +else + $(GOPATH)/bin/git-validation -v -run DCO,short-subject,dangling-whitespace -range $(EPOCH_TEST_COMMIT)..HEAD +endif + +.PHONY: .install.tools +install.tools: $(TOOLS:%=.install.%) + +.PHONY: .install.lint +.install.lint: + case "$$(go env GOVERSION)" in \ + go1.18.*) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3;; \ + go1.19.*) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.1;; \ + *) go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest;; \ + esac + +.PHONY: .install.gitvalidation +.install.gitvalidation: + go install github.com/vbatts/git-validation@latest + +.PHONY: clean +clean: ## clean all generated and compiled artifacts + rm -rf *~ $(OUTPUT_DIRNAME) header.html + +.PHONY: help +help: # Display help + @awk -F ':|##' '/^[^\t].+?:.*?##/ { printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF }' $(MAKEFILE_LIST) diff --git a/README.md b/README.md new file mode 100644 index 0000000..548510f --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# OCI Image Format Specification + +![GitHub Actions for Docs and Linting](https://img.shields.io/github/actions/workflow/status/opencontainers/image-spec/docs-and-linting.yml?branch=main&label=GHA%20docs%20and%20linting) +![License](https://img.shields.io/github/license/opencontainers/image-spec) +[![Go Reference](https://pkg.go.dev/badge/github.com/opencontainers/image-spec.svg)](https://pkg.go.dev/github.com/opencontainers/image-spec) + +The OCI Image Format project creates and maintains the software shipping container image format spec (OCI Image Format). + +**[The specification can be found here](spec.md).** + +This repository also provides [Go types](specs-go), [intra-blob validation tooling, and JSON Schema](schema). +The Go types and validation should be compatible with the current Go release; earlier Go releases are not supported. + +Additional documentation about how this group operates: + +- [Code of Conduct][code-of-conduct] +- [Roadmap](#roadmap) +- [Releases](RELEASES.md) +- [Project Documentation](project.md) + +## Running an OCI Image + +The OCI Image Format partner project is the [OCI Runtime Spec project](https://github.com/opencontainers/runtime-spec). +The Runtime Specification outlines how to run a "[filesystem bundle](https://github.com/opencontainers/runtime-spec/blob/master/bundle.md)" that is unpacked on disk. +At a high-level an OCI implementation would download an OCI Image then unpack that image into an OCI Runtime filesystem bundle. +At this point the OCI Runtime Bundle would be run by an OCI Runtime. + +This entire workflow supports the UX that users have come to expect from container engines like Docker and rkt: primarily, the ability to run an image with no additional arguments: + +- docker run example.com/org/app:v1.0.0 +- rkt run example.com/org/app,version=v1.0.0 + +To support this UX the OCI Image Format contains sufficient information to launch the application on the target platform (e.g. command, arguments, environment variables, etc). + +## Distributing an OCI Image + +The [OCI Distribution Spec Project](https://github.com/opencontainers/distribution-spec/) defines an API protocol to facilitate and standardize the distribution of content. +This API includes support for pushing and pulling OCI images to an OCI conformant registry. + +## FAQ + +**Q: What happens to AppC or Docker Image Formats?** + +A: Existing formats can continue to be a proving ground for technologies, as needed. +The OCI Image Format project strives to provide a dependable open specification that can be shared between different tools and be evolved for years or decades of compatibility; as the deb and rpm format have. + +Find more [FAQ on the OCI site](https://www.opencontainers.org/faq). + +## Roadmap + +The [GitHub milestones](https://github.com/opencontainers/image-spec/milestones) lay out the path to the future improvements. + +## Contributing + +Development happens on GitHub for the spec. +Issues are used for bugs and actionable items and longer discussions can happen on the [mailing list](#mailing-list). + +The specification and code is licensed under the Apache 2.0 license found in the `LICENSE` file of this repository. + +### Discuss your design + +The project welcomes submissions, but please let everyone know what you are working on. + +Before undertaking a nontrivial change to this specification, send mail to the [mailing list](#mailing-list) to discuss what you plan to do. +This gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits. +It also guarantees that the design is sound before code is written; a GitHub pull-request is not the place for high-level discussions. + +Typos and grammatical errors can go straight to a pull-request. +When in doubt, start on the [mailing-list](#mailing-list). + +### Meetings + +Please see the [OCI org repository README](https://github.com/opencontainers/org#meetings) for the most up-to-date information on OCI contributor and maintainer meeting schedules. +You can also find links to meeting agendas and minutes for all prior meetings. + +### Mailing List + +You can subscribe and join the mailing list on [Google Groups](https://groups.google.com/a/opencontainers.org/forum/#!forum/dev). + +### IRC + +OCI discussion happens on #opencontainers on Freenode ([logs][irc-logs]). + +### Markdown style + +To keep consistency throughout the Markdown files in the Open Container spec all files should be formatted one sentence per line. +This fixes two things: it makes diffing easier with git and it resolves fights about line wrapping length. +For example, this paragraph will span three lines in the Markdown source. + +### Git commit + +#### Sign your work + +The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. +The rules are pretty simple: if you can certify the below (from [developercertificate.org](https://developercertificate.org/)): + +```text +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +then you just add a line to every git commit message: + +```text +Signed-off-by: Joe Smith <joe@gmail.com> +``` + +using your real name (sorry, no pseudonyms or anonymous contributions.) + +You can add the sign off when creating the git commit via `git commit -s`. + +### Commit Style + +Simple house-keeping for clean git history. +Read more on [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) or the Discussion section of [`git-commit(1)`](https://git-scm.com/docs/git-commit). + +1. Separate the subject from body with a blank line +2. Limit the subject line to 50 characters +3. Capitalize the subject line +4. Do not end the subject line with a period +5. Use the imperative mood in the subject line +6. Wrap the body at 72 characters +7. Use the body to explain what and why vs. how + - If there was important/useful/essential conversation or information, copy or include a reference +8. When possible, one keyword to scope the change in the subject (i.e. "README: ...", "runtime: ...") + +[code-of-conduct]: https://github.com/opencontainers/org/blob/master/CODE_OF_CONDUCT.md +[irc-logs]: http://ircbot.wl.linuxfoundation.org/eavesdrop/%23opencontainers/ diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..009a43a --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,98 @@ +# Releases + +The release process hopes to encourage early, consistent consensus-building during project development. +The mechanisms used are regular community communication on the mailing list about progress, scheduled meetings for issue resolution and release triage, and regularly paced and communicated releases. +Releases are proposed and adopted or rejected using the usual [project governance](GOVERNANCE.md) rules and procedures. + +An anti-pattern that we want to avoid is heavy development or discussions "late cycle" around major releases. +We want to build a community that is involved and communicates consistently through all releases instead of relying on "silent periods" as a judge of stability. + +## Parallel releases + +A single project MAY consider several motions to release in parallel. +However each motion to release after the initial 0.1.0 MUST be based on a previous release that has already landed. + +For example, runtime-spec maintainers may propose a v1.0.0-rc2 on the 1st of the month and a v0.9.1 bugfix on the 2nd of the month. +They may not propose a v1.0.0-rc3 until the v1.0.0-rc2 is accepted (on the 7th if the vote initiated on the 1st passes). + +## Specifications + +The OCI maintains three categories of projects: specifications, applications, and conformance-testing tools. +However, specification releases have special restrictions in the [OCI charter][charter]: + +- They are the target of backwards compatibility (§7.g), and +- They are subject to the OFWa patent grant (§8.d and e). + +To avoid unfortunate side effects (onerous backwards compatibility requirements or Member resignations), the following additional procedures apply to specification releases: + +### Planning a release + +Every OCI specification project SHOULD hold meetings that involve maintainers reviewing pull requests, debating outstanding issues, and planning releases. +This meeting MUST be advertised on the project README and MAY happen on a phone call, video conference, or on IRC. +Maintainers MUST send updates to the <dev@opencontainers.org> with results of these meetings. + +Before the specification reaches v1.0.0, the meetings SHOULD be weekly. +Once a specification has reached v1.0.0, the maintainers may alter the cadence, but a meeting MUST be held within four weeks of the previous meeting. + +The release plans, corresponding milestones and estimated due dates MUST be published on GitHub (e.g. <https://github.com/opencontainers/runtime-spec/milestones>). +GitHub milestones and issues are only used for community organization and all releases MUST follow the [project governance](GOVERNANCE.md) rules and procedures. + +### Timelines + +Specifications have a variety of different timelines in their lifecycle. + +- Pre-v1.0.0 specifications SHOULD release on a monthly cadence to garner feedback. +- Major specification releases MUST release at least three release candidates spaced a minimum of one week apart. + This means a major release like a v1.0.0 or v2.0.0 release will take 1 month at minimum: one week for rc1, one week for rc2, one week for rc3, and one week for the major release itself. + Maintainers SHOULD strive to make zero breaking changes during this cycle of release candidates and SHOULD restart the three-candidate count when a breaking change is introduced. + For example if a breaking change is introduced in v1.0.0-rc2 then the series would end with v1.0.0-rc4 and v1.0.0. +- Minor and patch releases SHOULD be made on an as-needed basis. + +[charter]: https://github.com/opencontainers/tob/blob/main/CHARTER.md + +## Checklist + +Releases usually follow a few steps: + +- [ ] prepare a pull-request for the release + - [ ] a commit updating `./ChangeLog` + - [ ] `git log --oneline --no-merges --decorate --name-status v1.0.1..HEAD | vim -` + - [ ] `:% s/(pr\/\(\d*\))\(.*\)/\2 (#\1)/` to move the PR to the end of line and match previous formatting + - [ ] review `(^M|^A|^D)` for impact of the commit + - [ ] group commits to `Additions:`, `Minor fixes and documentation:`, `Breaking changes:` + - [ ] delete the `(^M|^A|^D)` lines, `:%!grep -vE '(^M|^A|^D)'` + - [ ] merge multi-commit PRs (so each line has a `(#num)` suffix) + - [ ] drop hash and indent, `:'<,'> s/^\w* /^I* /` + - [ ] a commit bumping `./specs-go/version.go` to next version and empty the `VersionDev` variable + - [ ] a commit adding back the "+dev" to `VersionDev` +- [ ] send email to <dev@opencontainers.org> + - [ ] copy the exact commit hash for bumping the version from the pull-request (since master always stays as "-dev") + - [ ] count the PRs since last release (that this version is tracking, in the cases of multiple branching), like `git log --pretty=oneline --no-merges --decorate $priorTag..$versionBumpCommit | grep \(pr\/ | wc -l` + - [ ] get the date for a week from now, like `TZ=UTC date --date='next week'` + - [ ] OPTIONAL find a cute animal gif to attach to the email, and subsequently the release description + - [ ] subject line like `[runtime-spec VOTE] tag $versionBumpCommit as $version (closes $dateWeekFromNowUTC)` + - [ ] email body like + +```text +Hey everyone, + +There have been $numPRs PRs merged since $priorTag release (https://github.com/opencontainers/image-spec/compare/$priorTag...$versionBumpCommit). + +$linkToPullRequest + +Please respond LGTM or REJECT (with reasoning). + +$sig +``` + +- [ ] edit/update the pull-request to link to the VOTE thread, from <https://groups.google.com/a/opencontainers.org/forum/#!forum/dev> +- [ ] a week later, if the vote passes, merge the PR + - [ ] `git tag -s $version $versionBumpCommit` + - [ ] `git push --tags` +- [ ] produce release documents + - [ ] git checkout the release tag, like `git checkout $version` + - [ ] `make docs` + - [ ] rename the output PDF and HTML file to include version, like `mv output/oci-runtime-spec.pdf output/oci-runtime-spec-$version.pdf`` + - [ ] attach these docs to the release on <https://github.com/opencontainers/runtime-spec/releases> + - [ ] link to the the VOTE thread and include the passing vote count + - [ ] link to the pull request that merged the release diff --git a/annotations.md b/annotations.md new file mode 100644 index 0000000..396da2f --- /dev/null +++ b/annotations.md @@ -0,0 +1,83 @@ +# Annotations + +Several components of the specification, like [Image Manifests](manifest.md) and [Descriptors](descriptor.md), feature an optional annotations property, whose format is common and defined in this section. + +This property contains arbitrary metadata. + +## Rules + +- Annotations MUST be a key-value map where both the key and value MUST be strings. +- While the value MUST be present, it MAY be an empty string. +- Keys MUST be unique within this map, and best practice is to namespace the keys. +- Keys SHOULD be named using a reverse domain notation - e.g. `com.example.myKey`. +- The prefix `org.opencontainers` is reserved for keys defined in Open Container Initiative (OCI) specifications and MUST NOT be used by other specifications and extensions. +- Keys using the `org.opencontainers.image` namespace are reserved for use in the OCI Image Specification and MUST NOT be used by other specifications and extensions, including other OCI specifications. +- If there are no annotations then this property MUST either be absent or be an empty map. +- Consumers MUST NOT generate an error if they encounter an unknown annotation key. + +## Pre-Defined Annotation Keys + +This specification defines the following annotation keys, intended for but not limited to [image index](image-index.md), image [manifest](manifest.md), and [descriptor](descriptor.md) authors. + +- **org.opencontainers.image.created** date and time on which the image was built, conforming to [RFC 3339][rfc3339]. +- **org.opencontainers.image.authors** contact details of the people or organization responsible for the image (freeform string) +- **org.opencontainers.image.url** URL to find more information on the image (string) +- **org.opencontainers.image.documentation** URL to get documentation on the image (string) +- **org.opencontainers.image.source** URL to get source code for building the image (string) +- **org.opencontainers.image.version** version of the packaged software + - The version MAY match a label or tag in the source code repository + - version MAY be [Semantic versioning-compatible](https://semver.org/) +- **org.opencontainers.image.revision** Source control revision identifier for the packaged software. +- **org.opencontainers.image.vendor** Name of the distributing entity, organization or individual. +- **org.opencontainers.image.licenses** License(s) under which contained software is distributed as an [SPDX License Expression][spdx-license-expression]. +- **org.opencontainers.image.ref.name** Name of the reference for a target (string). + - SHOULD only be considered valid when on descriptors on `index.json` within [image layout](image-layout.md). + - Character set of the value SHOULD conform to alphanum of `A-Za-z0-9` and separator set of `-._:@/+` + - The reference must match the following [grammar](considerations.md#ebnf): + + ```ebnf + ref ::= component ("/" component)* + component ::= alphanum (separator alphanum)* + alphanum ::= [A-Za-z0-9]+ + separator ::= [-._:@+] | "--" + ``` + +- **org.opencontainers.image.title** Human-readable title of the image (string) +- **org.opencontainers.image.description** Human-readable description of the software packaged in the image (string) +- **org.opencontainers.image.base.digest** [Digest](descriptor.md#digests) of the image this image is based on (string) + - This SHOULD be the immediate image sharing zero-indexed layers with the image, such as from a Dockerfile `FROM` statement. + - This SHOULD NOT reference any other images used to generate the contents of the image (e.g., multi-stage Dockerfile builds). +- **org.opencontainers.image.base.name** Image reference of the image this image is based on (string) + - This SHOULD be image references in the format defined by [distribution/distribution][distribution-reference]. + - This SHOULD be a fully qualified reference name, without any assumed default registry. (e.g., `registry.example.com/my-org/my-image:tag` instead of `my-org/my-image:tag`). + - This SHOULD be the immediate image sharing zero-indexed layers with the image, such as from a Dockerfile `FROM` statement. + - This SHOULD NOT reference any other images used to generate the contents of the image (e.g., multi-stage Dockerfile builds). + - If the `image.base.name` annotation is specified, the `image.base.digest` annotation SHOULD be the digest of the manifest referenced by the `image.ref.name` annotation. + +## Back-compatibility with Label Schema + +[Label Schema][label-schema] defined a number of conventional labels for container images, and these are now superceded by annotations with keys starting **org.opencontainers.image**. + +While users are encouraged to use the **org.opencontainers.image** keys, tools MAY choose to support compatible annotations using the **org.label-schema** prefix as follows. + +| `org.opencontainers.image` prefix | `org.label-schema` prefix | Compatibility notes | +|---------------------------|-------------------------|---------------------| +| `created` | `build-date` | Compatible | +| `url` | `url` | Compatible | +| `source` | `vcs-url` | Compatible | +| `version` | `version` | Compatible | +| `revision` | `vcs-ref` | Compatible | +| `vendor` | `vendor` | Compatible | +| `title` | `name` | Compatible | +| `description` | `description` | Compatible | +| `documentation` | `usage` | Value is compatible if the documentation is located by a URL | +| `authors` | | No equivalent in Label Schema | +| `licenses` | | No equivalent in Label Schema | +| `ref.name` | | No equivalent in Label Schema | +| | `schema-version`| No equivalent in the OCI Image Spec | +| | `docker.*`, `rkt.*` | No equivalent in the OCI Image Spec | + +[distribution-reference]: https://github.com/distribution/distribution/blob/d0deff9cd6c2b8c82c6f3d1c713af51df099d07b/reference/reference.go +[label-schema]: https://github.com/label-schema/label-schema.org/blob/gh-pages/rc1.md +[rfc3339]: https://tools.ietf.org/html/rfc3339#section-5.6 +[spdx-license-expression]: https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60 diff --git a/artifacts-guidance.md b/artifacts-guidance.md new file mode 100644 index 0000000..49b8c63 --- /dev/null +++ b/artifacts-guidance.md @@ -0,0 +1,6 @@ +# Guidance for Artifacts Authors + +Content other than OCI container images MAY be packaged using the image manifest. +When this is done, the `config.mediaType` value should not be a known OCI image config [media type](media-types.md). +Historically, due to registry limitations, some tools have created non-OCI conformant artifacts using the `application/vnd.oci.image.config.v1+json` value for `config.mediaType` and values specific to the artifact in `layer[*].mediaType`. +Implementation details and examples are provided in the [image manifest specification](manifest.md#guidelines-for-artifact-usage). diff --git a/config.md b/config.md new file mode 100644 index 0000000..23c109b --- /dev/null +++ b/config.md @@ -0,0 +1,320 @@ +# OCI Image Configuration + +An OCI _Image_ is an ordered collection of root filesystem changes and the corresponding execution parameters for use within a container runtime. +This specification outlines the JSON format describing images for use with a container runtime and execution tool and its relationship to filesystem changesets, described in [Layers](layer.md). + +This section defines the `application/vnd.oci.image.config.v1+json` [media type](media-types.md). + +## Terminology + +This specification uses the following terms: + +### [Layer](layer.md) + +- Image filesystems are composed of _layers_. +- Each layer represents a set of filesystem changes in a tar-based [layer format](layer.md), recording files to be added, changed, or deleted relative to its parent layer. +- Layers do not have configuration metadata such as environment variables or default arguments - these are properties of the image as a whole rather than any particular layer. +- Using a layer-based or union filesystem such as AUFS, or by computing the diff from filesystem snapshots, the filesystem changeset can be used to present a series of image layers as if they were one cohesive filesystem. + +### Image JSON + +- Each image has an associated JSON structure which describes some basic information about the image such as date created, author, as well as execution/runtime configuration like its entrypoint, default arguments, networking, and volumes. +- The JSON structure also references a cryptographic hash of each layer used by the image, and provides history information for those layers. +- This JSON is considered to be immutable, because changing it would change the computed [ImageID](#imageid). +- Changing it means creating a new derived image, instead of changing the existing image. + +### Layer DiffID + +A layer DiffID is the digest over the layer's uncompressed tar archive and serialized in the descriptor digest format, e.g., `sha256:a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9`. +Layers SHOULD be packed and unpacked reproducibly to avoid changing the layer DiffID, for example by using [tar-split][] to save the tar headers. + +NOTE: Do not confuse DiffIDs with [layer digests](manifest.md#image-manifest-property-descriptions), often referenced in the manifest, which are digests over compressed or uncompressed content. + +### Layer ChainID + +For convenience, it is sometimes useful to refer to a stack of layers with a single identifier. +While a layer's `DiffID` identifies a single changeset, the `ChainID` identifies the subsequent application of those changesets. +This ensures that we have handles referring to both the layer itself, as well as the result of the application of a series of changesets. +Use in combination with `rootfs.diff_ids` while applying layers to a root filesystem to uniquely and safely identify the result. + +#### Definition + +The `ChainID` of an applied set of layers is defined with the following recursion: + +```text +ChainID(L₀) = DiffID(L₀) +ChainID(L₀|...|Lₙ₋₁|Lₙ) = Digest(ChainID(L₀|...|Lₙ₋₁) + " " + DiffID(Lₙ)) +``` + +For this, we define the binary `|` operation to be the result of applying the right operand to the left operand. +For example, given base layer `A` and a changeset `B`, we refer to the result of applying `B` to `A` as `A|B`. + +Above, we define the `ChainID` for a single layer (`L₀`) as equivalent to the `DiffID` for that layer. +Otherwise, the `ChainID` for a set of applied layers (`L₀|...|Lₙ₋₁|Lₙ`) is defined as the recursion `Digest(ChainID(L₀|...|Lₙ₋₁) + " " + DiffID(Lₙ))`. + +#### Explanation + +Let's say we have layers A, B, C, ordered from bottom to top, where A is the base and C is the top. +Defining `|` as a binary application operator, the root filesystem may be `A|B|C`. +While it is implied that `C` is only useful when applied to `A|B`, the identifier `C` is insufficient to identify this result, as we'd have the equality `C = A|B|C`, which isn't true. + +The main issue is when we have two definitions of `C`, `C = C` and `C = A|B|C`. +If this is true (with some handwaving), `C = x|C` where `x = any application`. +This means that if an attacker can define `x`, relying on `C` provides no guarantee that the layers were applied in any order. + +The `ChainID` addresses this problem by being defined as a compound hash. +**We differentiate the changeset `C`, from the order-dependent application `A|B|C` by saying that the resulting rootfs is identified by ChainID(A|B|C), which can be calculated by `ImageConfig.rootfs`.** + +Let's expand the definition of `ChainID(A|B|C)` to explore its internal structure: + +```text +ChainID(A) = DiffID(A) +ChainID(A|B) = Digest(ChainID(A) + " " + DiffID(B)) +ChainID(A|B|C) = Digest(ChainID(A|B) + " " + DiffID(C)) +``` + +We can replace each definition and reduce to a single equality: + +```text +ChainID(A|B|C) = Digest(Digest(DiffID(A) + " " + DiffID(B)) + " " + DiffID(C)) +``` + +Hopefully, the above is illustrative of the _actual_ contents of the `ChainID`. +Most importantly, we can easily see that `ChainID(C) != ChainID(A|B|C)`, otherwise, `ChainID(C) = DiffID(C)`, which is the base case, could not be true. + +### ImageID + +Each image's ID is given by the SHA256 hash of its [configuration JSON](#image-json). +It is represented as a hexadecimal encoding of 256 bits, e.g., `sha256:a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9`. +Since the [configuration JSON](#image-json) that gets hashed references hashes of each layer in the image, this formulation of the ImageID makes images content-addressable. + +## Properties + +Note: Any OPTIONAL field MAY also be set to null, which is equivalent to being absent. + +- **created** _string_, OPTIONAL + + An combined date and time at which the image was created, formatted as defined by [RFC 3339, section 5.6][rfc3339-s5.6]. + +- **author** _string_, OPTIONAL + + Gives the name and/or email address of the person or entity which created and is responsible for maintaining the image. + +- **architecture** _string_, REQUIRED + + The CPU architecture which the binaries in this image are built to run on. + Configurations SHOULD use, and implementations SHOULD understand, values listed in the Go Language document for [`GOARCH`][go-environment]. + +- **os** _string_, REQUIRED + + The name of the operating system which the image is built to run on. + Configurations SHOULD use, and implementations SHOULD understand, values listed in the Go Language document for [`GOOS`][go-environment]. + +- **os.version** _string_, OPTIONAL + + This OPTIONAL property specifies the version of the operating system targeted by the referenced blob. + Implementations MAY refuse to use manifests where `os.version` is not known to work with the host OS version. + Valid values are implementation-defined. e.g. `10.0.14393.1066` on `windows`. + +- **os.features** _array of strings_, OPTIONAL + + This OPTIONAL property specifies an array of strings, each specifying a mandatory OS feature. + When `os` is `windows`, image indexes SHOULD use, and implementations SHOULD understand the following values: + + - `win32k`: image requires `win32k.sys` on the host (Note: `win32k.sys` is missing on Nano Server) + +- **variant** _string_, OPTIONAL + + The variant of the specified CPU architecture. + Configurations SHOULD use, and implementations SHOULD understand, `variant` values listed in the [Platform Variants](image-index.md#platform-variants) table. + +- **config** _object_, OPTIONAL + + The execution parameters which SHOULD be used as a base when running a container using the image. + This field can be `null`, in which case any execution parameters should be specified at creation of the container. + + - **User** _string_, OPTIONAL + + The username or UID which is a platform-specific structure that allows specific control over which user the process run as. + This acts as a default value to use when the value is not specified when creating a container. + For Linux based systems, all of the following are valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. + If `group`/`gid` is not specified, the default group and supplementary groups of the given `user`/`uid` in `/etc/passwd` and `/etc/group` from the container are applied. + If `group`/`gid` is specified, supplementary groups from the container are ignored. + + - **ExposedPorts** _object_, OPTIONAL + + A set of ports to expose from a container running this image. + Its keys can be in the format of: +`port/tcp`, `port/udp`, `port` with the default protocol being `tcp` if not specified. + These values act as defaults and are merged with any specified when creating a container. + **NOTE:** This JSON structure value is unusual because it is a direct JSON serialization of the Go type `map[string]struct{}` and is represented in JSON as an object mapping its keys to an empty object. + + - **Env** _array of strings_, OPTIONAL + + Entries are in the format of `VARNAME=VARVALUE`. + These values act as defaults and are merged with any specified when creating a container. + + - **Entrypoint** _array of strings_, OPTIONAL + + A list of arguments to use as the command to execute when the container starts. + These values act as defaults and may be replaced by an entrypoint specified when creating a container. + + - **Cmd** _array of strings_, OPTIONAL + + Default arguments to the entrypoint of the container. + These values act as defaults and may be replaced by any specified when creating a container. + If an `Entrypoint` value is not specified, then the first entry of the `Cmd` array SHOULD be interpreted as the executable to run. + + - **Volumes** _object_, OPTIONAL + + A set of directories describing where the process is likely to write data specific to a container instance. + **NOTE:** This JSON structure value is unusual because it is a direct JSON serialization of the Go type `map[string]struct{}` and is represented in JSON as an object mapping its keys to an empty object. + + - **WorkingDir** _string_, OPTIONAL + + Sets the current working directory of the entrypoint process in the container. + This value acts as a default and may be replaced by a working directory specified when creating a container. + + - **Labels** _object_, OPTIONAL + + The field contains arbitrary metadata for the container. + This property MUST use the [annotation rules](annotations.md#rules). + + - **StopSignal** _string_, OPTIONAL + + The field contains the system call signal that will be sent to the container to exit. The signal can be a signal name in the format `SIGNAME`, for instance `SIGKILL` or `SIGRTMIN+3`. + + - **ArgsEscaped** _boolean_, OPTIONAL + + `[Deprecated]` - This field is present only for legacy compatibility with Docker and should not be used by new image builders. + It is used by Docker for Windows images to indicate that the `Entrypoint` or `Cmd` or both, contains only a single element array, that is a pre-escaped, and combined into a single string `CommandLine`. + If `true` the value in `Entrypoint` or `Cmd` should be used as-is to avoid double escaping. + Note, the exact behavior of `ArgsEscaped` is complex and subject to implementation details in Moby project. + + - **Memory** _integer_, OPTIONAL + + This property is _reserved_ for use, to [maintain compatibility](media-types.md#compatibility-matrix). + + - **MemorySwap** _integer_, OPTIONAL + + This property is _reserved_ for use, to [maintain compatibility](media-types.md#compatibility-matrix). + + - **CpuShares** _integer_, OPTIONAL + + This property is _reserved_ for use, to [maintain compatibility](media-types.md#compatibility-matrix). + + - **Healthcheck** _object_, OPTIONAL + + This property is _reserved_ for use, to [maintain compatibility](media-types.md#compatibility-matrix). + +- **rootfs** _object_, REQUIRED + + The rootfs key references the layer content addresses used by the image. + This makes the image config hash depend on the filesystem hash. + + - **type** _string_, REQUIRED + + MUST be set to `layers`. + Implementations MUST generate an error if they encounter a unknown value while verifying or unpacking an image. + + - **diff_ids** _array of strings_, REQUIRED + + An array of layer content hashes (`DiffIDs`), in order from first to last. + +- **history** _array of objects_, OPTIONAL + + Describes the history of each layer. + The array is ordered from first to last. + The object has the following fields: + + - **created** _string_, OPTIONAL + + A combined date and time at which the layer was created, formatted as defined by [RFC 3339, section 5.6][rfc3339-s5.6]. + + - **author** _string_, OPTIONAL + + The author of the build point. + + - **created_by** _string_, OPTIONAL + + The command which created the layer. + + - **comment** _string_, OPTIONAL + + A custom message set when creating the layer. + + - **empty_layer** _boolean_, OPTIONAL + + This field is used to mark if the history item created a filesystem diff. + It is set to true if this history item doesn't correspond to an actual layer in the rootfs section (for example, Dockerfile's [ENV](https://docs.docker.com/engine/reference/builder/#/env) command results in no change to the filesystem). + +Any extra fields in the Image JSON struct are considered implementation specific and MUST NOT generate an error by any implementations which are unable to interpret them. + +Whitespace is OPTIONAL and implementations MAY have compact JSON with no whitespace. + +## Example + +Here is an example image configuration JSON document: + +```json,title=Image%20JSON&mediatype=application/vnd.oci.image.config.v1%2Bjson +{ + "created": "2015-10-31T22:22:56.015925234Z", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "architecture": "amd64", + "os": "linux", + "config": { + "User": "alice", + "ExposedPorts": { + "8080/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "FOO=oci_is_a", + "BAR=well_written_spec" + ], + "Entrypoint": [ + "/bin/my-app-binary" + ], + "Cmd": [ + "--foreground", + "--config", + "/etc/my-app.d/default.cfg" + ], + "Volumes": { + "/var/job-result-data": {}, + "/var/log/my-app-logs": {} + }, + "WorkingDir": "/home/alice", + "Labels": { + "com.example.project.git.url": "https://example.com/project.git", + "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b" + } + }, + "rootfs": { + "diff_ids": [ + "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + }, + "history": [ + { + "created": "2015-10-31T22:22:54.690851953Z", + "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" + }, + { + "created": "2015-10-31T22:22:55.613815829Z", + "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]", + "empty_layer": true + }, + { + "created": "2015-10-31T22:22:56.329850019Z", + "created_by": "/bin/sh -c apk add curl" + } + ] +} +``` + +[rfc3339-s5.6]: https://tools.ietf.org/html/rfc3339#section-5.6 +[go-environment]: https://golang.org/doc/install/source#environment +[tar-split]: https://github.com/vbatts/tar-split diff --git a/considerations.md b/considerations.md new file mode 100644 index 0000000..11d2a37 --- /dev/null +++ b/considerations.md @@ -0,0 +1,147 @@ +# Considerations + +## Extensibility + +Implementations storing or copying content MUST NOT modify or alter the content in a way that would change the digest of the content. Examples of these implementations include: + +- A [registry implementing the distribution specification][distribution-spec], including local registries, caching proxies +- An application which copies content to disk or between registries + +Implementations processing content SHOULD NOT generate an error if they encounter an unknown property in a known media type. Examples of these implementations include: + +- A [runtime implementing the runtime specification][runtime-spec] +- An implementation using OCI to retrieve and utilize artifacts, e.g.: a WASM runtime + +## Canonicalization + +- OCI Images are [content-addressable](https://en.wikipedia.org/wiki/Content-addressable_storage). See [descriptors](descriptor.md) for more. +- One benefit of content-addressable storage is easy deduplication. +- Many images might depend on a particular [layer](layer.md), but there will only be one blob in the [store](image-layout.md). +- With a different serialization, that same semantic layer would have a different hash, and if both versions of the layer are referenced there will be two blobs with the same semantic content. +- To allow efficient storage, implementations serializing content for blobs SHOULD use a canonical serialization. +- This increases the chance that different implementations can push the same semantic content to the store without creating redundant blobs. + +### JSON + +[JSON][JSON] content SHOULD be serialized as [canonical JSON][canonical-json]. +Of the [OCI Image Format Specification media types](media-types.md), all the types ending in `+json` contain JSON content. +Implementations: + +- [Go][Go]: [github.com/docker/go][docker-go], which claims to implement [canonical JSON][canonical-json] except for Unicode normalization. + +## EBNF + +For field formats described in this specification, we use a limited subset of [Extended Backus-Naur Form][ebnf], similar to that used by the [XML specification][xmlebnf]. +Grammars present in the OCI specification are regular and can be converted to a single regular expressions. +However, regular expressions are avoided to limit ambiguity between regular expression syntax. +By defining a subset of EBNF used here, the possibility of variation, misunderstanding or ambiguities from linking to a larger specification can be avoided. + +Grammars are made up of rules in the following form: + +```ebnf +symbol ::= expression +``` + +We can say we have the production identified by symbol if the input is matched by the expression. +Whitespace is completely ignored in rule definitions. + +### Expressions + +The simplest expression is the literal, surrounded by quotes: + +```ebnf +literal ::= "matchthis" +``` + +The above expression defines a symbol, "literal", that matches the exact input of "matchthis". +Character classes are delineated by brackets (`[]`), describing either a set, range or multiple range of characters: + +```ebnf +set := [abc] +range := [A-Z] +``` + +The above symbol "set" would match one character of either "a", "b" or "c". +The symbol "range" would match any character, "A" to "Z", inclusive. +Currently, only matching for 7-bit ascii literals and character classes is defined, as that is all that is required by this specification. +Multiple character ranges and explicit characters can be specified in a single character classes, as follows: + +```ebnf +multipleranges := [a-zA-Z=-] +``` + +The above matches the characters in the range `A` to `Z`, `a` to `z` and the individual characters `-` and `=`. + +Expressions can be made up of one or more expressions, such that one must be followed by the other. +This is known as an implicit concatenation operator. +For example, to satisfy the following rule, both `A` and `B` must be matched to satisfy the rule: + +```ebnf +symbol ::= A B +``` + +Each expression must be matched once and only once, `A` followed by `B`. +To support the description of repetition and optional match criteria, the postfix operators `*` and `+` are defined. +`*` indicates that the preceding expression can be matched zero or more times. +`+` indicates that the preceding expression must be matched one or more times. +These appear in the following form: + +```ebnf +zeroormore ::= expression* +oneormore ::= expression+ +``` + +Parentheses are used to group expressions into a larger expression: + +```ebnf +group ::= (A B) +``` + +Like simpler expressions above, operators can be applied to groups, as well. +To allow for alternates, we also define the infix operator `|`. + +```ebnf +oneof ::= A | B +``` + +The above indicates that the expression should match one of the expressions, `A` or `B`. + +### Precedence + +The operator precedence is in the following order: + +- Terminals (literals and character classes) +- Grouping `()` +- Unary operators `+*` +- Concatenation +- Alternates `|` + +The precedence can be better described using grouping to show equivalents. +Concatenation has higher precedence than alternates, such `A B | C D` is equivalent to `(A B) | (C D)`. +Unary operators have higher precedence than alternates and concatenation, such that `A+ | B+` is equivalent to `(A+) | (B+)`. + +### Examples + +The following combines the previous definitions to match a simple, relative path name, describing the individual components: + +```ebnf +path ::= component ("/" component)* +component ::= [a-z]+ +``` + +The production "component" is one or more lowercase letters. +A "path" is then at least one component, possibly followed by zero or more slash-component pairs. +The above can be converted into the following regular expression: + +```regex +[a-z]+(?:/[a-z]+)* +``` + +[canonical-json]: https://wiki.laptop.org/go/Canonical_JSON +[distribution-spec]: https://github.com/opencontainers/distribution-spec/blob/main/spec.md +[docker-go]: https://github.com/docker/go/ +[ebnf]: https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form +[Go]: https://golang.org/ +[JSON]: https://json.org/ +[runtime-spec]: https://github.com/opencontainers/runtime-spec/blob/main/spec.md +[xmlebnf]: https://www.w3.org/TR/REC-xml/#sec-notation diff --git a/conversion.md b/conversion.md new file mode 100644 index 0000000..81420fa --- /dev/null +++ b/conversion.md @@ -0,0 +1,130 @@ +# Conversion to OCI Runtime Configuration + +When extracting an OCI Image into an [OCI Runtime bundle][oci-runtime-bundle], two orthogonal components of the extraction are relevant: + +1. Extraction of the root filesystem from the set of [filesystem layers](layer.md). +2. Conversion of the [image configuration blob](config.md) to an [OCI Runtime configuration blob][oci-runtime-config]. + +This section defines how to convert an `application/vnd.oci.image.config.v1+json` blob to an [OCI runtime configuration blob][oci-runtime-config] (the latter component of extraction). +The former component of extraction is defined [elsewhere](layer.md) and is orthogonal to configuration of a runtime bundle. +The values of runtime configuration properties not specified by this document are implementation-defined. + +A converter MUST rely on the OCI image configuration to build the OCI runtime configuration as described by this document; this will create the "default generated runtime configuration". + +The "default generated runtime configuration" MAY be overridden or combined with externally provided inputs from the caller. +In addition, a converter MAY have its own implementation-defined defaults and extensions which MAY be combined with the "default generated runtime configuration". +The restrictions in this document refer only to combining implementation-defined defaults with the "default generated runtime configuration". +Externally provided inputs are considered to be a modification of the `application/vnd.oci.image.config.v1+json` used as a source, and such modifications have no restrictions. + +For example, externally provided inputs MAY cause an environment variable to be added, removed or changed. +However an implementation-defined default SHOULD NOT result in an environment variable being removed or changed. + +[oci-runtime-bundle]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/bundle.md +[oci-runtime-config]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md + +## Verbatim Fields + +Certain image configuration fields have an identical counterpart in the runtime configuration. +Some of these are purely annotation-based fields, and have been extracted into a [separate subsection](#annotation-fields). +A compliant configuration converter MUST extract the following fields verbatim to the corresponding field in the generated runtime configuration: + +| Image Field | Runtime Field | Notes | +| ------------------- | --------------- | ----- | +| `Config.WorkingDir` | `process.cwd` | | +| `Config.Env` | `process.env` | 1 | +| `Config.Entrypoint` | `process.args` | 2 | +| `Config.Cmd` | `process.args` | 2 | + +1. The converter MAY add additional entries to `process.env` but it SHOULD NOT add entries that have variable names present in `Config.Env`. +2. If both `Config.Entrypoint` and `Config.Cmd` are specified, the converter MUST append the value of `Config.Cmd` to the value of `Config.Entrypoint` and set `process.args` to that combined value. + +### Annotation Fields + +These fields all affect the `annotations` of the runtime configuration, and are thus subject to [precedence](#annotations). + +| Image Field | Runtime Field | Notes | +| ------------------- | --------------- | ----- | +| `os` | `annotations` | 1,2 | +| `architecture` | `annotations` | 1,3 | +| `variant` | `annotations` | 1,4 | +| `os.version` | `annotations` | 1,5 | +| `os.features` | `annotations` | 1,6 | +| `author` | `annotations` | 1,7 | +| `created` | `annotations` | 1,8 | +| `Config.Labels` | `annotations` | | +| `Config.StopSignal` | `annotations` | 1,9 | + +1. If a user has explicitly specified this annotation with `Config.Labels`, then the value specified in this field takes lower [precedence](#annotations) and the converter MUST instead use the value from `Config.Labels`. +2. The value of this field MUST be set as the value of `org.opencontainers.image.os` in `annotations`. +3. The value of this field MUST be set as the value of `org.opencontainers.image.architecture` in `annotations`. +4. The value of this field MUST be set as the value of `org.opencontainers.image.variant` in `annotations`. +5. The value of this field MUST be set as the value of `org.opencontainers.image.os.version` in `annotations`. +6. The value of this field MUST be set as the value of `org.opencontainers.image.os.features` in `annotations`. +7. The value of this field MUST be set as the value of `org.opencontainers.image.author` in `annotations`. +8. The value of this field MUST be set as the value of `org.opencontainers.image.created` in `annotations`. +9. The value of this field MUST be set as the value of `org.opencontainers.image.stopSignal` in `annotations`. + +## Parsed Fields + +Certain image configuration fields have a counterpart that must first be translated. +A compliant configuration converter SHOULD parse all of these fields and set the corresponding fields in the generated runtime configuration: + +| Image Field | Runtime Field | +| ------------------- | --------------- | +| `Config.User` | `process.user.*` | + +The method of parsing the above image fields are described in the following sections. + +### `Config.User` + +If the values of [`user` or `group`](config.md#properties) in `Config.User` are numeric (`uid` or `gid`) then the values MUST be copied verbatim to `process.user.uid` and `process.user.gid` respectively. +If the values of [`user` or `group`](config.md#properties) in `Config.User` are not numeric (`user` or `group`) then a converter SHOULD resolve the user information using a method appropriate for the container's context. +For Unix-like systems, this MAY involve resolution through NSS or parsing `/etc/passwd` from the extracted container's root filesystem to determine the values of `process.user.uid` and `process.user.gid`. + +In addition, a converter SHOULD set the value of `process.user.additionalGids` to a value corresponding to the user in the container's context described by `Config.User`. +For Unix-like systems, this MAY involve resolution through NSS or parsing `/etc/group` and determining the group memberships of the user specified in `process.user.uid`. +The converter SHOULD NOT modify `process.user.additionalGids` if the value of [`user`](config.md#properties) in `Config.User` is numeric or if `Config.User` specifies a group. + +If `Config.User` is not defined, the converted `process.user` value is implementation-defined. +If `Config.User` does not correspond to a user in the container's context, the converter MUST return an error. + +## Optional Fields + +Certain image configuration fields are not applicable to all conversion use cases, and thus are optional for configuration converters to implement. +A compliant configuration converter SHOULD provide a way for users to extract these fields into the generated runtime configuration: + +| Image Field | Runtime Field | Notes | +| --------------------- | ------------------ | ----- | +| `Config.ExposedPorts` | `annotations` | 1 | +| `Config.Volumes` | `mounts` | 2 | + +1. The runtime configuration does not have a corresponding field for this image field. + However, converters SHOULD set the [`org.opencontainers.image.exposedPorts` annotation](#configexposedports). +2. Implementations SHOULD provide mounts for these locations such that application data is not written to the container's root filesystem. + If a converter implements conversion for this field using mountpoints, it SHOULD set the `destination` of the mountpoint to the value specified in `Config.Volumes`. + An implementation MAY seed the contents of the mount with data in the image at the same location. + If a _new_ image is created from a container based on the image described by this configuration, data in these paths SHOULD NOT be included in the _new_ image. + The other `mounts` fields are platform and context dependent, and thus are implementation-defined. + Note that the implementation of `Config.Volumes` need not use mountpoints, as it is effectively a mask of the filesystem. + +### `Config.ExposedPorts` + +The OCI runtime configuration does not provide a way of expressing the concept of "container exposed ports". +However, converters SHOULD set the **org.opencontainers.image.exposedPorts** annotation, unless doing so will [cause a conflict](#annotations). + +**org.opencontainers.image.exposedPorts** is the list of values that correspond to the [keys defined for `Config.ExposedPorts`](config.md) (string, comma-separated values). + +## Annotations + +There are three ways of annotating an OCI image in this specification: + +1. `Config.Labels` in the [configuration](config.md) of the image. +2. `annotations` in the [manifest](manifest.md) of the image. +3. `annotations` in the [image index](image-index.md) of the image. + +In addition, there are also implicit annotations that are defined by this section which are determined from the values of the image configuration. +A converter SHOULD NOT attempt to extract annotations from [manifests](manifest.md) or [image indices](image-index.md). +If there is a conflict (same key but different value) between an implicit annotation (or annotation in [manifests](manifest.md) or [image indices](image-index.md)) and an explicitly specified annotation in `Config.Labels`, the value specified in `Config.Labels` MUST take precedence. + +A converter MAY add annotations which have keys not specified in the image. +A converter MUST NOT modify the values of annotations specified in the image. diff --git a/descriptor.md b/descriptor.md new file mode 100644 index 0000000..004ad0e --- /dev/null +++ b/descriptor.md @@ -0,0 +1,221 @@ +# OCI Content Descriptors + +- An OCI image consists of several different components, arranged in a [Merkle Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Merkle_tree). +- References between components in the graph are expressed through _Content Descriptors_. +- A Content Descriptor (or simply _Descriptor_) describes the disposition of the targeted content. +- A Content Descriptor includes the type of the content, a content identifier (_digest_), and the byte-size of the raw content. + Optionally, it includes the type of artifact it is describing. +- Descriptors SHOULD be embedded in other formats to securely reference external content. +- Other formats SHOULD use descriptors to securely reference external content. + +This section defines the `application/vnd.oci.descriptor.v1+json` [media type](media-types.md). + +## Properties + +A descriptor consists of a set of properties encapsulated in key-value fields. + +The following fields contain the primary properties that constitute a Descriptor: + +- **`mediaType`** *string* + + This REQUIRED property contains the media type of the referenced content. + Values MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2]. + + The OCI image specification defines [several of its own MIME types](media-types.md) for resources defined in the specification. + +- **`digest`** *string* + + This REQUIRED property is the _digest_ of the targeted content, conforming to the requirements outlined in [Digests](#digests). + Retrieved content SHOULD be verified against this digest when consumed via untrusted sources. + +- **`size`** *int64* + + This REQUIRED property specifies the size, in bytes, of the raw content. + This property exists so that a client will have an expected size for the content before processing. + If the length of the retrieved content does not match the specified length, the content SHOULD NOT be trusted. + +- **`urls`** *array of strings* + + This OPTIONAL property specifies a list of URIs from which this object MAY be downloaded. + Each entry MUST conform to [RFC 3986][rfc3986]. + Entries SHOULD use the `http` and `https` schemes, as defined in [RFC 7230][rfc7230-s2.7]. + +- **`annotations`** *string-string map* + + This OPTIONAL property contains arbitrary metadata for this descriptor. + This OPTIONAL property MUST use the [annotation rules](annotations.md#rules). + +- **`data`** *string* + + This OPTIONAL property contains an embedded representation of the referenced content. + Values MUST conform to the Base 64 encoding, as defined in [RFC 4648][rfc4648-s4]. + The decoded data MUST be identical to the referenced content and SHOULD be verified against the [`digest`](#digests) and `size` fields by content consumers. + See [Embedded Content](#embedded-content) for when this is appropriate. + +- **`artifactType`** *string* + + This OPTIONAL property contains the type of an artifact when the descriptor points to an artifact. + This is the value of the config descriptor `mediaType` when the descriptor references an [image manifest](manifest.md). + If defined, the value MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2], and MAY be registered with [IANA][iana]. + +Descriptors pointing to [`application/vnd.oci.image.manifest.v1+json`](manifest.md) SHOULD include the extended field `platform`, see [Image Index Property Descriptions](image-index.md#image-index-property-descriptions) for details. + +### Reserved + +Extended _Descriptor_ field additions proposed in other OCI specifications SHOULD first be considered for addition into this specification. + +## Digests + +The _digest_ property of a Descriptor acts as a content identifier, enabling [content addressability](https://en.wikipedia.org/wiki/Content-addressable_storage). +It uniquely identifies content by taking a [collision-resistant hash](https://en.wikipedia.org/wiki/Cryptographic_hash_function) of the bytes. +If the _digest_ can be communicated in a secure manner, one can verify content from an insecure source by recalculating the digest independently, ensuring the content has not been modified. + +The value of the `digest` property is a string consisting of an _algorithm_ portion and an _encoded_ portion. +The _algorithm_ specifies the cryptographic hash function and encoding used for the digest; the _encoded_ portion contains the encoded result of the hash function. + +A digest string MUST match the following [grammar](considerations.md#ebnf): + +```ebnf +digest ::= algorithm ":" encoded +algorithm ::= algorithm-component (algorithm-separator algorithm-component)* +algorithm-component ::= [a-z0-9]+ +algorithm-separator ::= [+._-] +encoded ::= [a-zA-Z0-9=_-]+ +``` + +Note that _algorithm_ MAY impose algorithm-specific restriction on the grammar of the _encoded_ portion. +See also [Registered Algorithms](#registered-algorithms). + +Some example digest strings include the following: + +digest | algorithm | Registered | +--------------------------------------------------------------------------|---------------------|------------| +`sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b` | [SHA-256](#sha-256) | Yes | +`sha512:401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b372742...` | [SHA-512](#sha-512) | Yes | +`multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8` | Multihash | No | +`sha256+b64u:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564` | SHA-256 with urlsafe base64 | No | + +Please see [Registered Algorithms](#registered-algorithms) for a list of registered algorithms. + +Implementations SHOULD allow digests with unrecognized algorithms to pass validation if they comply with the above grammar. +While `sha256` will only use hex encoded digests, separators in _algorithm_ and alphanumerics in _encoded_ are included to allow for extensions. +As an example, we can parameterize the encoding and algorithm as `multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8`, which would be considered valid but unregistered by this specification. + +### Verification + +Before consuming content targeted by a descriptor from untrusted sources, the byte content SHOULD be verified against the digest string. +Before calculating the digest, the size of the content SHOULD be verified to reduce hash collision space. +Heavy processing before calculating a hash SHOULD be avoided. +Implementations MAY employ [canonicalization](considerations.md#canonicalization) of the underlying content to ensure stable content identifiers. + +### Digest calculations + +A _digest_ is calculated by the following pseudo-code, where `H` is the selected hash algorithm, identified by string `<alg>`: + +```text +let ID(C) = Descriptor.digest +let C = <bytes> +let D = '<alg>:' + Encode(H(C)) +let verified = ID(C) == D +``` + +Above, we define the content identifier as `ID(C)`, extracted from the `Descriptor.digest` field. +Content `C` is a string of bytes. +Function `H` returns the hash of `C` in bytes and is passed to function `Encode` and prefixed with the algorithm to obtain the digest. +The result `verified` is true if `ID(C)` is equal to `D`, confirming that `C` is the content identified by `D`. +After verification, the following is true: + +```text +D == ID(C) == '<alg>:' + Encode(H(C)) +``` + +The _digest_ is confirmed as the content identifier by independently calculating the _digest_. + +### Registered algorithms + +While the _algorithm_ component of the digest string allows the use of a variety of cryptographic algorithms, compliant implementations SHOULD use [SHA-256](#sha-256). + +The following algorithm identifiers are currently defined by this specification: + +| algorithm identifier | algorithm | +|----------------------|---------------------| +| `sha256` | [SHA-256](#sha-256) | +| `sha512` | [SHA-512](#sha-512) | + +If a useful algorithm is not included in the above table, it SHOULD be submitted to this specification for registration. + +#### SHA-256 + +[SHA-256][rfc4634-s4.1] is a collision-resistant hash function, chosen for ubiquity, reasonable size and secure characteristics. +Implementations MUST implement SHA-256 digest verification for use in descriptors. + +When the _algorithm identifier_ is `sha256`, the _encoded_ portion MUST match `/[a-f0-9]{64}/`. +Note that `[A-F]` MUST NOT be used here. + +#### SHA-512 + +[SHA-512][rfc4634-s4.2] is a collision-resistant hash function which [may be more performant][sha256-vs-sha512] than [SHA-256](#sha-256) on some CPUs. +Implementations MAY implement SHA-512 digest verification for use in descriptors. + +When the _algorithm identifier_ is `sha512`, the _encoded_ portion MUST match `/[a-f0-9]{128}/`. +Note that `[A-F]` MUST NOT be used here. + +## Embedded Content + +In many contexts, such as when downloading content over a network, resolving a descriptor to its content has a measurable fixed "roundtrip" latency cost. +For large blobs, the fixed cost is usually inconsequential, as the majority of time will be spent actually fetching the content. +For very small blobs, the fixed cost can be quite significant. + +Implementations MAY choose to embed small pieces of content directly within a descriptor to avoid roundtrips. + +Implementations MUST NOT populate the `data` field in situations where doing so would modify existing content identifiers. +For example, a registry MUST NOT arbitrarily populate `data` fields within uploaded manifests, as that would modify the content identifier of those manifests. +In contrast, a client MAY populate the `data` field before uploading a manifest, because the manifest would not yet have a content identifier in the registry. + +Implementations SHOULD consider portability when deciding whether to embed data, as some providers are known to refuse to accept or parse manifests that exceed a certain size. + +## Examples + +The following example describes a [_Manifest_](manifest.md#image-manifest) with a content identifier of "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" and a size of 7682 bytes: + +```json,title=Content%20Descriptor&mediatype=application/vnd.oci.descriptor.v1%2Bjson +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +``` + +In the following example, the descriptor indicates that the referenced manifest is retrievable from a particular URL: + +```json,title=Content%20Descriptor&mediatype=application/vnd.oci.descriptor.v1%2Bjson +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "urls": [ + "https://example.com/example-manifest" + ] +} +``` + +In the following example, the descriptor indicates the type of artifact it is referencing: + +```json,title=Content%20Descriptor&mediatype=application/vnd.oci.descriptor.v1%2Bjson +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 123, + "digest": "sha256:87923725d74f4bfb94c9e86d64170f7521aad8221a5de834851470ca142da630", + "artifactType": "application/vnd.example.sbom.v1" +} +``` + +[rfc3986]: https://tools.ietf.org/html/rfc3986 +[rfc4634-s4.1]: https://tools.ietf.org/html/rfc4634#section-4.1 +[rfc4634-s4.2]: https://tools.ietf.org/html/rfc4634#section-4.2 +[rfc4648-s4]: https://tools.ietf.org/html/rfc4648#section-4 +[rfc6838]: https://tools.ietf.org/html/rfc6838 +[rfc6838-s4.2]: https://tools.ietf.org/html/rfc6838#section-4.2 +[rfc7230-s2.7]: https://tools.ietf.org/html/rfc7230#section-2.7 +[sha256-vs-sha512]: https://groups.google.com/a/opencontainers.org/forum/#!topic/dev/hsMw7cAwrZE +[iana]: https://www.iana.org/assignments/media-types/media-types.xhtml @@ -0,0 +1,18 @@ +module github.com/opencontainers/image-spec + +go 1.18 + +require ( + github.com/opencontainers/go-digest v1.0.0 + github.com/pkg/errors v0.9.1 + github.com/russross/blackfriday v1.6.0 + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 + github.com/xeipuuv/gojsonschema v1.2.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/identity/chainid.go b/identity/chainid.go new file mode 100644 index 0000000..0bb2853 --- /dev/null +++ b/identity/chainid.go @@ -0,0 +1,67 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package identity provides implementations of subtle calculations pertaining +// to image and layer identity. The primary item present here is the ChainID +// calculation used in identifying the result of subsequent layer applications. +// +// Helpers are also provided here to ease transition to the +// github.com/opencontainers/go-digest package, but that package may be used +// directly. +package identity + +import "github.com/opencontainers/go-digest" + +// ChainID takes a slice of digests and returns the ChainID corresponding to +// the last entry. Typically, these are a list of layer DiffIDs, with the +// result providing the ChainID identifying the result of sequential +// application of the preceding layers. +func ChainID(dgsts []digest.Digest) digest.Digest { + chainIDs := make([]digest.Digest, len(dgsts)) + copy(chainIDs, dgsts) + ChainIDs(chainIDs) + + if len(chainIDs) == 0 { + return "" + } + return chainIDs[len(chainIDs)-1] +} + +// ChainIDs calculates the recursively applied chain id for each identifier in +// the slice. The result is written direcly back into the slice such that the +// ChainID for each item will be in the respective position. +// +// By definition of ChainID, the zeroth element will always be the same before +// and after the call. +// +// As an example, given the chain of ids `[A, B, C]`, the result `[A, +// ChainID(A|B), ChainID(A|B|C)]` will be written back to the slice. +// +// The input is provided as a return value for convenience. +// +// Typically, these are a list of layer DiffIDs, with the +// result providing the ChainID for each the result of each layer application +// sequentially. +func ChainIDs(dgsts []digest.Digest) []digest.Digest { + if len(dgsts) < 2 { + return dgsts + } + + parent := digest.FromBytes([]byte(dgsts[0] + " " + dgsts[1])) + next := dgsts[1:] + next[0] = parent + ChainIDs(next) + + return dgsts +} diff --git a/identity/chainid_test.go b/identity/chainid_test.go new file mode 100644 index 0000000..241dd91 --- /dev/null +++ b/identity/chainid_test.go @@ -0,0 +1,95 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package identity + +import ( + _ "crypto/sha256" // required to install sha256 digest support + "reflect" + "testing" + + "github.com/opencontainers/go-digest" +) + +func TestChainID(t *testing.T) { + // To provide a good testing base, we define the individual links in a + // chain recursively, illustrating the calculations for each chain. + // + // Note that we use invalid digests for the unmodified identifiers here to + // make the computation more readable. + chainDigestAB := digest.FromString("sha256:a" + " " + "sha256:b") // chain for A|B + chainDigestABC := digest.FromString(chainDigestAB.String() + " " + "sha256:c") // chain for A|B|C + + for _, testcase := range []struct { + Name string + Digests []digest.Digest + Expected []digest.Digest + }{ + { + Name: "nil", + }, + { + Name: "empty", + Digests: []digest.Digest{}, + Expected: []digest.Digest{}, + }, + { + Name: "identity", + Digests: []digest.Digest{"sha256:a"}, + Expected: []digest.Digest{"sha256:a"}, + }, + { + Name: "two", + Digests: []digest.Digest{"sha256:a", "sha256:b"}, + Expected: []digest.Digest{"sha256:a", chainDigestAB}, + }, + { + Name: "three", + Digests: []digest.Digest{"sha256:a", "sha256:b", "sha256:c"}, + Expected: []digest.Digest{"sha256:a", chainDigestAB, chainDigestABC}, + }, + } { + t.Run(testcase.Name, func(t *testing.T) { + t.Log("before", testcase.Digests) + + var ids []digest.Digest + + if testcase.Digests != nil { + ids = make([]digest.Digest, len(testcase.Digests)) + copy(ids, testcase.Digests) + } + + ids = ChainIDs(ids) + t.Log("after", ids) + if !reflect.DeepEqual(ids, testcase.Expected) { + t.Errorf("unexpected chain: %v != %v", ids, testcase.Expected) + } + + if len(testcase.Digests) == 0 { + return + } + + // Make sure parent stays stable + if ids[0] != testcase.Digests[0] { + t.Errorf("parent changed: %v != %v", ids[0], testcase.Digests[0]) + } + + // make sure that the ChainID function takes the last element + id := ChainID(testcase.Digests) + if id != ids[len(ids)-1] { + t.Errorf("incorrect chain id returned from ChainID: %v != %v", id, ids[len(ids)-1]) + } + }) + } +} diff --git a/identity/helpers.go b/identity/helpers.go new file mode 100644 index 0000000..9d96eaa --- /dev/null +++ b/identity/helpers.go @@ -0,0 +1,40 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package identity + +import ( + _ "crypto/sha256" // side-effect to install impls, sha256 + _ "crypto/sha512" // side-effect to install impls, sha384/sh512 + + "io" + + digest "github.com/opencontainers/go-digest" +) + +// FromReader consumes the content of rd until io.EOF, returning canonical +// digest. +func FromReader(rd io.Reader) (digest.Digest, error) { + return digest.Canonical.FromReader(rd) +} + +// FromBytes digests the input and returns a Digest. +func FromBytes(p []byte) digest.Digest { + return digest.Canonical.FromBytes(p) +} + +// FromString digests the input and returns a Digest. +func FromString(s string) digest.Digest { + return digest.Canonical.FromString(s) +} diff --git a/image-index.md b/image-index.md new file mode 100644 index 0000000..c661760 --- /dev/null +++ b/image-index.md @@ -0,0 +1,187 @@ +# OCI Image Index Specification + +The image index is a higher-level manifest which points to specific [image manifests](manifest.md), ideal for one or more platforms. +While the use of an image index is OPTIONAL for image providers, image consumers SHOULD be prepared to process them. + +This section defines the `application/vnd.oci.image.index.v1+json` [media type](media-types.md). + +For the media type(s) that this document is compatible with, see the [matrix][matrix]. + +## _Image Index_ Property Descriptions + +- **`schemaVersion`** *int* + + This REQUIRED property specifies the image manifest schema version. + For this version of the specification, this MUST be `2` to ensure backward compatibility with older versions of Docker. + The value of this field will not change. + This field MAY be removed in a future version of the specification. + +- **`mediaType`** *string* + + This property SHOULD be used and [remain compatible][matrix] with earlier versions of this specification and with other similar external formats. + When used, this field MUST contain the media type `application/vnd.oci.image.index.v1+json`. + This field usage differs from the [descriptor](descriptor.md#properties) use of `mediaType`. + +- **`artifactType`** *string* + + This OPTIONAL property contains the type of an artifact when the manifest is used for an artifact. + If defined, the value MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2], and MAY be registered with [IANA][iana]. + +- **`manifests`** *array of objects* + + This REQUIRED property contains a list of [manifests](manifest.md) for specific platforms. + While this property MUST be present, the size of the array MAY be zero. + + Each object in `manifests` includes a set of [descriptor properties](descriptor.md#properties) with the following additional properties and restrictions: + + - **`mediaType`** *string* + + This [descriptor property](descriptor.md#properties) has additional restrictions for `manifests`. + Implementations MUST support at least the following media types: + + - [`application/vnd.oci.image.manifest.v1+json`](manifest.md) + + Also, implementations SHOULD support the following media types: + + - `application/vnd.oci.image.index.v1+json` (nested index) + + Image indexes concerned with portability SHOULD use one of the above media types. + Future versions of the spec MAY use a different mediatype (i.e. a new versioned format). + An encountered `mediaType` that is unknown to the implementation MUST NOT generate an error. + + - **`platform`** *object* + + This OPTIONAL property describes the minimum runtime requirements of the image. + This property SHOULD be present if its target is platform-specific. + + - **`architecture`** *string* + + This REQUIRED property specifies the CPU architecture. + Image indexes SHOULD use, and implementations SHOULD understand, values listed in the Go Language document for [`GOARCH`][go-environment2]. + + - **`os`** *string* + + This REQUIRED property specifies the operating system. + Image indexes SHOULD use, and implementations SHOULD understand, values listed in the Go Language document for [`GOOS`][go-environment2]. + + - **`os.version`** *string* + + This OPTIONAL property specifies the version of the operating system targeted by the referenced blob. + Implementations MAY refuse to use manifests where `os.version` is not known to work with the host OS version. + Valid values are implementation-defined. e.g. `10.0.14393.1066` on `windows`. + + - **`os.features`** *array of strings* + + This OPTIONAL property specifies an array of strings, each specifying a mandatory OS feature. + When `os` is `windows`, image indexes SHOULD use, and implementations SHOULD understand the following values: + + - `win32k`: image requires `win32k.sys` on the host (Note: `win32k.sys` is missing on Nano Server) + + When `os` is not `windows`, values are implementation-defined and SHOULD be submitted to this specification for standardization. + + - **`variant`** *string* + + This OPTIONAL property specifies the variant of the CPU. + Image indexes SHOULD use, and implementations SHOULD understand, `variant` values listed in the [Platform Variants](#platform-variants) table. + + - **`features`** *array of strings* + + This property is RESERVED for future versions of the specification. + + If multiple manifests match a client or runtime's requirements, the first matching entry SHOULD be used. + +- **`subject`** *[descriptor](descriptor.md)* + + This OPTIONAL property specifies a [descriptor](descriptor.md) of another manifest. + This value, used by the [`referrers` API][referrers-api], indicates a relationship to the specified manifest. + +- **`annotations`** *string-string map* + + This OPTIONAL property contains arbitrary metadata for the image index. + This OPTIONAL property MUST use the [annotation rules](annotations.md#rules). + + See [Pre-Defined Annotation Keys](annotations.md#pre-defined-annotation-keys). + +## Platform Variants + +When the variant of the CPU is not listed in the table, values are implementation-defined and SHOULD be submitted to this specification for standardization. + +| ISA/ABI | `architecture` | `variant` | +|-----------------|----------------|-------------| +| ARM 32-bit, v6 | `arm` | `v6` | +| ARM 32-bit, v7 | `arm` | `v7` | +| ARM 32-bit, v8 | `arm` | `v8` | +| ARM 64-bit, v8 | `arm64` | `v8` | + +## Example Image Index + +*Example showing a simple image index pointing to image manifests for two platforms:* + +```json,title=Image%20Index&mediatype=application/vnd.oci.image.index.v1%2Bjson +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} +``` + +## Example Image Index with multiple media types + +*Example showing an image index pointing to manifests with multiple media types:* + +```json,title=Image%20Index&mediatype=application/vnd.oci.image.index.v1%2Bjson +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 7682, + "digest": "sha256:601570aaff1b68a61eb9c85b8beca1644e698003e0cdb5bce960f193d265a8b7" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} +``` + +[go-environment2]: https://golang.org/doc/install/source#environment +[iana]: https://www.iana.org/assignments/media-types/media-types.xhtml +[matrix]: media-types.md#compatibility-matrix +[referrers-api]: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers +[rfc6838]: https://tools.ietf.org/html/rfc6838 +[rfc6838-s4.2]: https://tools.ietf.org/html/rfc6838#section-4.2 diff --git a/image-layout.md b/image-layout.md new file mode 100644 index 0000000..112561e --- /dev/null +++ b/image-layout.md @@ -0,0 +1,208 @@ +# OCI Image Layout Specification + +- The OCI Image Layout is the directory structure for OCI content-addressable blobs and [location-addressable](https://en.wikipedia.org/wiki/Content-addressable_storage#Content-addressed_vs._location-addressed) references (refs). +- This layout MAY be used in a variety of different transport mechanisms: archive formats (e.g. tar, zip), shared filesystem environments (e.g. nfs), or networked file fetching (e.g. http, ftp, rsync). + +Given an image layout and a ref, a tool can create an [OCI Runtime Specification bundle](https://github.com/opencontainers/runtime-spec/blob/v1.0.0/bundle.md) by: + +- Following the ref to find a [manifest](manifest.md#image-manifest), possibly via an [image index](image-index.md) +- [Applying the filesystem layers](layer.md#applying) in the specified order +- Converting the [image configuration](config.md) into an [OCI Runtime Specification `config.json`](https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md) + +## Content + +The image layout is as follows: + +- `blobs` directory + - Contains content-addressable blobs + - A blob has no schema and SHOULD be considered opaque + - Directory MUST exist and MAY be empty + - See [blobs](#blobs) section +- `oci-layout` file + - It MUST exist + - It MUST be a JSON object + - It MUST contain an `imageLayoutVersion` field + - See [oci-layout file](#oci-layout-file) section + - It MAY include additional fields +- `index.json` file + - It MUST exist + - It MUST be an [image index](image-index.md) JSON object. + - See [index.json](#indexjson-file) section + +## Example Layout + +This is an example image layout: + +```shell +$ cd example.com/app/ +$ find . -type f +./index.json +./oci-layout +./blobs/sha256/3588d02542238316759cbf24502f4344ffcc8a60c803870022f335d1390c13b4 +./blobs/sha256/4b0bc1c4050b03c95ef2a8e36e25feac42fd31283e8c30b3ee5df6b043155d3c +./blobs/sha256/7968321274dc6b6171697c33df7815310468e694ac5be0ec03ff053bb135e768 +``` + +Blobs are named by their contents: + +```shell +$ shasum -a 256 ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 +afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 +``` + +## Blobs + +- Object names in the `blobs` subdirectories are composed of a directory for each hash algorithm, the children of which will contain the actual content. +- The content of `blobs/<alg>/<encoded>` MUST match the digest `<alg>:<encoded>` (referenced per [descriptor](descriptor.md#digests)). For example, the content of `blobs/sha256/da39a3ee5e6b4b0d3255bfef95601890afd80709` MUST match the digest `sha256:da39a3ee5e6b4b0d3255bfef95601890afd80709`. +- The character set of the entry name for `<alg>` and `<encoded>` MUST match the respective grammar elements described in [descriptor](descriptor.md#digests). +- The blobs directory MAY contain blobs which are not referenced by any of the [refs](#indexjson-file). +- The blobs directory MAY be missing referenced blobs, in which case the missing blobs SHOULD be fulfilled by an external blob store. + +### Example Blobs + +```shell +$ cat ./blobs/sha256/9b97579de92b1c195b85bb42a11011378ee549b02d7fe9c17bf2a6b35d5cb079 | jq +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, +... +``` + +```shell +$ cat ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 | jq +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" + }, +... +``` + +```shell +$ cat ./blobs/sha256/5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270 | jq +{ + "architecture": "amd64", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "config": { + "Hostname": "8dfe43d80430", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": null, + "Image": "sha256:6986ae504bbf843512d680cc959484452034965db15f75ee8bdd1b107f61500b", +... +``` + +```shell +$ cat ./blobs/sha256/9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0 +[gzipped tar stream] +``` + +## oci-layout file + +This JSON object serves as a marker for the base of an Open Container Image Layout and to provide the version of the image-layout in use. +The `imageLayoutVersion` value will align with the OCI Image Specification version at the time changes to the layout are made, and will pin a given version until changes to the image layout are required. +This section defines the `application/vnd.oci.layout.header.v1+json` [media type](media-types.md). + +### oci-layout Example + +```json,title=OCI%20Layout&mediatype=application/vnd.oci.layout.header.v1%2Bjson +{ + "imageLayoutVersion": "1.0.0" +} +``` + +## index.json file + +This REQUIRED file is the entry point for references and descriptors of the image-layout. +The [image index](image-index.md) is a multi-descriptor entry point. + +This index provides an established path (`/index.json`) to have an entry point for an image-layout and to discover auxiliary descriptors. + +- No semantic restriction is given for the "org.opencontainers.image.ref.name" annotation of descriptors. +- In general the `mediaType` of each [descriptor][descriptors] object in the `manifests` field will be either `application/vnd.oci.image.index.v1+json` or `application/vnd.oci.image.manifest.v1+json`. +- Future versions of the spec MAY use a different mediatype (i.e. a new versioned format). +- An encountered `mediaType` that is unknown MUST NOT generate an error. + +**Implementor's Note:** +A common use case of descriptors with a "org.opencontainers.image.ref.name" annotation is representing a "tag" for a container image. +For example, an image may have a tag for different versions or builds of the software. +In the wild you often see "tags" like "v1.0.0-vendor.0", "2.0.0-debug", etc. +Those tags will often be represented in an image-layout repository with matching "org.opencontainers.image.ref.name" annotations like "v1.0.0-vendor.0", "2.0.0-debug", etc. + +### Index Example + +```json,title=Image%20Index&mediatype=application/vnd.oci.image.index.v1%2Bjson +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 7143, + "digest": "sha256:0228f90e926ba6b96e4f39cf294b2586d38fbb5a1e385c05cd1ee40ea54fe7fd", + "annotations": { + "org.opencontainers.image.ref.name": "stable-release" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.image.ref.name": "v1.0" + } + }, + { + "mediaType": "application/xml", + "size": 7143, + "digest": "sha256:b3d63d132d21c3ff4c35a061adf23cf43da8ae054247e32faa95494d904a007e", + "annotations": { + "org.freedesktop.specifications.metainfo.version": "1.0", + "org.freedesktop.specifications.metainfo.type": "AppStream" + } + } + ], + "annotations": { + "com.example.index.revision": "r124356" + } +} +``` + +This illustrates an index that provides two named references and an auxiliary mediatype for this image layout. + +The first named reference (`stable-release`) points to another index that might contain multiple references with distinct platforms and annotations. +Note that the [`org.opencontainers.image.ref.name` annotation](annotations.md) SHOULD only be considered valid when on descriptors on `index.json`. + +The second named reference (`v1.0`) points to a manifest that is specific to the linux/ppc64le platform. + +[descriptors]: ./descriptor.md diff --git a/img/build-diagram.png b/img/build-diagram.png Binary files differnew file mode 100644 index 0000000..9ad9b84 --- /dev/null +++ b/img/build-diagram.png diff --git a/img/media-types.dot b/img/media-types.dot new file mode 100644 index 0000000..8583d5f --- /dev/null +++ b/img/media-types.dot @@ -0,0 +1,17 @@ +digraph G { + { + imageIndex [shape=note, label="Image Index\n<<optional>>\napplication/vnd.oci.image.index.v1+json"] + { + rank=same + manifest [shape=note, label="Image manifest\napplication/vnd.oci.image.manifest.v1+json"] + } + config [shape=note, label="Image config JSON\napplication/vnd.oci.image.config.v1+json"] + layer [shape=note, label="Layer tar archive\napplication/vnd.oci.image.layer.v1.tar\napplication/vnd.oci.image.layer.v1.tar+gzip\napplication/vnd.oci.image.layer.nondistributable.v1.tar\napplication/vnd.oci.image.layer.nondistributable.v1.tar+gzip"] + } + + imageIndex -> imageIndex [label="1..*"] + imageIndex -> manifest [label="1..*"] + manifest -> config [label="1..1"] + manifest -> layer [label="1..*"] + manifest -> manifest [label="0..1"]; +} diff --git a/img/media-types.png b/img/media-types.png Binary files differnew file mode 100644 index 0000000..70d2fe6 --- /dev/null +++ b/img/media-types.png diff --git a/img/run-diagram.png b/img/run-diagram.png Binary files differnew file mode 100644 index 0000000..c1009f4 --- /dev/null +++ b/img/run-diagram.png diff --git a/implementations.md b/implementations.md new file mode 100644 index 0000000..cf4b201 --- /dev/null +++ b/implementations.md @@ -0,0 +1,26 @@ +# OCI Image Implementations + +Projects or Companies currently adopting the OCI Image Specification + +- [projectatomic/skopeo](https://github.com/projectatomic/skopeo) +- [Amazon Elastic Container Registry (ECR)](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-manifest-formats.html) ([announcement](https://aws.amazon.com/about-aws/whats-new/2017/01/amazon-ecr-supports-docker-image-manifest-v2-schema-2/)) +- [Azure Container Registry (ACR)](https://docs.microsoft.com/azure/container-registry/container-registry-image-formats#oci-images) +- [openSUSE/umoci](https://github.com/openSUSE/umoci) +- [cloudfoundry/grootfs](https://github.com/cloudfoundry/grootfs) ([source](https://github.com/cloudfoundry/grootfs/blob/c3da26e1e463b51be1add289032f3dca6698b335/fetcher/remote/docker_src.go)) +- [Mesos plans](https://issues.apache.org/jira/browse/MESOS-5011) ([design doc](https://docs.google.com/document/d/1Pus7D-inIBoLSIPyu3rl_apxvUhtp3rp0_b0Ttr2Xww/edit#heading=h.hrvk2wboog4p)) +- [Docker](https://github.com/docker) + - [docker/docker (`docker save/load` WIP)](https://github.com/docker/docker/pull/26369) + - [distribution/distribution (registry PR)](https://github.com/distribution/distribution/pull/2076) +- [containerd/containerd](https://github.com/containerd/containerd) +- [Containers](https://github.com/containers/) + - [containers/build](https://github.com/containers/build) + - [containers/image](https://github.com/containers/image) + - [containers/oci-spec-rs](https://github.com/containers/oci-spec-rs) + - [containers/libocispec](https://github.com/containers/libocispec) +- [krustlet/oci-distribution](https://github.com/krustlet/oci-distribution) +- [coreos/rkt](https://github.com/coreos/rkt) +- [box-builder/box](https://github.com/box-builder/box) +- [coolljt0725/docker2oci](https://github.com/coolljt0725/docker2oci) +- [regclient/regclient](https://github.com/regclient/regclient) + +_(to add your project please open a [pull-request](https://github.com/opencontainers/image-spec/pulls))_ diff --git a/layer.md b/layer.md new file mode 100644 index 0000000..fd12a5b --- /dev/null +++ b/layer.md @@ -0,0 +1,344 @@ +# Image Layer Filesystem Changeset + +This document describes how to serialize a filesystem and filesystem changes like removed files into a blob called a layer. +One or more layers are applied on top of each other to create a complete filesystem. +This document will use a concrete example to illustrate how to create and consume these filesystem layers. + +This section defines the `application/vnd.oci.image.layer.v1.tar`, `application/vnd.oci.image.layer.v1.tar+gzip`, `application/vnd.oci.image.layer.v1.tar+zstd`, `application/vnd.oci.image.layer.nondistributable.v1.tar`, `application/vnd.oci.image.layer.nondistributable.v1.tar+gzip`, and `application/vnd.oci.image.layer.nondistributable.v1.tar+zstd` [media types](media-types.md). + +## `+gzip` Media Types + +- The media type `application/vnd.oci.image.layer.v1.tar+gzip` represents an `application/vnd.oci.image.layer.v1.tar` payload which has been compressed with [gzip][rfc1952_2]. +- The media type `application/vnd.oci.image.layer.nondistributable.v1.tar+gzip` represents an `application/vnd.oci.image.layer.nondistributable.v1.tar` payload which has been compressed with [gzip][rfc1952_2]. + +## `+zstd` Media Types + +- The media type `application/vnd.oci.image.layer.v1.tar+zstd` represents an `application/vnd.oci.image.layer.v1.tar` payload which has been compressed with [zstd][rfc8478]. +- The media type `application/vnd.oci.image.layer.nondistributable.v1.tar+zstd` represents an `application/vnd.oci.image.layer.nondistributable.v1.tar` payload which has been compressed with [zstd][rfc8478]. + +## Distributable Format + +- Layer Changesets for the [media type](media-types.md) `application/vnd.oci.image.layer.v1.tar` MUST be packaged in [tar archive][tar-archive]. +- Layer Changesets for the [media type](media-types.md) `application/vnd.oci.image.layer.v1.tar` MUST NOT include duplicate entries for file paths in the resulting [tar archive][tar-archive]. + +## Change Types + +Types of changes that can occur in a changeset are: + +- Additions +- Modifications +- Removals + +Additions and Modifications are represented the same in the changeset tar archive. + +Removals are represented using "[whiteout](#whiteouts)" file entries (See [Representing Changes](#representing-changes)). + +### File Types + +Throughout this document section, the use of word "files" or "entries" includes the following, where supported: + +- regular files +- directories +- sockets +- symbolic links +- block devices +- character devices +- FIFOs + +### File Attributes + +Where supported, MUST include file attributes for Additions and Modifications include: + +- Modification Time (`mtime`) +- User ID (`uid`) + - User Name (`uname`) *secondary to `uid`* +- Group ID (`gid`) + - Group Name (`gname`) *secondary to `gid`* +- Mode (`mode`) +- Extended Attributes (`xattrs`) +- Symlink reference (`linkname` + symbolic link type) +- [Hardlink](#hardlinks) reference (`linkname`) + +[Sparse files](https://en.wikipedia.org/wiki/Sparse_file) SHOULD NOT be used because they lack consistent support across tar implementations. + +#### Hardlinks + +- Hardlinks are a [POSIX concept](https://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html) for having one or more directory entries for the same file on the same device. +- Not all filesystems support hardlinks (e.g. [FAT](https://en.wikipedia.org/wiki/File_Allocation_Table)). +- Hardlinks are possible with all [file types](#file-types) except `directories`. +- Non-directory files are considered "hardlinked" when their link count is greater than 1. +- Hardlinked files are on a same device (i.e. comparing Major:Minor pair) and have the same inode. +- The corresponding files that share the link with the > 1 linkcount may be outside the directory that the changeset is being produced from, in which case the `linkname` is not recorded in the changeset. +- Hardlinks are stored in a tar archive with type of a `1` char, per the [GNU Basic Tar Format][gnu-tar-standard] and [libarchive tar(5)][libarchive-tar]. +- While approaches to deriving new or changed hardlinks may vary, a possible approach is: + +```text +SET LinkMap to map[< Major:Minor String >]map[< inode integer >]< path string > +SET LinkNames to map[< src path string >]< dest path string > +FOR each path in root path + IF path type is directory + CONTINUE + ENDIF + SET filestat to stat(path) + IF filestat num of links == 1 + CONTINUE + ENDIF + IF LinkMap[filestat device][filestat inode] is not empty + SET LinkNames[path] to LinkMap[filestat device][filestat inode] + ELSE + SET LinkMap[filestat device][filestat inode] to path + ENDIF +END FOR +``` + +With this approach, the link map and links names of a directory could be compared against that of another directory to derive additions and changes to hardlinks. + +#### Platform-specific attributes + +Implementations on Windows MUST support these additional attributes, encoded in [PAX vendor +extensions](https://github.com/libarchive/libarchive/wiki/ManPageTar5#pax-interchange-format) as follows: + +- [Windows file attributes](https://msdn.microsoft.com/en-us/library/windows/desktop/gg258117(v=vs.85).aspx) (`MSWINDOWS.fileattr`) +- [Security descriptor](https://msdn.microsoft.com/en-us/library/cc230366.aspx) (`MSWINDOWS.rawsd`): base64-encoded self-relative binary security descriptor +- Mount points (`MSWINDOWS.mountpoint`): if present on a directory symbolic link, then the link should be created as a [directory junction](https://en.wikipedia.org/wiki/NTFS_junction_point) +- Creation time (`LIBARCHIVE.creationtime`) + +## Creating + +### Initial Root Filesystem + +The initial root filesystem is the base or parent layer. + +For this example, an image root filesystem has an initial state as an empty directory. +The name of the directory is not relevant to the layer itself, only for the purpose of producing comparisons. + +Here is an initial empty directory structure for a changeset, with a unique directory name `rootfs-c9d-v1`. + +```text +rootfs-c9d-v1/ +``` + +### Populate Initial Filesystem + +Files and directories are then created: + +```text +rootfs-c9d-v1/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +The `rootfs-c9d-v1` directory is then created as a plain [tar archive][tar-archive] with relative path to `rootfs-c9d-v1`. +Entries for the following files: + +```text +./ +./etc/ +./etc/my-app-config +./bin/ +./bin/my-app-binary +./bin/my-app-tools +``` + +### Populate a Comparison Filesystem + +Create a new directory and initialize it with a copy or snapshot of the prior root filesystem. +Example commands that can preserve [file attributes](#file-attributes) to make this copy are: + +- [cp(1)](https://linux.die.net/man/1/cp): `cp -a rootfs-c9d-v1/ rootfs-c9d-v1.s1/` +- [rsync(1)](https://linux.die.net/man/1/rsync): `rsync -aHAX rootfs-c9d-v1/ rootfs-c9d-v1.s1/` +- [tar(1)](https://linux.die.net/man/1/tar): `mkdir rootfs-c9d-v1.s1 && tar --acls --xattrs -C rootfs-c9d-v1/ -c . | tar -C rootfs-c9d-v1.s1/ --acls --xattrs -x` (including `--selinux` where supported) + +Any [changes](#change-types) to the snapshot MUST NOT change or affect the directory it was copied from. + +For example `rootfs-c9d-v1.s1` is an identical snapshot of `rootfs-c9d-v1`. +In this way `rootfs-c9d-v1.s1` is prepared for updates and alterations. + +**Implementor's Note**: *a copy-on-write or union filesystem can efficiently make directory snapshots* + +Initial layout of the snapshot: + +```text +rootfs-c9d-v1.s1/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +See [Change Types](#change-types) for more details on changes. + +For example, add a directory at `/etc/my-app.d` containing a default config file, removing the existing config file. +Also a change (in attribute or file content) to `./bin/my-app-tools` binary to handle the config layout change. + +Following these changes, the representation of the `rootfs-c9d-v1.s1` directory: + +```text +rootfs-c9d-v1.s1/ + etc/ + my-app.d/ + default.cfg + bin/ + my-app-binary + my-app-tools +``` + +### Determining Changes + +When two directories are compared, the relative root is the top-level directory. +The directories are compared, looking for files that have been [added, modified, or removed](#change-types). + +For this example, `rootfs-c9d-v1/` and `rootfs-c9d-v1.s1/` are recursively compared, each as relative root path. + +The following changeset is found: + +```text +Added: /etc/my-app.d/ +Added: /etc/my-app.d/default.cfg +Modified: /bin/my-app-tools +Deleted: /etc/my-app-config +``` + +This reflects the removal of `/etc/my-app-config` and creation of a file and directory at `/etc/my-app.d/default.cfg`. +`/bin/my-app-tools` has also been replaced with an updated version. + +### Representing Changes + +A [tar archive][tar-archive] is then created which contains _only_ this changeset: + +- Added and modified files and directories in their entirety +- Deleted files or directories marked with a [whiteout file](#whiteouts) + +The resulting tar archive for `rootfs-c9d-v1.s1` has the following entries: + +```text +./etc/my-app.d/ +./etc/my-app.d/default.cfg +./bin/my-app-tools +./etc/.wh.my-app-config +``` + +To signify that the resource `./etc/my-app-config` MUST be removed when the changeset is applied, the basename of the entry is prefixed with `.wh.`. + +## Applying Changesets + +- Layer Changesets of [media type](media-types.md) `application/vnd.oci.image.layer.v1.tar` are _applied_, rather than simply extracted as tar archives. +- Applying a layer changeset requires special consideration for the [whiteout](#whiteouts) files. +- In the absence of any [whiteout](#whiteouts) files in a layer changeset, the archive is extracted like a regular tar archive. + +### Changeset over existing files + +This section specifies applying an entry from a layer changeset if the target path already exists. + +If the entry and the existing path are both directories, then the existing path's attributes MUST be replaced by those of the entry in the changeset. +In all other cases, the implementation MUST do the semantic equivalent of the following: + +- removing the file path (e.g. [`unlink(2)`](https://linux.die.net/man/2/unlink) on Linux systems) +- recreating the file path, based on the contents and attributes of the changeset entry + +## Whiteouts + +- A whiteout file is an empty file with a special filename that signifies a path should be deleted. +- A whiteout filename consists of the prefix `.wh.` plus the basename of the path to be deleted. +- As files prefixed with `.wh.` are special whiteout markers, it is not possible to create a filesystem which has a file or directory with a name beginning with `.wh.`. +- Once a whiteout is applied, the whiteout itself MUST also be hidden. +- Whiteout files MUST only apply to resources in lower/parent layers. +- Files that are present in the same layer as a whiteout file can only be hidden by whiteout files in subsequent layers. + +The following is a base layer with several resources: + +```text +a/ +a/b/ +a/b/c/ +a/b/c/bar +``` + +When the next layer is created, the original `a/b` directory is deleted and recreated with `a/b/c/foo`: + +```text +a/ +a/.wh..wh..opq +a/b/ +a/b/c/ +a/b/c/foo +``` + +When processing the second layer, `a/.wh..wh..opq` is applied first, before creating the new version of `a/b`, regardless of the ordering in which the whiteout file was encountered. +For example, the following layer is equivalent to the layer above: + +```text +a/ +a/b/ +a/b/c/ +a/b/c/foo +a/.wh..wh..opq +``` + +Implementations SHOULD generate layers such that the whiteout files appear before sibling directory entries. + +### Opaque Whiteout + +- In addition to expressing that a single entry should be removed from a lower layer, layers may remove all of the children using an opaque whiteout entry. +- An opaque whiteout entry is a file with the name `.wh..wh..opq` indicating that all siblings are hidden in the lower layer. + +Let's take the following base layer as an example: + +```text +etc/ + my-app-config +bin/ + my-app-binary + my-app-tools + tools/ + my-app-tool-one +``` + +If all children of `bin/` are removed, the next layer would have the following: + +```text +bin/ + .wh..wh..opq +``` + +This is called _opaque whiteout_ format. +An _opaque whiteout_ file hides _all_ children of the `bin/` including sub-directories and all descendants. +Using _explicit whiteout_ files, this would be equivalent to the following: + +```text +bin/ + .wh.my-app-binary + .wh.my-app-tools + .wh.tools +``` + +In this case, a unique whiteout file is generated for each entry. +If there were more children of `bin/` in the base layer, there would be an entry for each. +Note that this opaque file will apply to _all_ children, including sub-directories, other resources and all descendants. + +Implementations SHOULD generate layers using _explicit whiteout_ files, but MUST accept both. + +Any given image is likely to be composed of several of these Image Filesystem Changeset tar archives. + +## Non-Distributable Layers + +> **NOTE**: Non-distributable layers are deprecated, and not recommended for future use. +> Implementations SHOULD NOT produce new non-distributable layers. + +Due to legal requirements, certain layers may not be regularly distributable. +Such "non-distributable" layers are typically downloaded directly from a distributor but never uploaded. + +Non-distributable layers SHOULD be tagged with an alternative mediatype of `application/vnd.oci.image.layer.nondistributable.v1.tar`. +Implementations SHOULD NOT upload layers tagged with this media type; however, such a media type SHOULD NOT affect whether an implementation downloads the layer. + +[Descriptors](descriptor.md) referencing non-distributable layers MAY include `urls` for downloading these layers directly; however, the presence of the `urls` field SHOULD NOT be used to determine whether or not a layer is non-distributable. + +[libarchive-tar]: https://github.com/libarchive/libarchive/wiki/ManPageTar5#POSIX_ustar_Archives +[gnu-tar-standard]: https://www.gnu.org/software/tar/manual/html_node/Standard.html +[rfc1952_2]: https://tools.ietf.org/html/rfc1952 +[tar-archive]: https://en.wikipedia.org/wiki/Tar_(computing) +[rfc8478]: https://tools.ietf.org/html/rfc8478 diff --git a/manifest.md b/manifest.md new file mode 100644 index 0000000..9839416 --- /dev/null +++ b/manifest.md @@ -0,0 +1,262 @@ +# OCI Image Manifest Specification + +There are three main goals of the Image Manifest Specification. +The first goal is content-addressable images, by supporting an image model where the image's configuration can be hashed to generate a unique ID for the image and its components. +The second goal is to allow multi-architecture images, through a "fat manifest" which references image manifests for platform-specific versions of an image. +In OCI, this is codified in an [image index](image-index.md). +The third goal is to be [translatable](conversion.md) to the [OCI Runtime Specification](https://github.com/opencontainers/runtime-spec). + +This section defines the `application/vnd.oci.image.manifest.v1+json` [media type](media-types.md). +For the media type(s) that this is compatible with see the [matrix](media-types.md#compatibility-matrix). + +## Image Manifest + +Unlike the [image index](image-index.md), which contains information about a set of images that can span a variety of architectures and operating systems, an image manifest provides a configuration and set of layers for a single container image for a specific architecture and operating system. + +## _Image Manifest_ Property Descriptions + +- **`schemaVersion`** *int* + + This REQUIRED property specifies the image manifest schema version. + For this version of the specification, this MUST be `2` to ensure backward compatibility with older versions of Docker. The value of this field will not change. This field MAY be removed in a future version of the specification. + +- **`mediaType`** *string* + + This property SHOULD be used and [remain compatible](media-types.md#compatibility-matrix) with earlier versions of this specification and with other similar external formats. + When used, this field MUST contain the media type `application/vnd.oci.image.manifest.v1+json`. + This field usage differs from the [descriptor](descriptor.md#properties) use of `mediaType`. + +- **`artifactType`** *string* + + This OPTIONAL property contains the type of an artifact when the manifest is used for an artifact. + This MUST be set when `config.mediaType` is set to the [empty value](#guidance-for-an-empty-descriptor). + If defined, the value MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2], and MAY be registered with [IANA][iana]. + Implementations storing or copying image manifests MUST NOT error on encountering an `artifactType` that is unknown to the implementation. + +- **`config`** *[descriptor](descriptor.md)* + + This REQUIRED property references a configuration object for a container, by digest. + Beyond the [descriptor requirements](descriptor.md#properties), the value has the following additional restrictions: + + - **`mediaType`** *string* + + This [descriptor property](descriptor.md#properties) has additional restrictions for `config`. + + Implementations MUST NOT attempt to parse the referenced content if this media type is unknown and instead consider the referenced content as arbitrary binary data (e.g.: as `application/octet-stream`). + + Implementations storing or copying image manifests MUST NOT error on encountering a value that is unknown to the implementation. + + Implementations MUST support at least the following media types: + + - [`application/vnd.oci.image.config.v1+json`](config.md) + + Manifests for container images concerned with portability SHOULD use one of the above media types. + Manifests for artifacts concerned with portability SHOULD use `config.mediaType` as described in [Guidelines for Artifact Usage](#guidelines-for-artifact-usage). + + If the manifest uses a different media type than the above, it MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2], and MAY be registered with [IANA][iana]. + + To set an effectively null or empty config and maintain portability see the [guidance for an empty descriptor](#guidance-for-an-empty-descriptor) below, and `DescriptorEmptyJSON` of the reference code. + +- **`layers`** *array of objects* + + Each item in the array MUST be a [descriptor](descriptor.md). + For portability, `layers` SHOULD have at least one entry. + See the [guidance for an empty descriptor](#guidance-for-an-empty-descriptor) below, and `DescriptorEmptyJSON` of the reference code. + + When the `config.mediaType` is set to `application/vnd.oci.image.config.v1+json`, the following additional restrictions apply: + + - The array MUST have the base layer at index 0. + - Subsequent layers MUST then follow in stack order (i.e. from `layers[0]` to `layers[len(layers)-1]`). + - The final filesystem layout MUST match the result of [applying](layer.md#applying-changesets) the layers to an empty directory. + - The [ownership, mode, and other attributes](layer.md#file-attributes) of the initial empty directory are unspecified. + + Beyond the [descriptor requirements](descriptor.md#properties), the value has the following additional restrictions: + + - **`mediaType`** *string* + + This [descriptor property](descriptor.md#properties) has additional restrictions for `layers[]`. + Implementations MUST support at least the following media types: + + - [`application/vnd.oci.image.layer.v1.tar`](layer.md) + - [`application/vnd.oci.image.layer.v1.tar+gzip`](layer.md#gzip-media-types) + - [`application/vnd.oci.image.layer.nondistributable.v1.tar`](layer.md#non-distributable-layers) + - [`application/vnd.oci.image.layer.nondistributable.v1.tar+gzip`](layer.md#gzip-media-types) + + Manifests concerned with portability SHOULD use one of the above media types. + Implementations storing or copying image manifests MUST NOT error on encountering a `mediaType` that is unknown to the implementation. + + Entries in this field will frequently use the `+gzip` types. + + If the manifest uses a different media type than the above, it MUST comply with [RFC 6838][rfc6838], including the [naming requirements in its section 4.2][rfc6838-s4.2], and MAY be registered with [IANA][iana]. + + See [Guidelines for Artifact Usage](#guidelines-for-artifact-usage) for other uses of the `layers`. + +- **`subject`** *[descriptor](descriptor.md)* + + This OPTIONAL property specifies a [descriptor](descriptor.md) of another manifest. + This value, used by the [`referrers` API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers), indicates a relationship to the specified manifest. + +- **`annotations`** *string-string map* + + This OPTIONAL property contains arbitrary metadata for the image manifest. + This OPTIONAL property MUST use the [annotation rules](annotations.md#rules). + + See [Pre-Defined Annotation Keys](annotations.md#pre-defined-annotation-keys). + +## Example Image Manifest + +*Example showing an image manifest:* + +```json,title=Manifest&mediatype=application/vnd.oci.image.manifest.v1%2Bjson +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", + "size": 7023 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0", + "size": 32654 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + "size": 16724 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + "size": 73109 + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "size": 7682 + }, + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} +``` + +## Guidance for an Empty Descriptor + +*Implementers note*: The following is considered GUIDANCE for portability. + +Parts of the spec necessitate including a descriptor to a blob where some implementations of artifacts do not have associated content. +While an empty blob (`size` of 0) may be preferable, practice has shown that not to be ubiquitously supported. +The media type `application/vnd.oci.empty.v1+json` (`MediaTypeEmptyJSON`) has been specified for a descriptor that has no content for the implementation. +The blob payload is the most minimal content that is still a valid JSON object: `{}` (`size` of 2). +The blob digest of `{}` is `sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a`. +The data field is optional, and if included is the base64 encoding of `{}`: `e30=`. + +The resulting descriptor shown here is also defined in reference code as `DescriptorEmptyJSON`: + +```json,title=empty%20config&mediatype=application/vnd.oci.descriptor.v1%2Bjson +{ + "mediaType": "application/vnd.oci.empty.v1+json", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2, + "data": "e30=" +} +``` + +## Guidelines for Artifact Usage + +Content other than OCI container images MAY be packaged using the image manifest. +When this is done, the `config.mediaType` value MUST be set to a value specific to the artifact type or the [empty value](#guidance-for-an-empty-descriptor). +If the `config.mediaType` is set to the empty value, the `artifactType` MUST be defined. +If the artifact does not need layers, a single layer SHOULD be included with a non-zero size. +The suggested content for an unused `layers` array is the [empty descriptor](#guidance-for-an-empty-descriptor). + +The design of the artifact depends on what content is being packaged with the artifact. +The decision tree below and the associated examples MAY be used to design new artifacts: + +1. Does the artifact consist of at least one file or blob? + If yes, continue to 2. + If no, specify the `artifactType`, and set the `config` and a single `layers` element to the empty descriptor value. + Here is an example of this with annotations included: + + ```json,title=Minimal%20artifact&mediatype=application/vnd.oci.image.manifest.v1%2Bjson + { + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example+type", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.empty.v1+json", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2 + } + ], + "annotations": { + "oci.opencontainers.image.created": "2023-01-02T03:04:05Z", + "com.example.data": "payload" + } + } + ``` + +2. Does the artifact have additional JSON formatted metadata as configuration? + If yes, continue to 3. + If no, specify the `artifactType`, include the artifact in the `layers`, and set `config` to the empty descriptor value. + Here is an example of this with a single layer: + + ```json,title=Artifact%20without%20config&mediatype=application/vnd.oci.image.manifest.v1%2Bjson + { + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example+type", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2 + }, + "layers": [ + { + "mediaType": "application/vnd.example+type", + "digest": "sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317", + "size": 1234 + } + ] + } + ``` + +3. For artifacts with a config blob, specify the `artifactType` to a common value for your artifact tooling, specify the `config` with the metadata for this artifact, and include the artifact in the `layers`. + Here is an example of this: + + ```json,title=Artifact%20with%20config&mediatype=application/vnd.oci.image.manifest.v1%2Bjson + { + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example+type", + "config": { + "mediaType": "application/vnd.example.config.v1+json", + "digest": "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + "size": 123 + }, + "layers": [ + { + "mediaType": "application/vnd.example.data.v1.tar+gzip", + "digest": "sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317", + "size": 1234 + } + ] + } + ``` + +_Implementers note:_ artifacts have historically been created without an `artifactType` field, and tooling to work with artifacts should fallback to the `config.mediaType` value. + +[iana]: https://www.iana.org/assignments/media-types/media-types.xhtml +[rfc6838]: https://tools.ietf.org/html/rfc6838 +[rfc6838-s4.2]: https://tools.ietf.org/html/rfc6838#section-4.2 diff --git a/media-types.md b/media-types.md new file mode 100644 index 0000000..8cc372e --- /dev/null +++ b/media-types.md @@ -0,0 +1,90 @@ +# OCI Image Media Types + +The following media types identify the formats described here and their referenced resources: + +- `application/vnd.oci.descriptor.v1+json`: [Content Descriptor](descriptor.md) +- `application/vnd.oci.layout.header.v1+json`: [OCI Layout](image-layout.md#oci-layout-file) +- `application/vnd.oci.image.index.v1+json`: [Image Index](image-index.md) +- `application/vnd.oci.image.manifest.v1+json`: [Image manifest](manifest.md#image-manifest) +- `application/vnd.oci.image.config.v1+json`: [Image config](config.md) +- `application/vnd.oci.image.layer.v1.tar`: ["Layer", as a tar archive](layer.md) +- `application/vnd.oci.image.layer.v1.tar+gzip`: ["Layer", as a tar archive](layer.md#gzip-media-types) compressed with [gzip][rfc1952] +- `application/vnd.oci.image.layer.v1.tar+zstd`: ["Layer", as a tar archive](layer.md#zstd-media-types) compressed with [zstd][rfc8478] +- `application/vnd.oci.empty.v1+json`: [Empty for unused descriptors](manifest.md#guidance-for-an-empty-descriptor) + +The following media types identify a ["Layer" with distribution restrictions](layer.md#non-distributable-layers), but are **deprecated** and not recommended for future use: + +- `application/vnd.oci.image.layer.nondistributable.v1.tar`: "Layer", as a tar archive +- `application/vnd.oci.image.layer.nondistributable.v1.tar+gzip`: ["Layer", as a tar archive with distribution restrictions](layer.md#gzip-media-types) compressed with [gzip][rfc1952] +- `application/vnd.oci.image.layer.nondistributable.v1.tar+zstd`: ["Layer", as a tar archive with distribution restrictions](layer.md#zstd-media-types) compressed with [zstd][rfc8478] + +## Media Type Conflicts + +[Blob](image-layout.md) retrieval methods MAY return media type metadata. +For example, a HTTP response might return a manifest with the Content-Type header set to `application/vnd.oci.image.manifest.v1+json`. +Implementations MAY also have expectations for the blob's media type and digest (e.g. from a [descriptor](descriptor.md) referencing the blob). + +- Implementations that do not have an expected media type for the blob SHOULD respect the returned media type. +- Implementations that have an expected media type which matches the returned media type SHOULD respect the matched media type. +- Implementations that have an expected media type which does not match the returned media type SHOULD: + - Respect the expected media type if the blob matches the expected digest. + Implementations MAY warn about the media type mismatch. + - Return an error if the blob does not match the expected digest (as [recommended for descriptors](descriptor.md#properties)). + - Return an error if they do not have an expected digest. + +## Compatibility Matrix + +The OCI Image Specification strives to be backwards and forwards compatible when possible. +Breaking compatibility with existing systems creates a burden on users whether they are build systems, distribution systems, container engines, etc. +This section shows where the OCI Image Specification is compatible with formats external to the OCI Image and different versions of this specification. + +### application/vnd.oci.image.index.v1+json + +Similar/related schema: + +- [application/vnd.docker.distribution.manifest.list.v2+json](https://github.com/distribution/distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list) + - `.annotations`: only present in OCI + - `.[]manifests.annotations`: only present in OCI + - `.[]manifests.urls`: only present in OCI + +### application/vnd.oci.image.manifest.v1+json + +Similar/related schema: + +- [application/vnd.docker.distribution.manifest.v2+json](https://github.com/distribution/distribution/blob/master/docs/spec/manifest-v2-2.md#image-manifest-field-descriptions) + - `.annotations`: only present in OCI + - `.config.annotations`: only present in OCI + - `.config.urls`: only present in OCI + - `.[]layers.annotations`: only present in OCI + +### application/vnd.oci.image.layer.v1.tar+gzip + +Interchangeable and fully compatible mime-types: + +- [application/vnd.docker.image.rootfs.diff.tar.gzip](https://github.com/moby/moby/blob/v20.10.8/image/spec/v1.2.md#creating-an-image-filesystem-changeset) + +### application/vnd.oci.image.config.v1+json + +Similar/related schema: + +- [application/vnd.docker.container.image.v1+json](https://github.com/moby/moby/blob/v20.10.8/image/spec/v1.2.md#image-json-description) (Docker Image Spec v1.2) + - `.config.Memory`: only present in Docker, and reserved in OCI + - `.config.MemorySwap`: only present in Docker, and reserved in OCI + - `.config.CpuShares`: only present in Docker, and reserved in OCI + - `.config.Healthcheck`: only present in Docker, and reserved in OCI +- [Moby/Docker](https://github.com/moby/moby) + - `.config.ArgsEscaped`: Windows-specific Moby/Docker extension, deprecated in OCI, available for compatibility with older images. + +`.config.StopSignal` and `.config.Labels` are accidentally undocumented in Docker Image Spec v1.2, but these fields are not OCI-specific concepts. + +## Relations + +The following figure shows how the above media types reference each other: + +![media types](img/media-types.png) + +[Descriptors](descriptor.md) are used for all references. +The image-index being a "fat manifest" references a list of image manifests per target platform. An image manifest references exactly one target configuration and possibly many layers. + +[rfc1952]: https://tools.ietf.org/html/rfc1952 +[rfc8478]: https://tools.ietf.org/html/rfc8478 diff --git a/project.md b/project.md new file mode 100644 index 0000000..8b75c8e --- /dev/null +++ b/project.md @@ -0,0 +1,6 @@ +# Project docs + +## Release Process + +- `git tag` the prior commit (preferably signed tag) +- Make a release on [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec/releases) for the version. Attach the produced docs. diff --git a/schema/backwards_compatibility_test.go b/schema/backwards_compatibility_test.go new file mode 100644 index 0000000..ca17bbc --- /dev/null +++ b/schema/backwards_compatibility_test.go @@ -0,0 +1,223 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + _ "crypto/sha256" + "strings" + "testing" + + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/schema" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +var compatMap = map[string]string{ + "application/vnd.docker.distribution.manifest.list.v2+json": v1.MediaTypeImageIndex, + "application/vnd.docker.distribution.manifest.v2+json": v1.MediaTypeImageManifest, + "application/vnd.docker.image.rootfs.diff.tar.gzip": v1.MediaTypeImageLayerGzip, + "application/vnd.docker.container.image.v1+json": v1.MediaTypeImageConfig, +} + +// convertFormats converts Docker v2.2 image format JSON documents to OCI +// format by simply replacing instances of the strings found in the compatMap +// found in the input string. +func convertFormats(input string) string { + out := input + for k, v := range compatMap { + out = strings.Replace(out, k, v, -1) + } + return out +} + +func TestBackwardsCompatibilityImageIndex(t *testing.T) { + for i, tt := range []struct { + imageIndex string + digest digest.Digest + fail bool + }{ + { + digest: "sha256:4ffd0883f25635999f04ea543240a27c9a4341979ff7d46a9774f71512eebb1f", + imageIndex: `{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 2094, + "digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 1922, + "digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 2084, + "digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + "platform": { + "architecture": "s390x", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 2084, + "digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v7" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 2090, + "digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + } + } + ] +}`, + fail: false, + }, + } { + got := digest.FromString(tt.imageIndex) + if tt.digest != got { + t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got) + } + + imageIndex := convertFormats(tt.imageIndex) + r := strings.NewReader(imageIndex) + err := schema.ValidatorMediaTypeImageIndex.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} + +func TestBackwardsCompatibilityManifest(t *testing.T) { + for i, tt := range []struct { + manifest string + digest digest.Digest + fail bool + }{ + // manifest pulled from docker hub using hash value + // + // curl -L -H "Authorization: Bearer ..." -H \ + // "Accept: application/vnd.docker.distribution.manifest.v2+json" \ + // https://registry-1.docker.io/v2/library/docker/manifests/sha256:888206c77cd2811ec47e752ba291e5b7734e3ef137dfd222daadaca39a9f17bc + { + digest: "sha256:888206c77cd2811ec47e752ba291e5b7734e3ef137dfd222daadaca39a9f17bc", + manifest: `{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/octet-stream", + "size": 3210, + "digest": "sha256:5359a4f250650c20227055957e353e8f8a74152f35fe36f00b6b1f9fc19c8861" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2310272, + "digest": "sha256:fae91920dcd4542f97c9350b3157139a5d901362c2abec284de5ebd1b45b4957" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 913022, + "digest": "sha256:f384f6ab36adad485192f09379c0b58dc612a3cde82c551e082a7c29a87c95da" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 9861668, + "digest": "sha256:ed0d2dd5e1a0e5e650a330a864c8a122e9aa91fa6ba9ac6f0bd1882e59df55e7" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 465, + "digest": "sha256:ec4d00b58417c45f7ddcfde7bcad8c9d62a7d6d5d17cdc1f7d79bcb2e22c1491" + } + ] +}`, + fail: false, + }, + } { + got := digest.FromString(tt.manifest) + if tt.digest != got { + t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got) + } + + manifest := convertFormats(tt.manifest) + r := strings.NewReader(manifest) + err := schema.ValidatorMediaTypeManifest.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} + +func TestBackwardsCompatibilityConfig(t *testing.T) { + for i, tt := range []struct { + config string + digest digest.Digest + fail bool + }{ + // config pulled from docker hub blob store + // + // $ TOKEN=$(curl https://auth.docker.io/token\?service\=registry.docker.io\&scope\=repository:library/docker:pull | jq -r .token) + // $ CONFIG_DIGEST=$(curl -H "Authorization: Bearer ${TOKEN}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' https://index.docker.io/v2/library/docker/manifests/1.12.1 | jq -r .config.digest) + // $ curl -LH "Authorization: Bearer ${TOKEN}" https://index.docker.io/v2/library/docker/blobs/${CONFIG_DIGEST} + { + digest: "sha256:a059ea7356d5b5a9e0f6352bfa463e7bd4721c2ade3ef168603826e0de6fe54b", + config: `{"architecture":"amd64","config":{"Hostname":"09713501c176","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","DOCKER_BUCKET=get.docker.com","DOCKER_VERSION=1.12.1","DOCKER_SHA256=05ceec7fd937e1416e5dce12b0b6e1c655907d349d52574319a1e875077ccb79"],"Cmd":["sh"],"Image":"sha256:32e2e3ccf2a4fbaa75b078bf539cd5ea2e374a4242665a5ec3f3c01e7a3eefb8","Volumes":null,"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":[],"Labels":{}},"container":"15a30be053fb3069a7879b4ea537e84689d8e8e8ba94dc4dd499271506803ba1","container_config":{"Hostname":"09713501c176","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","DOCKER_BUCKET=get.docker.com","DOCKER_VERSION=1.12.1","DOCKER_SHA256=05ceec7fd937e1416e5dce12b0b6e1c655907d349d52574319a1e875077ccb79"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"sh\"]"],"Image":"sha256:32e2e3ccf2a4fbaa75b078bf539cd5ea2e374a4242665a5ec3f3c01e7a3eefb8","Volumes":null,"WorkingDir":"","Entrypoint":["docker-entrypoint.sh"],"OnBuild":[],"Labels":{}},"created":"2016-10-10T23:04:00.821781828Z","docker_version":"1.12.1","history":[{"created":"2016-09-23T16:29:57.276868291Z","created_by":"/bin/sh -c #(nop) ADD file:d6ee3ba7a4d59b161917082cc7242c660c61bb3f3cc1549c7e2dfff2b0de7104 in / "},{"created":"2016-09-23T16:36:54.024611637Z","created_by":"/bin/sh -c apk add --no-cache \t\tca-certificates \t\tcurl \t\topenssl"},{"created":"2016-09-23T16:36:54.365914519Z","created_by":"/bin/sh -c #(nop) ENV DOCKER_BUCKET=get.docker.com","empty_layer":true},{"created":"2016-09-23T16:36:54.662005049Z","created_by":"/bin/sh -c #(nop) ENV DOCKER_VERSION=1.12.1","empty_layer":true},{"created":"2016-09-23T16:36:54.946033025Z","created_by":"/bin/sh -c #(nop) ENV DOCKER_SHA256=05ceec7fd937e1416e5dce12b0b6e1c655907d349d52574319a1e875077ccb79","empty_layer":true},{"created":"2016-09-23T16:36:58.535084011Z","created_by":"/bin/sh -c set -x \t\u0026\u0026 curl -fSL \"https://${DOCKER_BUCKET}/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz\" -o docker.tgz \t\u0026\u0026 echo \"${DOCKER_SHA256} *docker.tgz\" | sha256sum -c - \t\u0026\u0026 tar -xzvf docker.tgz \t\u0026\u0026 mv docker/* /usr/local/bin/ \t\u0026\u0026 rmdir docker \t\u0026\u0026 rm docker.tgz \t\u0026\u0026 docker -v"},{"created":"2016-10-10T23:04:00.334158993Z","created_by":"/bin/sh -c #(nop) COPY file:399605dc1850a60a586b5494ab538bad495fd6f94eabca0c5f8a26468ce6030f in /usr/local/bin/ "},{"created":"2016-10-10T23:04:00.577900192Z","created_by":"/bin/sh -c #(nop) ENTRYPOINT [\"docker-entrypoint.sh\"]","empty_layer":true},{"created":"2016-10-10T23:04:00.821781828Z","created_by":"/bin/sh -c #(nop) CMD [\"sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:9007f5987db353ec398a223bc5a135c5a9601798ba20a1abba537ea2f8ac765f","sha256:1b06990ff0df8dad281fad7e6e4c5e91f32f8f8c095d6c74cf1e90a6f4407e28","sha256:9d12251ce74aac7619a83641ab72431a8d82e58bcd8a262c2bb0cdb280f1f3b5","sha256:17a7f292c2427adfc75c3a789bab8efec925dc38c5437bf83d2f528013ab80e2"]}}`, + fail: false, + }, + { + // fedora:23 from docker hub + // both Entrypoint and Cmd can be nullable + digest: "sha256:a20665eb1fe2912accb3d5dadaed360430df0d1aa46874875886947d61d3d4ee", + config: `{"architecture":"amd64","author":"Patrick Uiterwijk \u003cpatrick@puiterwijk.org\u003e","config":{"Hostname":"8dfe43d80430","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":null,"Image":"sha256:6986ae504bbf843512d680cc959484452034965db15f75ee8bdd1b107f61500b","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"container":"6249cd2c4b1d6b1bf05903364cbcb95781508994d6407c1564d494e748ea1b41","container_config":{"Hostname":"8dfe43d80430","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ADD file:293a6e463aa402bb8f80eb5cfc937f375cedc6843abaeb9eccfe3923bb3fc80b in /"],"Image":"sha256:6986ae504bbf843512d680cc959484452034965db15f75ee8bdd1b107f61500b","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2016-06-10T18:44:31.784795904Z","docker_version":"1.10.3","history":[{"created":"2016-06-10T18:44:03.360264073Z","author":"Patrick Uiterwijk \u003cpatrick@puiterwijk.org\u003e","created_by":"/bin/sh -c #(nop) MAINTAINER Patrick Uiterwijk \u003cpatrick@puiterwijk.org\u003e","empty_layer":true},{"created":"2016-06-10T18:44:31.784795904Z","author":"Patrick Uiterwijk \u003cpatrick@puiterwijk.org\u003e","created_by":"/bin/sh -c #(nop) ADD file:293a6e463aa402bb8f80eb5cfc937f375cedc6843abaeb9eccfe3923bb3fc80b in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:d43f38155a799dc53d8fbb9f3bc11f51805f4027cd5c3d10b9823201cd5b9400"]}}`, + fail: false, + }, + } { + got := digest.FromString(tt.config) + if tt.digest != got { + t.Errorf("test %d: expected digest %s but got %s", i, tt.digest, got) + } + + config := convertFormats(tt.config) + r := strings.NewReader(config) + err := schema.ValidatorMediaTypeImageConfig.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/config-schema.json b/schema/config-schema.json new file mode 100644 index 0000000..0c325f1 --- /dev/null +++ b/schema/config-schema.json @@ -0,0 +1,155 @@ +{ + "description": "OpenContainer Config Specification", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://opencontainers.org/schema/image/config", + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "author": { + "type": "string" + }, + "architecture": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "os": { + "type": "string" + }, + "os.version": { + "type": "string" + }, + "os.features": { + "type": "array", + "items": { + "type": "string" + } + }, + "config": { + "type": "object", + "properties": { + "User": { + "type": "string" + }, + "ExposedPorts": { + "$ref": "defs.json#/definitions/mapStringObject" + }, + "Env": { + "type": "array", + "items": { + "type": "string" + } + }, + "Entrypoint": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "Cmd": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "Volumes": { + "oneOf": [ + { + "$ref": "defs.json#/definitions/mapStringObject" + }, + { + "type": "null" + } + ] + }, + "WorkingDir": { + "type": "string" + }, + "Labels": { + "oneOf": [ + { + "$ref": "defs.json#/definitions/mapStringString" + }, + { + "type": "null" + } + ] + }, + "StopSignal": { + "type": "string" + }, + "ArgsEscaped": { + "type": "boolean" + } + } + }, + "rootfs": { + "type": "object", + "properties": { + "diff_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "layers" + ] + } + }, + "required": [ + "diff_ids", + "type" + ] + }, + "history": { + "type": "array", + "items": { + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + }, + "author": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "empty_layer": { + "type": "boolean" + } + } + } + } + }, + "required": [ + "architecture", + "os", + "rootfs" + ] +} diff --git a/schema/config_test.go b/schema/config_test.go new file mode 100644 index 0000000..9daa41e --- /dev/null +++ b/schema/config_test.go @@ -0,0 +1,260 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" +) + +func TestConfig(t *testing.T) { + for i, tt := range []struct { + config string + fail bool + }{ + // expected failure: field "os" has numeric value, must be string + { + config: ` +{ + "architecture": "amd64", + "os": 123, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + // expected failure: field "variant" has numeric value, must be string + { + config: ` +{ + "architecture": "arm64", + "variant": 123, + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + + // expected failure: field "config.User" has numeric value, must be string + { + config: ` +{ + "created": "2015-10-31T22:22:56.015925234Z", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "architecture": "amd64", + "os": "linux", + "config": { + "User": 1234 + }, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + + // expected failue: history has string value, must be an array + { + config: ` +{ + "history": "should be an array", + "architecture": "amd64", + "os": 123, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + + // expected failure: Env has numeric value, must be a string + { + config: ` +{ + "architecture": "amd64", + "os": 123, + "config": { + "Env": [ + 7353 + ] + }, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + + // expected failure: config.Volumes has string array, must be an object (string set) + { + config: ` +{ + "architecture": "amd64", + "os": 123, + "config": { + "Volumes": [ + "/var/job-result-data", + "/var/log/my-app-logs" + ] + }, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + + // expected failue: invalid JSON + { + config: `invalid JSON`, + fail: true, + }, + + // valid config with optional fields + { + config: ` +{ + "created": "2015-10-31T22:22:56.015925234Z", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "architecture": "arm64", + "variant": "v8", + "os": "linux", + "config": { + "User": "1:1", + "ExposedPorts": { + "8080/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "FOO=docker_is_a_really", + "BAR=great_tool_you_know" + ], + "Entrypoint": [ + "/bin/sh" + ], + "Cmd": [ + "--foreground", + "--config", + "/etc/my-app.d/default.cfg" + ], + "Volumes": { + "/var/job-result-data": {}, + "/var/log/my-app-logs": {} + }, + "StopSignal": "SIGKILL", + "WorkingDir": "/home/alice", + "Labels": { + "com.example.project.git.url": "https://example.com/project.git", + "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b" + } + }, + "rootfs": { + "diff_ids": [ + "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827", + "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d" + ], + "type": "layers" + }, + "history": [ + { + "created": "2015-10-31T22:22:54.690851953Z", + "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" + }, + { + "created": "2015-10-31T22:22:55.613815829Z", + "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]", + "empty_layer": true + } + ] +} +`, + fail: false, + }, + + // valid config with only required fields + { + config: ` +{ + "architecture": "amd64", + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: false, + }, + // expected failure: Env is invalid + { + config: ` +{ + "architecture": "amd64", + "os": "linux", + "config": { + "Env": [ + "foo" + ] + }, + "rootfs": { + "diff_ids": [ + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ], + "type": "layers" + } +} +`, + fail: true, + }, + } { + r := strings.NewReader(tt.config) + err := schema.ValidatorMediaTypeImageConfig.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/content-descriptor.json b/schema/content-descriptor.json new file mode 100644 index 0000000..b2f6dee --- /dev/null +++ b/schema/content-descriptor.json @@ -0,0 +1,41 @@ +{ + "description": "OpenContainer Content Descriptor Specification", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://opencontainers.org/schema/descriptor", + "type": "object", + "properties": { + "mediaType": { + "description": "the mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "size": { + "description": "the size in bytes of the referenced object", + "$ref": "defs.json#/definitions/int64" + }, + "digest": { + "description": "the cryptographic checksum digest of the object, in the pattern '<algorithm>:<encoded>'", + "$ref": "defs-descriptor.json#/definitions/digest" + }, + "urls": { + "description": "a list of urls from which this object may be downloaded", + "$ref": "defs-descriptor.json#/definitions/urls" + }, + "data": { + "description": "an embedding of the targeted content (base64 encoded)", + "$ref": "defs.json#/definitions/base64" + }, + "artifactType": { + "description": "the IANA media type of this artifact", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "annotations": { + "id": "https://opencontainers.org/schema/descriptor/annotations", + "$ref": "defs-descriptor.json#/definitions/annotations" + } + }, + "required": [ + "mediaType", + "size", + "digest" + ] +} diff --git a/schema/defs-descriptor.json b/schema/defs-descriptor.json new file mode 100644 index 0000000..dad2b0a --- /dev/null +++ b/schema/defs-descriptor.json @@ -0,0 +1,26 @@ +{ + "description": "Definitions particular to OpenContainer Descriptor Specification", + "definitions": { + "mediaType": { + "id": "https://opencontainers.org/schema/image/descriptor/mediaType", + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$" + }, + "digest": { + "description": "the cryptographic checksum digest of the object, in the pattern '<algorithm>:<encoded>'", + "type": "string", + "pattern": "^[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$" + }, + "urls": { + "description": "a list of urls from which this object may be downloaded", + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "annotations": { + "$ref": "defs.json#/definitions/mapStringString" + } + } +} diff --git a/schema/defs.json b/schema/defs.json new file mode 100644 index 0000000..220f5d4 --- /dev/null +++ b/schema/defs.json @@ -0,0 +1,97 @@ +{ + "description": "Definitions used throughout the OpenContainer Specification", + "definitions": { + "int8": { + "type": "integer", + "minimum": -128, + "maximum": 127 + }, + "int16": { + "type": "integer", + "minimum": -32768, + "maximum": 32767 + }, + "int32": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "int64": { + "type": "integer", + "minimum": -9223372036854776000, + "maximum": 9223372036854776000 + }, + "uint8": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "uint16": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "uint32": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295 + }, + "uint64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709552000 + }, + "uint16Pointer": { + "oneOf": [ + { + "$ref": "#/definitions/uint16" + }, + { + "type": "null" + } + ] + }, + "uint64Pointer": { + "oneOf": [ + { + "$ref": "#/definitions/uint64" + }, + { + "type": "null" + } + ] + }, + "base64": { + "type": "string", + "media": { + "binaryEncoding": "base64" + } + }, + "stringPointer": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mapStringString": { + "type": "object", + "patternProperties": { + ".{1,}": { + "type": "string" + } + } + }, + "mapStringObject": { + "type": "object", + "patternProperties": { + ".{1,}": { + "type": "object" + } + } + } + } +} diff --git a/schema/descriptor_test.go b/schema/descriptor_test.go new file mode 100644 index 0000000..c3bce28 --- /dev/null +++ b/schema/descriptor_test.go @@ -0,0 +1,354 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" +) + +func TestDescriptor(t *testing.T) { + for i, tt := range []struct { + descriptor string + fail bool + }{ + // valid descriptor + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: false, + }, + + // expected failure: mediaType missing + { + descriptor: ` +{ + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: mediaType does not match pattern (no subtype) + { + descriptor: ` +{ + "mediaType": "application", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: mediaType does not match pattern (invalid first type character) + { + descriptor: ` +{ + "mediaType": ".foo/bar", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: mediaType does not match pattern (invalid first subtype character) + { + descriptor: ` +{ + "mediaType": "foo/.bar", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected success: mediaType has type and subtype as long as possible + { + descriptor: ` +{ + "mediaType": "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567/1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: false, + }, + + // expected failure: mediaType does not match pattern (type too long) + { + descriptor: ` +{ + "mediaType": "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678/bar", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: mediaType does not match pattern (subtype too long) + { + descriptor: ` +{ + "mediaType": "foo/12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: size missing + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: size is a string, expected integer + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": "7682", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: digest missing + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682 +} +`, + fail: true, + }, + + // expected failure: digest does not match pattern (no algorithm) + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": ":5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: digest does not match pattern (no hash) + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256" +} +`, + fail: true, + }, + + // expected failure: digest does not match pattern (invalid aglorithm characters) + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "SHA256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" +} +`, + fail: true, + }, + + // expected failure: digest does not match pattern (characters needs to be lower for sha256) + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5B0BCABD1ED22E9FB1310CF6C2DEC7CDEF19F0AD69EFA1F392E94A4333501270" +} +`, + fail: true, + }, + + // expected success: valid URL entry + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "urls": [ + "https://example.com/foo" + ] +} +`, + fail: false, + }, + + // expected failure: urls does not match format (invalide url characters) + { + descriptor: ` +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "urls": [ + "value" + ] +} +`, + fail: true, + }, + + // expected success: artifactType is present and an IANA compliant value + { + descriptor: ` + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" + } + `, + fail: false, + }, + + // expected failure: artifactType does not match pattern (invalid first subtype character) + { + descriptor: ` + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "foo/.bar", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" + } + `, + fail: true, + }, + + // expected success: data field is present and has base64 content + { + descriptor: ` + { + "mediaType": "text/plain", + "size": 34, + "data": "aHR0cHM6Ly9naXRodWIuY29tL29wZW5jb250YWluZXJzCg==", + "digest": "sha256:2690af59371e9eca9453dc29882643f46e5ca47ec2862bd517b5e17351325153" + } + `, + fail: false, + }, + + { + descriptor: `{ + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" +}`, + }, + { + descriptor: `{ + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }`, + }, + { + descriptor: `{ + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }`, + }, + { + descriptor: ` + { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256.foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }`, + }, + { + descriptor: `{ + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8" + }`, + }, + { + // fail: repeated separators in algorithm + descriptor: `{ + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+foo+-b:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }`, + fail: true, + }, + { + descriptor: `{ + "digest": "sha256+b64u:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564", + "size": 1000000, + "mediaType": "application/vnd.oci.image.config.v1+json" + }`, + }, + { + // test for those who cannot use modulo arithmetic to recover padding. + descriptor: `{ + "digest": "sha256+b64u.unknownlength:LCa0a2j_xo_5m0U8HTBBNBNCLXBkg7-g-YpeiGJm564=", + "size": 1000000, + "mediaType": "application/vnd.oci.image.config.v1+json" + }`, + }, + { + descriptor: ` + { + "mediaType": "text/plain", + "size": 34, + "data": "aHR0cHM6Ly9naXRodWIuY29tL29wZW5jb250YWluZXJzCg", + "digest": "sha256:2690af59371e9eca9453dc29882643f46e5ca47ec2862bd517b5e17351325153" + } + `, + fail: true, + }, + } { + r := strings.NewReader(tt.descriptor) + err := schema.ValidatorMediaTypeDescriptor.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/doc.go b/schema/doc.go new file mode 100644 index 0000000..5ea5914 --- /dev/null +++ b/schema/doc.go @@ -0,0 +1,16 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package schema defines the OCI image media types, schema definitions and validation functions. +package schema diff --git a/schema/error.go b/schema/error.go new file mode 100644 index 0000000..baf8751 --- /dev/null +++ b/schema/error.go @@ -0,0 +1,57 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "bufio" + "encoding/json" + "io" +) + +// A SyntaxError is a description of a JSON syntax error +// including line, column and offset in the JSON file. +type SyntaxError struct { + msg string + Line, Col int + Offset int64 +} + +func (e *SyntaxError) Error() string { return e.msg } + +// WrapSyntaxError checks whether the given error is a *json.SyntaxError +// and converts it into a *schema.SyntaxError containing line/col information using the given reader. +// If the given error is not a *json.SyntaxError it is returned unchanged. +func WrapSyntaxError(r io.Reader, err error) error { + if serr, ok := err.(*json.SyntaxError); ok { + buf := bufio.NewReader(r) + line := 0 + col := 0 + for i := int64(0); i < serr.Offset; i++ { + b, berr := buf.ReadByte() + if berr != nil { + break + } + if b == '\n' { + line++ + col = 1 + } else { + col++ + } + } + return &SyntaxError{serr.Error(), line, col, serr.Offset} + } + + return err +} diff --git a/schema/image-index-schema.json b/schema/image-index-schema.json new file mode 100644 index 0000000..2e5dbf5 --- /dev/null +++ b/schema/image-index-schema.json @@ -0,0 +1,100 @@ +{ + "description": "OpenContainer Image Index Specification", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://opencontainers.org/schema/image/index", + "type": "object", + "properties": { + "schemaVersion": { + "description": "This field specifies the image index schema version as an integer", + "id": "https://opencontainers.org/schema/image/index/schemaVersion", + "type": "integer", + "minimum": 2, + "maximum": 2 + }, + "mediaType": { + "description": "the mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "artifactType": { + "description": "the artifact mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "subject": { + "$ref": "content-descriptor.json" + }, + "manifests": { + "type": "array", + "items": { + "id": "https://opencontainers.org/schema/image/manifestDescriptor", + "type": "object", + "required": [ + "mediaType", + "size", + "digest" + ], + "properties": { + "mediaType": { + "description": "the mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "size": { + "description": "the size in bytes of the referenced object", + "$ref": "defs.json#/definitions/int64" + }, + "digest": { + "description": "the cryptographic checksum digest of the object, in the pattern '<algorithm>:<encoded>'", + "$ref": "defs-descriptor.json#/definitions/digest" + }, + "urls": { + "description": "a list of urls from which this object may be downloaded", + "$ref": "defs-descriptor.json#/definitions/urls" + }, + "platform": { + "id": "https://opencontainers.org/schema/image/platform", + "type": "object", + "required": [ + "architecture", + "os" + ], + "properties": { + "architecture": { + "id": "https://opencontainers.org/schema/image/platform/architecture", + "type": "string" + }, + "os": { + "id": "https://opencontainers.org/schema/image/platform/os", + "type": "string" + }, + "os.version": { + "id": "https://opencontainers.org/schema/image/platform/os.version", + "type": "string" + }, + "os.features": { + "id": "https://opencontainers.org/schema/image/platform/os.features", + "type": "array", + "items": { + "type": "string" + } + }, + "variant": { + "type": "string" + } + } + }, + "annotations": { + "id": "https://opencontainers.org/schema/image/descriptor/annotations", + "$ref": "defs-descriptor.json#/definitions/annotations" + } + } + } + }, + "annotations": { + "id": "https://opencontainers.org/schema/image/index/annotations", + "$ref": "defs-descriptor.json#/definitions/annotations" + } + }, + "required": [ + "schemaVersion", + "manifests" + ] +} diff --git a/schema/image-layout-schema.json b/schema/image-layout-schema.json new file mode 100644 index 0000000..874d217 --- /dev/null +++ b/schema/image-layout-schema.json @@ -0,0 +1,18 @@ +{ + "description": "OpenContainer Image Layout Schema", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://opencontainers.org/schema/image/layout", + "type": "object", + "properties": { + "imageLayoutVersion": { + "description": "version of the OCI Image Layout (in the oci-layout file)", + "type": "string", + "enum": [ + "1.0.0" + ] + } + }, + "required": [ + "imageLayoutVersion" + ] +} diff --git a/schema/image-manifest-schema.json b/schema/image-manifest-schema.json new file mode 100644 index 0000000..9bce5a1 --- /dev/null +++ b/schema/image-manifest-schema.json @@ -0,0 +1,45 @@ +{ + "description": "OpenContainer Image Manifest Specification", + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://opencontainers.org/schema/image/manifest", + "type": "object", + "properties": { + "schemaVersion": { + "description": "This field specifies the image manifest schema version as an integer", + "id": "https://opencontainers.org/schema/image/manifest/schemaVersion", + "type": "integer", + "minimum": 2, + "maximum": 2 + }, + "mediaType": { + "description": "the mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "artifactType": { + "description": "the artifact mediatype of the referenced object", + "$ref": "defs-descriptor.json#/definitions/mediaType" + }, + "config": { + "$ref": "content-descriptor.json" + }, + "subject": { + "$ref": "content-descriptor.json" + }, + "layers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "content-descriptor.json" + } + }, + "annotations": { + "id": "https://opencontainers.org/schema/image/manifest/annotations", + "$ref": "defs-descriptor.json#/definitions/annotations" + } + }, + "required": [ + "schemaVersion", + "config", + "layers" + ] +} diff --git a/schema/imageindex_test.go b/schema/imageindex_test.go new file mode 100644 index 0000000..7b04935 --- /dev/null +++ b/schema/imageindex_test.go @@ -0,0 +1,316 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" +) + +func TestImageIndex(t *testing.T) { + for i, tt := range []struct { + imageIndex string + fail bool + }{ + // expected failure: mediaType does not match pattern + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "invalid", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // expected failure: manifest.size is string, expected integer + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": "7682", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // expected failure: manifest.digest is missing, expected required + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // expected failure: in the optional field platform platform.architecture is missing, expected required + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // expected failure: invalid referenced manifest media type + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "invalid", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // expected failure: empty referenced manifest media type + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} +`, + fail: true, + }, + + // valid image index, with optional fields + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} +`, + fail: false, + }, + + // valid image index, with required fields only + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ] +} +`, + fail: false, + }, + + // valid image index, with customized media type of referenced manifest + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/customized.manifest+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + } + ] +} +`, + fail: false, + }, + + // valid image index with artifactType and manifests + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.index.v1+json", + "artifactType": "application/vnd.example+type", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270" + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example1+type", + "size": 506, + "digest": "sha256:99953afc4b90c7d78079d189ae10da0a1002e6be5e9e8dedaf9f7f29def42111" + } + ] +} +`, + fail: false, + }, + + // valid image index with a subject field + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ], + "subject" : { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e" + } +} +`, + fail: false, + }, + + // expected failure, invalid subject field + { + imageIndex: ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ], + "subject" : "nope" +} +`, + fail: true, + }, + } { + r := strings.NewReader(tt.imageIndex) + err := schema.ValidatorMediaTypeImageIndex.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/imagelayout_test.go b/schema/imagelayout_test.go new file mode 100644 index 0000000..144ff08 --- /dev/null +++ b/schema/imagelayout_test.go @@ -0,0 +1,56 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" +) + +func TestImageLayout(t *testing.T) { + for i, tt := range []struct { + imageLayout string + fail bool + }{ + // expected faulure: imageLayoutVersion does not match pattern + { + imageLayout: ` +{ + "imageLayoutVersion": 1.0.0 +} +`, + fail: true, + }, + + // validate layout + { + imageLayout: ` +{ + "imageLayoutVersion": "1.0.0" +} +`, + fail: false, + }, + } { + r := strings.NewReader(tt.imageLayout) + err := schema.ValidatorMediaTypeLayoutHeader.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/loader.go b/schema/loader.go new file mode 100644 index 0000000..d773758 --- /dev/null +++ b/schema/loader.go @@ -0,0 +1,125 @@ +// Copyright 2018 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/xeipuuv/gojsonreference" + "github.com/xeipuuv/gojsonschema" +) + +// fsLoaderFactory implements gojsonschema.JSONLoaderFactory by reading files under the specified namespaces from the root of fs. +type fsLoaderFactory struct { + namespaces []string + fs http.FileSystem +} + +// newFSLoaderFactory returns a fsLoaderFactory reading files under the specified namespaces from the root of fs. +func newFSLoaderFactory(namespaces []string, fs http.FileSystem) *fsLoaderFactory { + return &fsLoaderFactory{ + namespaces: namespaces, + fs: fs, + } +} + +func (factory *fsLoaderFactory) New(source string) gojsonschema.JSONLoader { + return &fsLoader{ + factory: factory, + source: source, + } +} + +// refContents returns the contents of ref, if available in fsLoaderFactory. +func (factory *fsLoaderFactory) refContents(ref gojsonreference.JsonReference) ([]byte, error) { + refStr := ref.String() + path := "" + for _, ns := range factory.namespaces { + if strings.HasPrefix(refStr, ns) { + path = "/" + strings.TrimPrefix(refStr, ns) + break + } + } + if path == "" { + return nil, fmt.Errorf("schema reference %#v unexpectedly not available in fsLoaderFactory with namespaces %#v", path, factory.namespaces) + } + + f, err := factory.fs.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + return io.ReadAll(f) +} + +// fsLoader implements gojsonschema.JSONLoader by reading the document named by source from a fsLoaderFactory. +type fsLoader struct { + factory *fsLoaderFactory + source string +} + +// JsonSource implements gojsonschema.JSONLoader.JsonSource. The "Json" capitalization needs to be maintained to conform to the interface. +func (l *fsLoader) JsonSource() interface{} { // revive:disable-line:var-naming + return l.source +} + +func (l *fsLoader) LoadJSON() (interface{}, error) { + // Based on gojsonschema.jsonReferenceLoader.LoadJSON. + reference, err := gojsonreference.NewJsonReference(l.source) + if err != nil { + return nil, err + } + + refToURL := reference + refToURL.GetUrl().Fragment = "" + + body, err := l.factory.refContents(refToURL) + if err != nil { + return nil, err + } + + return decodeJSONUsingNumber(bytes.NewReader(body)) +} + +// decodeJSONUsingNumber returns JSON parsed from an io.Reader +func decodeJSONUsingNumber(r io.Reader) (interface{}, error) { + // Copied from gojsonschema. + var document interface{} + + decoder := json.NewDecoder(r) + decoder.UseNumber() + + err := decoder.Decode(&document) + if err != nil { + return nil, err + } + + return document, nil +} + +// JsonReference implements gojsonschema.JSONLoader.JsonReference. The "Json" capitalization needs to be maintained to conform to the interface. +func (l *fsLoader) JsonReference() (gojsonreference.JsonReference, error) { // revive:disable-line:var-naming + return gojsonreference.NewJsonReference(l.JsonSource().(string)) +} + +func (l *fsLoader) LoaderFactory() gojsonschema.JSONLoaderFactory { + return l.factory +} diff --git a/schema/manifest_test.go b/schema/manifest_test.go new file mode 100644 index 0000000..732b156 --- /dev/null +++ b/schema/manifest_test.go @@ -0,0 +1,347 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" +) + +func TestManifest(t *testing.T) { + for i, tt := range []struct { + manifest string + fail bool + }{ + // expected failure: mediaType does not match pattern + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "invalid", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 148, + "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd" + } + ] +} +`, + fail: true, + }, + + // expected failure: config.size is a string, expected integer + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": "1470", + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 148, + "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd" + } + ] +} +`, + fail: true, + }, + + // expected failure: layers.size is string, expected integer + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": "675598", + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + } + ] +} +`, + fail: true, + }, + + // valid manifest with optional fields + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 675598, + "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 156, + "digest": "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 148, + "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd" + } + ], + "annotations": { + "key1": "value1", + "key2": "value2" + } +} +`, + fail: false, + }, + + // valid manifest with only required fields + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 675598, + "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 156, + "digest": "sha256:2b689805fbd00b2db1df73fae47562faac1a626d5f61744bfe29946ecff5d73d" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 148, + "digest": "sha256:c57089565e894899735d458f0fd4bb17a0f1e0df8d72da392b85c9b35ee777cd" + } + ] +} +`, + fail: false, + }, + + // expected failure: empty layer, expected at least one + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [] +} +`, + fail: true, + }, + + // expected pass: test bounds of algorithm field in digest. + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "sha256+foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "sha256.foo-bar:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8" + } + ] +} +`, + }, + + // expected success: subject field with a valid descriptor + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + } + ], + "subject" : { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1234, + "digest": "sha256:220a60ecd4a3c32c282622a625a54db9ba0ff55b5ba9c29c7064a2bc358b6a3e" + } +} +`, + fail: false, + }, + + // expected failure: subject field with invalid value (something that is not a descriptor) + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + } + ], + "subject" : ".nope" +} +`, + fail: true, + }, + + // expected failure: push bounds of algorithm field in digest too far. + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 1470, + "digest": "sha256+b64:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1470, + "digest": "sha256+foo+-b:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + } + ] +} +`, + fail: true, + }, + + // valid manifest for an artifact with a dedicated config + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.example.config+json", + "size": 1470, + "digest": "sha256:c86f7763873b6c0aae22d963bab59b4f5debbed6685761b5951584f6efb0633b" + }, + "layers": [ + { + "mediaType": "application/vnd.example.data+type", + "size": 675598, + "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827" + } + ] +} +`, + fail: false, + }, + + // valid manifest for an artifact using the empty config and artifactType + { + manifest: ` +{ + "schemaVersion": 2, + "mediaType" : "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example+type", + "config": { + "mediaType": "application/vnd.oci.empty.v1+json", + "size": 2, + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" + }, + "layers": [ + { + "mediaType": "application/vnd.example+type", + "size": 675598, + "digest": "sha256:9d3dd9504c685a304985025df4ed0283e47ac9ffa9bd0326fddf4d59513f0827" + } + ] +} +`, + fail: false, + }, + } { + r := strings.NewReader(tt.manifest) + err := schema.ValidatorMediaTypeManifest.Validate(r) + + if got := err != nil; tt.fail != got { + t.Errorf("test %d: expected validation failure %t but got %t, err %v", i, tt.fail, got, err) + } + } +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 0000000..7a338d8 --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,78 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "embed" + "net/http" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Media types for the OCI image formats +const ( + ValidatorMediaTypeDescriptor Validator = v1.MediaTypeDescriptor + ValidatorMediaTypeLayoutHeader Validator = v1.MediaTypeLayoutHeader + ValidatorMediaTypeManifest Validator = v1.MediaTypeImageManifest + ValidatorMediaTypeImageIndex Validator = v1.MediaTypeImageIndex + ValidatorMediaTypeImageConfig Validator = v1.MediaTypeImageConfig + ValidatorMediaTypeImageLayer unimplemented = v1.MediaTypeImageLayer +) + +var ( + // fs stores the embedded http.FileSystem + // having the OCI JSON schema files in root "/". + //go:embed *.json + fs embed.FS + + // schemaNamespaces is a set of URI prefixes which are treated as containing the schema files of fs. + // This is necessary because *.json schema files in this directory use "id" and "$ref" attributes which evaluate to such URIs, e.g. + // ./image-manifest-schema.json URI contains + // "id": "https://opencontainers.org/schema/image/manifest", + // and + // "$ref": "content-descriptor.json" + // which evaluates as a link to https://opencontainers.org/schema/image/content-descriptor.json . + // + // To support such links without accessing the network (and trying to load content which is not hosted at these URIs), + // fsLoaderFactory accepts any URI starting with one of the schemaNamespaces below, + // and uses _escFS to load them from the root of its in-memory filesystem tree. + // + // (Note that this must contain subdirectories before its parent directories for fsLoaderFactory.refContents to work.) + schemaNamespaces = []string{ + "https://opencontainers.org/schema/image/descriptor/", + "https://opencontainers.org/schema/image/index/", + "https://opencontainers.org/schema/image/manifest/", + "https://opencontainers.org/schema/image/", + "https://opencontainers.org/schema/descriptor/", + "https://opencontainers.org/schema/", + } + + // specs maps OCI schema media types to schema URIs. + // These URIs are expected to be used only by fsLoaderFactory (which trims schemaNamespaces defined above) + // and should never cause a network access. + specs = map[Validator]string{ + ValidatorMediaTypeDescriptor: "https://opencontainers.org/schema/content-descriptor.json", + ValidatorMediaTypeLayoutHeader: "https://opencontainers.org/schema/image/image-layout-schema.json", + ValidatorMediaTypeManifest: "https://opencontainers.org/schema/image/image-manifest-schema.json", + ValidatorMediaTypeImageIndex: "https://opencontainers.org/schema/image/image-index-schema.json", + ValidatorMediaTypeImageConfig: "https://opencontainers.org/schema/image/config-schema.json", + } +) + +// FileSystem returns an in-memory filesystem including the schema files. +// The schema files are located at the root directory. +func FileSystem() http.FileSystem { + return http.FS(fs) +} diff --git a/schema/spec_test.go b/schema/spec_test.go new file mode 100644 index 0000000..e8dde99 --- /dev/null +++ b/schema/spec_test.go @@ -0,0 +1,216 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema_test + +import ( + "bytes" + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/opencontainers/image-spec/schema" + "github.com/pkg/errors" + "github.com/russross/blackfriday" +) + +var ( + errFormatInvalid = errors.New("format: invalid") +) + +func TestValidateDescriptor(t *testing.T) { + validate(t, "../descriptor.md") +} + +func TestValidateManifest(t *testing.T) { + validate(t, "../manifest.md") +} + +func TestValidateImageIndex(t *testing.T) { + validate(t, "../image-index.md") +} + +func TestValidateImageLayout(t *testing.T) { + validate(t, "../image-layout.md") +} + +func TestValidateConfig(t *testing.T) { + validate(t, "../config.md") +} + +func TestSchemaFS(t *testing.T) { + expectedSchemaFileNames, err := filepath.Glob("*.json") + if err != nil { + t.Error(err) + } + + dir, err := schema.FileSystem().Open("/") + if err != nil { + t.Fatal(err) + } + + files, err := dir.Readdir(-1) + if err != nil { + t.Fatal(err) + } + var schemaFileNames []string + for _, f := range files { + schemaFileNames = append(schemaFileNames, f.Name()) + } + + if !reflect.DeepEqual(schemaFileNames, expectedSchemaFileNames) { + t.Fatalf("got %v, expected %v", schemaFileNames, expectedSchemaFileNames) + } +} + +// TODO(sur): include examples from all specification files +func validate(t *testing.T, name string) { + m, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer m.Close() + + examples, err := extractExamples(m) + if err != nil { + t.Fatal(err) + } + + for _, example := range examples { + if example.Err == errFormatInvalid && example.Mediatype == "" { // ignore + continue + } + + if example.Err != nil { + printFields(t, "error", example.Mediatype, example.Title, example.Err) + t.Error(err) + continue + } + + err = schema.Validator(example.Mediatype).Validate(strings.NewReader(example.Body)) + if err == nil { + printFields(t, "ok", example.Mediatype, example.Title) + t.Log(example.Body, "---") + continue + } + + var errs []error + if verr, ok := errors.Cause(err).(schema.ValidationError); ok { + errs = verr.Errs + } else { + printFields(t, "error", example.Mediatype, example.Title, err) + t.Error(err) + t.Log(example.Body, "---") + continue + } + + for _, err := range errs { + printFields(t, "invalid", example.Mediatype, example.Title) + t.Error(err) + fmt.Println(example.Body, "---") + continue + } + } +} + +// renderer allows one to incercept fenced blocks in markdown documents. +type renderer struct { + blackfriday.Renderer + fn func(text []byte, lang string) +} + +func (r *renderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + r.fn(text, lang) + r.Renderer.BlockCode(out, text, lang) +} + +type example struct { + Lang string // gets raw "lang" field + Title string + Mediatype string + Body string + Err error + + // TODO(stevvooe): Figure out how to keep track of revision, file, line so + // that we can trace back verification output. +} + +// parseExample treats the field as a syntax,attribute tuple separated by a comma. +// Attributes are encoded as a url values. +// +// An example of this is `json,title=Foo%20Bar&mediatype=application/json. We +// get that the "lang" is json, the title is "Foo Bar" and the mediatype is +// "application/json". +// +// This preserves syntax highlighting and lets us tag examples with further +// metadata. +func parseExample(lang, body string) (e example) { + e.Lang = lang + e.Body = body + + parts := strings.SplitN(lang, ",", 2) + if len(parts) < 2 { + e.Err = errFormatInvalid + return + } + + m, err := url.ParseQuery(parts[1]) + if err != nil { + e.Err = err + return + } + + e.Mediatype = m.Get("mediatype") + e.Title = m.Get("title") + return +} + +func extractExamples(rd io.Reader) ([]example, error) { + p, err := io.ReadAll(rd) + if err != nil { + return nil, err + } + + var examples []example + renderer := &renderer{ + Renderer: blackfriday.HtmlRenderer(0, "test test", ""), + fn: func(text []byte, lang string) { + examples = append(examples, parseExample(lang, string(text))) + }, + } + + // just pass over the markdown and ignore the rendered result. We just want + // the side-effect of calling back for each code block. + // TODO(stevvooe): Consider just parsing these with a scanner. It will be + // faster and we can retain file, line no. + blackfriday.MarkdownOptions(p, renderer, blackfriday.Options{ + Extensions: blackfriday.EXTENSION_FENCED_CODE, + }) + + return examples, nil +} + +// printFields prints each value tab separated. +func printFields(t *testing.T, vs ...interface{}) { + var ss []string + for _, f := range vs { + ss = append(ss, fmt.Sprint(f)) + } + t.Log(strings.Join(ss, "\t")) +} diff --git a/schema/validator.go b/schema/validator.go new file mode 100644 index 0000000..4b8cd08 --- /dev/null +++ b/schema/validator.go @@ -0,0 +1,253 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schema + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "regexp" + + digest "github.com/opencontainers/go-digest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/xeipuuv/gojsonschema" +) + +// Validator wraps a media type string identifier +// and implements validation against a JSON schema. +type Validator string + +type validateFunc func(r io.Reader) error + +var mapValidate = map[Validator]validateFunc{ + ValidatorMediaTypeImageConfig: validateConfig, + ValidatorMediaTypeDescriptor: validateDescriptor, + ValidatorMediaTypeImageIndex: validateIndex, + ValidatorMediaTypeManifest: validateManifest, +} + +// ValidationError contains all the errors that happened during validation. +type ValidationError struct { + Errs []error +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("%v", e.Errs) +} + +// Validate validates the given reader against the schema of the wrapped media type. +func (v Validator) Validate(src io.Reader) error { + buf, err := io.ReadAll(src) + if err != nil { + return errors.Wrap(err, "unable to read the document file") + } + + if f, ok := mapValidate[v]; ok { + if f == nil { + return fmt.Errorf("internal error: mapValidate[%q] is nil", v) + } + err = f(bytes.NewReader(buf)) + if err != nil { + return err + } + } + + sl := newFSLoaderFactory(schemaNamespaces, FileSystem()).New(specs[v]) + ml := gojsonschema.NewStringLoader(string(buf)) + + result, err := gojsonschema.Validate(sl, ml) + if err != nil { + return errors.Wrapf( + WrapSyntaxError(bytes.NewReader(buf), err), + "schema %s: unable to validate", v) + } + + if result.Valid() { + return nil + } + + errs := make([]error, 0, len(result.Errors())) + for _, desc := range result.Errors() { + errs = append(errs, fmt.Errorf("%s", desc)) + } + + return ValidationError{ + Errs: errs, + } +} + +type unimplemented string + +func (v unimplemented) Validate(_ io.Reader) error { + return fmt.Errorf("%s: unimplemented", v) +} + +func validateManifest(r io.Reader) error { + header := v1.Manifest{} + + buf, err := io.ReadAll(r) + if err != nil { + return errors.Wrapf(err, "error reading the io stream") + } + + err = json.Unmarshal(buf, &header) + if err != nil { + return errors.Wrap(err, "manifest format mismatch") + } + + if header.Config.MediaType != string(v1.MediaTypeImageConfig) { + fmt.Printf("warning: config %s has an unknown media type: %s\n", header.Config.Digest, header.Config.MediaType) + } + + for _, layer := range header.Layers { + if layer.MediaType != string(v1.MediaTypeImageLayer) && + layer.MediaType != string(v1.MediaTypeImageLayerGzip) && + layer.MediaType != string(v1.MediaTypeImageLayerZstd) && + layer.MediaType != string(v1.MediaTypeImageLayerNonDistributable) && //nolint:staticcheck + layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableGzip) && //nolint:staticcheck + layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableZstd) { //nolint:staticcheck + fmt.Printf("warning: layer %s has an unknown media type: %s\n", layer.Digest, layer.MediaType) + } + } + return nil +} + +func validateDescriptor(r io.Reader) error { + header := v1.Descriptor{} + + buf, err := io.ReadAll(r) + if err != nil { + return errors.Wrapf(err, "error reading the io stream") + } + + err = json.Unmarshal(buf, &header) + if err != nil { + return errors.Wrap(err, "descriptor format mismatch") + } + + err = header.Digest.Validate() + if err == digest.ErrDigestUnsupported { + // we ignore unsupported algorithms + fmt.Printf("warning: unsupported digest: %q: %v\n", header.Digest, err) + return nil + } + return err +} + +func validateIndex(r io.Reader) error { + header := v1.Index{} + + buf, err := io.ReadAll(r) + if err != nil { + return errors.Wrapf(err, "error reading the io stream") + } + + err = json.Unmarshal(buf, &header) + if err != nil { + return errors.Wrap(err, "index format mismatch") + } + + for _, manifest := range header.Manifests { + if manifest.MediaType != string(v1.MediaTypeImageManifest) { + fmt.Printf("warning: manifest %s has an unknown media type: %s\n", manifest.Digest, manifest.MediaType) + } + if manifest.Platform != nil { + checkPlatform(manifest.Platform.OS, manifest.Platform.Architecture) + checkArchitecture(manifest.Platform.Architecture, manifest.Platform.Variant) + } + + } + + return nil +} + +func validateConfig(r io.Reader) error { + header := v1.Image{} + + buf, err := io.ReadAll(r) + if err != nil { + return errors.Wrapf(err, "error reading the io stream") + } + + err = json.Unmarshal(buf, &header) + if err != nil { + return errors.Wrap(err, "config format mismatch") + } + + checkPlatform(header.OS, header.Architecture) + checkArchitecture(header.Architecture, header.Variant) + + envRegexp := regexp.MustCompile(`^[^=]+=.*$`) + for _, e := range header.Config.Env { + if !envRegexp.MatchString(e) { + return errors.Errorf("unexpected env: %q", e) + } + } + + return nil +} + +func checkArchitecture(Architecture string, Variant string) { + validCombins := map[string][]string{ + "arm": {"", "v6", "v7", "v8"}, + "arm64": {"", "v8"}, + "386": {""}, + "amd64": {""}, + "ppc64": {""}, + "ppc64le": {""}, + "mips64": {""}, + "mips64le": {""}, + "s390x": {""}, + "riscv64": {""}, + } + for arch, variants := range validCombins { + if arch == Architecture { + for _, variant := range variants { + if variant == Variant { + return + } + } + fmt.Printf("warning: combination of architecture %q and variant %q is not valid.\n", Architecture, Variant) + } + } + fmt.Printf("warning: architecture %q is not supported yet.\n", Architecture) +} + +func checkPlatform(OS string, Architecture string) { + validCombins := map[string][]string{ + "android": {"arm"}, + "darwin": {"386", "amd64", "arm", "arm64"}, + "dragonfly": {"amd64"}, + "freebsd": {"386", "amd64", "arm"}, + "linux": {"386", "amd64", "arm", "arm64", "ppc64", "ppc64le", "mips64", "mips64le", "s390x", "riscv64"}, + "netbsd": {"386", "amd64", "arm"}, + "openbsd": {"386", "amd64", "arm"}, + "plan9": {"386", "amd64"}, + "solaris": {"amd64"}, + "windows": {"386", "amd64"}} + for os, archs := range validCombins { + if os == OS { + for _, arch := range archs { + if arch == Architecture { + return + } + } + fmt.Printf("warning: combination of os %q and architecture %q is invalid.\n", OS, Architecture) + } + } + fmt.Printf("warning: operating system %q of the bundle is not supported yet.\n", OS) +} @@ -0,0 +1,70 @@ +# Open Container Initiative + +## Image Format Specification + +This specification defines an OCI Image, consisting of an [image manifest](manifest.md), an [image index](image-index.md) (optional), a set of [filesystem layers](layer.md), and a [configuration](config.md). + +The goal of this specification is to enable the creation of interoperable tools for building, transporting, and preparing a container image to run. + +### Table of Contents + +- [Notational Conventions](#notational-conventions) +- [Overview](#overview) + - [Understanding the Specification](#understanding-the-specification) + - [Media Types](media-types.md) +- [Content Descriptors](descriptor.md) +- [Image Layout](image-layout.md) +- [Image Manifest](manifest.md) +- [Image Index](image-index.md) +- [Filesystem Layers](layer.md) +- [Image Configuration](config.md) +- [Annotations](annotations.md) +- [Conversion](conversion.md) +- [Considerations](considerations.md) + - [Extensibility](considerations.md#extensibility) + - [Canonicalization](considerations.md#canonicalization) + +## Notational Conventions + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119) (Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997). + +The key words "unspecified", "undefined", and "implementation-defined" are to be interpreted as described in the [rationale for the C99 standard][c99-unspecified]. + +An implementation is not compliant if it fails to satisfy one or more of the MUST, MUST NOT, REQUIRED, SHALL, or SHALL NOT requirements for the protocols it implements. +An implementation is compliant if it satisfies all the MUST, MUST NOT, REQUIRED, SHALL, and SHALL NOT requirements for the protocols it implements. + +## Overview + +At a high level the image manifest contains metadata about the contents and dependencies of the image including the content-addressable identity of one or more [filesystem layer changeset](layer.md) archives that will be unpacked to make up the final runnable filesystem. +The image configuration includes information such as application arguments, environments, etc. +The image index is a higher-level manifest which points to a list of manifests and descriptors. +Typically, these manifests may provide different implementations of the image, possibly varying by platform or other attributes. + +![build diagram](img/build-diagram.png) + +Once built the OCI Image can then be discovered by name, downloaded, verified by hash, trusted through a signature, and unpacked into an [OCI Runtime Bundle](https://github.com/opencontainers/runtime-spec/blob/master/bundle.md). + +![runtime diagram](img/run-diagram.png) + +### Understanding the Specification + +The [OCI Image Media Types](media-types.md) document is a starting point to understanding the overall structure of the specification. + +The high-level components of the spec include: + +- [Image Manifest](manifest.md) - a document describing the components that make up a container image +- [Image Index](image-index.md) - an annotated list of manifests +- [Image Layout](image-layout.md) - a filesystem layout representing the contents of an image +- [Filesystem Layer](layer.md) - a changeset that describes a container's filesystem +- [Image Configuration](config.md) - a document determining layer ordering and configuration of the image suitable for translation into a [runtime bundle][runtime-spec] +- [Conversion](conversion.md) - a document describing how this translation should occur +- [Artifacts Guidance](artifacts-guidance.md) - a document describing how to use the spec for packaging content other than OCI images +- [Descriptor](descriptor.md) - a reference that describes the type, metadata and content address of referenced content + +Future versions of this specification may include the following OPTIONAL features: + +- Signatures that are based on signing image content address +- Naming that is federated based on DNS and can be delegated + +[c99-unspecified]: https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf#page=18 +[runtime-spec]: https://github.com/opencontainers/runtime-spec diff --git a/specs-go/v1/annotations.go b/specs-go/v1/annotations.go new file mode 100644 index 0000000..581cf7c --- /dev/null +++ b/specs-go/v1/annotations.go @@ -0,0 +1,62 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // AnnotationCreated is the annotation key for the date and time on which the image was built (date-time string as defined by RFC 3339). + AnnotationCreated = "org.opencontainers.image.created" + + // AnnotationAuthors is the annotation key for the contact details of the people or organization responsible for the image (freeform string). + AnnotationAuthors = "org.opencontainers.image.authors" + + // AnnotationURL is the annotation key for the URL to find more information on the image. + AnnotationURL = "org.opencontainers.image.url" + + // AnnotationDocumentation is the annotation key for the URL to get documentation on the image. + AnnotationDocumentation = "org.opencontainers.image.documentation" + + // AnnotationSource is the annotation key for the URL to get source code for building the image. + AnnotationSource = "org.opencontainers.image.source" + + // AnnotationVersion is the annotation key for the version of the packaged software. + // The version MAY match a label or tag in the source code repository. + // The version MAY be Semantic versioning-compatible. + AnnotationVersion = "org.opencontainers.image.version" + + // AnnotationRevision is the annotation key for the source control revision identifier for the packaged software. + AnnotationRevision = "org.opencontainers.image.revision" + + // AnnotationVendor is the annotation key for the name of the distributing entity, organization or individual. + AnnotationVendor = "org.opencontainers.image.vendor" + + // AnnotationLicenses is the annotation key for the license(s) under which contained software is distributed as an SPDX License Expression. + AnnotationLicenses = "org.opencontainers.image.licenses" + + // AnnotationRefName is the annotation key for the name of the reference for a target. + // SHOULD only be considered valid when on descriptors on `index.json` within image layout. + AnnotationRefName = "org.opencontainers.image.ref.name" + + // AnnotationTitle is the annotation key for the human-readable title of the image. + AnnotationTitle = "org.opencontainers.image.title" + + // AnnotationDescription is the annotation key for the human-readable description of the software packaged in the image. + AnnotationDescription = "org.opencontainers.image.description" + + // AnnotationBaseImageDigest is the annotation key for the digest of the image's base image. + AnnotationBaseImageDigest = "org.opencontainers.image.base.digest" + + // AnnotationBaseImageName is the annotation key for the image reference of the image's base image. + AnnotationBaseImageName = "org.opencontainers.image.base.name" +) diff --git a/specs-go/v1/config.go b/specs-go/v1/config.go new file mode 100644 index 0000000..36b0aeb --- /dev/null +++ b/specs-go/v1/config.go @@ -0,0 +1,111 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "time" + + digest "github.com/opencontainers/go-digest" +) + +// ImageConfig defines the execution parameters which should be used as a base when running a container using an image. +type ImageConfig struct { + // User defines the username or UID which the process in the container should run as. + User string `json:"User,omitempty"` + + // ExposedPorts a set of ports to expose from a container running this image. + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` + + // Env is a list of environment variables to be used in a container. + Env []string `json:"Env,omitempty"` + + // Entrypoint defines a list of arguments to use as the command to execute when the container starts. + Entrypoint []string `json:"Entrypoint,omitempty"` + + // Cmd defines the default arguments to the entrypoint of the container. + Cmd []string `json:"Cmd,omitempty"` + + // Volumes is a set of directories describing where the process is likely write data specific to a container instance. + Volumes map[string]struct{} `json:"Volumes,omitempty"` + + // WorkingDir sets the current working directory of the entrypoint process in the container. + WorkingDir string `json:"WorkingDir,omitempty"` + + // Labels contains arbitrary metadata for the container. + Labels map[string]string `json:"Labels,omitempty"` + + // StopSignal contains the system call signal that will be sent to the container to exit. + StopSignal string `json:"StopSignal,omitempty"` + + // ArgsEscaped + // + // Deprecated: This field is present only for legacy compatibility with + // Docker and should not be used by new image builders. It is used by Docker + // for Windows images to indicate that the `Entrypoint` or `Cmd` or both, + // contains only a single element array, that is a pre-escaped, and combined + // into a single string `CommandLine`. If `true` the value in `Entrypoint` or + // `Cmd` should be used as-is to avoid double escaping. + // https://github.com/opencontainers/image-spec/pull/892 + ArgsEscaped bool `json:"ArgsEscaped,omitempty"` +} + +// RootFS describes a layer content addresses +type RootFS struct { + // Type is the type of the rootfs. + Type string `json:"type"` + + // DiffIDs is an array of layer content hashes (DiffIDs), in order from bottom-most to top-most. + DiffIDs []digest.Digest `json:"diff_ids"` +} + +// History describes the history of a layer. +type History struct { + // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. + Created *time.Time `json:"created,omitempty"` + + // CreatedBy is the command which created the layer. + CreatedBy string `json:"created_by,omitempty"` + + // Author is the author of the build point. + Author string `json:"author,omitempty"` + + // Comment is a custom message set when creating the layer. + Comment string `json:"comment,omitempty"` + + // EmptyLayer is used to mark if the history item created a filesystem diff. + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +// Image is the JSON structure which describes some basic information about the image. +// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. +type Image struct { + // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. + Created *time.Time `json:"created,omitempty"` + + // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. + Author string `json:"author,omitempty"` + + // Platform describes the platform which the image in the manifest runs on. + Platform + + // Config defines the execution parameters which should be used as a base when running a container using the image. + Config ImageConfig `json:"config,omitempty"` + + // RootFS references the layer content addresses used by the image. + RootFS RootFS `json:"rootfs"` + + // History describes the history of each layer. + History []History `json:"history,omitempty"` +} diff --git a/specs-go/v1/descriptor.go b/specs-go/v1/descriptor.go new file mode 100644 index 0000000..1881b11 --- /dev/null +++ b/specs-go/v1/descriptor.go @@ -0,0 +1,80 @@ +// Copyright 2016-2022 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import digest "github.com/opencontainers/go-digest" + +// Descriptor describes the disposition of targeted content. +// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype +// when marshalled to JSON. +type Descriptor struct { + // MediaType is the media type of the object this schema refers to. + MediaType string `json:"mediaType"` + + // Digest is the digest of the targeted content. + Digest digest.Digest `json:"digest"` + + // Size specifies the size in bytes of the blob. + Size int64 `json:"size"` + + // URLs specifies a list of URLs from which this object MAY be downloaded + URLs []string `json:"urls,omitempty"` + + // Annotations contains arbitrary metadata relating to the targeted content. + Annotations map[string]string `json:"annotations,omitempty"` + + // Data is an embedding of the targeted content. This is encoded as a base64 + // string when marshalled to JSON (automatically, by encoding/json). If + // present, Data can be used directly to avoid fetching the targeted content. + Data []byte `json:"data,omitempty"` + + // Platform describes the platform which the image in the manifest runs on. + // + // This should only be used when referring to a manifest. + Platform *Platform `json:"platform,omitempty"` + + // ArtifactType is the IANA media type of this artifact. + ArtifactType string `json:"artifactType,omitempty"` +} + +// Platform describes the platform which the image in the manifest runs on. +type Platform struct { + // Architecture field specifies the CPU architecture, for example + // `amd64` or `ppc64le`. + Architecture string `json:"architecture"` + + // OS specifies the operating system, for example `linux` or `windows`. + OS string `json:"os"` + + // OSVersion is an optional field specifying the operating system + // version, for example on Windows `10.0.14393.1066`. + OSVersion string `json:"os.version,omitempty"` + + // OSFeatures is an optional field specifying an array of strings, + // each listing a required OS feature (for example on Windows `win32k`). + OSFeatures []string `json:"os.features,omitempty"` + + // Variant is an optional field specifying a variant of the CPU, for + // example `v7` to specify ARMv7 when architecture is `arm`. + Variant string `json:"variant,omitempty"` +} + +// DescriptorEmptyJSON is the descriptor of a blob with content of `{}`. +var DescriptorEmptyJSON = Descriptor{ + MediaType: MediaTypeEmptyJSON, + Digest: `sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a`, + Size: 2, + Data: []byte(`{}`), +} diff --git a/specs-go/v1/index.go b/specs-go/v1/index.go new file mode 100644 index 0000000..e2bed9d --- /dev/null +++ b/specs-go/v1/index.go @@ -0,0 +1,38 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/opencontainers/image-spec/specs-go" + +// Index references manifests for various platforms. +// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON. +type Index struct { + specs.Versioned + + // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.index.v1+json` + MediaType string `json:"mediaType,omitempty"` + + // ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact. + ArtifactType string `json:"artifactType,omitempty"` + + // Manifests references platform specific manifests. + Manifests []Descriptor `json:"manifests"` + + // Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest. + Subject *Descriptor `json:"subject,omitempty"` + + // Annotations contains arbitrary metadata for the image index. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/specs-go/v1/layout.go b/specs-go/v1/layout.go new file mode 100644 index 0000000..c5503cb --- /dev/null +++ b/specs-go/v1/layout.go @@ -0,0 +1,32 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // ImageLayoutFile is the file name containing ImageLayout in an OCI Image Layout + ImageLayoutFile = "oci-layout" + // ImageLayoutVersion is the version of ImageLayout + ImageLayoutVersion = "1.0.0" + // ImageIndexFile is the file name of the entry point for references and descriptors in an OCI Image Layout + ImageIndexFile = "index.json" + // ImageBlobsDir is the directory name containing content addressable blobs in an OCI Image Layout + ImageBlobsDir = "blobs" +) + +// ImageLayout is the structure in the "oci-layout" file, found in the root +// of an OCI Image-layout directory. +type ImageLayout struct { + Version string `json:"imageLayoutVersion"` +} diff --git a/specs-go/v1/manifest.go b/specs-go/v1/manifest.go new file mode 100644 index 0000000..26fec52 --- /dev/null +++ b/specs-go/v1/manifest.go @@ -0,0 +1,41 @@ +// Copyright 2016-2022 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/opencontainers/image-spec/specs-go" + +// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON. +type Manifest struct { + specs.Versioned + + // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` + MediaType string `json:"mediaType,omitempty"` + + // ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact. + ArtifactType string `json:"artifactType,omitempty"` + + // Config references a configuration object for a container, by digest. + // The referenced configuration object is a JSON blob that the runtime uses to set up the container. + Config Descriptor `json:"config"` + + // Layers is an indexed list of layers referenced by the manifest. + Layers []Descriptor `json:"layers"` + + // Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest. + Subject *Descriptor `json:"subject,omitempty"` + + // Annotations contains arbitrary metadata for the image manifest. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/specs-go/v1/mediatype.go b/specs-go/v1/mediatype.go new file mode 100644 index 0000000..892ba3d --- /dev/null +++ b/specs-go/v1/mediatype.go @@ -0,0 +1,75 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // MediaTypeDescriptor specifies the media type for a content descriptor. + MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json" + + // MediaTypeLayoutHeader specifies the media type for the oci-layout. + MediaTypeLayoutHeader = "application/vnd.oci.layout.header.v1+json" + + // MediaTypeImageManifest specifies the media type for an image manifest. + MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" + + // MediaTypeImageIndex specifies the media type for an image index. + MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" + + // MediaTypeImageLayer is the media type used for layers referenced by the manifest. + MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar" + + // MediaTypeImageLayerGzip is the media type used for gzipped layers + // referenced by the manifest. + MediaTypeImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip" + + // MediaTypeImageLayerZstd is the media type used for zstd compressed + // layers referenced by the manifest. + MediaTypeImageLayerZstd = "application/vnd.oci.image.layer.v1.tar+zstd" + + // MediaTypeImageLayerNonDistributable is the media type for layers referenced by + // the manifest but with distribution restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar" + + // MediaTypeImageLayerNonDistributableGzip is the media type for + // gzipped layers referenced by the manifest but with distribution + // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" + + // MediaTypeImageLayerNonDistributableZstd is the media type for zstd + // compressed layers referenced by the manifest but with distribution + // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" + + // MediaTypeImageConfig specifies the media type for the image configuration. + MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json" + + // MediaTypeEmptyJSON specifies the media type for an unused blob containing the value `{}` + MediaTypeEmptyJSON = "application/vnd.oci.empty.v1+json" +) diff --git a/specs-go/version.go b/specs-go/version.go new file mode 100644 index 0000000..11e09b5 --- /dev/null +++ b/specs-go/version.go @@ -0,0 +1,32 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specs + +import "fmt" + +const ( + // VersionMajor is for an API incompatible changes + VersionMajor = 1 + // VersionMinor is for functionality in a backwards-compatible manner + VersionMinor = 1 + // VersionPatch is for backwards-compatible bug fixes + VersionPatch = 0 + + // VersionDev indicates development branch. Releases will be empty string. + VersionDev = "-rc.5" +) + +// Version is the specification version that the package types support. +var Version = fmt.Sprintf("%d.%d.%d%s", VersionMajor, VersionMinor, VersionPatch, VersionDev) diff --git a/specs-go/versioned.go b/specs-go/versioned.go new file mode 100644 index 0000000..58a1510 --- /dev/null +++ b/specs-go/versioned.go @@ -0,0 +1,23 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specs + +// Versioned provides a struct with the manifest schemaVersion and mediaType. +// Incoming content with unknown schema version can be decoded against this +// struct to check the version. +type Versioned struct { + // SchemaVersion is the image manifest schema that this image follows + SchemaVersion int `json:"schemaVersion"` +} |