From 3ea8049e27bc845132404ef3cb797ae75ee4b681 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Dec 2016 13:33:37 -0800 Subject: [PATCH] identity: add implementation of ChainID The specification defines an algorithm to calculate a `ChainID`, which can be used to identify the result of subsequent applications of layers. Because this algorithm is subtle and only needs to implemented in a single place, we provide a reference implementation. For convenience, we provide functions that calculate all the chain ids and just the top-level one. It is is integrated with the distribution/digest type for safety and convenience. As part of this, the `identity` package has been introduced. For consuming code, a few helpers have been provide to ease transition as the name of the upstream package has not yet been finalized. Users of this package should employ `FromBytes`, `FromString` and `FromReader` where appropriate, which should ease the transition if these packages change. Tests are formulated based on pre-calculation of chain identifiers to ensure correctness. Signed-off-by: Stephen J Day --- identity/chainid.go | 62 ++++++++++++++++++++++++++ identity/chainid_test.go | 95 ++++++++++++++++++++++++++++++++++++++++ identity/helpers.go | 25 +++++++++++ 3 files changed, 182 insertions(+) create mode 100644 identity/chainid.go create mode 100644 identity/chainid_test.go create mode 100644 identity/helpers.go diff --git a/identity/chainid.go b/identity/chainid.go new file mode 100644 index 000000000..da6335187 --- /dev/null +++ b/identity/chainid.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 chain provides implementations of the ChainID calculation used in +// identifying the result of subsequent layer applications. +package identity + +import "github.com/docker/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 000000000..8fb4271f8 --- /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/docker/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 000000000..183b672b6 --- /dev/null +++ b/identity/helpers.go @@ -0,0 +1,25 @@ +package identity + +import ( + _ "crypto/sha256" + _ "crypto/sha512" + "io" + + digest "github.com/docker/go-digest" +) + +// FromReader returns the most valid digest for the underlying content using +// the canonical digest algorithm. +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) +}