diff --git a/specs-go/chain/chainid.go b/specs-go/chain/chainid.go new file mode 100644 index 000000000..4c299d2e1 --- /dev/null +++ b/specs-go/chain/chainid.go @@ -0,0 +1,41 @@ +package chain + +import "github.com/docker/distribution/digest" + +// ChainID takes a slice of digests, corresponding to the unpacked layer +// identifiers, and returns the chainID for the top-most layer resulting from +// that compaction order. +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 exmaple, 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. +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/specs-go/chain/chainid_test.go b/specs-go/chain/chainid_test.go new file mode 100644 index 000000000..b878f7881 --- /dev/null +++ b/specs-go/chain/chainid_test.go @@ -0,0 +1,81 @@ +package chain + +import ( + _ "crypto/sha256" // required to install sha256 digest support + "reflect" + "testing" + + "github.com/docker/distribution/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]) + } + }) + } +}