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) +}